Skip to content

Instantly share code, notes, and snippets.

@bddicken
Last active March 21, 2024 13:43
Show Gist options
  • Save bddicken/7df50409870aff80e59894602de392bd to your computer and use it in GitHub Desktop.
Save bddicken/7df50409870aff80e59894602de392bd to your computer and use it in GitHub Desktop.
Image Binning
<html>
<head>
<script is:inline defer src="https://cdn.jsdelivr.net/npm/img-comparison-slider@8/dist/index.js"></script>
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/img-comparison-slider@8/dist/styles.css" />
<script is:inline src="/assets/code/ImageManipulator.js"></script>
<style>
.slider {
width: 100%;
fill: #fff;
stroke: #fff;
}
.sliderContainer {
padding: 5px;
}
.newFile {
padding:5px;
}
.noHighlight {
outline: none;
}
.doubleStyle {
--divider-width: 4px;
--divider-color: #808080;
--default-handle-opacity: 1;
--default-handle-width: 100px;
}
.comparisonContainer {
max-width: 800px;
min-width: 100px;
width: 90%;
}
</style>
<script>
"use strict";
class ImageManipulator {
constructor(imageName, containerID, width, height) {
this.container = document.getElementById(containerID);
this.imageName = imageName;
this.updateWH(width, height);
this.sliders = [];
this.canvas = this.initCanvas("canvas");
this.baseCanvas = this.initCanvas("canvasBase");
this.initImage(this.canvas);
this.initImage(this.baseCanvas);
this.addSliders();
}
updateWH(width, height) {
this.width = 1400;
this.height = Math.floor((height / width) * 1400);
}
updateImage(e) {
this.fillCanvas(this.canvas, 0, 0, this.width, this.height, [255, 255, 255]);
this.binnify();
}
addSlider(c) {
const slider = this.container.getElementsByClassName(c)[0];
slider.addEventListener("input", event => this.updateImage());
this.sliders.push(slider);
}
addSliders() {
this.addSlider("binSize");
this.addSlider("binGap");
}
initCanvas(id) {
const canvas = this.container.getElementsByClassName(id)[0];
canvas.width = this.width;
canvas.height = this.height;
canvas.style.width = "100%";
return canvas;
}
initImage(canvas) {
var image = new Image();
image.src = this.imageName;
image.addEventListener("load", e => {
const ctx = canvas.getContext("2d");
ctx.imageSmoothingEnabled = false;
ctx.drawImage(image, 0, 0, this.width, this.height);
this.updateImage(null);
});
}
fillCanvas(canvas, x, y, w, h, rgb) {
const ctx = this.canvas.getContext("2d");
const pData = ctx.getImageData(0, 0, this.width, this.height);
const pixels = pData.data;
this.fillRect(pixels, x, y, w, h, rgb);
ctx.putImageData(pData, 0, 0);
}
fillRect(pixels, x, y, w, h, rgb) {
for (let x2 = x; x2 < w; x2++) {
for (let y2 = y; y2 < h; y2++) {
let pi = this.pidx(pixels, x2, y2);
pixels[pi] = rgb[0];
pixels[pi + 1] = rgb[1];
pixels[pi + 2] = rgb[2];
}
}
}
binnify() {
const binSize = Number(this.sliders[0].value);
const binGap = Number(this.sliders[1].value);
// The context / pixels for the base image canvas
const ctx = this.baseCanvas.getContext("2d");
const pData = ctx.getImageData(0, 0, this.width, this.height);
const pixels = pData.data;
// The context / pixels for the canvas to modify with binning
const ctx2 = this.canvas.getContext("2d");
const pData2 = ctx2.getImageData(0, 0, this.width, this.height);
const pixels2 = pData2.data;
// The actual bin size, accounting for gap
const actual = binSize - binGap;
// iterate through all bins
for (let x = 0; x < this.width - binSize; x += binSize) {
for (let y = 0; y < this.height - binSize; y += binSize) {
let average = this.getAverageBrightness(pixels, x, y, x + binSize, y + binSize);
let bracket = actual - Math.floor(average / (255 / actual));
let pixelizeGap = Math.floor((actual - bracket) / 2) + binGap;
this.fillRect(pixels2, x + pixelizeGap, y + pixelizeGap, x + binSize - pixelizeGap, y + binSize - pixelizeGap, [0, 0, 0]);
}
}
ctx2.putImageData(pData2, 0, 0);
}
pidx(pixels, x, y) {
return Math.floor(pixels.length / this.height) * y + x * 4;
}
getAverageBrightness(pixels, x, y, x2, y2) {
let total = 0;
for (let x3 = x; x3 < x2; x3++) {
for (let y3 = y; y3 < y2; y3++) {
let i = this.pidx(pixels, x3, y3);
total += pixels[i] + pixels[i + 1] + pixels[i + 2];
}
}
return total / ((x2 - x) * (y2 - y) * 3);
}
}
function initializeFileLoad(im, containerID) {
const loader = im.container.getElementsByClassName("imgFile")[0];
loader.addEventListener("change", event => {
loadImage(im, containerID);
});
}
function loadImage(im, containerID) {
var input, file, fr, img;
if (typeof window.FileReader !== "function") {
alert("The file API isn't supported on this browser yet.");
return;
}
const container = document.getElementById(containerID);
input = container.getElementsByClassName("imgFile")[0];
if (!input || !input.files) {
alert(
"There was a problem. Does your browser support the files API? Maybe try a different browser."
);
} else if (!input.files[0]) {
alert("Please select a file before clicking 'Load'");
} else {
file = input.files[0];
fr = new FileReader();
fr.onload = () => {
img = new Image();
img.onload = () => {
imageLoaded(im, containerID);
};
img.src = fr.result;
};
fr.readAsDataURL(file);
}
function configureImage(im, containerID, c, img) {
const w = 1400;
const h = Math.floor((img.height / img.width) * 1400);
const container = document.getElementById(containerID);
var canvas = container.getElementsByClassName(c)[0];
canvas.width = w;
canvas.height = h;
im.updateWH(img.width, img.height);
var ctx = canvas.getContext("2d");
ctx.drawImage(img, 0, 0, w, h);
}
function imageLoaded(im, containerID) {
configureImage(im, containerID, "canvas", img);
configureImage(im, containerID, "canvasBase", img);
im.updateImage();
}
}
</script>
</head>
<body>
<div id="example" class="comparisonContainer">
<img-comparison-slider class="noHighlight doubleStyle">
<canvas slot="first" class="canvas"></canvas>
<canvas slot="second" class="canvasBase"></canvas>
</img-comparison-slider>
<div class="sliderContainer">
<input name="binSize" class="binSize slider" type="range" min="1" max="100" step="1" value="12" />
<label for="binSize"><p style="display:inline;">Bucket Size</p></label>
</div>
<div class="sliderContainer">
<input name="binGap" class="binGap slider" type="range" min="1" max="10" step="1" value="0" />
<label for="binGap"><p style="display:inline;">Gap Size</p></label>
</div>
<form action='#' onsubmit="return false;">
<input class="newFile imgFile" type="file" />
</form>
</div>
<script>
im1 = new ImageManipulator('./coffee.jpeg', "example", 1400, 1400);
initializeFileLoad(im1, "example");
</script>
</body>
</html>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment