image-js 1.6.2 → 1.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/image-js.esm.js +8322 -7458
- package/dist/image-js.esm.js.map +1 -1
- package/dist/image-js.esm.min.js +2 -2
- package/dist/image-js.esm.min.js.map +1 -1
- package/dist/image-js.umd.js +8322 -7457
- package/dist/image-js.umd.js.map +1 -1
- package/dist/image-js.umd.min.js +2 -2
- package/dist/image-js.umd.min.js.map +1 -1
- package/dist-types/image-js.d.ts +40 -3
- package/lib/featureMatching/keypoints/getEigenvaluesForScore.d.ts +2 -2
- package/lib/featureMatching/keypoints/getEigenvaluesForScore.d.ts.map +1 -1
- package/lib/featureMatching/keypoints/getEigenvaluesForScore.js +27 -22
- package/lib/featureMatching/keypoints/getEigenvaluesForScore.js.map +1 -1
- package/lib/featureMatching/keypoints/getFastKeypoints.d.ts +4 -0
- package/lib/featureMatching/keypoints/getFastKeypoints.d.ts.map +1 -1
- package/lib/featureMatching/keypoints/getFastKeypoints.js +20 -5
- package/lib/featureMatching/keypoints/getFastKeypoints.js.map +1 -1
- package/lib/featureMatching/keypoints/getHarrisScore.js +1 -1
- package/lib/featureMatching/keypoints/getHarrisScore.js.map +1 -1
- package/lib/featureMatching/keypoints/getShiTomasiScore.d.ts +1 -2
- package/lib/featureMatching/keypoints/getShiTomasiScore.d.ts.map +1 -1
- package/lib/featureMatching/keypoints/getShiTomasiScore.js +1 -1
- package/lib/featureMatching/keypoints/getShiTomasiScore.js.map +1 -1
- package/lib/featureMatching/visualize/drawKeypoints.d.ts +2 -1
- package/lib/featureMatching/visualize/drawKeypoints.d.ts.map +1 -1
- package/lib/featureMatching/visualize/drawKeypoints.js +13 -0
- package/lib/featureMatching/visualize/drawKeypoints.js.map +1 -1
- package/lib/maskAnalysis/getContourMoore.d.ts.map +1 -1
- package/lib/maskAnalysis/getContourMoore.js +4 -2
- package/lib/maskAnalysis/getContourMoore.js.map +1 -1
- package/lib/maskAnalysis/getConvexityDefects.d.ts +17 -0
- package/lib/maskAnalysis/getConvexityDefects.d.ts.map +1 -0
- package/lib/maskAnalysis/getConvexityDefects.js +75 -0
- package/lib/maskAnalysis/getConvexityDefects.js.map +1 -0
- package/lib/maskAnalysis/getExternalContour.d.ts +1 -1
- package/lib/maskAnalysis/getExternalContour.d.ts.map +1 -1
- package/lib/maskAnalysis/index.d.ts +1 -0
- package/lib/maskAnalysis/index.d.ts.map +1 -1
- package/lib/maskAnalysis/index.js +1 -0
- package/lib/maskAnalysis/index.js.map +1 -1
- package/lib/roi/getExternalContour.d.ts +1 -1
- package/lib/roi/getExternalContour.d.ts.map +1 -1
- package/package.json +2 -2
- package/src/featureMatching/keypoints/getEigenvaluesForScore.ts +29 -27
- package/src/featureMatching/keypoints/getFastKeypoints.ts +23 -5
- package/src/featureMatching/keypoints/getHarrisScore.ts +1 -1
- package/src/featureMatching/keypoints/getShiTomasiScore.ts +2 -3
- package/src/featureMatching/visualize/drawKeypoints.ts +20 -3
- package/src/maskAnalysis/getContourMoore.ts +4 -3
- package/src/maskAnalysis/getConvexityDefects.ts +105 -0
- package/src/maskAnalysis/index.ts +1 -0
|
@@ -1,51 +1,53 @@
|
|
|
1
|
-
import { EigenvalueDecomposition, Matrix
|
|
1
|
+
import { EigenvalueDecomposition, Matrix } from 'ml-matrix';
|
|
2
2
|
|
|
3
3
|
import type { Image } from '../../Image.ts';
|
|
4
|
+
import { rawDirectConvolution } from '../../filters/convolution.ts';
|
|
4
5
|
import type { Point } from '../../index_full.ts';
|
|
5
6
|
import { SOBEL_X, SOBEL_Y } from '../../utils/constants/kernels.js';
|
|
6
|
-
|
|
7
7
|
/**
|
|
8
8
|
* A function that calculates eigenvalues to calculate feature score for Harris and Shi-Tomasi algorithms.
|
|
9
9
|
* @param image - Image take data from.
|
|
10
10
|
* @param origin - Center of the window, where the corner should be.
|
|
11
|
-
* @param
|
|
11
|
+
* @param cropSize - Size of the window, where data should be scanned.
|
|
12
12
|
* @returns Array of two eigenvalues.
|
|
13
13
|
*/
|
|
14
14
|
export function getEigenvaluesForScore(
|
|
15
15
|
image: Image,
|
|
16
16
|
origin: Point,
|
|
17
|
-
|
|
17
|
+
cropSize = 5,
|
|
18
18
|
) {
|
|
19
|
-
if (!(
|
|
19
|
+
if (!(cropSize % 2)) {
|
|
20
20
|
throw new TypeError('windowSize must be an odd integer');
|
|
21
21
|
}
|
|
22
|
-
|
|
22
|
+
const kernelRadius = (SOBEL_X.length - 1) / 2;
|
|
23
|
+
const windowRadius = (cropSize - 1) / 2;
|
|
24
|
+
const padded = cropSize + 2 * kernelRadius;
|
|
23
25
|
const cropOrigin = {
|
|
24
|
-
row: origin.row -
|
|
25
|
-
column: origin.column -
|
|
26
|
+
row: origin.row - windowRadius - kernelRadius,
|
|
27
|
+
column: origin.column - windowRadius - kernelRadius,
|
|
26
28
|
};
|
|
27
29
|
const window = image.crop({
|
|
28
30
|
origin: cropOrigin,
|
|
29
|
-
width:
|
|
30
|
-
height:
|
|
31
|
-
});
|
|
32
|
-
const xDerivative = window.gradientFilter({ kernelX: SOBEL_X });
|
|
33
|
-
const yDerivative = window.gradientFilter({ kernelY: SOBEL_Y });
|
|
34
|
-
|
|
35
|
-
const xMatrix = new WrapperMatrix1D(xDerivative.getRawImage().data, {
|
|
36
|
-
rows: xDerivative.height,
|
|
31
|
+
width: padded,
|
|
32
|
+
height: padded,
|
|
37
33
|
});
|
|
38
|
-
const
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
34
|
+
const xDerivative = rawDirectConvolution(window, SOBEL_X);
|
|
35
|
+
const yDerivative = rawDirectConvolution(window, SOBEL_Y);
|
|
36
|
+
|
|
37
|
+
let xxSum = 0;
|
|
38
|
+
let xySum = 0;
|
|
39
|
+
let yySum = 0;
|
|
40
|
+
|
|
41
|
+
for (let i = kernelRadius; i < window.height - kernelRadius; i++) {
|
|
42
|
+
for (let j = kernelRadius; j < window.width - kernelRadius; j++) {
|
|
43
|
+
const idx = i * window.width + j;
|
|
44
|
+
const gx = xDerivative[idx];
|
|
45
|
+
const gy = yDerivative[idx];
|
|
46
|
+
xxSum += gx * gx;
|
|
47
|
+
xySum += gx * gy;
|
|
48
|
+
yySum += gy * gy;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
49
51
|
|
|
50
52
|
const structureTensor = new Matrix([
|
|
51
53
|
[xxSum, xySum],
|
|
@@ -41,6 +41,10 @@ export interface GetFastKeypointsOptions extends IsFastKeypointOptions {
|
|
|
41
41
|
* @default `'FAST'`
|
|
42
42
|
*/
|
|
43
43
|
scoreAlgorithm?: 'HARRIS' | 'FAST' | 'TOMASI';
|
|
44
|
+
/**
|
|
45
|
+
* Value between 0 and 1 that signifies a percentage from maximum score found in the image. Points can be filtered based on this relative maximum score.
|
|
46
|
+
*/
|
|
47
|
+
qualityThreshold?: number;
|
|
44
48
|
/**
|
|
45
49
|
* Options for the Harris score computation.
|
|
46
50
|
*/
|
|
@@ -72,7 +76,7 @@ export function getFastKeypoints(
|
|
|
72
76
|
options: GetFastKeypointsOptions = {},
|
|
73
77
|
): FastKeypoint[] {
|
|
74
78
|
const { fastRadius = 3, scoreAlgorithm = 'FAST', scoreOptions } = options;
|
|
75
|
-
|
|
79
|
+
const windowSize = scoreOptions?.windowSize ?? 7;
|
|
76
80
|
const circlePoints = getCirclePoints(fastRadius);
|
|
77
81
|
const compassPoints = getCompassPoints(fastRadius);
|
|
78
82
|
|
|
@@ -81,6 +85,7 @@ export function getFastKeypoints(
|
|
|
81
85
|
nbContiguousPixels = (3 / 4) * circlePoints.length,
|
|
82
86
|
threshold = 20,
|
|
83
87
|
nonMaxSuppression = true,
|
|
88
|
+
qualityThreshold = 0,
|
|
84
89
|
} = options;
|
|
85
90
|
|
|
86
91
|
checkProcessable(image, {
|
|
@@ -108,13 +113,14 @@ export function getFastKeypoints(
|
|
|
108
113
|
.with('TOMASI', () => tomasiScore)
|
|
109
114
|
.exhaustive();
|
|
110
115
|
|
|
116
|
+
const padding = scoreAlgorithm === 'FAST' ? 0 : (windowSize + 1) / 2;
|
|
111
117
|
const allKeypoints: FastKeypoint[] = [];
|
|
112
118
|
|
|
113
119
|
const scoreArray = new Float64Array(image.size).fill(
|
|
114
120
|
Number.NEGATIVE_INFINITY,
|
|
115
121
|
);
|
|
116
|
-
for (let row =
|
|
117
|
-
for (let column =
|
|
122
|
+
for (let row = padding; row < image.height - padding; row++) {
|
|
123
|
+
for (let column = padding; column < image.width - padding; column++) {
|
|
118
124
|
const corner = { row, column };
|
|
119
125
|
if (
|
|
120
126
|
isFastKeypoint(corner, image, circlePoints, compassPoints, {
|
|
@@ -157,8 +163,20 @@ export function getFastKeypoints(
|
|
|
157
163
|
}
|
|
158
164
|
}
|
|
159
165
|
}
|
|
166
|
+
let maxScore = -Infinity;
|
|
167
|
+
for (const k of keypoints) {
|
|
168
|
+
if (k.score > maxScore) maxScore = k.score;
|
|
169
|
+
}
|
|
170
|
+
const filtered: FastKeypoint[] = [];
|
|
171
|
+
// threshold by default negative infinity
|
|
172
|
+
const filterThreshold = qualityThreshold * maxScore;
|
|
160
173
|
|
|
161
|
-
|
|
174
|
+
for (const k of keypoints) {
|
|
175
|
+
if (k.score >= filterThreshold) {
|
|
176
|
+
k.score /= maxScore;
|
|
177
|
+
filtered.push(k);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
162
180
|
|
|
163
|
-
return
|
|
181
|
+
return filtered.toSorted((a, b) => b.score - a.score).slice(0, maxNbFeatures);
|
|
164
182
|
}
|
|
@@ -24,7 +24,7 @@ export function getHarrisScore(
|
|
|
24
24
|
origin: Point,
|
|
25
25
|
options: GetHarrisScoreOptions = {},
|
|
26
26
|
): number {
|
|
27
|
-
const { harrisConstant = 0.04, windowSize } = options;
|
|
27
|
+
const { harrisConstant = 0.04, windowSize = 5 } = options;
|
|
28
28
|
const eigenValues = getEigenvaluesForScore(image, origin, windowSize);
|
|
29
29
|
|
|
30
30
|
return (
|
|
@@ -4,11 +4,10 @@ import type { Point } from '../../index_full.ts';
|
|
|
4
4
|
import { getEigenvaluesForScore } from './getEigenvaluesForScore.js';
|
|
5
5
|
|
|
6
6
|
export interface GetShiTomasiScoreOptions {
|
|
7
|
-
qualityLevel?: number;
|
|
8
7
|
/**
|
|
9
8
|
* Size of the window to compute the Harris score.
|
|
10
9
|
* Should be an odd number so that the window can be centered on the corner.
|
|
11
|
-
* @default `
|
|
10
|
+
* @default `5`
|
|
12
11
|
*/
|
|
13
12
|
windowSize?: number;
|
|
14
13
|
}
|
|
@@ -29,7 +28,7 @@ export function getShiTomasiScore(
|
|
|
29
28
|
origin: Point,
|
|
30
29
|
options: GetShiTomasiScoreOptions = {},
|
|
31
30
|
): number {
|
|
32
|
-
const { windowSize =
|
|
31
|
+
const { windowSize = 5 } = options;
|
|
33
32
|
|
|
34
33
|
const eigenValues = getEigenvaluesForScore(image, origin, windowSize);
|
|
35
34
|
return Math.min(eigenValues[0], eigenValues[1]);
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { Image } from '../../Image.js';
|
|
2
2
|
import type { Point } from '../../geometry/index.js';
|
|
3
|
+
import type { DrawTextOptions } from '../../index_full.ts';
|
|
4
|
+
import { drawText } from '../../index_full.ts';
|
|
3
5
|
import { sum } from '../../utils/geometry/points.js';
|
|
4
6
|
import { getOutputImage } from '../../utils/getOutputImage.js';
|
|
5
7
|
import type { GetColorsOptions } from '../featureMatching.types.js';
|
|
@@ -47,7 +49,7 @@ export interface DrawKeypointsOptions {
|
|
|
47
49
|
/**
|
|
48
50
|
* Options for the coloring of the keypoints depending on their score (useful if showScore = true).
|
|
49
51
|
*/
|
|
50
|
-
showScoreOptions?: GetColorsOptions;
|
|
52
|
+
showScoreOptions?: GetColorsOptions & DrawTextOptions;
|
|
51
53
|
}
|
|
52
54
|
|
|
53
55
|
export interface DrawOrientedKeypointsOptions extends DrawKeypointsOptions {
|
|
@@ -89,7 +91,8 @@ export function drawKeypoints(
|
|
|
89
91
|
} = options;
|
|
90
92
|
let { maxNbKeypoints = keypoints.length } = options;
|
|
91
93
|
const { strokeColor = [255, 0, 0] } = options;
|
|
92
|
-
|
|
94
|
+
const fontColor = showScoreOptions?.fontColor ?? [0, 0, 255];
|
|
95
|
+
const font = showScoreOptions?.font ?? '20px Helvetica';
|
|
93
96
|
if (maxNbKeypoints > keypoints.length) {
|
|
94
97
|
maxNbKeypoints = keypoints.length;
|
|
95
98
|
}
|
|
@@ -118,6 +121,7 @@ export function drawKeypoints(
|
|
|
118
121
|
strokeColor: keypointColor,
|
|
119
122
|
out: newImage,
|
|
120
123
|
});
|
|
124
|
+
|
|
121
125
|
if (
|
|
122
126
|
isOrientedFastKeypoint(keypoint) &&
|
|
123
127
|
(options as DrawOrientedKeypointsOptions).showOrientation
|
|
@@ -135,7 +139,20 @@ export function drawKeypoints(
|
|
|
135
139
|
});
|
|
136
140
|
}
|
|
137
141
|
}
|
|
138
|
-
|
|
142
|
+
if (showScore) {
|
|
143
|
+
drawText(
|
|
144
|
+
newImage,
|
|
145
|
+
keypoints.map((k) => ({
|
|
146
|
+
content: (k.score * 100).toFixed(2),
|
|
147
|
+
position: sum(k.origin, origin),
|
|
148
|
+
font,
|
|
149
|
+
fontColor: [...fontColor, 255],
|
|
150
|
+
})),
|
|
151
|
+
{
|
|
152
|
+
out: newImage,
|
|
153
|
+
},
|
|
154
|
+
);
|
|
155
|
+
}
|
|
139
156
|
return newImage;
|
|
140
157
|
}
|
|
141
158
|
|
|
@@ -55,9 +55,10 @@ export function getContourMoore(mask: Mask): Point[] {
|
|
|
55
55
|
const [row, col] = getCoordsFromIndex(index, mask.width);
|
|
56
56
|
const startingPoint = { column: col, row };
|
|
57
57
|
let currentPoint = startingPoint;
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
58
|
+
if (!visited[index]) {
|
|
59
|
+
visited[index] = 1;
|
|
60
|
+
contour.push(currentPoint);
|
|
61
|
+
}
|
|
61
62
|
|
|
62
63
|
const firstNext = findNextPoint(mask, currentPoint, { column: col - 1, row });
|
|
63
64
|
if (!firstNext) {
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { Point } from '../index_full.ts';
|
|
2
|
+
|
|
3
|
+
export interface ConvexityDefectsOptions {
|
|
4
|
+
/**
|
|
5
|
+
* Depth threshold to remove smaller convexity defaults.
|
|
6
|
+
* @default 0
|
|
7
|
+
*/
|
|
8
|
+
depthThreshold?: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Find border points that are farthest from its convex hull lines. Inspired by openCV's convexityDefects.
|
|
13
|
+
* @param borderPoints - Mask's border points.
|
|
14
|
+
* @param hullPoints - Mask's convex hull points.
|
|
15
|
+
* @param options - Convexity defects options.
|
|
16
|
+
* @returns Array of convexity defects.
|
|
17
|
+
*/
|
|
18
|
+
export function getConvexityDefects(
|
|
19
|
+
borderPoints: Point[],
|
|
20
|
+
hullPoints: Point[],
|
|
21
|
+
options: ConvexityDefectsOptions = {},
|
|
22
|
+
): Point[] {
|
|
23
|
+
const { depthThreshold = 0 } = options;
|
|
24
|
+
const defects = [];
|
|
25
|
+
|
|
26
|
+
if (hullPoints.length === 0) {
|
|
27
|
+
throw new RangeError(
|
|
28
|
+
'No hull points were defined for convexity defects detection.',
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
if (borderPoints.length === 0) {
|
|
32
|
+
throw new RangeError(
|
|
33
|
+
'No border points were defined for convexity defects detection.',
|
|
34
|
+
);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const lastHullPoint = hullPoints.at(-1) as Point;
|
|
38
|
+
|
|
39
|
+
let currBorderIndex = borderPoints.findIndex((bp) =>
|
|
40
|
+
limitReached(bp, lastHullPoint),
|
|
41
|
+
);
|
|
42
|
+
|
|
43
|
+
if (currBorderIndex === -1) {
|
|
44
|
+
throw new RangeError(
|
|
45
|
+
'Could not find a border point matching the convex hull endpoint.',
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
for (let i = hullPoints.length - 1; i >= 0; i--) {
|
|
49
|
+
const currHullPoint = hullPoints.at(i);
|
|
50
|
+
const nextHullPoint = hullPoints.at(i - 1);
|
|
51
|
+
if (!currHullPoint || !nextHullPoint) continue;
|
|
52
|
+
const v0x = nextHullPoint.column - currHullPoint.column;
|
|
53
|
+
const v0y = nextHullPoint.row - currHullPoint.row;
|
|
54
|
+
const edgeLenSq = v0x * v0x + v0y * v0y;
|
|
55
|
+
let maxDepth = 0;
|
|
56
|
+
let maxDepthIndex = -1;
|
|
57
|
+
const segmentStart = currBorderIndex;
|
|
58
|
+
let reached = false;
|
|
59
|
+
for (let j = 0; j < borderPoints.length; j++) {
|
|
60
|
+
const checkIndex = (segmentStart + j) % borderPoints.length;
|
|
61
|
+
const bp = borderPoints[checkIndex];
|
|
62
|
+
if (limitReached(bp, nextHullPoint)) {
|
|
63
|
+
currBorderIndex = (checkIndex + 1) % borderPoints.length;
|
|
64
|
+
reached = true;
|
|
65
|
+
break;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
const v1x = bp.column - currHullPoint.column;
|
|
69
|
+
const v1y = bp.row - currHullPoint.row;
|
|
70
|
+
|
|
71
|
+
const t = Math.max(0, Math.min(1, (v1x * v0x + v1y * v0y) / edgeLenSq));
|
|
72
|
+
const closestX = currHullPoint.column + t * v0x;
|
|
73
|
+
const closestY = currHullPoint.row + t * v0y;
|
|
74
|
+
const depth = Math.hypot(bp.column - closestX, bp.row - closestY);
|
|
75
|
+
if (depth > maxDepth) {
|
|
76
|
+
maxDepth = depth;
|
|
77
|
+
maxDepthIndex = checkIndex;
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
if (!reached) {
|
|
81
|
+
throw new RangeError(
|
|
82
|
+
'Could not reach the next hull point while scanning border points; hull and border may be inconsistent.',
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
if (maxDepth > depthThreshold) {
|
|
86
|
+
const defectPoint = borderPoints[maxDepthIndex];
|
|
87
|
+
if (defectPoint) defects.push(defectPoint);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
return defects;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Checks if borderPoint reached convex hull's point.
|
|
95
|
+
* @param borderPoint - Border point.
|
|
96
|
+
* @param hullPoint - Endpoint of convex hull segment.
|
|
97
|
+
* @returns whether endpoint is reached.
|
|
98
|
+
*/
|
|
99
|
+
function limitReached(borderPoint: Point, hullPoint: Point): boolean {
|
|
100
|
+
const tolerance = 1;
|
|
101
|
+
return (
|
|
102
|
+
Math.abs(borderPoint.column - hullPoint.column) <= tolerance &&
|
|
103
|
+
Math.abs(borderPoint.row - hullPoint.row) <= tolerance
|
|
104
|
+
);
|
|
105
|
+
}
|