image-js
Version:
Image processing and manipulation in JavaScript
240 lines (225 loc) • 6.39 kB
text/typescript
import type { Mask } from '../Mask.js';
import type { Point } from '../geometry/index.js';
import { toDegrees } from '../utils/geometry/angles.js';
import { rotate } from '../utils/geometry/points.js';
import type { Feret, FeretDiameter } from './maskAnalysis.types.js';
import { getAngle } from './utils/getAngle.js';
/**
* Computes the Feret diameters.
* @see {@link https://www.sympatec.com/en/particle-measurement/glossary/particle-shape/#}
* @see {@link http://portal.s2nano.org:8282/files/TEM_protocol_NANoREG.pdf}
* @param mask - The mask of the ROI.
* @returns The Feret diameters.
*/
export function getFeret(mask: Mask): Feret {
const hull = mask.getConvexHull();
const hullPoints = hull.points;
if (hull.surface === 0) {
return {
minDiameter: {
length: 0,
points: [
{ column: 0, row: 0 },
{ column: 0, row: 0 },
],
angle: 0,
calliperLines: [
[
{ column: 0, row: 0 },
{ column: 0, row: 0 },
],
[
{ column: 0, row: 0 },
{ column: 0, row: 0 },
],
],
},
maxDiameter: {
length: 0,
points: [
{ column: 0, row: 0 },
{ column: 0, row: 0 },
],
angle: 0,
calliperLines: [
[
{ column: 0, row: 0 },
{ column: 0, row: 0 },
],
[
{ column: 0, row: 0 },
{ column: 0, row: 0 },
],
],
},
aspectRatio: 1,
};
}
// Compute minimum diameter
let minWidth = Number.POSITIVE_INFINITY;
let minWidthAngle = 0;
let minLinePoints: Point[] = [];
let minLines: [[Point, Point], [Point, Point]] | undefined;
for (let i = 0; i < hullPoints.length; i++) {
const angle = getAngle(
hullPoints[i],
hullPoints[(i + 1) % hullPoints.length],
);
// We rotate so that it is parallel to X axis.
const rotatedPoints = rotate(-angle, hullPoints);
let currentWidth = 0;
let currentMinLinePoints: Point[] = [];
for (let j = 0; j < hullPoints.length; j++) {
const absWidth = Math.abs(rotatedPoints[i].row - rotatedPoints[j].row);
if (absWidth > currentWidth) {
currentWidth = absWidth;
currentMinLinePoints = [rotatedPoints[i], rotatedPoints[j]];
}
}
if (currentWidth < minWidth) {
minWidth = currentWidth;
minWidthAngle = angle;
minLinePoints = currentMinLinePoints;
const { minIndex: currentMin, maxIndex: currentMax } =
findPointIndexesOfExtremeColumns(rotatedPoints);
minLines = getMinLines(
minWidthAngle,
currentMin,
currentMax,
rotatedPoints,
minLinePoints,
);
}
}
const minDiameter: FeretDiameter = {
points: rotate(minWidthAngle, minLinePoints),
length: minWidth,
angle: toDegrees(minWidthAngle),
calliperLines: minLines as [[Point, Point], [Point, Point]],
};
// Compute maximum diameter
let maxLinePoints: Point[] = [];
let maxSquaredWidth = 0;
let maxLineIndex: number[] = [];
for (let i = 0; i < hullPoints.length - 1; i++) {
for (let j = i + 1; j < hullPoints.length; j++) {
const currentSquaredWidth =
(hullPoints[i].column - hullPoints[j].column) ** 2 +
(hullPoints[i].row - hullPoints[j].row) ** 2;
if (currentSquaredWidth > maxSquaredWidth) {
maxSquaredWidth = currentSquaredWidth;
maxLinePoints = [hullPoints[i], hullPoints[j]];
maxLineIndex = [i, j];
}
}
}
const maxAngle = getAngle(maxLinePoints[0], maxLinePoints[1]);
const rotatedMaxPoints = rotate(-maxAngle, hullPoints);
const { minIndex: currentMin, maxIndex: currentMax } =
findPointsIndexesOfExtremeRows(rotatedMaxPoints);
const maxLines = getMaxLines(
maxAngle,
currentMin,
currentMax,
rotatedMaxPoints,
maxLineIndex,
);
const maxDiameter = {
length: Math.sqrt(maxSquaredWidth),
angle: toDegrees(getAngle(maxLinePoints[0], maxLinePoints[1])),
points: maxLinePoints,
calliperLines: maxLines,
};
return {
minDiameter,
maxDiameter,
aspectRatio: minDiameter.length / maxDiameter.length,
};
}
function findPointIndexesOfExtremeColumns(points: Point[]): {
minIndex: number;
maxIndex: number;
} {
let maxIndex = 0;
let minIndex = 0;
for (let i = 0; i < points.length; i++) {
if (points[i].column > points[maxIndex].column) {
maxIndex = i;
}
if (points[i].column < points[minIndex].column) {
minIndex = i;
}
}
return { minIndex, maxIndex };
}
function findPointsIndexesOfExtremeRows(points: Point[]): {
minIndex: number;
maxIndex: number;
} {
let maxIndex = 0;
let minIndex = 0;
for (let i = 0; i < points.length; i++) {
if (points[i].row > points[maxIndex].row) {
maxIndex = i;
}
if (points[i].row < points[minIndex].row) {
minIndex = i;
}
}
return { minIndex, maxIndex };
}
function getMinLines(
angle: number,
min: number,
max: number,
rotatedPoints: Point[],
feretPoints: Point[],
): [[Point, Point], [Point, Point]] {
const minLine1: [Point, Point] = [
{ column: rotatedPoints[min].column, row: feretPoints[0].row },
{
column: rotatedPoints[max].column,
row: feretPoints[0].row,
},
];
const minLine2: [Point, Point] = [
{
column: rotatedPoints[min].column,
row: feretPoints[1].row,
},
{
column: rotatedPoints[max].column,
row: feretPoints[1].row,
},
];
return [rotate(angle, minLine1), rotate(angle, minLine2)] as [
[Point, Point],
[Point, Point],
];
}
function getMaxLines(
angle: number,
min: number,
max: number,
rotatedPoints: Point[],
index: number[],
): [[Point, Point], [Point, Point]] {
const maxLine1: [Point, Point] = [
{ column: rotatedPoints[index[0]].column, row: rotatedPoints[min].row },
{
column: rotatedPoints[index[0]].column,
row: rotatedPoints[max].row,
},
];
const maxLine2: [Point, Point] = [
{ column: rotatedPoints[index[1]].column, row: rotatedPoints[min].row },
{
column: rotatedPoints[index[1]].column,
row: rotatedPoints[max].row,
},
];
return [rotate(angle, maxLine1), rotate(angle, maxLine2)] as [
[Point, Point],
[Point, Point],
];
}