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.
Files changed (51) hide show
  1. package/dist/image-js.esm.js +8322 -7458
  2. package/dist/image-js.esm.js.map +1 -1
  3. package/dist/image-js.esm.min.js +2 -2
  4. package/dist/image-js.esm.min.js.map +1 -1
  5. package/dist/image-js.umd.js +8322 -7457
  6. package/dist/image-js.umd.js.map +1 -1
  7. package/dist/image-js.umd.min.js +2 -2
  8. package/dist/image-js.umd.min.js.map +1 -1
  9. package/dist-types/image-js.d.ts +40 -3
  10. package/lib/featureMatching/keypoints/getEigenvaluesForScore.d.ts +2 -2
  11. package/lib/featureMatching/keypoints/getEigenvaluesForScore.d.ts.map +1 -1
  12. package/lib/featureMatching/keypoints/getEigenvaluesForScore.js +27 -22
  13. package/lib/featureMatching/keypoints/getEigenvaluesForScore.js.map +1 -1
  14. package/lib/featureMatching/keypoints/getFastKeypoints.d.ts +4 -0
  15. package/lib/featureMatching/keypoints/getFastKeypoints.d.ts.map +1 -1
  16. package/lib/featureMatching/keypoints/getFastKeypoints.js +20 -5
  17. package/lib/featureMatching/keypoints/getFastKeypoints.js.map +1 -1
  18. package/lib/featureMatching/keypoints/getHarrisScore.js +1 -1
  19. package/lib/featureMatching/keypoints/getHarrisScore.js.map +1 -1
  20. package/lib/featureMatching/keypoints/getShiTomasiScore.d.ts +1 -2
  21. package/lib/featureMatching/keypoints/getShiTomasiScore.d.ts.map +1 -1
  22. package/lib/featureMatching/keypoints/getShiTomasiScore.js +1 -1
  23. package/lib/featureMatching/keypoints/getShiTomasiScore.js.map +1 -1
  24. package/lib/featureMatching/visualize/drawKeypoints.d.ts +2 -1
  25. package/lib/featureMatching/visualize/drawKeypoints.d.ts.map +1 -1
  26. package/lib/featureMatching/visualize/drawKeypoints.js +13 -0
  27. package/lib/featureMatching/visualize/drawKeypoints.js.map +1 -1
  28. package/lib/maskAnalysis/getContourMoore.d.ts.map +1 -1
  29. package/lib/maskAnalysis/getContourMoore.js +4 -2
  30. package/lib/maskAnalysis/getContourMoore.js.map +1 -1
  31. package/lib/maskAnalysis/getConvexityDefects.d.ts +17 -0
  32. package/lib/maskAnalysis/getConvexityDefects.d.ts.map +1 -0
  33. package/lib/maskAnalysis/getConvexityDefects.js +75 -0
  34. package/lib/maskAnalysis/getConvexityDefects.js.map +1 -0
  35. package/lib/maskAnalysis/getExternalContour.d.ts +1 -1
  36. package/lib/maskAnalysis/getExternalContour.d.ts.map +1 -1
  37. package/lib/maskAnalysis/index.d.ts +1 -0
  38. package/lib/maskAnalysis/index.d.ts.map +1 -1
  39. package/lib/maskAnalysis/index.js +1 -0
  40. package/lib/maskAnalysis/index.js.map +1 -1
  41. package/lib/roi/getExternalContour.d.ts +1 -1
  42. package/lib/roi/getExternalContour.d.ts.map +1 -1
  43. package/package.json +2 -2
  44. package/src/featureMatching/keypoints/getEigenvaluesForScore.ts +29 -27
  45. package/src/featureMatching/keypoints/getFastKeypoints.ts +23 -5
  46. package/src/featureMatching/keypoints/getHarrisScore.ts +1 -1
  47. package/src/featureMatching/keypoints/getShiTomasiScore.ts +2 -3
  48. package/src/featureMatching/visualize/drawKeypoints.ts +20 -3
  49. package/src/maskAnalysis/getContourMoore.ts +4 -3
  50. package/src/maskAnalysis/getConvexityDefects.ts +105 -0
  51. package/src/maskAnalysis/index.ts +1 -0
@@ -1,51 +1,53 @@
1
- import { EigenvalueDecomposition, Matrix, WrapperMatrix1D } from 'ml-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 windowSize - Size of the window, where data should be scanned.
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
- windowSize = 7,
17
+ cropSize = 5,
18
18
  ) {
19
- if (!(windowSize % 2)) {
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 - (windowSize - 1) / 2,
25
- column: origin.column - (windowSize - 1) / 2,
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: windowSize,
30
- height: windowSize,
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 yMatrix = new WrapperMatrix1D(yDerivative.getRawImage().data, {
39
- rows: yDerivative.height,
40
- });
41
-
42
- const xx = xMatrix.mmul(xMatrix);
43
- const xy = yMatrix.mmul(xMatrix);
44
- const yy = yMatrix.mmul(yMatrix);
45
-
46
- const xxSum = xx.sum();
47
- const xySum = xy.sum();
48
- const yySum = yy.sum();
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 = 0; row < image.height; row++) {
117
- for (let column = 0; column < image.width; 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
- keypoints.sort((a, b) => b.score - a.score);
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 keypoints.slice(0, maxNbFeatures);
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 `7`
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 = 7 } = options;
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
- visited[index] = 1;
60
- contour.push(currentPoint);
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
+ }
@@ -1 +1,2 @@
1
1
  export * from './maskAnalysis.types.js';
2
+ export * from './getConvexityDefects.ts';