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/README.md +304 -304
- package/README.pypi.md +132 -132
- package/bin/cli.js +55 -51
- package/index.d.ts +101 -97
- package/index.js +36 -32
- package/lib/cache.js +321 -317
- package/lib/cv.js +146 -142
- package/lib/detect.js +135 -131
- package/lib/doctor.js +92 -88
- package/lib/errors.js +125 -121
- package/lib/vlm.js +353 -349
- package/package.json +56 -56
package/lib/cv.js
CHANGED
|
@@ -1,142 +1,146 @@
|
|
|
1
|
-
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
2/159, 4/159, 5/159, 4/159, 2/159
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
-grayPixels[
|
|
50
|
-
grayPixels[(y + 1) * width + (x - 1)] +
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
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
|
-
*
|
|
3
|
-
*
|
|
4
|
-
*/
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
const
|
|
15
|
-
const
|
|
16
|
-
const
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
}
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
return
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
const
|
|
76
|
-
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
const
|
|
86
|
-
const
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
const
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const
|
|
104
|
-
const
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
const
|
|
108
|
-
const
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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
|
+
};
|