mirror of
https://github.com/javl/image2cpp.git
synced 2026-07-27 19:56:07 +00:00
Add dithering options
This commit is contained in:
@@ -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
@@ -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
@@ -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);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user