image-js
Version:
Image processing and manipulation in JavaScript
144 lines (128 loc) • 4.08 kB
text/typescript
import assert from 'node:assert';
import type { Mask } from '../Mask.ts';
import type { Point } from '../utils/geometry/points.ts';
import {
getBitSafe,
getCoordsFromIndex,
} from './utils/getExternalContourUtils.ts';
interface Direction {
/**
* Horizontal direction change.
*/
dc: number;
/**
* Vertical direction change.
*/
dr: number;
}
/**
* Possible directions.
*/
const directions: Direction[] = [
{ dc: -1, dr: 0 }, // left
{ dc: -1, dr: -1 }, // up-left
{ dc: 0, dr: -1 }, // up
{ dc: 1, dr: -1 }, // up-right
{ dc: 1, dr: 0 }, // right
{ dc: 1, dr: 1 }, // down-right
{ dc: 0, dr: 1 }, // down
{ dc: -1, dr: 1 }, // down-left
];
// Lookup table for directions' index.
const directionIndexLookup = new Int8Array(9).fill(-1);
for (let i = 0; i < directions.length; i++) {
const { dc, dr } = directions[i];
directionIndexLookup[(dr + 1) * 3 + (dc + 1)] = i;
}
/**
* Return an array with the coordinates of the pixels that are on the border of the mask using Moore's tracing algorithm.
* The reference is the top-left corner of the ROI.
* @param mask - Mask to process.
* @returns The array of border pixels.
*/
export function getContourMoore(mask: Mask): Point[] {
let index = 0;
const contour: Point[] = [];
const visited = new Uint8Array(mask.width * mask.height);
while (mask.getBitByIndex(index) !== 1 && index !== mask.size) {
index++;
}
if (index === mask.size) return contour;
const [row, col] = getCoordsFromIndex(index, mask.width);
const startingPoint = { column: col, row };
let currentPoint = startingPoint;
visited[index] = 1;
contour.push(currentPoint);
const firstNext = findNextPoint(mask, currentPoint, { column: col - 1, row });
if (!firstNext) {
return contour;
}
let startingBacktrackPoint: Point = firstNext.prevPoint;
let backtrackPoint = firstNext.prevPoint;
currentPoint = firstNext.currPoint;
while (true) {
index = currentPoint.row * mask.width + currentPoint.column;
if (!visited[index]) {
visited[index] = 1;
contour.push(currentPoint);
}
const next = findNextPoint(mask, currentPoint, backtrackPoint);
assert.ok(next, 'Next border point is undefined.');
backtrackPoint = next.prevPoint;
currentPoint = next.currPoint;
if (
currentPoint.column === startingPoint.column &&
currentPoint.row === startingPoint.row
) {
if (
backtrackPoint.column === startingBacktrackPoint.column &&
backtrackPoint.row === startingBacktrackPoint.row
) {
break;
} else {
startingBacktrackPoint = backtrackPoint;
}
}
}
return contour;
}
/**
* Finds next point to trace.
* @param mask - Mask in check.
* @param current - Current border point.
* @param previous - Last non-border point to start search from.
* @returns Object with new border point and last non-border point.
*/
function findNextPoint(
mask: Mask,
current: Point,
previous: Point,
): { prevPoint: Point; currPoint: Point } | undefined {
let prevPoint = previous;
// Vector from current back to previous
const backDc = previous.column - current.column;
const backDr = previous.row - current.row;
const startIndex = directionIndexLookup[(backDr + 1) * 3 + (backDc + 1)];
// Scan clockwise starting from that index.
for (let i = 0; i < directions.length; i++) {
const { dc, dr } = directions.at(
(startIndex + i) % directions.length,
) as Direction;
const newCol = current.column + dc;
const newRow = current.row + dr;
if (getBitSafe(mask, newCol, newRow) === 1) {
// Stores found border point and last non-border point that
// was found. Will be used as a starting point in the next
// iteration.
return {
prevPoint,
currPoint: { column: newCol, row: newRow },
};
} else {
prevPoint = { column: newCol, row: newRow };
}
}
// Is triggered only if the point has no neighbours. So it is undefined
// only if mask is made of one pixel.
return undefined;
}