Last active
March 21, 2024 13:43
-
-
Save bddicken/7df50409870aff80e59894602de392bd to your computer and use it in GitHub Desktop.
Image Binning
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
<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