Created
June 10, 2026 02:49
-
-
Save soruly/11bcb33f96728a9a62c1feda8c244143 to your computer and use it in GitHub Desktop.
detect black borders using canvas with vanilla js
This file contains hidden or 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
| <style> | |
| body { | |
| font-family: monospace; | |
| } | |
| header { | |
| display: flex; | |
| flex-direction: column; | |
| gap: 10px; | |
| padding: 10px; | |
| } | |
| main { | |
| display: grid; | |
| grid-template-columns: repeat(3, 1fr); | |
| gap: 10px; | |
| } | |
| canvas { | |
| max-width: 100%; | |
| } | |
| .item { | |
| display: flex; | |
| flex-direction: column; | |
| } | |
| </style> | |
| <body> | |
| <header> | |
| <div> | |
| <label for="colorTolerance">Black Tolerance (0-255): </label> | |
| <input type="number" id="colorTolerance" value="5" min="0" max="255" /> | |
| <span>How black is considered black.</span> | |
| </div> | |
| <div> | |
| <label for="blackPercentage">Black Percentage (0-100): </label> | |
| <input type="number" id="blackPercentage" value="95" min="0" max="100" /> | |
| <span>% of pixels in a row/col needs to be "black" to be considered as a black row/col.</span> | |
| <span>Scan outward from the center, but if center is dark, scan from the edge</span> | |
| </div> | |
| </header> | |
| <main></main> | |
| </body> | |
| <script> | |
| function getVideoFrameRect(imgElement, colorTolerance = 5, blackPercentage = 0.95) { | |
| const canvas = document.createElement("canvas"); | |
| const ctx = canvas.getContext("2d"); | |
| const width = imgElement.naturalWidth; | |
| const height = imgElement.naturalHeight; | |
| canvas.width = width; | |
| canvas.height = height; | |
| ctx.drawImage(imgElement, 0, 0); | |
| const data = ctx.getImageData(0, 0, width, height).data; | |
| // Helper to check if a pixel is dark | |
| function isDark(x, y) { | |
| const i = (y * width + x) * 4; | |
| return ( | |
| data[i] <= colorTolerance && data[i + 1] <= colorTolerance && data[i + 2] <= colorTolerance | |
| ); | |
| } | |
| // Helper: Check if a specific row is mostly dark | |
| function isRowDark(y) { | |
| let darkPixelCount = 0; | |
| for (let x = 0; x < width; x++) { | |
| if (isDark(x, y)) darkPixelCount++; | |
| } | |
| return darkPixelCount > width * blackPercentage; | |
| } | |
| // Helper: Check if a specific column is mostly dark | |
| function isColDark(x) { | |
| let darkPixelCount = 0; | |
| for (let y = 0; y < height; y++) { | |
| if (isDark(x, y)) darkPixelCount++; | |
| } | |
| return darkPixelCount > height * blackPercentage; | |
| } | |
| let top, bottom, left, right; | |
| // Scan outward from the center, but if center is dark, scan from the edge | |
| const centerY = Math.floor(height / 2); | |
| const centerX = Math.floor(width / 2); | |
| if (!isDark(centerX, centerY)) { | |
| top = centerY; | |
| bottom = centerY; | |
| left = centerX; | |
| right = centerX; | |
| while (top > 0 && !isRowDark(top - 1)) top--; | |
| while (bottom < height - 1 && !isRowDark(bottom + 1)) bottom++; | |
| while (left > 0 && !isColDark(left - 1)) left--; | |
| while (right < width - 1 && !isColDark(right + 1)) right++; | |
| } else { | |
| top = 0; | |
| bottom = height - 1; | |
| left = 0; | |
| right = width - 1; | |
| while (top < height && isRowDark(top)) top++; | |
| while (bottom > top && isRowDark(bottom)) bottom--; | |
| while (left < width && isColDark(left)) left++; | |
| while (right > left && isColDark(right)) right--; | |
| } | |
| return { | |
| x: left, | |
| y: top, | |
| width: right - left + 1, | |
| height: bottom - top + 1, | |
| }; | |
| } | |
| const getNearestAspectRatio = (width, height, targetAspectRatios, threshold = 0.05) => { | |
| const aspectRatio = width / height; | |
| let bestRatio = null; | |
| let minDiff = Infinity; | |
| for (const targetRatio of targetAspectRatios) { | |
| const diff = Math.abs(aspectRatio - targetRatio); | |
| if (diff < minDiff) { | |
| minDiff = diff; | |
| bestRatio = targetRatio; | |
| } | |
| } | |
| if (minDiff <= threshold) { | |
| return bestRatio; | |
| } | |
| return null; | |
| }; | |
| const input = [ | |
| "Gv46fTDa4AIdIGY.jpg", | |
| "photo_2025-07-16_01-42-49.jpg", | |
| "photo_2025-07-16_01-59-53.jpg", | |
| "photo_2025-07-16_02-41-40.jpg", | |
| "photo_2025-07-16_01-59-51.jpg", | |
| "photo_2025-07-16_02-00-02.jpg", | |
| "photo_2025-07-16_01-42-55.jpg", | |
| "photo_2025-07-16_01-42-39.jpg", | |
| "photo_2025-07-16_01-42-18.jpg", | |
| "photo_2025-07-16_01-43-01.jpg", | |
| "photo_2025-07-16_01-59-31.jpg", | |
| "photo_2025-07-16_01-42-35.jpg", | |
| "photo_2025-07-16_01-42-41.jpg", | |
| "dark-bad.jpg", | |
| "photo_2025-07-16_01-43-01 - Copy.jpg", | |
| "distorted-good.jpg", | |
| "cropped-good.jpg", | |
| ]; | |
| const images = []; | |
| for (const image of input) { | |
| const img = new Image(); | |
| const item = { name: image, img, loaded: false }; | |
| images.push(item); | |
| img.onload = () => { | |
| item.loaded = true; | |
| runDetect(); | |
| }; | |
| img.src = "input/" + image; | |
| } | |
| const aspectRatioText = {}; | |
| aspectRatioText[4 / 3] = "4:3"; | |
| aspectRatioText[16 / 9] = "16:9"; | |
| aspectRatioText[21 / 9] = "21:9"; | |
| function runDetect() { | |
| document.querySelector("main").innerHTML = ""; | |
| for (const item of images) { | |
| if (!item.loaded) continue; | |
| const img = item.img; | |
| let x = 0; | |
| let y = 0; | |
| let width = img.naturalWidth; | |
| let height = img.naturalHeight; | |
| const aspectRatio = getNearestAspectRatio( | |
| img.naturalWidth, | |
| img.naturalHeight, | |
| [4 / 3, 16 / 9, 21 / 9], | |
| 0.05, | |
| ); | |
| const box = getVideoFrameRect( | |
| img, | |
| parseInt(document.getElementById("colorTolerance").value), | |
| parseInt(document.getElementById("blackPercentage").value) / 100, | |
| ); | |
| x = box.x; | |
| y = box.y; | |
| width = box.width; | |
| height = box.height; | |
| const detectedAspectRatio = getNearestAspectRatio( | |
| width, | |
| height, | |
| [4 / 3, 16 / 9, 21 / 9], | |
| 0.05, | |
| ); | |
| const canvas = document.createElement("canvas"); | |
| canvas.width = img.naturalWidth; | |
| canvas.height = img.naturalHeight; | |
| const ctx = canvas.getContext("2d"); | |
| ctx.drawImage(img, 0, 0); | |
| ctx.strokeStyle = "red"; | |
| ctx.lineWidth = 3; | |
| ctx.strokeRect(x, y, width, height); | |
| const div = document.createElement("div"); | |
| div.className = "item"; | |
| div.appendChild(canvas); | |
| div.appendChild( | |
| document.createTextNode( | |
| `(${aspectRatio ? aspectRatioText[aspectRatio] : "n/a"}) ${ | |
| img.naturalWidth | |
| }x${img.naturalHeight} => ${width}x${height} (${detectedAspectRatio ? aspectRatioText[detectedAspectRatio] : "n/a"})`, | |
| ), | |
| ); | |
| document.querySelector("main").appendChild(div); | |
| } | |
| } | |
| document.addEventListener("input", (e) => { | |
| runDetect(); | |
| }); | |
| document.addEventListener("DOMContentLoaded", () => { | |
| runDetect(); | |
| }); | |
| </script> |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment