You can edit the below JavaScript code to customize the image tool.
Apply Changes
function processImage(originalImg, minSizePercent = 0.05, maxSizePercent = 25.0, aspectTolerance = 0.5) {
const MAX_DIM = 800;
let scale = 1;
if (originalImg.width > MAX_DIM || originalImg.height > MAX_DIM) {
scale = Math.min(MAX_DIM / originalImg.width, MAX_DIM / originalImg.height);
}
const w = Math.round(originalImg.width * scale);
const h = Math.round(originalImg.height * scale);
const canvas = document.createElement('canvas');
canvas.width = w;
canvas.height = h;
const ctx = canvas.getContext('2d');
ctx.drawImage(originalImg, 0, 0, w, h);
let imgData;
try {
imgData = ctx.getImageData(0, 0, w, h);
} catch (e) {
ctx.fillStyle = "black";
ctx.fillRect(0, 0, w, h);
ctx.fillStyle = "red";
ctx.font = "20px sans-serif";
ctx.fillText("Error: Cannot access image data (CORS).", 20, 40);
return canvas;
}
// 1. Convert to Grayscale
let grayImage = new Uint8Array(w * h);
for(let i = 0; i < w * h; i++) {
let r = imgData.data[i*4];
let g = imgData.data[i*4+1];
let b = imgData.data[i*4+2];
grayImage[i] = (r*0.299 + g*0.587 + b*0.114) | 0;
}
// 2. Fast Box Blur to remove textures
function boxBlur(input, passes=1) {
let curr = input;
for(let p=0; p<passes; p++) {
let next = new Uint8Array(w * h);
for(let y=1; y<h-1; y++){
for(let x=1; x<w-1; x++){
let sum = curr[(y-1)*w + x-1] + curr[(y-1)*w + x] + curr[(y-1)*w + x+1] +
curr[y*w + x-1] + curr[y*w + x] + curr[y*w + x+1] +
curr[(y+1)*w + x-1] + curr[(y+1)*w + x] + curr[(y+1)*w + x+1];
next[y*w + x] = (sum / 9) | 0;
}
}
curr = next;
}
return curr;
}
let blurred = boxBlur(grayImage, 2);
// 3. Morphological Operations (Erosion and Dilation via Cross Kernel)
function erode(input) {
let out = new Uint8Array(w * h);
for(let y=1; y<h-1; y++){
for(let x=1; x<w-1; x++){
let idx = y*w + x;
if(input[idx] && input[idx-1] && input[idx+1] && input[idx-w] && input[idx+w]) out[idx] = 1;
}
}
return out;
}
function dilate(input) {
let out = new Uint8Array(w * h);
for(let y=1; y<h-1; y++){
for(let x=1; x<w-1; x++){
let idx = y*w + x;
if(input[idx] || input[idx-1] || input[idx+1] || input[idx-w] || input[idx+w]) out[idx] = 1;
}
}
return out;
}
// 4. Multiple Ensemble Object Segmenters (Adaptive Thresholding + Sobel Edge Inv)
let intImg = new Int32Array(w * h);
for(let y=0; y<h; y++){
let sum = 0;
for(let x=0; x<w; x++){
sum += blurred[y*w + x];
if (y === 0) intImg[x] = sum;
else intImg[y*w + x] = intImg[(y-1)*w + x] + sum;
}
}
let binaryBright = new Uint8Array(w * h);
let binaryDark = new Uint8Array(w * h);
let s = Math.floor(Math.max(w, h) * 0.2); // 20% window
let s2 = (s / 2) | 0;
let C = 7;
for(let y=0; y<h; y++){
for(let x=0; x<w; x++){
let x1 = Math.max(0, x - s2); let y1 = Math.max(0, y - s2);
let x2 = Math.min(w - 1, x + s2); let y2 = Math.min(h - 1, y + s2);
let t1 = intImg[y2 * w + x2];
let t2 = y1 > 0 ? intImg[(y1 - 1) * w + x2] : 0;
let t3 = x1 > 0 ? intImg[y2 * w + (x1 - 1)] : 0;
let t4 = (y1 > 0 && x1 > 0) ? intImg[(y1 - 1) * w + (x1 - 1)] : 0;
let mean = (t1 - t2 - t3 + t4) / ((y2 - y1 + 1) * (x2 - x1 + 1));
let val = blurred[y * w + x];
if (val > mean + C) binaryBright[y*w + x] = 1;
if (val < mean - C) binaryDark[y*w + x] = 1;
}
}
binaryBright = dilate(erode(binaryBright));
binaryDark = dilate(erode(binaryDark));
let edges = new Uint8Array(w * h);
for(let y=1; y<h-1; y++){
for(let x=1; x<w-1; x++){
let px00 = blurred[(y-1)*w + x-1]; let px01 = blurred[(y-1)*w + x]; let px02 = blurred[(y-1)*w + x+1];
let px10 = blurred[y*w + x-1]; let px12 = blurred[y*w + x+1];
let px20 = blurred[(y+1)*w + x-1]; let px21 = blurred[(y+1)*w + x]; let px22 = blurred[(y+1)*w + x+1];
let gx = (px02 - px00) + 2*(px12 - px10) + (px22 - px20);
let gy = (px20 - px00) + 2*(px21 - px01) + (px22 - px02);
if(Math.sqrt(gx*gx + gy*gy) > 60) edges[y*w + x] = 1;
}
}
edges = dilate(edges);
let invertedEdges = new Uint8Array(w * h);
for(let i=0; i<w*h; i++) invertedEdges[i] = edges[i] === 1 ? 0 : 1;
// 5. Fast Connected Component Labeling
function getComponents(binaryMap, minPrune, maxPrune) {
let labels = new Int32Array(w * h);
for(let i=0; i<w*h; i++) labels[i] = i;
function findRoot(i) {
let root = i;
while(root !== labels[root]) root = labels[root];
let curr = i;
while(curr !== root) { let nxt = labels[curr]; labels[curr] = root; curr = nxt; }
return root;
}
for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++) {
let idx = y * w + x;
if (binaryMap[idx] === 1) {
let hasTop = y > 0 && binaryMap[idx - w] === 1;
let hasLeft = x > 0 && binaryMap[idx - 1] === 1;
if (hasTop && hasLeft) {
let rootTop = findRoot(idx - w);
let rootLeft = findRoot(idx - 1);
if(rootTop !== rootLeft) labels[rootTop] = rootLeft;
labels[idx] = rootLeft;
} else if (hasTop) { labels[idx] = findRoot(idx - w); }
else if (hasLeft) { labels[idx] = findRoot(idx - 1); }
}
}
}
let components = new Map();
for (let y = 0; y < h; y++) {
for (let x = 0; x < w; x++) {
let idx = y * w + x;
if (binaryMap[idx] === 1) {
let root = findRoot(idx);
let c = components.get(root);
if (!c) {
components.set(root, { minX: x, maxX: x, minY: y, maxY: y, count: 1 });
} else {
if (x < c.minX) c.minX = x; if (x > c.maxX) c.maxX = x;
if (y < c.minY) c.minY = y; if (y > c.maxY) c.maxY = y;
c.count++;
}
}
}
}
let valid = [];
for (let c of components.values()) {
if (c.count >= minPrune && c.count <= maxPrune) valid.push(c);
}
return valid;
}
let minArea = (w * h) * (Number(minSizePercent) / 100);
let maxArea = (w * h) * (Number(maxSizePercent) / 100);
let compEdges = getComponents(invertedEdges, minArea, maxArea);
let compBright = getComponents(binaryBright, minArea, maxArea);
let compDark = getComponents(binaryDark, minArea, maxArea);
let allDetections = [...compEdges, ...compBright, ...compDark];
// 6. Circular Form Filtering & NMS Deduplication
let sandDollars = [];
let minAspect = 1.0 - Number(aspectTolerance);
let maxAspect = 1.0 + Number(aspectTolerance);
for (let c of allDetections) {
// Discard items glued perfectly to the physical image wall (likely background regions)
if (c.minX <= 2 || c.minY <= 2 || c.maxX >= w - 3 || c.maxY >= h - 3) continue;
let wBox = c.maxX - c.minX + 1; let hBox = c.maxY - c.minY + 1;
let aspect = wBox / hBox;
let fillRatio = c.count / (wBox * hBox); // perfect circle = 0.785
if (aspect >= minAspect && aspect <= maxAspect && fillRatio >= 0.4 && fillRatio <= 0.95) {
c.width = wBox; c.height = hBox;
c.cx = c.minX + wBox/2; c.cy = c.minY + hBox/2;
c.score = Math.abs(1 - aspect) + Math.abs(0.785 - fillRatio);
sandDollars.push(c);
}
}
sandDollars.sort((a, b) => a.score - b.score);
let finalDetections = [];
for (let current of sandDollars) {
let overlap = false;
for (let kept of finalDetections) {
let dx = current.cx - kept.cx; let dy = current.cy - kept.cy;
let dist = Math.sqrt(dx*dx + dy*dy);
let radiusProtection = Math.min(current.width, kept.width) / 2;
if (dist < radiusProtection) { overlap = true; break; }
}
if (!overlap) finalDetections.push(current);
}
// 7. Render Bounding Circles and Labels
ctx.lineWidth = 4;
for(let i=0; i < finalDetections.length; i++) {
let c = finalDetections[i];
let r = Math.max(c.width, c.height) / 2;
ctx.beginPath();
ctx.arc(c.cx, c.cy, r, 0, Math.PI * 2);
ctx.strokeStyle = '#00FF00'; ctx.lineWidth = 4; ctx.stroke();
ctx.strokeStyle = '#FFFFFF'; ctx.lineWidth = 2; ctx.stroke();
ctx.fillStyle = '#FF4500';
ctx.beginPath();
ctx.arc(c.cx - r*0.7, c.cy - r*0.7, 16, 0, Math.PI*2);
ctx.fill(); ctx.stroke();
ctx.fillStyle = '#FFFFFF';
ctx.font = 'bold 16px sans-serif';
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
ctx.fillText((i+1).toString(), c.cx - r*0.7, c.cy - r*0.7);
}
// 8. Fun Meme Overlay Text
let count = finalDetections.length;
let message = count > 0
? `Aw, barnacles! I counted ${count} sand dollar${count === 1 ? '' : 's'}!`
: "Aw, barnacles, I'm outta sand dollars already...";
let fontSize = Math.max(20, Math.floor(w / 25));
ctx.font = `bold ${fontSize}px 'Comic Sans MS', sans-serif`;
ctx.fillStyle = "yellow";
ctx.strokeStyle = "black";
ctx.lineWidth = 4;
ctx.textAlign = "center";
ctx.textBaseline = "top";
ctx.strokeText(message, w/2, 20);
ctx.fillText(message, w/2, 20);
return canvas;
}
Apply Changes