Add dithering options

This commit is contained in:
javl
2023-05-01 22:42:21 +02:00
parent cf0884ceb9
commit 98b6f3093b
3 changed files with 248 additions and 21 deletions
+1 -1
View File
@@ -34,7 +34,7 @@ Initial code by [javl](https://github.com/javl) with aditional code by (in alpha
* [whoisnian](https://github.com/whoisnian)
* [wiredolphin](https://github.com/wiredolphin).
The example sketch is based on code by [Adafruit](https://github.com/adafruit).
The example sketch is based on code by [Adafruit](https://github.com/adafruit). Dithering code from [stellar-L3N-etag](https://github.com/reece15/stellar-L3N-etag).
### License
image2cpp is released under GPL v3. This means you can use the project in any way you want (use, adapt, distribute, etc.) as long as you share any changes and link back to this repo. See [LICENSE.md](https://github.com/javl/image2cpp/blob/master/LICENSE.md) for more info.
+211
View File
@@ -0,0 +1,211 @@
const bwrPalette = [
[0, 0, 0, 255],
[255, 255, 255, 255],
[255, 0, 0, 255]
]
const bwPalette = [
[0, 0, 0, 255],
[255, 255, 255, 255],
]
function dithering(ctx, width, height, threshold, typeIndex) {
console.log('typeindex: ', typeIndex);
const type = ['binary', 'bayer', 'floysteinberg', 'atkinson'][typeIndex];
console.log('type: ', type);
const bayerThresholdMap = [
[ 15, 135, 45, 165 ],
[ 195, 75, 225, 105 ],
[ 60, 180, 30, 150 ],
[ 240, 120, 210, 90 ]
];
const lumR = [];
const lumG = [];
const lumB = [];
for (let i=0; i<256; i++) {
lumR[i] = i*0.299;
lumG[i] = i*0.587;
lumB[i] = i*0.114;
}
const imageData = ctx.getImageData(0, 0, width, height);
const imageDataLength = imageData.data.length;
// Greyscale luminance (sets r pixels to luminance of rgb)
for (let i = 0; i <= imageDataLength; i += 4) {
imageData.data[i] = Math.floor(lumR[imageData.data[i]] + lumG[imageData.data[i+1]] + lumB[imageData.data[i+2]]);
}
const w = imageData.width;
let newPixel, err;
for (let currentPixel = 0; currentPixel <= imageDataLength; currentPixel+=4) {
if (type ==="binary") {
// No dithering
imageData.data[currentPixel] = imageData.data[currentPixel] < threshold ? 0 : 255;
} else if (type ==="bayer") {
// 4x4 Bayer ordered dithering algorithm
var x = currentPixel/4 % w;
var y = Math.floor(currentPixel/4 / w);
var map = Math.floor( (imageData.data[currentPixel] + bayerThresholdMap[x%4][y%4]) / 2 );
imageData.data[currentPixel] = (map < threshold) ? 0 : 255;
} else if (type ==="floydsteinberg") {
// Floyda€"Steinberg dithering algorithm
newPixel = imageData.data[currentPixel] < 129 ? 0 : 255;
err = Math.floor((imageData.data[currentPixel] - newPixel) / 16);
imageData.data[currentPixel] = newPixel;
imageData.data[currentPixel + 4 ] += err*7;
imageData.data[currentPixel + 4*w - 4 ] += err*3;
imageData.data[currentPixel + 4*w ] += err*5;
imageData.data[currentPixel + 4*w + 4 ] += err*1;
} else {
// Bill Atkinson's dithering algorithm
newPixel = imageData.data[currentPixel] < threshold ? 0 : 255;
err = Math.floor((imageData.data[currentPixel] - newPixel) / 8);
imageData.data[currentPixel] = newPixel;
imageData.data[currentPixel + 4 ] += err;
imageData.data[currentPixel + 8 ] += err;
imageData.data[currentPixel + 4*w - 4 ] += err;
imageData.data[currentPixel + 4*w ] += err;
imageData.data[currentPixel + 4*w + 4 ] += err;
imageData.data[currentPixel + 8*w ] += err;
}
// Set g and b pixels equal to r
imageData.data[currentPixel + 1] = imageData.data[currentPixel + 2] = imageData.data[currentPixel];
}
ctx.putImageData(imageData, 0, 0);
}
function canvas2bytes(canvas, type='bw') {
const ctx = canvas.getContext("2d");
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const arr = [];
let buffer = [];
for (let x = canvas.width - 1; x >= 0; x--) {
for (let y = 0; y < canvas.height; y++) {
const index = (canvas.width * 4 * y) + x * 4;
if (type !== 'bwr') {
buffer.push(imageData.data[index] > 0 && imageData.data[index+1] > 0 && imageData.data[index+2] > 0 ? 1 : 0);
} else {
buffer.push(imageData.data[index] > 0 && imageData.data[index+1] === 0 && imageData.data[index+2] === 0 ? 1 : 0);
}
if (buffer.length === 8) {
arr.push(parseInt(buffer.join(''), 2));
buffer = [];
}
}
}
return arr;
}
function getColorDistance(rgba1, rgba2) {
const [r1, b1, g1] = rgba1;
const [r2, b2, g2] = rgba2;
const rm = (r1 + r2 ) / 2;
const r = r1 - r2;
const g = g1 - g2;
const b = b1 - b2;
return Math.sqrt((2 + rm / 256) * r * r + 4 * g * g + (2 + (255 - rm) / 256) * b * b);
}
function getNearColor(pixel, palette) {
let minDistance = 255 * 255 * 3 + 1;
let paletteIndex = 0;
for (let i = 0; i < palette.length; i++) {
const targetColor = palette[i];
const distance = getColorDistance(pixel, targetColor);
if (distance < minDistance) {
minDistance = distance;
paletteIndex = i;
}
}
return palette[paletteIndex];
}
function getNearColorV2(color, palette) {
let minDistanceSquared = 255*255 + 255*255 + 255*255 + 1;
let bestIndex = 0;
for (let i = 0; i < palette.length; i++) {
let rdiff = (color[0] & 0xff) - (palette[i][0] & 0xff);
let gdiff = (color[1] & 0xff) - (palette[i][1] & 0xff);
let bdiff = (color[2] & 0xff) - (palette[i][2] & 0xff);
let distanceSquared = rdiff*rdiff + gdiff*gdiff + bdiff*bdiff;
if (distanceSquared < minDistanceSquared) {
minDistanceSquared = distanceSquared;
bestIndex = i;
}
}
return palette[bestIndex];
}
function updatePixel(imageData, index, color) {
imageData[index] = color[0];
imageData[index+1] = color[1];
imageData[index+2] = color[2];
imageData[index+3] = color[3];
}
function getColorErr(color1, color2, rate) {
const res = [];
for (let i = 0; i < 3; i++) {
res.push(Math.floor((color1[i] - color2[i]) / rate));
}
return res;
}
function updatePixelErr(imageData, index, err, rate) {
imageData[index] += err[0] * rate;
imageData[index+1] += err[1] * rate;
imageData[index+2] += err[2] * rate;
}
function ditheringCanvasByPalette(canvas, palette, type) {
palette = palette || bwrPalette;
const ctx = canvas.getContext('2d');
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
const w = imageData.width;
for (let currentPixel = 0; currentPixel <= imageData.data.length; currentPixel+=4) {
const newColor = getNearColorV2(imageData.data.slice(currentPixel, currentPixel+4), palette);
if (type === "bwr_floydsteinberg") {
const err = getColorErr(imageData.data.slice(currentPixel, currentPixel+4), newColor, 16);
updatePixel(imageData.data, currentPixel, newColor);
updatePixelErr(imageData.data, currentPixel +4, err, 7);
updatePixelErr(imageData.data, currentPixel + 4*w - 4, err, 3);
updatePixelErr(imageData.data, currentPixel + 4*w, err, 5);
updatePixelErr(imageData.data, currentPixel + 4*w + 4, err, 1);
} else {
const err = getColorErr(imageData.data.slice(currentPixel, currentPixel+4), newColor, 8);
updatePixel(imageData.data, currentPixel, newColor);
updatePixelErr(imageData.data, currentPixel +4, err, 1);
updatePixelErr(imageData.data, currentPixel +8, err, 1);
updatePixelErr(imageData.data, currentPixel +4 * w - 4, err, 1);
updatePixelErr(imageData.data, currentPixel +4 * w, err, 1);
updatePixelErr(imageData.data, currentPixel +4 * w + 4, err, 1);
updatePixelErr(imageData.data, currentPixel +8 * w, err, 1);
}
}
ctx.putImageData(imageData, 0, 0);
}
+36 -20
View File
@@ -11,6 +11,7 @@
<head>
<meta charset="UTF-8">
<title>image2cpp</title>
<script src="./dithering.js"></script>
<style type="text/css">
* {
@@ -303,9 +304,21 @@
</div>
<div class="table-row">
<div class="table-cell"><label>Brightness / alpha threshold: </label></div>
<div class="table-cell"><label for="ditheringMode">Dithering: </label></div>
<div class="table-cell">
<input id="threshold" class="size-input" type="number" min="0" max="255" name="threshold" oninput="updateInteger('threshold')" value="128"/>
<select id="ditheringMode" onchange="updateInteger('ditheringMode')">
<option value="0">Binary</option>
<option value="1">Bayer</option>
<option value="2">Floyd-Steinberg</option>
<option value="3">Atkinson</option>
</select>
</div>
</div>
<div class="table-row">
<div class="table-cell"><label for="ditheringThreshold">Brightness / alpha threshold: </label></div>
<div class="table-cell">
<input id="ditheringThreshold" class="size-input" type="number" min="0" max="255" name="ditheringThreshold" oninput="updateInteger('ditheringThreshold')" value="128"/>
<div class="note">
<i>0 - 255; if the brightness of a pixel is above the given level the pixel becomes white, otherwise they become black. When using alpha, opaque and transparent are used instead.</i></div>
</div>
@@ -484,6 +497,7 @@
</div>
<script type="text/javascript">
document.getElementById("copy-button").disabled = true;
var __output;
var ConversionFunctions = {
@@ -499,7 +513,7 @@
for(var index = 0; index < data.length; index += 4){
// Get the average of the RGB (we ignore A)
var avg = (data[index] + data[index + 1] + data[index + 2]) / 3;
if(avg > settings["threshold"]){
if(avg > settings["thresholdThreshold"]){
number += Math.pow(2, byteIndex);
}
byteIndex--;
@@ -550,7 +564,7 @@
for (var y = 7; y >= 0; y--){
var index = ((p*8)+y)*(settings["screenWidth"]*4)+x*4;
var avg = (data[index] + data[index +1] + data[index +2]) / 3;
if (avg > settings["threshold"]){
if (avg > settings["ditheringThreshold"]){
number += Math.pow(2, byteIndex);
}
byteIndex--;
@@ -641,7 +655,7 @@
for(var index = 0; index < data.length; index += 4){
// Get alpha part of the image data
var alpha = data[index + 3];
if(alpha > settings["threshold"]){
if(alpha > settings["ditheringThreshold"]){
number += Math.pow(2, byteIndex);
}
byteIndex--;
@@ -739,7 +753,8 @@
scale: "1",
drawMode: "horizontal",
removeZeroesCommas: false,
threshold: 128,
ditheringThreshold: 128,
ditheringMode: 0,
outputFormat: "plain",
invertColors: false,
rotate180: false,
@@ -755,6 +770,7 @@
// Easy way to update settings controlled by a textfield
function updateInteger(fieldName){
console.log('get fieldname', fieldName);
settings[fieldName] = document.getElementById(fieldName).value;
update();
}
@@ -816,19 +832,19 @@
}
}
// Make the canvas black and white
function blackAndWhite(canvas, ctx){
var imageData = ctx.getImageData(0,0,canvas.width, canvas.height);
var data = imageData.data;
for (var i = 0; i < data.length; i += 4) {
var avg = (data[i] + data[i +1] + data[i +2]) / 3;
avg > settings["threshold"] ? avg = 255 : avg = 0;
data[i] = avg; // red
data[i + 1] = avg; // green
data[i + 2] = avg; // blue
}
ctx.putImageData(imageData, 0, 0);
}
// // Make the canvas black and white
// function blackAndWhite(canvas, ctx){
// var imageData = ctx.getImageData(0,0,canvas.width, canvas.height);
// var data = imageData.data;
// for (var i = 0; i < data.length; i += 4) {
// var avg = (data[i] + data[i +1] + data[i +2]) / 3;
// avg > settings["threshold"] ? avg = 255 : avg = 0;
// data[i] = avg; // red
// data[i + 1] = avg; // green
// data[i + 2] = avg; // blue
// }
// ctx.putImageData(imageData, 0, 0);
// }
// Invert the colors of the canvas
function invert(canvas, ctx) {
@@ -914,7 +930,7 @@
// Make sure the image is black and white
if(settings.conversionFunction == ConversionFunctions.horizontal1bit
|| settings.conversionFunction == ConversionFunctions.vertical1bit) {
blackAndWhite(canvas, ctx);
dithering(ctx, canvas.width, canvas.height, settings.ditheringThreshold, settings.ditheringMode);(canvas, ctx);
if(settings["invertColors"]){
invert(canvas, ctx);
}