All files / qr-scanner/src/detect sample.ts

100% Statements 31/31
100% Branches 25/25
100% Functions 1/1
100% Lines 27/27

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78                                                                  468x 468x 468x   468x 29552x 2902956x 2902956x   29552x   29552x 2902908x 2902908x 2902908x 2902908x 2902908x   2902906x 2899974x 2899974x                   2932x 2902908x 2902908x 2902908x 2902908x 2902908x 2902908x 2902908x 2902908x       466x    
import { BitMatrix } from "../image/bitmatrix";
import type { SamplingMode } from "../types";
import type { PerspectiveTransform } from "./perspective";
 
/**
 * Reading the module grid out of a binarized image through the perspective
 * transform.
 *
 * Each module is sampled at its centre — `(column + 0.5, row + 0.5)` in module
 * coordinates — which is the point furthest from every neighbour and therefore
 * the most forgiving of a slightly wrong transform.
 */
 
/**
 * Sample a `dimension × dimension` module grid.
 *
 * @param bits - The binarized image.
 * @param transform - Module coordinates → image pixels.
 * @param dimension - Modules per side.
 * @param mode - `center` reads one pixel per module. `majority5` reads the
 *   centre plus four neighbours a third of a module out and takes the majority,
 *   which rescues frames where a module is barely two pixels wide and a single
 *   sample can land on a compression artefact.
 * @returns The grid, or `null` when the transform maps any module outside the
 *   image — a symbol running off the edge of the frame cannot be read, and
 *   silently sampling the border would produce a plausible wrong grid.
 */
export function sampleGrid(
  bits: BitMatrix,
  transform: PerspectiveTransform,
  dimension: number,
  mode: SamplingMode = "center",
): BitMatrix | null {
  const grid = BitMatrix.square(dimension);
  const row = new Float64Array(dimension * 2);
  const offsets = mode === "majority5" ? [0, -1 / 3, 1 / 3] : null;
 
  for (let y = 0; y < dimension; y++) {
    for (let x = 0; x < dimension; x++) {
      row[x * 2] = x + 0.5;
      row[x * 2 + 1] = y + 0.5;
    }
    transform.mapInto(row);
 
    for (let x = 0; x < dimension; x++) {
      const px = row[x * 2]!;
      const py = row[x * 2 + 1]!;
      const ix = Math.floor(px);
      const iy = Math.floor(py);
      if (ix < 0 || iy < 0 || ix >= bits.width || iy >= bits.height) return null;
 
      if (!offsets) {
        grid.data[y * dimension + x] = bits.data[iy * bits.width + ix]!;
        continue;
      }
 
      // Five taps: the centre and one a third of a module out along each axis.
      // The module-space offset is converted to pixels from the transform's own
      // step to the neighbouring module — measured as a **distance**, not as an
      // x-difference: for a symbol rotated near 90° the x-step collapses to
      // zero and all five taps would land inside a single pixel, quietly
      // turning majority sampling back into centre sampling exactly when the
      // frame needed it most.
      const neighbour = x + 1 < dimension ? (x + 1) * 2 : (x - 1) * 2;
      const step = Math.hypot(row[neighbour]! - px, row[neighbour + 1]! - py);
      const delta = Math.max(1, Math.round(step / 3));
      let dark = bits.getSafe(ix, iy) ? 1 : 0;
      dark += bits.getSafe(ix - delta, iy) ? 1 : 0;
      dark += bits.getSafe(ix + delta, iy) ? 1 : 0;
      dark += bits.getSafe(ix, iy - delta) ? 1 : 0;
      dark += bits.getSafe(ix, iy + delta) ? 1 : 0;
      grid.data[y * dimension + x] = dark >= 3 ? 1 : 0;
    }
  }
 
  return grid;
}