Draw on the canvas below — any stroke, letter, or shape. The grid overlaid on the canvas is the HOG cell grid. Inside each cell the demo computes the gradient at every pixel, bins the angle into one of 9 orientation buckets, and draws a small oriented line proportional to the total weight in that bucket.
<!-- HOG Descriptor demo — draw on canvas, see gradient cell arrows -->
<p class="hint">Draw anything on the canvas. Each grid cell shows oriented lines (HOG arrows) weighted by the gradient magnitude inside it — stronger edges produce longer arrows.</p>
<div class="controls">
<label for="brushSize">Brush size:</label>
<input type="range" id="brushSize" min="4" max="24" value="10" title="Adjust brush size">
<button id="clearBtn" type="button">Clear</button>
<button id="showGrad" type="button">Hide HOG</button>
</div>
<div class="canvas-wrap">
<canvas id="drawCanvas" width="288" height="288" aria-label="Drawing canvas — draw here to create edges"></canvas>
<canvas id="hogCanvas" width="288" height="288" aria-label="HOG overlay — shows gradient orientation arrows per cell"></canvas>
</div>
<p class="status" id="status">Draw on the canvas to see HOG cells fill with gradient arrows.</p>
/* Layout for the HOG descriptor demo */
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; color: #222; }
.hint { font-size: .9rem; color: #444; margin-bottom: .6rem; line-height: 1.45; }
.controls { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; margin-bottom: .5rem; }
.controls label { font-size: .85rem; color: #555; white-space: nowrap; }
.controls input[type=range] { width: 80px; cursor: pointer; }
button { font: 600 13px system-ui; padding: .4rem .8rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 7px; cursor: pointer; }
button#showGrad { background: #fff; color: #1d3557; }
button#showGrad.active { background: #1d3557; color: #fff; }
.canvas-wrap { position: relative; display: inline-block; border: 1px solid #cdd9e3; border-radius: 6px; overflow: hidden; }
canvas { display: block; }
#hogCanvas { position: absolute; top: 0; left: 0; pointer-events: none; }
.status { font-size: .9rem; color: #555; margin-top: .5rem; min-height: 1.3em; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// HOG parameters
const CELL = 32; // pixels per cell (cell size)
const BINS = 9; // number of unsigned orientation bins (0–180 degrees)
const BIN_ANGLE = 180 / BINS; // degrees per bin
const drawCanvas = document.getElementById('drawCanvas');
const hogCanvas = document.getElementById('hogCanvas');
const dCtx = drawCanvas.getContext('2d');
const hCtx = hogCanvas.getContext('2d');
const W = drawCanvas.width;
const H = drawCanvas.height;
const COLS = W / CELL;
const ROWS = H / CELL;
// Fill canvas with a light background
dCtx.fillStyle = '#f5f7fa';
dCtx.fillRect(0, 0, W, H);
let drawing = false;
let showHog = true;
function getBrush() { return parseInt(document.getElementById('brushSize').value, 10); }
// Drawing event handlers — track mouse and touch
function getPos(e) {
const r = drawCanvas.getBoundingClientRect();
const src = e.touches ? e.touches[0] : e;
return { x: src.clientX - r.left, y: src.clientY - r.top };
}
function startDraw(e) { drawing = true; const p = getPos(e); dCtx.beginPath(); dCtx.moveTo(p.x, p.y); e.preventDefault(); }
function moveDraw(e) {
if (!drawing) return;
const p = getPos(e);
dCtx.lineWidth = getBrush();
dCtx.lineCap = 'round';
dCtx.strokeStyle = '#1d2a38';
dCtx.lineTo(p.x, p.y);
dCtx.stroke();
dCtx.beginPath();
dCtx.moveTo(p.x, p.y);
if (showHog) computeHog();
e.preventDefault();
}
function endDraw() { drawing = false; if (showHog) computeHog(); }
drawCanvas.addEventListener('mousedown', startDraw);
drawCanvas.addEventListener('mousemove', moveDraw);
drawCanvas.addEventListener('mouseup', endDraw);
drawCanvas.addEventListener('mouseleave',endDraw);
drawCanvas.addEventListener('touchstart', startDraw, { passive: false });
drawCanvas.addEventListener('touchmove', moveDraw, { passive: false });
drawCanvas.addEventListener('touchend', endDraw);
// Clear button resets canvas and overlay
document.getElementById('clearBtn').addEventListener('click', () => {
dCtx.fillStyle = '#f5f7fa';
dCtx.fillRect(0, 0, W, H);
hCtx.clearRect(0, 0, W, H);
document.getElementById('status').textContent = 'Draw on the canvas to see HOG cells fill with gradient arrows.';
});
// Toggle the HOG overlay on or off
const showBtn = document.getElementById('showGrad');
showBtn.addEventListener('click', () => {
showHog = !showHog;
showBtn.classList.toggle('active', showHog);
showBtn.textContent = showHog ? 'Hide HOG' : 'Show HOG';
if (showHog) computeHog(); else hCtx.clearRect(0, 0, W, H);
});
// Compute and draw HOG overlay for the entire canvas
function computeHog() {
const img = dCtx.getImageData(0, 0, W, H).data;
// Convert an (x,y) pixel to grayscale brightness
function gray(x, y) {
const i = (y * W + x) * 4;
return 0.299 * img[i] + 0.587 * img[i+1] + 0.114 * img[i+2];
}
// Compute gradient magnitude and unsigned orientation at (x,y)
function grad(x, y) {
const gx = (x > 0 && x < W-1) ? gray(x+1, y) - gray(x-1, y) : 0;
const gy = (y > 0 && y < H-1) ? gray(x, y+1) - gray(x, y-1) : 0;
const mag = Math.sqrt(gx*gx + gy*gy);
let ang = (Math.atan2(Math.abs(gy), gx) * 180 / Math.PI) % 180;
if (ang < 0) ang += 180;
return { mag, ang };
}
hCtx.clearRect(0, 0, W, H);
let totalMag = 0;
let activeCells = 0;
// Loop over each HOG cell in the grid
for (let row = 0; row < ROWS; row++) {
for (let col = 0; col < COLS; col++) {
const hist = new Float32Array(BINS);
for (let dy = 0; dy < CELL; dy++) {
for (let dx = 0; dx < CELL; dx++) {
const px = col * CELL + dx;
const py = row * CELL + dy;
const { mag, ang } = grad(px, py);
// Accumulate gradient magnitude into the nearest orientation bin
const bin = Math.floor(ang / BIN_ANGLE) % BINS;
hist[bin] += mag;
}
}
// Find the peak bin to normalize arrow lengths
const maxH = Math.max(...hist);
totalMag += maxH;
if (maxH < 8) continue;
activeCells++;
const cx = col * CELL + CELL / 2;
const cy = row * CELL + CELL / 2;
const scale = (CELL * 0.45) / maxH;
// Draw one oriented line per bin, scaled by its weight
for (let b = 0; b < BINS; b++) {
if (hist[b] < 2) continue;
const angle = (b * BIN_ANGLE + BIN_ANGLE / 2) * Math.PI / 180;
const len = hist[b] * scale;
const dx = Math.cos(angle) * len;
const dy = Math.sin(angle) * len;
const alpha = Math.min(1, hist[b] / maxH);
hCtx.strokeStyle = `rgba(220,50,50,${alpha.toFixed(2)})`;
hCtx.lineWidth = 1.8;
hCtx.lineCap = 'round';
hCtx.beginPath();
hCtx.moveTo(cx - dx, cy - dy);
hCtx.lineTo(cx + dx, cy + dy);
hCtx.stroke();
}
}
}
// Draw the cell grid overlay in light blue
hCtx.strokeStyle = 'rgba(100,130,160,0.25)';
hCtx.lineWidth = 0.8;
for (let r = 0; r <= ROWS; r++) { hCtx.beginPath(); hCtx.moveTo(0, r*CELL); hCtx.lineTo(W, r*CELL); hCtx.stroke(); }
for (let c = 0; c <= COLS; c++) { hCtx.beginPath(); hCtx.moveTo(c*CELL, 0); hCtx.lineTo(c*CELL, H); hCtx.stroke(); }
const statusEl = document.getElementById('status');
if (activeCells === 0) {
statusEl.textContent = 'Draw on the canvas to see HOG cells fill with gradient arrows.';
} else {
statusEl.textContent = i18nFmt('%n% active cells with gradient signal detected.', { n: activeCells });
}
}
// Initial HOG pass on blank canvas (no arrows visible until drawing)
computeHog();
<!-- HOG Descriptor demo — draw on canvas, see gradient cell arrows -->
<p class="hint">Draw anything on the canvas. Each grid cell shows oriented lines (HOG arrows) weighted by the gradient magnitude inside it — stronger edges produce longer arrows.</p>
<div class="controls">
<label for="brushSize">Brush size:</label>
<input type="range" id="brushSize" min="4" max="24" value="10" title="Adjust brush size">
<button id="clearBtn" type="button">Clear</button>
<button id="showGrad" type="button">Hide HOG</button>
</div>
<div class="canvas-wrap">
<canvas id="drawCanvas" width="288" height="288" aria-label="Drawing canvas — draw here to create edges"></canvas>
<canvas id="hogCanvas" width="288" height="288" aria-label="HOG overlay — shows gradient orientation arrows per cell"></canvas>
</div>
<p class="status" id="status">Draw on the canvas to see HOG cells fill with gradient arrows.</p>
/* Layout for the HOG descriptor demo */
* { box-sizing: border-box; margin: 0; padding: 0; }
body { font-family: system-ui, sans-serif; color: #222; }
.hint { font-size: .9rem; color: #444; margin-bottom: .6rem; line-height: 1.45; }
.controls { display: flex; align-items: center; gap: .6rem; flex-wrap: wrap; margin-bottom: .5rem; }
.controls label { font-size: .85rem; color: #555; white-space: nowrap; }
.controls input[type=range] { width: 80px; cursor: pointer; }
button { font: 600 13px system-ui; padding: .4rem .8rem; border: 1px solid #1d3557;
background: #1d3557; color: #fff; border-radius: 7px; cursor: pointer; }
button#showGrad { background: #fff; color: #1d3557; }
button#showGrad.active { background: #1d3557; color: #fff; }
.canvas-wrap { position: relative; display: inline-block; border: 1px solid #cdd9e3; border-radius: 6px; overflow: hidden; }
canvas { display: block; }
#hogCanvas { position: absolute; top: 0; left: 0; pointer-events: none; }
.status { font-size: .9rem; color: #555; margin-top: .5rem; min-height: 1.3em; }
const i18nFmt = (s, v) => s.replace(/%(\w+)%/g, (_, k) => String(v[k] ?? ''));
// HOG parameters
const CELL = 32; // pixels per cell (cell size)
const BINS = 9; // number of unsigned orientation bins (0–180 degrees)
const BIN_ANGLE = 180 / BINS; // degrees per bin
const drawCanvas = document.getElementById('drawCanvas');
const hogCanvas = document.getElementById('hogCanvas');
const dCtx = drawCanvas.getContext('2d');
const hCtx = hogCanvas.getContext('2d');
const W = drawCanvas.width;
const H = drawCanvas.height;
const COLS = W / CELL;
const ROWS = H / CELL;
// Fill canvas with a light background
dCtx.fillStyle = '#f5f7fa';
dCtx.fillRect(0, 0, W, H);
let drawing = false;
let showHog = true;
function getBrush() { return parseInt(document.getElementById('brushSize').value, 10); }
// Drawing event handlers — track mouse and touch
function getPos(e) {
const r = drawCanvas.getBoundingClientRect();
const src = e.touches ? e.touches[0] : e;
return { x: src.clientX - r.left, y: src.clientY - r.top };
}
function startDraw(e) { drawing = true; const p = getPos(e); dCtx.beginPath(); dCtx.moveTo(p.x, p.y); e.preventDefault(); }
function moveDraw(e) {
if (!drawing) return;
const p = getPos(e);
dCtx.lineWidth = getBrush();
dCtx.lineCap = 'round';
dCtx.strokeStyle = '#1d2a38';
dCtx.lineTo(p.x, p.y);
dCtx.stroke();
dCtx.beginPath();
dCtx.moveTo(p.x, p.y);
if (showHog) computeHog();
e.preventDefault();
}
function endDraw() { drawing = false; if (showHog) computeHog(); }
drawCanvas.addEventListener('mousedown', startDraw);
drawCanvas.addEventListener('mousemove', moveDraw);
drawCanvas.addEventListener('mouseup', endDraw);
drawCanvas.addEventListener('mouseleave',endDraw);
drawCanvas.addEventListener('touchstart', startDraw, { passive: false });
drawCanvas.addEventListener('touchmove', moveDraw, { passive: false });
drawCanvas.addEventListener('touchend', endDraw);
// Clear button resets canvas and overlay
document.getElementById('clearBtn').addEventListener('click', () => {
dCtx.fillStyle = '#f5f7fa';
dCtx.fillRect(0, 0, W, H);
hCtx.clearRect(0, 0, W, H);
document.getElementById('status').textContent = 'Draw on the canvas to see HOG cells fill with gradient arrows.';
});
// Toggle the HOG overlay on or off
const showBtn = document.getElementById('showGrad');
showBtn.addEventListener('click', () => {
showHog = !showHog;
showBtn.classList.toggle('active', showHog);
showBtn.textContent = showHog ? 'Hide HOG' : 'Show HOG';
if (showHog) computeHog(); else hCtx.clearRect(0, 0, W, H);
});
// Compute and draw HOG overlay for the entire canvas
function computeHog() {
const img = dCtx.getImageData(0, 0, W, H).data;
// Convert an (x,y) pixel to grayscale brightness
function gray(x, y) {
const i = (y * W + x) * 4;
return 0.299 * img[i] + 0.587 * img[i+1] + 0.114 * img[i+2];
}
// Compute gradient magnitude and unsigned orientation at (x,y)
function grad(x, y) {
const gx = (x > 0 && x < W-1) ? gray(x+1, y) - gray(x-1, y) : 0;
const gy = (y > 0 && y < H-1) ? gray(x, y+1) - gray(x, y-1) : 0;
const mag = Math.sqrt(gx*gx + gy*gy);
let ang = (Math.atan2(Math.abs(gy), gx) * 180 / Math.PI) % 180;
if (ang < 0) ang += 180;
return { mag, ang };
}
hCtx.clearRect(0, 0, W, H);
let totalMag = 0;
let activeCells = 0;
// Loop over each HOG cell in the grid
for (let row = 0; row < ROWS; row++) {
for (let col = 0; col < COLS; col++) {
const hist = new Float32Array(BINS);
for (let dy = 0; dy < CELL; dy++) {
for (let dx = 0; dx < CELL; dx++) {
const px = col * CELL + dx;
const py = row * CELL + dy;
const { mag, ang } = grad(px, py);
// Accumulate gradient magnitude into the nearest orientation bin
const bin = Math.floor(ang / BIN_ANGLE) % BINS;
hist[bin] += mag;
}
}
// Find the peak bin to normalize arrow lengths
const maxH = Math.max(...hist);
totalMag += maxH;
if (maxH < 8) continue;
activeCells++;
const cx = col * CELL + CELL / 2;
const cy = row * CELL + CELL / 2;
const scale = (CELL * 0.45) / maxH;
// Draw one oriented line per bin, scaled by its weight
for (let b = 0; b < BINS; b++) {
if (hist[b] < 2) continue;
const angle = (b * BIN_ANGLE + BIN_ANGLE / 2) * Math.PI / 180;
const len = hist[b] * scale;
const dx = Math.cos(angle) * len;
const dy = Math.sin(angle) * len;
const alpha = Math.min(1, hist[b] / maxH);
hCtx.strokeStyle = `rgba(220,50,50,${alpha.toFixed(2)})`;
hCtx.lineWidth = 1.8;
hCtx.lineCap = 'round';
hCtx.beginPath();
hCtx.moveTo(cx - dx, cy - dy);
hCtx.lineTo(cx + dx, cy + dy);
hCtx.stroke();
}
}
}
// Draw the cell grid overlay in light blue
hCtx.strokeStyle = 'rgba(100,130,160,0.25)';
hCtx.lineWidth = 0.8;
for (let r = 0; r <= ROWS; r++) { hCtx.beginPath(); hCtx.moveTo(0, r*CELL); hCtx.lineTo(W, r*CELL); hCtx.stroke(); }
for (let c = 0; c <= COLS; c++) { hCtx.beginPath(); hCtx.moveTo(c*CELL, 0); hCtx.lineTo(c*CELL, H); hCtx.stroke(); }
const statusEl = document.getElementById('status');
if (activeCells === 0) {
statusEl.textContent = 'Draw on the canvas to see HOG cells fill with gradient arrows.';
} else {
statusEl.textContent = i18nFmt('%n% active cells with gradient signal detected.', { n: activeCells });
}
}
// Initial HOG pass on blank canvas (no arrows visible until drawing)
computeHog();
Notice how straight lines produce a single strong arrow aligned with the edge, while curves fill multiple buckets. Blank regions produce no arrows at all. That is exactly what HOG sends to a classifier: not pixels, but a compact summary of which directions have edges and how strongly.
Comments
Loading comments...