The canvas below draws a checkerboard pattern and runs the Harris detector on every pixel. Red dots mark the corners that scored above the threshold; the color of each pixel's background shows its raw corner response (brighter = stronger response).
<!-- Harris Corner Detection demo — checkerboard with overlay -->
<div class="controls">
<label>Threshold: <span id="threshVal">40</span>%
<input type="range" id="thresh" min="1" max="99" value="40">
</label>
<label>Cell size:
<select id="cellSz">
<option value="16">16 px</option>
<option value="24" selected>24 px</option>
<option value="32">32 px</option>
</select>
</label>
<button id="btnRun" type="button">Redraw</button>
</div>
<div class="canvas-wrap">
<canvas id="cvs"></canvas>
</div>
<div class="legend">
<span class="dot corner"></span> Corner
<span class="dot edge"></span> Edge / gradient
<span class="dot flat"></span> Flat
</div>
<div id="status" class="status"></div>
/* Styles for the Harris corner detection demo */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.controls { display: flex; gap: .7rem; flex-wrap: wrap; align-items: center; margin-bottom: .6rem; }
label { font-size: .88rem; display: flex; align-items: center; gap: .35rem; }
input[type=range] { width: 110px; cursor: pointer; }
select { font-size: .88rem; border: 1px solid #aaa; border-radius: 6px; padding: .15rem .3rem; }
button { font: 600 13px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
.canvas-wrap { overflow: auto; max-width: 100%; }
canvas { display: block; image-rendering: pixelated; border: 1px solid #ccc; border-radius: 4px; }
.legend { font-size: .82rem; margin-top: .5rem; display: flex; gap: .9rem; align-items: center; }
.dot { display: inline-block; width: 12px; height: 12px; border-radius: 50%; }
.dot.corner { background: #e63946; }
.dot.edge { background: #457b9d; }
.dot.flat { background: #ccc; }
.status { font-size: .88rem; font-weight: 600; margin-top: .4rem; min-height: 1.2em; color: #1d3557; }
/* Harris Corner Detection demo — structure tensor on a checkerboard */
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
const cvs = document.getElementById('cvs');
const ctx = cvs.getContext('2d');
const thresh = document.getElementById('thresh');
const threshVal = document.getElementById('threshVal');
const cellSzSel = document.getElementById('cellSz');
const statusEl = document.getElementById('status');
/* Draw a black-and-white checkerboard on the canvas */
function drawCheckerboard(cellPx, cols, rows) {
cvs.width = cols * cellPx;
cvs.height = rows * cellPx;
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
ctx.fillStyle = (r + c) % 2 === 0 ? '#ffffff' : '#222222';
ctx.fillRect(c * cellPx, r * cellPx, cellPx, cellPx);
}
}
}
/* Compute Sobel gradients Ix and Iy for every pixel */
function sobelGradients(img, W, H) {
const d = img.data;
const Ix = new Float32Array(W * H);
const Iy = new Float32Array(W * H);
const gray = new Float32Array(W * H);
/* Convert to grayscale using luminance weights */
for (let i = 0; i < W * H; i++) {
gray[i] = 0.299 * d[i * 4] + 0.587 * d[i * 4 + 1] + 0.114 * d[i * 4 + 2];
}
/* Sobel 3x3 kernel for horizontal and vertical gradients */
for (let y = 1; y < H - 1; y++) {
for (let x = 1; x < W - 1; x++) {
const idx = y * W + x;
Ix[idx] =
-gray[(y-1)*W+(x-1)] + gray[(y-1)*W+(x+1)]
- 2*gray[y*W+(x-1)] + 2*gray[y*W+(x+1)]
- gray[(y+1)*W+(x-1)] + gray[(y+1)*W+(x+1)];
Iy[idx] =
-gray[(y-1)*W+(x-1)] - 2*gray[(y-1)*W+x] - gray[(y-1)*W+(x+1)]
+ gray[(y+1)*W+(x-1)] + 2*gray[(y+1)*W+x] + gray[(y+1)*W+(x+1)];
}
}
return { Ix, Iy };
}
/* Compute Harris corner response R = det(M) - k * trace(M)^2 for each pixel */
function harrisResponse(Ix, Iy, W, H, winR, k) {
const R = new Float32Array(W * H);
for (let y = winR; y < H - winR; y++) {
for (let x = winR; x < W - winR; x++) {
let Sxx = 0, Sxy = 0, Syy = 0;
/* Accumulate the 2x2 structure tensor over the window */
for (let dy = -winR; dy <= winR; dy++) {
for (let dx = -winR; dx <= winR; dx++) {
const i = (y + dy) * W + (x + dx);
Sxx += Ix[i] * Ix[i];
Sxy += Ix[i] * Iy[i];
Syy += Iy[i] * Iy[i];
}
}
/* R = det - k * trace^2 (Harris 1988) */
const det = Sxx * Syy - Sxy * Sxy;
const trace = Sxx + Syy;
R[y * W + x] = det - k * trace * trace;
}
}
return R;
}
/* Main run function — draw checkerboard then compute and overlay Harris response */
function run() {
const cellPx = parseInt(cellSzSel.value, 10);
const cols = 10, rows = 10;
drawCheckerboard(cellPx, cols, rows);
const W = cvs.width, H = cvs.height;
const img = ctx.getImageData(0, 0, W, H);
const { Ix, Iy } = sobelGradients(img, W, H);
const R = harrisResponse(Ix, Iy, W, H, 2, 0.04);
/* Find the max positive response for normalization */
let maxR = 0;
for (let i = 0; i < R.length; i++) if (R[i] > maxR) maxR = R[i];
/* Absolute threshold from the slider percentage */
const threshFrac = parseInt(thresh.value, 10) / 100;
const threshAbs = threshFrac * maxR;
/* Paint the response overlay on top of the checkerboard */
const out = ctx.createImageData(W, H);
for (let i = 0; i < W * H; i++) {
const r = R[i];
if (r > threshAbs) {
/* Red for confirmed corners above threshold */
out.data[i*4] = 230;
out.data[i*4+1] = 57;
out.data[i*4+2] = 70;
out.data[i*4+3] = 200;
} else if (r > 0) {
/* Blue tint for edge-like positive responses below threshold */
const t = r / maxR;
out.data[i*4] = Math.round(69 + t * 60);
out.data[i*4+1] = Math.round(123 + t * 40);
out.data[i*4+2] = Math.round(157 + t * 30);
out.data[i*4+3] = Math.round(t * 160 + 20);
} else {
/* Transparent for flat or negative response */
out.data[i*4+3] = 0;
}
}
ctx.putImageData(out, 0, 0);
/* Count detected corner pixels */
let corners = 0;
for (let i = 0; i < R.length; i++) if (R[i] > threshAbs) corners++;
statusEl.textContent = i18nFmt('Detected %n% corner pixel(s) above threshold.', { n: corners });
}
thresh.addEventListener('input', () => {
threshVal.textContent = thresh.value;
run();
});
cellSzSel.addEventListener('change', run);
document.getElementById('btnRun').addEventListener('click', run);
run();
<!-- Harris Corner Detection demo — checkerboard with overlay -->
<div class="controls">
<label>Threshold: <span id="threshVal">40</span>%
<input type="range" id="thresh" min="1" max="99" value="40">
</label>
<label>Cell size:
<select id="cellSz">
<option value="16">16 px</option>
<option value="24" selected>24 px</option>
<option value="32">32 px</option>
</select>
</label>
<button id="btnRun" type="button">Redraw</button>
</div>
<div class="canvas-wrap">
<canvas id="cvs"></canvas>
</div>
<div class="legend">
<span class="dot corner"></span> Corner
<span class="dot edge"></span> Edge / gradient
<span class="dot flat"></span> Flat
</div>
<div id="status" class="status"></div>
/* Styles for the Harris corner detection demo */
* { box-sizing: border-box; }
body { font-family: system-ui, sans-serif; margin: 0; color: #222; }
.controls { display: flex; gap: .7rem; flex-wrap: wrap; align-items: center; margin-bottom: .6rem; }
label { font-size: .88rem; display: flex; align-items: center; gap: .35rem; }
input[type=range] { width: 110px; cursor: pointer; }
select { font-size: .88rem; border: 1px solid #aaa; border-radius: 6px; padding: .15rem .3rem; }
button { font: 600 13px system-ui; padding: .4rem .85rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 8px; cursor: pointer; }
.canvas-wrap { overflow: auto; max-width: 100%; }
canvas { display: block; image-rendering: pixelated; border: 1px solid #ccc; border-radius: 4px; }
.legend { font-size: .82rem; margin-top: .5rem; display: flex; gap: .9rem; align-items: center; }
.dot { display: inline-block; width: 12px; height: 12px; border-radius: 50%; }
.dot.corner { background: #e63946; }
.dot.edge { background: #457b9d; }
.dot.flat { background: #ccc; }
.status { font-size: .88rem; font-weight: 600; margin-top: .4rem; min-height: 1.2em; color: #1d3557; }
/* Harris Corner Detection demo — structure tensor on a checkerboard */
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
const cvs = document.getElementById('cvs');
const ctx = cvs.getContext('2d');
const thresh = document.getElementById('thresh');
const threshVal = document.getElementById('threshVal');
const cellSzSel = document.getElementById('cellSz');
const statusEl = document.getElementById('status');
/* Draw a black-and-white checkerboard on the canvas */
function drawCheckerboard(cellPx, cols, rows) {
cvs.width = cols * cellPx;
cvs.height = rows * cellPx;
for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
ctx.fillStyle = (r + c) % 2 === 0 ? '#ffffff' : '#222222';
ctx.fillRect(c * cellPx, r * cellPx, cellPx, cellPx);
}
}
}
/* Compute Sobel gradients Ix and Iy for every pixel */
function sobelGradients(img, W, H) {
const d = img.data;
const Ix = new Float32Array(W * H);
const Iy = new Float32Array(W * H);
const gray = new Float32Array(W * H);
/* Convert to grayscale using luminance weights */
for (let i = 0; i < W * H; i++) {
gray[i] = 0.299 * d[i * 4] + 0.587 * d[i * 4 + 1] + 0.114 * d[i * 4 + 2];
}
/* Sobel 3x3 kernel for horizontal and vertical gradients */
for (let y = 1; y < H - 1; y++) {
for (let x = 1; x < W - 1; x++) {
const idx = y * W + x;
Ix[idx] =
-gray[(y-1)*W+(x-1)] + gray[(y-1)*W+(x+1)]
- 2*gray[y*W+(x-1)] + 2*gray[y*W+(x+1)]
- gray[(y+1)*W+(x-1)] + gray[(y+1)*W+(x+1)];
Iy[idx] =
-gray[(y-1)*W+(x-1)] - 2*gray[(y-1)*W+x] - gray[(y-1)*W+(x+1)]
+ gray[(y+1)*W+(x-1)] + 2*gray[(y+1)*W+x] + gray[(y+1)*W+(x+1)];
}
}
return { Ix, Iy };
}
/* Compute Harris corner response R = det(M) - k * trace(M)^2 for each pixel */
function harrisResponse(Ix, Iy, W, H, winR, k) {
const R = new Float32Array(W * H);
for (let y = winR; y < H - winR; y++) {
for (let x = winR; x < W - winR; x++) {
let Sxx = 0, Sxy = 0, Syy = 0;
/* Accumulate the 2x2 structure tensor over the window */
for (let dy = -winR; dy <= winR; dy++) {
for (let dx = -winR; dx <= winR; dx++) {
const i = (y + dy) * W + (x + dx);
Sxx += Ix[i] * Ix[i];
Sxy += Ix[i] * Iy[i];
Syy += Iy[i] * Iy[i];
}
}
/* R = det - k * trace^2 (Harris 1988) */
const det = Sxx * Syy - Sxy * Sxy;
const trace = Sxx + Syy;
R[y * W + x] = det - k * trace * trace;
}
}
return R;
}
/* Main run function — draw checkerboard then compute and overlay Harris response */
function run() {
const cellPx = parseInt(cellSzSel.value, 10);
const cols = 10, rows = 10;
drawCheckerboard(cellPx, cols, rows);
const W = cvs.width, H = cvs.height;
const img = ctx.getImageData(0, 0, W, H);
const { Ix, Iy } = sobelGradients(img, W, H);
const R = harrisResponse(Ix, Iy, W, H, 2, 0.04);
/* Find the max positive response for normalization */
let maxR = 0;
for (let i = 0; i < R.length; i++) if (R[i] > maxR) maxR = R[i];
/* Absolute threshold from the slider percentage */
const threshFrac = parseInt(thresh.value, 10) / 100;
const threshAbs = threshFrac * maxR;
/* Paint the response overlay on top of the checkerboard */
const out = ctx.createImageData(W, H);
for (let i = 0; i < W * H; i++) {
const r = R[i];
if (r > threshAbs) {
/* Red for confirmed corners above threshold */
out.data[i*4] = 230;
out.data[i*4+1] = 57;
out.data[i*4+2] = 70;
out.data[i*4+3] = 200;
} else if (r > 0) {
/* Blue tint for edge-like positive responses below threshold */
const t = r / maxR;
out.data[i*4] = Math.round(69 + t * 60);
out.data[i*4+1] = Math.round(123 + t * 40);
out.data[i*4+2] = Math.round(157 + t * 30);
out.data[i*4+3] = Math.round(t * 160 + 20);
} else {
/* Transparent for flat or negative response */
out.data[i*4+3] = 0;
}
}
ctx.putImageData(out, 0, 0);
/* Count detected corner pixels */
let corners = 0;
for (let i = 0; i < R.length; i++) if (R[i] > threshAbs) corners++;
statusEl.textContent = i18nFmt('Detected %n% corner pixel(s) above threshold.', { n: corners });
}
thresh.addEventListener('input', () => {
threshVal.textContent = thresh.value;
run();
});
cellSzSel.addEventListener('change', run);
document.getElementById('btnRun').addEventListener('click', run);
run();
Drag the threshold slider to control how selective the detector is. Lower the threshold and more corners light up — including some that are barely distinguishable from edges. Raise it and only the sharpest corners survive. This trade-off between recall (finding everything) and precision (finding only true corners) is the same one that every feature detector faces.
Comments
Loading comments...