@vitavision/chess-corners
v0.8.0
Published
WebAssembly bindings for the ChESS corner detector
Readme
@vitavision/chess-corners
WebAssembly bindings for the ChESS corner detector. Detect chessboard corners with subpixel accuracy directly in the browser.
Previously published as
chess-corners-wasmon npm (≤ 0.6.x). The package was renamed to@vitavision/chess-cornersin 0.7.0; the legacy name is deprecated. Migrate by replacing your dependency name — the API is unchanged.
Installation
npm install @vitavision/chess-cornersBuilding from source
Requires wasm-pack:
wasm-pack build crates/chess-corners-wasm --target webThe npm-ready package is generated in crates/chess-corners-wasm/pkg/
under the @vitavision/chess-corners name (the published name is set
by the release workflow; locally wasm-pack derives it from the Rust
crate name chess-corners-wasm).
To target a bundler (Webpack, Vite, etc.) instead:
wasm-pack build crates/chess-corners-wasm --target bundlerUsage
Initialization
import init, { ChessDetector } from '@vitavision/chess-corners';
// Initialize the WASM module (required once before any API calls).
await init();Detect corners from an image file
const detector = new ChessDetector();
// Load an image onto a canvas to get pixel data.
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const img = new Image();
img.src = 'board.png';
await img.decode();
canvas.width = img.width;
canvas.height = img.height;
ctx.drawImage(img, 0, 0);
const imageData = ctx.getImageData(0, 0, img.width, img.height);
// detect_rgba accepts RGBA pixels from canvas and converts to grayscale internally.
const corners = detector.detect_rgba(imageData.data, img.width, img.height);
// corners is a Float32Array with stride 9 per corner:
// [x, y, response, contrast, fit_rms,
// axis0_angle, axis0_sigma, axis1_angle, axis1_sigma, ...]
for (let i = 0; i < corners.length; i += 9) {
const x = corners[i];
const y = corners[i + 1];
const response = corners[i + 2];
const contrast = corners[i + 3];
const axis0_angle = corners[i + 5]; // radians, in [0, PI)
const axis1_angle = corners[i + 7]; // radians, in (axis0, axis0 + PI)
console.log(`Corner at (${x.toFixed(2)}, ${y.toFixed(2)}), strength=${response.toFixed(1)}`);
}Webcam streaming
const detector = new ChessDetector();
detector.set_pyramid_levels(3); // enable multiscale for better detection
const video = document.querySelector('video');
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
function processFrame() {
canvas.width = video.videoWidth;
canvas.height = video.videoHeight;
ctx.drawImage(video, 0, 0);
const imageData = ctx.getImageData(0, 0, canvas.width, canvas.height);
// Reuses internal buffers across frames automatically.
const corners = detector.detect_rgba(imageData.data, canvas.width, canvas.height);
drawCorners(corners); // your rendering logic
requestAnimationFrame(processFrame);
}
processFrame();Response map visualization
const detector = new ChessDetector();
// Get the raw ChESS response as a Float32Array (row-major, width x height).
const response = detector.response_rgba(imageData.data, width, height);
const rWidth = detector.response_width();
const rHeight = detector.response_height();
// Render as a heatmap on a canvas.
const out = ctx.createImageData(rWidth, rHeight);
const maxVal = Math.max(...response);
for (let i = 0; i < response.length; i++) {
const v = Math.floor(255 * response[i] / maxVal);
out.data[4 * i] = v; // R
out.data[4 * i + 1] = 0; // G
out.data[4 * i + 2] = 255 - v; // B
out.data[4 * i + 3] = 255; // A
}
ctx.putImageData(out, 0, 0);Configuration
const detector = new ChessDetector();
// Or start with the multiscale preset:
// const detector = ChessDetector.multiscale();
// Threshold (fraction of max response, default 0.2).
detector.set_threshold(0.15);
// Non-maximum suppression radius (default 2).
detector.set_nms_radius(3);
// Broad mode uses the wider, more blur-tolerant detector sampling pattern.
detector.set_broad_mode(false);
// Minimum cluster size to accept a corner (default 2).
detector.set_min_cluster_size(2);
// Pyramid levels: 1 = single-scale, 3 = recommended multiscale.
detector.set_pyramid_levels(3);
// Minimum pyramid level size in pixels (default 128).
detector.set_pyramid_min_size(128);
// Subpixel refiner: "center_of_mass" (default), "forstner", or "saddle_point".
detector.set_refiner("forstner");Typed configuration (full surface)
For deeper tuning — refiner subconfig, Radon detector parameters,
descriptor mode, coarse-to-fine radii — construct a typed
ChessConfig and seed the detector with ChessDetector.withConfig.
Every public Rust facade field is reachable through the typed
classes and exposed with TypeScript types in the generated
.d.ts.
Nested config edits propagate naturally — getters hand back a wrapper backed by the same shared cell as the parent, so chained mutation works without a round-trip:
import init, {
ChessConfig,
ChessDetector,
DetectorMode,
PeakFitMode,
RefinementMethod,
} from '@vitavision/chess-corners';
await init();
const cfg = ChessConfig.multiscale();
cfg.detectorMode = DetectorMode.Radon;
cfg.thresholdValue = 0.15;
cfg.refiner.kind = RefinementMethod.RadonPeak;
cfg.refiner.forstner.maxOffset = 2.0;
cfg.radonDetector.rayRadius = 5;
cfg.radonDetector.imageUpsample = 2;
cfg.radonDetector.peakFit = PeakFitMode.Gaussian;
const detector = ChessDetector.withConfig(cfg);
// `getConfig()` returns a *snapshot* — its cells are independent of
// the detector's live state. Use `applyConfig()` to commit changes
// made on the snapshot.
const snapshot = detector.getConfig();
snapshot.nmsRadius = 4;
detector.applyConfig(snapshot);Setters that take a nested wrapper (e.g. cfg.refiner = newRefiner)
reseat cfg's shared cells to point at newRefiner's cells, so
future cfg.refiner.* calls observe newRefiner's state. JS code
that already held the previous cfg.refiner keeps observing the
previous cells — matching natural JS attribute-replacement semantics.
The legacy set_* shortcut methods continue to work and edit the
same underlying configuration, so the two styles can be mixed at
will.
API Reference
ChessDetector
| Method | Description |
|--------|-------------|
| new ChessDetector() | Create detector with default single-scale config |
| ChessDetector.multiscale() | Create detector with 3-level pyramid preset |
| detect(pixels, w, h) | Detect corners from grayscale Uint8Array |
| detect_rgba(pixels, w, h) | Detect corners from RGBA Uint8Array |
| response(pixels, w, h) | Compute response map from grayscale pixels |
| response_rgba(pixels, w, h) | Compute response map from RGBA pixels |
| response_width() | Width of the last computed response map |
| response_height() | Height of the last computed response map |
| set_threshold(rel) | Set relative threshold (0.0-1.0) |
| set_nms_radius(r) | Set NMS radius |
| set_broad_mode(v) | Toggle broad detector mode |
| set_min_cluster_size(v) | Set min cluster size |
| set_pyramid_levels(n) | Set pyramid depth |
| set_pyramid_min_size(v) | Set min pyramid level size |
| set_refiner(name) | Set subpixel refiner |
Output format
Corners (detect / detect_rgba): Float32Array with stride 9 per corner:
| Offset | Field | Description |
|--------|-------|-------------|
| i + 0 | x | Subpixel x coordinate |
| i + 1 | y | Subpixel y coordinate |
| i + 2 | response | ChESS response strength |
| i + 3 | contrast | Fitted bright/dark amplitude |
| i + 4 | fit_rms | RMS residual of the two-axis fit |
| i + 5 | axis0_angle | First grid axis, radians in [0, π) |
| i + 6 | axis0_sigma | 1σ uncertainty of axis0_angle |
| i + 7 | axis1_angle | Second grid axis, radians in (axis0, axis0 + π) |
| i + 8 | axis1_sigma | 1σ uncertainty of axis1_angle |
Rotating CCW from axis0_angle toward axis1_angle traverses a dark sector of the corner. The two grid axes are not assumed orthogonal, so the layout correctly captures projective warp.
Response map (response / response_rgba): Float32Array in row-major order, dimensions available via response_width() / response_height().
Binary size
~51 KB raw, ~23 KB gzipped (single-scale, no parallelism, no SIMD).
License
MIT
