termux-vision 1.6.0 → 1.8.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/lib/cv.js CHANGED
@@ -1,142 +1,146 @@
1
- /**
2
- * Fast Pure-JS Spatial Filters & Classical Computer Vision for Node.js.
3
- * Open-Source under Apache License 2.0.
4
- */
5
-
6
- 'use strict';
7
-
8
- function gaussianBlur(grayPixels, width, height) {
9
- const output = new Float32Array(width * height);
10
- // 5x5 normalized Gaussian kernel with sigma=1.4
11
- const kernel = [
12
- 2/159, 4/159, 5/159, 4/159, 2/159,
13
- 4/159, 9/159, 12/159, 9/159, 4/159,
14
- 5/159, 12/159, 15/159, 12/159, 5/159,
15
- 4/159, 9/159, 12/159, 9/159, 4/159,
16
- 2/159, 4/159, 5/159, 4/159, 2/159
17
- ];
18
-
19
- for (let y = 2; y < height - 2; y++) {
20
- for (let x = 2; x < width - 2; x++) {
21
- let sum = 0;
22
- let k = 0;
23
- for (let ky = -2; ky <= 2; ky++) {
24
- for (let kx = -2; kx <= 2; kx++) {
25
- sum += grayPixels[(y + ky) * width + (x + kx)] * kernel[k++];
26
- }
27
- }
28
- output[y * width + x] = sum;
29
- }
30
- }
31
- return output;
32
- }
33
-
34
- function sobel(grayPixels, width, height) {
35
- const gradX = new Float32Array(width * height);
36
- const gradY = new Float32Array(width * height);
37
- const magnitude = new Float32Array(width * height);
38
- const angleDeg = new Float32Array(width * height);
39
-
40
- for (let y = 1; y < height - 1; y++) {
41
- for (let x = 1; x < width - 1; x++) {
42
- const idx = y * width + x;
43
- const gx =
44
- -grayPixels[(y - 1) * width + (x - 1)] + grayPixels[(y - 1) * width + (x + 1)] +
45
- -2 * grayPixels[y * width + (x - 1)] + 2 * grayPixels[y * width + (x + 1)] +
46
- -grayPixels[(y + 1) * width + (x - 1)] + grayPixels[(y + 1) * width + (x + 1)];
47
-
48
- const gy =
49
- -grayPixels[(y - 1) * width + (x - 1)] - 2 * grayPixels[(y - 1) * width + x] - grayPixels[(y - 1) * width + (x + 1)] +
50
- grayPixels[(y + 1) * width + (x - 1)] + 2 * grayPixels[(y + 1) * width + x] + grayPixels[(y + 1) * width + (x + 1)];
51
-
52
- gradX[idx] = gx;
53
- gradY[idx] = gy;
54
- magnitude[idx] = Math.sqrt(gx * gx + gy * gy);
55
- let deg = Math.atan2(gy, gx) * (180.0 / Math.PI);
56
- if (deg < 0) deg += 180.0;
57
- angleDeg[idx] = deg;
58
- }
59
- }
60
-
61
- return { gradX, gradY, magnitude, angleDeg };
62
- }
63
-
64
- function canny(grayPixels, width, height, lowThreshold = 40.0, highThreshold = 120.0) {
65
- const blurred = gaussianBlur(grayPixels, width, height);
66
- const { magnitude, angleDeg } = sobel(blurred, width, height);
67
- const dst = new Uint8Array(width * height);
68
- const queue = [];
69
-
70
- // 1. NMS & Double Thresholding
71
- for (let y = 1; y < height - 1; y++) {
72
- for (let x = 1; x < width - 1; x++) {
73
- const idx = y * width + x;
74
- const c = magnitude[idx];
75
- if (c < lowThreshold) continue;
76
-
77
- const deg = angleDeg[idx];
78
- let p1 = 0.0, p2 = 0.0;
79
-
80
- if ((deg >= 0 && deg < 22.5) || (deg >= 157.5 && deg <= 180)) {
81
- p1 = magnitude[y * width + (x - 1)];
82
- p2 = magnitude[y * width + (x + 1)];
83
- } else if (deg >= 22.5 && deg < 67.5) {
84
- p1 = magnitude[(y - 1) * width + (x + 1)];
85
- p2 = magnitude[(y + 1) * width + (x - 1)];
86
- } else if (deg >= 67.5 && deg < 112.5) {
87
- p1 = magnitude[(y - 1) * width + x];
88
- p2 = magnitude[(y + 1) * width + x];
89
- } else {
90
- p1 = magnitude[(y - 1) * width + (x - 1)];
91
- p2 = magnitude[(y + 1) * width + (x + 1)];
92
- }
93
-
94
- if (c >= p1 && c >= p2) {
95
- if (c >= highThreshold) {
96
- dst[idx] = 255;
97
- queue.push(idx);
98
- } else {
99
- dst[idx] = 75; // Weak edge candidate
100
- }
101
- }
102
- }
103
- }
104
-
105
- // 2. 8-connected BFS Hysteresis Tracking
106
- const dx = [-1, 0, 1, -1, 1, -1, 0, 1];
107
- const dy = [-1, -1, -1, 0, 0, 1, 1, 1];
108
- let qHead = 0;
109
-
110
- while (qHead < queue.length) {
111
- const curr = queue[qHead++];
112
- const cx = curr % width;
113
- const cy = Math.floor(curr / width);
114
-
115
- for (let k = 0; k < 8; k++) {
116
- const nx = cx + dx[k];
117
- const ny = cy + dy[k];
118
- if (nx >= 0 && nx < width && ny >= 0 && ny < height) {
119
- const nIdx = ny * width + nx;
120
- if (dst[nIdx] === 75) {
121
- dst[nIdx] = 255;
122
- queue.push(nIdx);
123
- }
124
- }
125
- }
126
- }
127
-
128
- // 3. Suppress remaining unconnected weak edges
129
- for (let i = 0; i < dst.length; i++) {
130
- if (dst[i] === 75) {
131
- dst[i] = 0;
132
- }
133
- }
134
-
135
- return dst;
136
- }
137
-
138
- module.exports = {
139
- gaussianBlur,
140
- sobel,
141
- canny
142
- };
1
+ /**
2
+ * Copyright (c) 2026 Uno Kim / AMEVA Foundation. All rights reserved.
3
+ * SPDX-License-Identifier: Apache-2.0 OR Commercial-License
4
+ */
5
+ /**
6
+ * Fast Pure-JS Spatial Filters & Classical Computer Vision for Node.js.
7
+ * Open-Source under Apache License 2.0.
8
+ */
9
+
10
+ 'use strict';
11
+
12
+ function gaussianBlur(grayPixels, width, height) {
13
+ const output = new Float32Array(width * height);
14
+ // 5x5 normalized Gaussian kernel with sigma=1.4
15
+ const kernel = [
16
+ 2/159, 4/159, 5/159, 4/159, 2/159,
17
+ 4/159, 9/159, 12/159, 9/159, 4/159,
18
+ 5/159, 12/159, 15/159, 12/159, 5/159,
19
+ 4/159, 9/159, 12/159, 9/159, 4/159,
20
+ 2/159, 4/159, 5/159, 4/159, 2/159
21
+ ];
22
+
23
+ for (let y = 2; y < height - 2; y++) {
24
+ for (let x = 2; x < width - 2; x++) {
25
+ let sum = 0;
26
+ let k = 0;
27
+ for (let ky = -2; ky <= 2; ky++) {
28
+ for (let kx = -2; kx <= 2; kx++) {
29
+ sum += grayPixels[(y + ky) * width + (x + kx)] * kernel[k++];
30
+ }
31
+ }
32
+ output[y * width + x] = sum;
33
+ }
34
+ }
35
+ return output;
36
+ }
37
+
38
+ function sobel(grayPixels, width, height) {
39
+ const gradX = new Float32Array(width * height);
40
+ const gradY = new Float32Array(width * height);
41
+ const magnitude = new Float32Array(width * height);
42
+ const angleDeg = new Float32Array(width * height);
43
+
44
+ for (let y = 1; y < height - 1; y++) {
45
+ for (let x = 1; x < width - 1; x++) {
46
+ const idx = y * width + x;
47
+ const gx =
48
+ -grayPixels[(y - 1) * width + (x - 1)] + grayPixels[(y - 1) * width + (x + 1)] +
49
+ -2 * grayPixels[y * width + (x - 1)] + 2 * grayPixels[y * width + (x + 1)] +
50
+ -grayPixels[(y + 1) * width + (x - 1)] + grayPixels[(y + 1) * width + (x + 1)];
51
+
52
+ const gy =
53
+ -grayPixels[(y - 1) * width + (x - 1)] - 2 * grayPixels[(y - 1) * width + x] - grayPixels[(y - 1) * width + (x + 1)] +
54
+ grayPixels[(y + 1) * width + (x - 1)] + 2 * grayPixels[(y + 1) * width + x] + grayPixels[(y + 1) * width + (x + 1)];
55
+
56
+ gradX[idx] = gx;
57
+ gradY[idx] = gy;
58
+ magnitude[idx] = Math.sqrt(gx * gx + gy * gy);
59
+ let deg = Math.atan2(gy, gx) * (180.0 / Math.PI);
60
+ if (deg < 0) deg += 180.0;
61
+ angleDeg[idx] = deg;
62
+ }
63
+ }
64
+
65
+ return { gradX, gradY, magnitude, angleDeg };
66
+ }
67
+
68
+ function canny(grayPixels, width, height, lowThreshold = 40.0, highThreshold = 120.0) {
69
+ const blurred = gaussianBlur(grayPixels, width, height);
70
+ const { magnitude, angleDeg } = sobel(blurred, width, height);
71
+ const dst = new Uint8Array(width * height);
72
+ const queue = [];
73
+
74
+ // 1. NMS & Double Thresholding
75
+ for (let y = 1; y < height - 1; y++) {
76
+ for (let x = 1; x < width - 1; x++) {
77
+ const idx = y * width + x;
78
+ const c = magnitude[idx];
79
+ if (c < lowThreshold) continue;
80
+
81
+ const deg = angleDeg[idx];
82
+ let p1 = 0.0, p2 = 0.0;
83
+
84
+ if ((deg >= 0 && deg < 22.5) || (deg >= 157.5 && deg <= 180)) {
85
+ p1 = magnitude[y * width + (x - 1)];
86
+ p2 = magnitude[y * width + (x + 1)];
87
+ } else if (deg >= 22.5 && deg < 67.5) {
88
+ p1 = magnitude[(y - 1) * width + (x + 1)];
89
+ p2 = magnitude[(y + 1) * width + (x - 1)];
90
+ } else if (deg >= 67.5 && deg < 112.5) {
91
+ p1 = magnitude[(y - 1) * width + x];
92
+ p2 = magnitude[(y + 1) * width + x];
93
+ } else {
94
+ p1 = magnitude[(y - 1) * width + (x - 1)];
95
+ p2 = magnitude[(y + 1) * width + (x + 1)];
96
+ }
97
+
98
+ if (c >= p1 && c >= p2) {
99
+ if (c >= highThreshold) {
100
+ dst[idx] = 255;
101
+ queue.push(idx);
102
+ } else {
103
+ dst[idx] = 75; // Weak edge candidate
104
+ }
105
+ }
106
+ }
107
+ }
108
+
109
+ // 2. 8-connected BFS Hysteresis Tracking
110
+ const dx = [-1, 0, 1, -1, 1, -1, 0, 1];
111
+ const dy = [-1, -1, -1, 0, 0, 1, 1, 1];
112
+ let qHead = 0;
113
+
114
+ while (qHead < queue.length) {
115
+ const curr = queue[qHead++];
116
+ const cx = curr % width;
117
+ const cy = Math.floor(curr / width);
118
+
119
+ for (let k = 0; k < 8; k++) {
120
+ const nx = cx + dx[k];
121
+ const ny = cy + dy[k];
122
+ if (nx >= 0 && nx < width && ny >= 0 && ny < height) {
123
+ const nIdx = ny * width + nx;
124
+ if (dst[nIdx] === 75) {
125
+ dst[nIdx] = 255;
126
+ queue.push(nIdx);
127
+ }
128
+ }
129
+ }
130
+ }
131
+
132
+ // 3. Suppress remaining unconnected weak edges
133
+ for (let i = 0; i < dst.length; i++) {
134
+ if (dst[i] === 75) {
135
+ dst[i] = 0;
136
+ }
137
+ }
138
+
139
+ return dst;
140
+ }
141
+
142
+ module.exports = {
143
+ gaussianBlur,
144
+ sobel,
145
+ canny
146
+ };
package/lib/detect.js CHANGED
@@ -1,131 +1,135 @@
1
- /**
2
- * BoundingBox Geometry & Non-Maximum Suppression (NMS) for Node.js.
3
- * Open-Source under Apache License 2.0.
4
- */
5
-
6
- 'use strict';
7
-
8
- function computeIoU(boxA, boxB) {
9
- const x1 = Math.max(boxA.x, boxB.x);
10
- const y1 = Math.max(boxA.y, boxB.y);
11
- const x2 = Math.min(boxA.x + boxA.width, boxB.x + boxB.width);
12
- const y2 = Math.min(boxA.y + boxA.height, boxB.y + boxB.height);
13
-
14
- const interArea = Math.max(0, x2 - x1) * Math.max(0, y2 - y1);
15
- const areaA = boxA.width * boxA.height;
16
- const areaB = boxB.width * boxB.height;
17
- const unionArea = areaA + areaB - interArea;
18
-
19
- return unionArea <= 0 ? 0 : interArea / unionArea;
20
- }
21
-
22
- function nms(boxes, iouThreshold = 0.45) {
23
- if (!boxes || boxes.length === 0) return [];
24
- const sorted = [...boxes].sort((a, b) => (b.score || 0) - (a.score || 0));
25
- const keep = [];
26
-
27
- for (let i = 0; i < sorted.length; i++) {
28
- const current = sorted[i];
29
- let suppressed = false;
30
- for (let j = 0; j < keep.length; j++) {
31
- if (computeIoU(current, keep[j]) > iouThreshold) {
32
- suppressed = true;
33
- break;
34
- }
35
- }
36
- if (!suppressed) {
37
- keep.push(current);
38
- }
39
- }
40
-
41
- return keep;
42
- }
43
-
44
- function computeIntegralImage(grayPixels, width, height) {
45
- const stride = width + 1;
46
- const integral = new Float64Array((width + 1) * (height + 1));
47
-
48
- for (let y = 0; y < height; y++) {
49
- let rowSum = 0.0;
50
- const dstRow = (y + 1) * stride;
51
- const srcRow = y * width;
52
-
53
- for (let x = 0; x < width; x++) {
54
- rowSum += grayPixels[srcRow + x];
55
- integral[dstRow + x + 1] = integral[dstRow - stride + x + 1] + rowSum;
56
- }
57
- }
58
- return integral;
59
- }
60
-
61
- function boxSum(integral, stride, x1, y1, x2, y2) {
62
- return (
63
- integral[y2 * stride + x2] -
64
- integral[y1 * stride + x2] -
65
- integral[y2 * stride + x1] +
66
- integral[y1 * stride + x1]
67
- );
68
- }
69
-
70
- function detectFaces(grayPixels, width, height, options = {}) {
71
- const scaleFactor = options.scaleFactor || 1.2;
72
- const minSize = options.minSize || 24;
73
- const maxSize = options.maxSize || Math.min(width, height);
74
- const integral = computeIntegralImage(grayPixels, width, height);
75
- const stride = width + 1;
76
-
77
- const candidates = [];
78
- let scale = minSize / 24.0;
79
-
80
- while (true) {
81
- const winW = Math.floor(24 * scale);
82
- const winH = Math.floor(24 * scale);
83
- if (winW > width || winH > height || winW > maxSize || winH > maxSize) break;
84
-
85
- const step = Math.max(2, Math.floor(4 * scale));
86
- const s2 = scale * scale;
87
-
88
- for (let y = 0; y <= height - winH; y += step) {
89
- for (let x = 0; x <= width - winW; x += step) {
90
- // Stage 1: Eye darkness vs cheek
91
- const eX = x + Math.floor(2 * scale), eY = y + Math.floor(6 * scale), eW = Math.floor(20 * scale), eH = Math.floor(6 * scale);
92
- const eyes = boxSum(integral, stride, eX, eY, eX + eW, eY + eH);
93
- const cX = x + Math.floor(2 * scale), cY = y + Math.floor(12 * scale), cW = Math.floor(20 * scale), cH = Math.floor(6 * scale);
94
- const cheeks = boxSum(integral, stride, cX, cY, cX + cW, cY + cH);
95
- const val1 = -1.0 * eyes + 1.0 * cheeks;
96
- if (val1 < -10.0 * s2 ? -1.0 : 1.2 < 0.2) continue;
97
-
98
- // Stage 2: Nose bridge
99
- const nX = x + Math.floor(10 * scale), nY = y + Math.floor(6 * scale), nW = Math.floor(4 * scale), nH = Math.floor(12 * scale);
100
- const nose = boxSum(integral, stride, nX, nY, nX + nW, nY + nH);
101
- const lX = x + Math.floor(6 * scale), rX = x + Math.floor(14 * scale);
102
- const lCheek = boxSum(integral, stride, lX, nY, lX + nW, nY + nH);
103
- const rCheek = boxSum(integral, stride, rX, nY, rX + nW, nY + nH);
104
- const val2 = -1.0 * lCheek + 2.0 * nose - 1.0 * rCheek;
105
- if (val2 < -5.0 * s2 ? -0.8 : 1.5 < 0.3) continue;
106
-
107
- const area = winW * winH;
108
- const meanScore = boxSum(integral, stride, x, y, x + winW, y + winH) / (area || 1);
109
- candidates.push({
110
- x,
111
- y,
112
- width: winW,
113
- height: winH,
114
- score: meanScore,
115
- className: 'face_candidate'
116
- });
117
- }
118
- }
119
- scale *= scaleFactor;
120
- }
121
-
122
- return nms(candidates, 0.3);
123
- }
124
-
125
- module.exports = {
126
- computeIoU,
127
- nms,
128
- computeIntegralImage,
129
- boxSum,
130
- detectFaces
131
- };
1
+ /**
2
+ * Copyright (c) 2026 Uno Kim / AMEVA Foundation. All rights reserved.
3
+ * SPDX-License-Identifier: Apache-2.0 OR Commercial-License
4
+ */
5
+ /**
6
+ * BoundingBox Geometry & Non-Maximum Suppression (NMS) for Node.js.
7
+ * Open-Source under Apache License 2.0.
8
+ */
9
+
10
+ 'use strict';
11
+
12
+ function computeIoU(boxA, boxB) {
13
+ const x1 = Math.max(boxA.x, boxB.x);
14
+ const y1 = Math.max(boxA.y, boxB.y);
15
+ const x2 = Math.min(boxA.x + boxA.width, boxB.x + boxB.width);
16
+ const y2 = Math.min(boxA.y + boxA.height, boxB.y + boxB.height);
17
+
18
+ const interArea = Math.max(0, x2 - x1) * Math.max(0, y2 - y1);
19
+ const areaA = boxA.width * boxA.height;
20
+ const areaB = boxB.width * boxB.height;
21
+ const unionArea = areaA + areaB - interArea;
22
+
23
+ return unionArea <= 0 ? 0 : interArea / unionArea;
24
+ }
25
+
26
+ function nms(boxes, iouThreshold = 0.45) {
27
+ if (!boxes || boxes.length === 0) return [];
28
+ const sorted = [...boxes].sort((a, b) => (b.score || 0) - (a.score || 0));
29
+ const keep = [];
30
+
31
+ for (let i = 0; i < sorted.length; i++) {
32
+ const current = sorted[i];
33
+ let suppressed = false;
34
+ for (let j = 0; j < keep.length; j++) {
35
+ if (computeIoU(current, keep[j]) > iouThreshold) {
36
+ suppressed = true;
37
+ break;
38
+ }
39
+ }
40
+ if (!suppressed) {
41
+ keep.push(current);
42
+ }
43
+ }
44
+
45
+ return keep;
46
+ }
47
+
48
+ function computeIntegralImage(grayPixels, width, height) {
49
+ const stride = width + 1;
50
+ const integral = new Float64Array((width + 1) * (height + 1));
51
+
52
+ for (let y = 0; y < height; y++) {
53
+ let rowSum = 0.0;
54
+ const dstRow = (y + 1) * stride;
55
+ const srcRow = y * width;
56
+
57
+ for (let x = 0; x < width; x++) {
58
+ rowSum += grayPixels[srcRow + x];
59
+ integral[dstRow + x + 1] = integral[dstRow - stride + x + 1] + rowSum;
60
+ }
61
+ }
62
+ return integral;
63
+ }
64
+
65
+ function boxSum(integral, stride, x1, y1, x2, y2) {
66
+ return (
67
+ integral[y2 * stride + x2] -
68
+ integral[y1 * stride + x2] -
69
+ integral[y2 * stride + x1] +
70
+ integral[y1 * stride + x1]
71
+ );
72
+ }
73
+
74
+ function detectFaces(grayPixels, width, height, options = {}) {
75
+ const scaleFactor = options.scaleFactor || 1.2;
76
+ const minSize = options.minSize || 24;
77
+ const maxSize = options.maxSize || Math.min(width, height);
78
+ const integral = computeIntegralImage(grayPixels, width, height);
79
+ const stride = width + 1;
80
+
81
+ const candidates = [];
82
+ let scale = minSize / 24.0;
83
+
84
+ while (true) {
85
+ const winW = Math.floor(24 * scale);
86
+ const winH = Math.floor(24 * scale);
87
+ if (winW > width || winH > height || winW > maxSize || winH > maxSize) break;
88
+
89
+ const step = Math.max(2, Math.floor(4 * scale));
90
+ const s2 = scale * scale;
91
+
92
+ for (let y = 0; y <= height - winH; y += step) {
93
+ for (let x = 0; x <= width - winW; x += step) {
94
+ // Stage 1: Eye darkness vs cheek
95
+ const eX = x + Math.floor(2 * scale), eY = y + Math.floor(6 * scale), eW = Math.floor(20 * scale), eH = Math.floor(6 * scale);
96
+ const eyes = boxSum(integral, stride, eX, eY, eX + eW, eY + eH);
97
+ const cX = x + Math.floor(2 * scale), cY = y + Math.floor(12 * scale), cW = Math.floor(20 * scale), cH = Math.floor(6 * scale);
98
+ const cheeks = boxSum(integral, stride, cX, cY, cX + cW, cY + cH);
99
+ const val1 = -1.0 * eyes + 1.0 * cheeks;
100
+ if (val1 < -10.0 * s2 ? -1.0 : 1.2 < 0.2) continue;
101
+
102
+ // Stage 2: Nose bridge
103
+ const nX = x + Math.floor(10 * scale), nY = y + Math.floor(6 * scale), nW = Math.floor(4 * scale), nH = Math.floor(12 * scale);
104
+ const nose = boxSum(integral, stride, nX, nY, nX + nW, nY + nH);
105
+ const lX = x + Math.floor(6 * scale), rX = x + Math.floor(14 * scale);
106
+ const lCheek = boxSum(integral, stride, lX, nY, lX + nW, nY + nH);
107
+ const rCheek = boxSum(integral, stride, rX, nY, rX + nW, nY + nH);
108
+ const val2 = -1.0 * lCheek + 2.0 * nose - 1.0 * rCheek;
109
+ if (val2 < -5.0 * s2 ? -0.8 : 1.5 < 0.3) continue;
110
+
111
+ const area = winW * winH;
112
+ const meanScore = boxSum(integral, stride, x, y, x + winW, y + winH) / (area || 1);
113
+ candidates.push({
114
+ x,
115
+ y,
116
+ width: winW,
117
+ height: winH,
118
+ score: meanScore,
119
+ className: 'face_candidate'
120
+ });
121
+ }
122
+ }
123
+ scale *= scaleFactor;
124
+ }
125
+
126
+ return nms(candidates, 0.3);
127
+ }
128
+
129
+ module.exports = {
130
+ computeIoU,
131
+ nms,
132
+ computeIntegralImage,
133
+ boxSum,
134
+ detectFaces
135
+ };