image-js
Version:
Image processing and manipulation in JavaScript
165 lines (149 loc) • 4.94 kB
text/typescript
import { match } from 'ts-pattern';
import type { Image } from '../../Image.js';
import type { Point } from '../../geometry/index.js';
import {
getCirclePoints,
getCompassPoints,
} from '../../utils/geometry/getCirclePoints.js';
import { getIndex } from '../../utils/getIndex.js';
import { surroundingPixels } from '../../utils/surroundingPixels.js';
import checkProcessable from '../../utils/validators/checkProcessable.js';
import type { GetHarrisScoreOptions } from '../featureMatching.types.js';
import { getFastScore } from './getFastScore.js';
import { getHarrisScore } from './getHarrisScore.js';
import type { GetShiTomasiScoreOptions } from './getShiTomasiScore.ts';
import { getShiTomasiScore } from './getShiTomasiScore.ts';
import type { IsFastKeypointOptions } from './isFastKeypoint.js';
import { isFastKeypoint } from './isFastKeypoint.js';
export interface GetFastKeypointsOptions extends IsFastKeypointOptions {
/**
* Maximum number of features to return.
* @default `500`
*/
maxNbFeatures?: number;
/**
* Whether to apply non-max suppression to the keypoints.
* This removes all keypoints which
* don't have the highest value within the adjacent keypoints.
* @default `true`
*/
nonMaxSuppression?: boolean;
/**
* Radius of the circle used for the algorithm.
* @default `3`
*/
fastRadius?: number;
/**
* Algorithm to use to compute corners score.
* @default `'FAST'`
*/
scoreAlgorithm?: 'HARRIS' | 'FAST' | 'TOMASI';
/**
* Options for the Harris score computation.
*/
scoreOptions?: GetHarrisScoreOptions | GetShiTomasiScoreOptions;
}
export interface FastKeypoint {
/**
* Location of the keypoint in the image.
*/
origin: Point;
/**
* Score of the keypoint, the bigger it is, the better the feature.
* It is the criteria used for the non-maximal suppression.
*/
score: number;
}
/**
* Find the features in a GREY image according to the FAST (Features from Accelerated Segment Test) algorithm.
* Based on the paper Machine Learning for High-Speed Corner Detection.
* DOI: https://doi.org/10.1007/11744023_34.
* @param image - The image to process.
* @param options - Get FAST keypoints options.
* @returns The FAST keypoints.
*/
export function getFastKeypoints(
image: Image,
options: GetFastKeypointsOptions = {},
): FastKeypoint[] {
const { fastRadius = 3, scoreAlgorithm = 'FAST', scoreOptions } = options;
const circlePoints = getCirclePoints(fastRadius);
const compassPoints = getCompassPoints(fastRadius);
const {
maxNbFeatures = 500,
nbContiguousPixels = (3 / 4) * circlePoints.length,
threshold = 20,
nonMaxSuppression = true,
} = options;
checkProcessable(image, {
channels: [1],
alpha: false,
});
function harrisScore(image: Image, corner: Point) {
return getHarrisScore(image, corner, scoreOptions as GetHarrisScoreOptions);
}
function fastScore(image: Image, corner: Point) {
return getFastScore(image, corner, threshold, circlePoints);
}
function tomasiScore(image: Image, corner: Point) {
return getShiTomasiScore(
image,
corner,
scoreOptions as GetShiTomasiScoreOptions,
);
}
const getScore = match(scoreAlgorithm)
.with('HARRIS', () => harrisScore)
.with('FAST', () => fastScore)
.with('TOMASI', () => tomasiScore)
.exhaustive();
const allKeypoints: FastKeypoint[] = [];
const scoreArray = new Float64Array(image.size).fill(
Number.NEGATIVE_INFINITY,
);
for (let row = 0; row < image.height; row++) {
for (let column = 0; column < image.width; column++) {
const corner = { row, column };
if (
isFastKeypoint(corner, image, circlePoints, compassPoints, {
nbContiguousPixels,
threshold,
})
) {
const score = getScore(image, corner);
scoreArray[getIndex(corner.column, corner.row, image, 0)] = score;
allKeypoints.push({ origin: corner, score });
}
}
}
let keypoints: FastKeypoint[] = [];
if (!nonMaxSuppression) {
keypoints = allKeypoints;
} else {
// Non-Maximal Suppression
for (const keypoint of allKeypoints) {
const currentScore =
scoreArray[
getIndex(keypoint.origin.column, keypoint.origin.row, image, 0)
];
for (let i = 0; i < surroundingPixels.length; i++) {
const neighbour = surroundingPixels[i];
const neighbourScore =
scoreArray[
getIndex(
keypoint.origin.column + neighbour.column,
keypoint.origin.row + neighbour.row,
image,
0,
)
];
if (neighbourScore > currentScore) break;
if (i === surroundingPixels.length - 1) {
keypoints.push(keypoint);
}
}
}
}
keypoints.sort((a, b) => b.score - a.score);
return keypoints.slice(0, maxNbFeatures);
}