Improved OpenStMap image processing with sharpening - we now have a separate test file so we can try different setups more easily

master
Gordon Williams 2023-06-30 16:41:58 +01:00
parent 426fa159f5
commit ded2107f61
4 changed files with 228 additions and 23 deletions

View File

@ -0,0 +1,106 @@
/* Image filtering code that helps to transform the OSM tile
into something that's usable on a 3bpp screen.
Stick this in a file so we can
*/
function imageFilterFor3BPP(srcData, dstData, options) {
options = options || {};
if (options.colLo === undefined)
options.colLo = 115; // when adding contrast/saturation, this is the max saturaton we add
if (options.colHi === undefined)
options.colHi = 250;
if (options.sharpen === undefined)
options.sharpen = true;
if (options.dither === undefined)
options.dither = false;
const width = srcData.width;
const height = srcData.height;
var rgbaSrc = srcData.data;
var rgbaDst = dstData.data;
function getPixel(x,y) {
if (x<0) x=0;
if (y<0) y=0;
if (x>=width) x=width-1;
if (y>=height) y=height-1;
var i = (x + y*width)*4;
return [
rgbaSrc[i+0], rgbaSrc[i+1], rgbaSrc[i+2]
];
}
function dmul(a, mul) { return a.map(a => a.map(n=>n*mul)); };
const KS = 5; // kernel size
const KO = 2; // kernel offset
const K = dmul([ // 5x5 sharpening kernel
[ 1,4,6,4,1 ],
[4,16,24,16,4],
[6,24,-476,24,6],
[4,16,24,16,4],
[ 1,4,6,4,1 ],
], -1/256);
/*const KS = 7; // kernel size
const KO = 3; // kernel offset
const K = dmul([ // 7x7 sharpening (gaussian - 2x middle pixel)
[ 0, 0, 1, 2, 1, 0, 0 ],
[ 0, 3,13,22,13, 3, 0 ],
[ 1,13,59,97,59,13, 1 ],
[ 2,22,97,159-2006,97,22,2 ],
[ 1,13,59,97,59,13, 1 ],
[ 0, 3,13,22,13, 3, 0 ],
[ 0, 0, 1, 2, 1, 0, 0 ],
], -1/1003);*/
const DITHERM = 3; // dither width -1 (dither must be power 2)
const DITHER = dmul([ // dithering matrix
[ 0,1,2,3 ],
[ 1,2,3,0 ],
[ 2,3,2,1 ],
[ 3,2,1,0 ],
], 256/4);
var idx=0;
for (var y=0;y<height;y++) {
for (var x=0;x<width;x++) {
var col;
if (options.sharpen) {
// Apply a sharpening filter
var col = [0,0,0];
for (var ky=0;ky<KS;ky++)
for (var kx=0;kx<KS;kx++) {
var c = getPixel(x+kx-KO, y+ky-KO);
col[0] += c[0] * K[kx][ky];
col[1] += c[1] * K[kx][ky];
col[2] += c[2] * K[kx][ky];
}
for (var n=0;n<3;n++) {
col[n] = Math.round(col[n]);
if (col[n]<0) col[n]=0;
if (col[n]>255) col[n]=255;
}
} else { // if not sharpening, just get pixel
col = getPixel(x,y);
}
// increase saturation / contrast
var min = Math.min(col[0], col[1], col[2]);
var max = Math.max(col[0], col[1], col[2]);
var d = max-min;
if (min>options.colLo) min=options.colLo;
if (max<options.colHi) max=options.colHi;
d = max-min;
for (var n=0;n<3;n++) {
col[n] = (col[n]-min) * 256 / d;
if (options.dither) { // && col[n]<192 is more pleasant
if (col[n] > DITHER[x&DITHERM][y&DITHERM]) // dither
col[n] = 255;
else
col[n] = 0;
}
rgbaDst[idx+n] = col[n];
}
rgbaDst[idx+3] = 255;
idx+=4;
}
}
}

View File

@ -69,7 +69,7 @@
<script src="../../webtools/heatshrink.js"></script>
<script src="../../webtools/imageconverter.js"></script>
<script src="https://unpkg.com/leaflet-geosearch@3.6.0/dist/bundle.min.js"></script>
<script src="imagefilter.js"></script>
<script>
/*
@ -262,27 +262,10 @@ TODO:
turn the saturation up to maximum, so when thresholded it
works a lot better */
var imageData = ctx.getImageData(0,0,width,height);
var rgba = imageData.data;
var l = width*height*4;
for (var i=0;i<l;i+=4) {
var min = Math.min(rgba[i+0],rgba[i+1],rgba[i+2]);
var max = Math.max(rgba[i+0],rgba[i+1],rgba[i+2]);
var d = max-min;
if (max<120 || (d<8 && max<215)) { // black, or a darker grey
rgba[i+0]=0;
rgba[i+1]=0;
rgba[i+2]=0;
} else if (min>240 || d<8) { // white or grey
rgba[i+0]=255;
rgba[i+1]=255;
rgba[i+2]=255;
} else { // another colour - use max saturation
rgba[i+0] = (rgba[i+0]-min) * 255 / d;
rgba[i+1] = (rgba[i+1]-min) * 255 / d;
rgba[i+2] = (rgba[i+2]-min) * 255 / d;
}
}
ctx.putImageData(imageData,0,0);
var dstData = ctx.createImageData(width, height);
var filterOptions = {};
imageFilterFor3BPP(imageData, dstData, filterOptions);
ctx.putImageData(dstData,0,0);
}
console.log("Compression options", options);
var w = Math.round(width / TILESIZE);
@ -296,7 +279,7 @@ TODO:
options.width = TILESIZE;
options.height = TILESIZE;
var imgstr = imageconverter.RGBAtoString(rgba, options);
ctx.putImageData(imageData,x*TILESIZE, y*TILESIZE); // write preview
//ctx.putImageData(imageData,x*TILESIZE, y*TILESIZE); // write preview
/*var compress = 'require("heatshrink").decompress('
if (!imgstr.startsWith(compress)) throw "Data in wrong format";
imgstr = imgstr.slice(compress.length,-1);*/

View File

@ -0,0 +1,116 @@
<html>
<head>
<title>3 bit filter test</title>
</head>
<style>
</style>
</head>
<body>
<p>This file tests imagefilter.js to allow different filtering options
to be tested, so we can quickly see the easiest way of transforming OpenStreetMap tiles
into 3 bits
</p>
<canvas id="maptiles" style="display:none"></canvas>
<button id="getmap">Test</button><br/>
<input type="checkbox" id="finaldither" checked></input><span>Final dither (Bangle.js preview)</span><br/>
<input type="checkbox" id="sharpen" checked></input><span>Sharpen</span>
<input type="checkbox" id="dither"></input><span>Line Dither</span><br/>
<input type="range" id="slider_lo" min="0" max="255" value="115">Lo threshold<br/>
<input type="range" id="slider_hi" min="0" max="255" value="250">Hi threshold<br/>
<script src="../../../webtools/heatshrink.js"></script>
<script src="../../../webtools/imageconverter.js"></script>
<script src="../imagefilter.js"></script>
<script>
var TILESIZE = 96
// convert canvas into an actual tiled image file
function tilesLoaded(ctx, width, height, mapImageFile) {
var filterOptions = {
colLo : 0|document.getElementById("slider_lo").value,
colHi : 0|document.getElementById("slider_hi").value,
sharpen : 0|document.getElementById("sharpen").checked,
dither : 0|document.getElementById("dither").checked
}
console.log("filterOptions", filterOptions);
var preview = document.getElementById("finaldither").checked;
var options = {
compression:false, output:"raw",
mode:"3bit",
diffusion:"bayer2"
};
/* If in 3 bit mode, go through all the data beforehand and
turn the saturation up to maximum, so when thresholded it
works a lot better */
var imageData = ctx.getImageData(0,0,width,height);
var dstData = ctx.createImageData(width, height);
imageFilterFor3BPP(imageData, dstData, filterOptions);
ctx.putImageData(dstData,0,0);
console.log("Compression options", options);
var tiledImage;
if (preview) {
var w = Math.round(width / TILESIZE);
var h = Math.round(height / TILESIZE);
for (var y=0;y<h;y++) {
for (var x=0;x<w;x++) {
var imageData = ctx.getImageData(x*TILESIZE, y*TILESIZE, TILESIZE, TILESIZE);
var rgba = imageData.data;
options.rgbaOut = rgba;
options.width = TILESIZE;
options.height = TILESIZE;
var imgstr = imageconverter.RGBAtoString(rgba, options);
ctx.putImageData(imageData,x*TILESIZE, y*TILESIZE); // write preview
/*var compress = 'require("heatshrink").decompress('
if (!imgstr.startsWith(compress)) throw "Data in wrong format";
imgstr = imgstr.slice(compress.length,-1);*/
if (tiledImage) tiledImage += imgstr.substr(3); // skip header
else tiledImage = imgstr; // for first image, keep the header
}
}
}
return [{
name:mapImageFile,
content:tiledImage
}];
}
// Render everything to a canvas...
var canvas = document.getElementById("maptiles");
canvas.style.display="";
var ctx = canvas.getContext('2d');
var img = new Image();
img.crossOrigin = "Anonymous";
img.onload = function(){
canvas.width = img.width;
canvas.height = img.height;
ctx.drawImage(img,0,0);
tilesLoaded(ctx, canvas.width, canvas.height, "");
};
img.src = "osm-test.png"
function update() {
ctx.drawImage(img,0,0);
tilesLoaded(ctx, canvas.width, canvas.height, "");
}
document.getElementById("getmap").addEventListener("click", update);
document.getElementById("slider_lo").addEventListener("mouseup",update);
document.getElementById("slider_hi").addEventListener("mouseup",update);
document.getElementById("finaldither").addEventListener("click",update);
document.getElementById("sharpen").addEventListener("click",update);
document.getElementById("dither").addEventListener("click",update);
</script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB