@myvillage/cli 2.16.0 → 2.16.1
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/package.json +1 -1
- package/src/utils/image-backbone.js +70 -18
package/package.json
CHANGED
|
@@ -14,7 +14,7 @@
|
|
|
14
14
|
* datasets/models never pulls the heavy runtime in.
|
|
15
15
|
*/
|
|
16
16
|
import { readFileSync, existsSync } from 'fs';
|
|
17
|
-
import { join, dirname, extname } from 'path';
|
|
17
|
+
import { join, dirname, extname, basename } from 'path';
|
|
18
18
|
import { fileURLToPath } from 'url';
|
|
19
19
|
import { createRequire } from 'module';
|
|
20
20
|
|
|
@@ -27,6 +27,16 @@ function requirePkg(name) {
|
|
|
27
27
|
export const BACKBONE_ID = 'mobilenet-v3-small';
|
|
28
28
|
export const INPUT_SIZE = 224;
|
|
29
29
|
export const EMBED_DIM = 1024;
|
|
30
|
+
/**
|
|
31
|
+
* Pictures up to this size are embedded exactly as lab-runtime embeds them
|
|
32
|
+
* (its MAX_MEGAPIXELS), so a shared model reads them the same way. Anything
|
|
33
|
+
* bigger, like a 48 MP phone photo, is box-shrunk to fit first: it keeps the
|
|
34
|
+
* float copy small and averages the detail the 224×224 resize would skip.
|
|
35
|
+
*/
|
|
36
|
+
const MAX_DIRECT_PIXELS = 16 * 1000 * 1000;
|
|
37
|
+
/** jpeg-js's working memory cap; 512 MB stops short of 48 MP phone photos.
|
|
38
|
+
* Its 100 MP resolution cap still applies. */
|
|
39
|
+
const JPEG_MEMORY_MB = 2048;
|
|
30
40
|
|
|
31
41
|
const BACKBONE_DIR = join(
|
|
32
42
|
dirname(fileURLToPath(import.meta.url)),
|
|
@@ -99,18 +109,66 @@ function decodeImage(path) {
|
|
|
99
109
|
if (!existsSync(path)) throw new Error(`Image not found: ${path}`);
|
|
100
110
|
const buf = readFileSync(path);
|
|
101
111
|
const ext = extname(path).toLowerCase();
|
|
102
|
-
if (ext
|
|
103
|
-
|
|
104
|
-
const { PNG } = requirePkg('pngjs');
|
|
105
|
-
const png = PNG.sync.read(buf);
|
|
106
|
-
return { width: png.width, height: png.height, data: png.data };
|
|
112
|
+
if (ext !== '.png' && ext !== '.jpg' && ext !== '.jpeg') {
|
|
113
|
+
throw new Error(`Unsupported image type "${ext}". Use .jpg, .jpeg, or .png.`);
|
|
107
114
|
}
|
|
108
|
-
|
|
115
|
+
try {
|
|
116
|
+
if (ext === '.png') {
|
|
117
|
+
// Lazy require keeps these decoders out of the hot path for text/tabular.
|
|
118
|
+
const { PNG } = requirePkg('pngjs');
|
|
119
|
+
const png = PNG.sync.read(buf);
|
|
120
|
+
return { width: png.width, height: png.height, data: png.data };
|
|
121
|
+
}
|
|
109
122
|
const jpeg = requirePkg('jpeg-js');
|
|
110
|
-
const img = jpeg.decode(buf, { useTArray: true, maxMemoryUsageInMB:
|
|
123
|
+
const img = jpeg.decode(buf, { useTArray: true, maxMemoryUsageInMB: JPEG_MEMORY_MB });
|
|
111
124
|
return { width: img.width, height: img.height, data: img.data };
|
|
125
|
+
} catch (e) {
|
|
126
|
+
const name = basename(path);
|
|
127
|
+
if (/maxResolutionInMP|maxMemoryUsageInMB/.test(e?.message || '')) {
|
|
128
|
+
throw new Error(`The picture "${name}" is too big to read. Make it smaller (under 100 megapixels) and add it again.`);
|
|
129
|
+
}
|
|
130
|
+
throw new Error(`The picture "${name}" couldn't be read. Try a different JPEG or PNG.`);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* RGBA bytes → RGB floats in [0,1]. Pictures over MAX_DIRECT_PIXELS are
|
|
136
|
+
* averaged down by the smallest whole factor that fits (any leftover edge
|
|
137
|
+
* pixels are dropped); smaller ones are copied pixel for pixel.
|
|
138
|
+
*/
|
|
139
|
+
function toRgb({ width, height, data }) {
|
|
140
|
+
let k = 1;
|
|
141
|
+
while (Math.floor(width / k) * Math.floor(height / k) > MAX_DIRECT_PIXELS) k++;
|
|
142
|
+
const w = Math.floor(width / k);
|
|
143
|
+
const h = Math.floor(height / k);
|
|
144
|
+
const rgb = new Float32Array(w * h * 3);
|
|
145
|
+
if (k === 1) {
|
|
146
|
+
for (let i = 0, j = 0; i < data.length; i += 4, j += 3) {
|
|
147
|
+
rgb[j] = data[i] / 255;
|
|
148
|
+
rgb[j + 1] = data[i + 1] / 255;
|
|
149
|
+
rgb[j + 2] = data[i + 2] / 255;
|
|
150
|
+
}
|
|
151
|
+
return { rgb, w, h };
|
|
152
|
+
}
|
|
153
|
+
const scale = 1 / (k * k * 255);
|
|
154
|
+
for (let y = 0; y < h; y++) {
|
|
155
|
+
for (let x = 0; x < w; x++) {
|
|
156
|
+
let r = 0, g = 0, b = 0;
|
|
157
|
+
for (let dy = 0; dy < k; dy++) {
|
|
158
|
+
let i = ((y * k + dy) * width + x * k) * 4;
|
|
159
|
+
for (let dx = 0; dx < k; dx++, i += 4) {
|
|
160
|
+
r += data[i];
|
|
161
|
+
g += data[i + 1];
|
|
162
|
+
b += data[i + 2];
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
const j = (y * w + x) * 3;
|
|
166
|
+
rgb[j] = r * scale;
|
|
167
|
+
rgb[j + 1] = g * scale;
|
|
168
|
+
rgb[j + 2] = b * scale;
|
|
169
|
+
}
|
|
112
170
|
}
|
|
113
|
-
|
|
171
|
+
return { rgb, w, h };
|
|
114
172
|
}
|
|
115
173
|
|
|
116
174
|
/**
|
|
@@ -120,17 +178,11 @@ function decodeImage(path) {
|
|
|
120
178
|
export async function embedImage(path) {
|
|
121
179
|
const tf = await import('@tensorflow/tfjs');
|
|
122
180
|
const model = await getBackbone();
|
|
123
|
-
const {
|
|
181
|
+
const { rgb, w, h } = toRgb(decodeImage(path));
|
|
124
182
|
|
|
125
183
|
return tf.tidy(() => {
|
|
126
|
-
//
|
|
127
|
-
const
|
|
128
|
-
for (let i = 0, j = 0; i < data.length; i += 4, j += 3) {
|
|
129
|
-
rgb[j] = data[i] / 255;
|
|
130
|
-
rgb[j + 1] = data[i + 1] / 255;
|
|
131
|
-
rgb[j + 2] = data[i + 2] / 255;
|
|
132
|
-
}
|
|
133
|
-
const img = tf.tensor3d(rgb, [height, width, 3]);
|
|
184
|
+
// RGB float [0,1], shaped [h, w, 3], resized to the backbone input.
|
|
185
|
+
const img = tf.tensor3d(rgb, [h, w, 3]);
|
|
134
186
|
const resized = tf.image.resizeBilinear(img, [INPUT_SIZE, INPUT_SIZE]);
|
|
135
187
|
const batched = resized.expandDims(0); // [1, 224, 224, 3]
|
|
136
188
|
const out = model.predict(batched); // [1, 1024]
|