mirror of
https://github.com/javl/image2cpp.git
synced 2026-07-28 04:05:35 +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)
|
* [whoisnian](https://github.com/whoisnian)
|
||||||
* [wiredolphin](https://github.com/wiredolphin).
|
* [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
|
### 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.
|
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>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>image2cpp</title>
|
<title>image2cpp</title>
|
||||||
|
<script src="./dithering.js"></script>
|
||||||
|
|
||||||
<style type="text/css">
|
<style type="text/css">
|
||||||
* {
|
* {
|
||||||
@@ -303,9 +304,21 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="table-row">
|
<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">
|
<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">
|
<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>
|
<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>
|
</div>
|
||||||
@@ -484,6 +497,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script type="text/javascript">
|
<script type="text/javascript">
|
||||||
|
|
||||||
document.getElementById("copy-button").disabled = true;
|
document.getElementById("copy-button").disabled = true;
|
||||||
var __output;
|
var __output;
|
||||||
var ConversionFunctions = {
|
var ConversionFunctions = {
|
||||||
@@ -499,7 +513,7 @@
|
|||||||
for(var index = 0; index < data.length; index += 4){
|
for(var index = 0; index < data.length; index += 4){
|
||||||
// Get the average of the RGB (we ignore A)
|
// Get the average of the RGB (we ignore A)
|
||||||
var avg = (data[index] + data[index + 1] + data[index + 2]) / 3;
|
var avg = (data[index] + data[index + 1] + data[index + 2]) / 3;
|
||||||
if(avg > settings["threshold"]){
|
if(avg > settings["thresholdThreshold"]){
|
||||||
number += Math.pow(2, byteIndex);
|
number += Math.pow(2, byteIndex);
|
||||||
}
|
}
|
||||||
byteIndex--;
|
byteIndex--;
|
||||||
@@ -550,7 +564,7 @@
|
|||||||
for (var y = 7; y >= 0; y--){
|
for (var y = 7; y >= 0; y--){
|
||||||
var index = ((p*8)+y)*(settings["screenWidth"]*4)+x*4;
|
var index = ((p*8)+y)*(settings["screenWidth"]*4)+x*4;
|
||||||
var avg = (data[index] + data[index +1] + data[index +2]) / 3;
|
var avg = (data[index] + data[index +1] + data[index +2]) / 3;
|
||||||
if (avg > settings["threshold"]){
|
if (avg > settings["ditheringThreshold"]){
|
||||||
number += Math.pow(2, byteIndex);
|
number += Math.pow(2, byteIndex);
|
||||||
}
|
}
|
||||||
byteIndex--;
|
byteIndex--;
|
||||||
@@ -641,7 +655,7 @@
|
|||||||
for(var index = 0; index < data.length; index += 4){
|
for(var index = 0; index < data.length; index += 4){
|
||||||
// Get alpha part of the image data
|
// Get alpha part of the image data
|
||||||
var alpha = data[index + 3];
|
var alpha = data[index + 3];
|
||||||
if(alpha > settings["threshold"]){
|
if(alpha > settings["ditheringThreshold"]){
|
||||||
number += Math.pow(2, byteIndex);
|
number += Math.pow(2, byteIndex);
|
||||||
}
|
}
|
||||||
byteIndex--;
|
byteIndex--;
|
||||||
@@ -739,7 +753,8 @@
|
|||||||
scale: "1",
|
scale: "1",
|
||||||
drawMode: "horizontal",
|
drawMode: "horizontal",
|
||||||
removeZeroesCommas: false,
|
removeZeroesCommas: false,
|
||||||
threshold: 128,
|
ditheringThreshold: 128,
|
||||||
|
ditheringMode: 0,
|
||||||
outputFormat: "plain",
|
outputFormat: "plain",
|
||||||
invertColors: false,
|
invertColors: false,
|
||||||
rotate180: false,
|
rotate180: false,
|
||||||
@@ -755,6 +770,7 @@
|
|||||||
|
|
||||||
// Easy way to update settings controlled by a textfield
|
// Easy way to update settings controlled by a textfield
|
||||||
function updateInteger(fieldName){
|
function updateInteger(fieldName){
|
||||||
|
console.log('get fieldname', fieldName);
|
||||||
settings[fieldName] = document.getElementById(fieldName).value;
|
settings[fieldName] = document.getElementById(fieldName).value;
|
||||||
update();
|
update();
|
||||||
}
|
}
|
||||||
@@ -816,19 +832,19 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Make the canvas black and white
|
// // Make the canvas black and white
|
||||||
function blackAndWhite(canvas, ctx){
|
// function blackAndWhite(canvas, ctx){
|
||||||
var imageData = ctx.getImageData(0,0,canvas.width, canvas.height);
|
// var imageData = ctx.getImageData(0,0,canvas.width, canvas.height);
|
||||||
var data = imageData.data;
|
// var data = imageData.data;
|
||||||
for (var i = 0; i < data.length; i += 4) {
|
// for (var i = 0; i < data.length; i += 4) {
|
||||||
var avg = (data[i] + data[i +1] + data[i +2]) / 3;
|
// var avg = (data[i] + data[i +1] + data[i +2]) / 3;
|
||||||
avg > settings["threshold"] ? avg = 255 : avg = 0;
|
// avg > settings["threshold"] ? avg = 255 : avg = 0;
|
||||||
data[i] = avg; // red
|
// data[i] = avg; // red
|
||||||
data[i + 1] = avg; // green
|
// data[i + 1] = avg; // green
|
||||||
data[i + 2] = avg; // blue
|
// data[i + 2] = avg; // blue
|
||||||
}
|
// }
|
||||||
ctx.putImageData(imageData, 0, 0);
|
// ctx.putImageData(imageData, 0, 0);
|
||||||
}
|
// }
|
||||||
|
|
||||||
// Invert the colors of the canvas
|
// Invert the colors of the canvas
|
||||||
function invert(canvas, ctx) {
|
function invert(canvas, ctx) {
|
||||||
@@ -914,7 +930,7 @@
|
|||||||
// Make sure the image is black and white
|
// Make sure the image is black and white
|
||||||
if(settings.conversionFunction == ConversionFunctions.horizontal1bit
|
if(settings.conversionFunction == ConversionFunctions.horizontal1bit
|
||||||
|| settings.conversionFunction == ConversionFunctions.vertical1bit) {
|
|| settings.conversionFunction == ConversionFunctions.vertical1bit) {
|
||||||
blackAndWhite(canvas, ctx);
|
dithering(ctx, canvas.width, canvas.height, settings.ditheringThreshold, settings.ditheringMode);(canvas, ctx);
|
||||||
if(settings["invertColors"]){
|
if(settings["invertColors"]){
|
||||||
invert(canvas, ctx);
|
invert(canvas, ctx);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user