browser-visual-search 0.1.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/LICENSE +21 -0
- package/README.md +59 -0
- package/dist/embedding/embed.d.ts +10 -0
- package/dist/embedding/embed.d.ts.map +1 -0
- package/dist/embedding/embed.js +83 -0
- package/dist/embedding/embed.js.map +1 -0
- package/dist/embedding/index.d.ts +2 -0
- package/dist/embedding/index.d.ts.map +1 -0
- package/dist/embedding/index.js +2 -0
- package/dist/embedding/index.js.map +1 -0
- package/dist/index.d.ts +7 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +4 -0
- package/dist/index.js.map +1 -0
- package/dist/search/build-index.d.ts +17 -0
- package/dist/search/build-index.d.ts.map +1 -0
- package/dist/search/build-index.js +110 -0
- package/dist/search/build-index.js.map +1 -0
- package/dist/search/index.d.ts +3 -0
- package/dist/search/index.d.ts.map +1 -0
- package/dist/search/index.js +3 -0
- package/dist/search/index.js.map +1 -0
- package/dist/search/load-index.d.ts +14 -0
- package/dist/search/load-index.d.ts.map +1 -0
- package/dist/search/load-index.js +60 -0
- package/dist/search/load-index.js.map +1 -0
- package/dist/segmentation/index.d.ts +2 -0
- package/dist/segmentation/index.d.ts.map +1 -0
- package/dist/segmentation/index.js +2 -0
- package/dist/segmentation/index.js.map +1 -0
- package/dist/segmentation/postprocess.d.ts +41 -0
- package/dist/segmentation/postprocess.d.ts.map +1 -0
- package/dist/segmentation/postprocess.js +166 -0
- package/dist/segmentation/postprocess.js.map +1 -0
- package/dist/segmentation/preprocess.d.ts +31 -0
- package/dist/segmentation/preprocess.d.ts.map +1 -0
- package/dist/segmentation/preprocess.js +61 -0
- package/dist/segmentation/preprocess.js.map +1 -0
- package/dist/segmentation/segment.d.ts +9 -0
- package/dist/segmentation/segment.d.ts.map +1 -0
- package/dist/segmentation/segment.js +26 -0
- package/dist/segmentation/segment.js.map +1 -0
- package/dist/types.d.ts +33 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/package.json +45 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Rainer Simon
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# Browser Visual Search
|
|
2
|
+
|
|
3
|
+
In-browser visual search powered by FastSAM, CLIP and onnxruntime-web.
|
|
4
|
+
|
|
5
|
+
**Work in progress**
|
|
6
|
+
|
|
7
|
+
## Installation
|
|
8
|
+
```bash
|
|
9
|
+
npm install browser-visual-search
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
## Usage
|
|
13
|
+
```ts
|
|
14
|
+
import { buildIndex, loadIndex } from 'browser-visual-search';
|
|
15
|
+
|
|
16
|
+
// Build (and auto-save) an index from a directory
|
|
17
|
+
const dirHandle = await window.showDirectoryPicker({ mode: 'readwrite' });
|
|
18
|
+
|
|
19
|
+
const index = await buildIndex(dirHandle, {
|
|
20
|
+
segmenterUrl: '/models/fastsam-s.onnx',
|
|
21
|
+
embedderUrl: '/models/clip-vit-b32-visual.onnx',
|
|
22
|
+
onProgress: (p) => console.log(p),
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
// Load a previously built index
|
|
26
|
+
const index = await loadIndex(dirHandle, {
|
|
27
|
+
embedderUrl: '/models/clip-vit-b32-visual.onnx',
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
// Query by image or region
|
|
31
|
+
const results = await index.query(file, /* bbox? */ [0.1, 0.1, 0.5, 0.5], { topK: 20 });
|
|
32
|
+
// → [{ imageId, bbox, area, score }, ...]
|
|
33
|
+
```
|
|
34
|
+
|
|
35
|
+
The index is persisted automatically to `.visual-search/` inside the chosen directory. Subsequent `buildIndex` calls are incremental — only new images are processed.
|
|
36
|
+
|
|
37
|
+
## Models
|
|
38
|
+
|
|
39
|
+
You need to build the ONNX models yourself – see [scripts/README](scripts) for instructions.
|
|
40
|
+
|
|
41
|
+
## Development
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npm install
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
**Unit tests** (Vitest, runs in browser via Playwright):
|
|
48
|
+
```bash
|
|
49
|
+
npm test
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
Tests expect model files at `/assets/models/`.
|
|
53
|
+
|
|
54
|
+
**Test pages** — start a dev server (e.g. Vite) and open:
|
|
55
|
+
|
|
56
|
+
| Page | Purpose |
|
|
57
|
+
|---|---|
|
|
58
|
+
| `/test.html` | Build an index, inspect segments, run per-image embedding |
|
|
59
|
+
| `/search.html` | Load a saved index and run visual queries |
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { BBox } from '../types.js';
|
|
2
|
+
export declare const embedImage: (file: File, bbox?: BBox, options?: {
|
|
3
|
+
embedderUrl: string;
|
|
4
|
+
executionProviders?: string[];
|
|
5
|
+
}) => Promise<Float32Array>;
|
|
6
|
+
export declare const embedBatch: (bitmap: ImageBitmap, bboxes: Array<BBox | undefined>, options: {
|
|
7
|
+
embedderUrl: string;
|
|
8
|
+
executionProviders?: string[];
|
|
9
|
+
}, batchSize?: number) => Promise<Float32Array[]>;
|
|
10
|
+
//# sourceMappingURL=embed.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"embed.d.ts","sourceRoot":"","sources":["../../src/embedding/embed.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAoDxC,eAAO,MAAM,UAAU,GACrB,MAAM,IAAI,EACV,OAAO,IAAI,EACX,UAAS;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAA;CAAwB,KACpF,OAAO,CAAC,YAAY,CActB,CAAA;AAED,eAAO,MAAM,UAAU,GACrB,QAAQ,WAAW,EACnB,QAAQ,KAAK,CAAC,IAAI,GAAG,SAAS,CAAC,EAC/B,SAAS;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAA;CAAE,EAC/D,kBAAc,KACb,OAAO,CAAC,YAAY,EAAE,CA6BxB,CAAA"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import * as ort from 'onnxruntime-web';
|
|
2
|
+
const EMBEDDING_DIM = 512;
|
|
3
|
+
const CLIP_INPUT_SIZE = 224;
|
|
4
|
+
let embedderSession = null;
|
|
5
|
+
const loadEmbedder = async (url, providers = ['webgpu', 'wasm']) => {
|
|
6
|
+
if (!embedderSession)
|
|
7
|
+
embedderSession = await ort.InferenceSession.create(url, { executionProviders: providers });
|
|
8
|
+
return embedderSession;
|
|
9
|
+
};
|
|
10
|
+
const cropToClipTensor = (bitmap, bbox) => {
|
|
11
|
+
const { width: W, height: H } = bitmap;
|
|
12
|
+
let sx = 0, sy = 0, sw = W, sh = H;
|
|
13
|
+
if (bbox) {
|
|
14
|
+
sx = Math.round(bbox[0] * W);
|
|
15
|
+
sy = Math.round(bbox[1] * H);
|
|
16
|
+
sw = Math.max(1, Math.round(bbox[2] * W));
|
|
17
|
+
sh = Math.max(1, Math.round(bbox[3] * H));
|
|
18
|
+
}
|
|
19
|
+
const S = CLIP_INPUT_SIZE;
|
|
20
|
+
const canvas = new OffscreenCanvas(S, S);
|
|
21
|
+
const ctx = canvas.getContext('2d');
|
|
22
|
+
ctx.drawImage(bitmap, sx, sy, sw, sh, 0, 0, S, S);
|
|
23
|
+
const { data } = ctx.getImageData(0, 0, S, S);
|
|
24
|
+
const MEAN = [0.48145466, 0.4578275, 0.40821073];
|
|
25
|
+
const STD = [0.26862954, 0.26130258, 0.27577711];
|
|
26
|
+
const tensor = new Float32Array(3 * S * S);
|
|
27
|
+
const plane = S * S;
|
|
28
|
+
for (let i = 0; i < plane; i++) {
|
|
29
|
+
tensor[i] = (data[i * 4] / 255 - MEAN[0]) / STD[0]; // R
|
|
30
|
+
tensor[plane + i] = (data[i * 4 + 1] / 255 - MEAN[1]) / STD[1]; // G
|
|
31
|
+
tensor[plane * 2 + i] = (data[i * 4 + 2] / 255 - MEAN[2]) / STD[2]; // B
|
|
32
|
+
}
|
|
33
|
+
return tensor;
|
|
34
|
+
};
|
|
35
|
+
const l2Normalise = (vec) => {
|
|
36
|
+
let norm = 0;
|
|
37
|
+
for (let i = 0; i < vec.length; i++)
|
|
38
|
+
norm += vec[i] * vec[i];
|
|
39
|
+
norm = Math.sqrt(norm);
|
|
40
|
+
if (norm > 0)
|
|
41
|
+
for (let i = 0; i < vec.length; i++)
|
|
42
|
+
vec[i] /= norm;
|
|
43
|
+
return vec;
|
|
44
|
+
};
|
|
45
|
+
export const embedImage = async (file, bbox, options = { embedderUrl: '' }) => {
|
|
46
|
+
if (!options.embedderUrl)
|
|
47
|
+
throw new Error('embedderUrl is required');
|
|
48
|
+
const bitmap = await createImageBitmap(file);
|
|
49
|
+
const embedder = await loadEmbedder(options.embedderUrl, options.executionProviders);
|
|
50
|
+
const tensor = cropToClipTensor(bitmap, bbox);
|
|
51
|
+
const inputTensor = new ort.Tensor('float32', tensor, [1, 3, CLIP_INPUT_SIZE, CLIP_INPUT_SIZE]);
|
|
52
|
+
const feeds = { [embedder.inputNames[0]]: inputTensor };
|
|
53
|
+
const output = await embedder.run(feeds);
|
|
54
|
+
const raw = output[embedder.outputNames[0]].data;
|
|
55
|
+
bitmap.close();
|
|
56
|
+
return l2Normalise(new Float32Array(raw));
|
|
57
|
+
};
|
|
58
|
+
export const embedBatch = async (bitmap, bboxes, options, batchSize = 32) => {
|
|
59
|
+
const embedder = await loadEmbedder(options.embedderUrl, options.executionProviders);
|
|
60
|
+
const results = [];
|
|
61
|
+
for (let i = 0; i < bboxes.length; i += batchSize) {
|
|
62
|
+
const batchBBoxes = bboxes.slice(i, i + batchSize);
|
|
63
|
+
const tensors = batchBBoxes.map(bbox => cropToClipTensor(bitmap, bbox));
|
|
64
|
+
// Stack into a single [B, 3, 224, 224] tensor
|
|
65
|
+
const B = tensors.length;
|
|
66
|
+
const flat = new Float32Array(B * 3 * CLIP_INPUT_SIZE * CLIP_INPUT_SIZE);
|
|
67
|
+
tensors.forEach((t, j) => flat.set(t, j * t.length));
|
|
68
|
+
const inputTensor = new ort.Tensor('float32', flat, [B, 3, CLIP_INPUT_SIZE, CLIP_INPUT_SIZE]);
|
|
69
|
+
const feeds = { [embedder.inputNames[0]]: inputTensor };
|
|
70
|
+
const output = await embedder.run(feeds);
|
|
71
|
+
const raw = output[embedder.outputNames[0]].data;
|
|
72
|
+
// Split [B, 512] output back into individual vectors
|
|
73
|
+
for (let j = 0; j < B; j++) {
|
|
74
|
+
const vec = new Float32Array(EMBEDDING_DIM);
|
|
75
|
+
for (let k = 0; k < EMBEDDING_DIM; k++) {
|
|
76
|
+
vec[k] = raw[j * EMBEDDING_DIM + k];
|
|
77
|
+
}
|
|
78
|
+
results.push(l2Normalise(vec));
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return results;
|
|
82
|
+
};
|
|
83
|
+
//# sourceMappingURL=embed.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"embed.js","sourceRoot":"","sources":["../../src/embedding/embed.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/embedding/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/embedding/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC"}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
export { embedImage, embedBatch } from './embedding/index.js';
|
|
2
|
+
export { segmentImage } from './segmentation/index.js';
|
|
3
|
+
export { buildIndex, loadIndex } from './search/index.js';
|
|
4
|
+
export type { BBox, IndexedImage, IndexedImageSegment, SearchOptions, SearchResult, Segment, VisualSearchIndex } from './types.js';
|
|
5
|
+
export type { BuildOptions, BuildProgress } from './search/build-index.js';
|
|
6
|
+
export type { LoadIndexOptions } from './search/load-index.js';
|
|
7
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAC9D,OAAO,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AACvD,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC;AAE1D,YAAY,EACV,IAAI,EACJ,YAAY,EACZ,mBAAmB,EACnB,aAAa,EACb,YAAY,EACZ,OAAO,EACP,iBAAiB,EAClB,MAAM,YAAY,CAAC;AAEpB,YAAY,EACV,YAAY,EACZ,aAAa,EACd,MAAM,yBAAyB,CAAC;AAEjC,YAAY,EACV,gBAAgB,EACjB,MAAM,wBAAwB,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,UAAU,EAAE,UAAU,EAAE,MAAM,sBAAsB,CAAC;AAC9D,OAAO,EAAE,YAAY,EAAE,MAAM,yBAAyB,CAAC;AACvD,OAAO,EAAE,UAAU,EAAE,SAAS,EAAE,MAAM,mBAAmB,CAAC"}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { type LoadIndexOptions } from './load-index.js';
|
|
2
|
+
import type { VisualSearchIndex } from '../types.js';
|
|
3
|
+
export interface BuildOptions extends LoadIndexOptions {
|
|
4
|
+
segmenterUrl: string;
|
|
5
|
+
forceReindex?: boolean;
|
|
6
|
+
onProgress?: (progress: BuildProgress) => void;
|
|
7
|
+
}
|
|
8
|
+
export type BuildProgress = {
|
|
9
|
+
phase: 'loading' | 'scanning' | 'saving' | 'done';
|
|
10
|
+
} | {
|
|
11
|
+
phase: 'indexing';
|
|
12
|
+
total: number;
|
|
13
|
+
completed: number;
|
|
14
|
+
currentFile?: string;
|
|
15
|
+
};
|
|
16
|
+
export declare const buildIndex: (dirHandle: FileSystemDirectoryHandle, opts: BuildOptions) => Promise<VisualSearchIndex>;
|
|
17
|
+
//# sourceMappingURL=build-index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build-index.d.ts","sourceRoot":"","sources":["../../src/search/build-index.ts"],"names":[],"mappings":"AAEA,OAAO,EAAyC,KAAK,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAC/F,OAAO,KAAK,EAAqC,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAExF,MAAM,WAAW,YAAa,SAAQ,gBAAgB;IAEpD,YAAY,EAAE,MAAM,CAAC;IAErB,YAAY,CAAC,EAAE,OAAO,CAAC;IAEvB,UAAU,CAAC,EAAE,CAAC,QAAQ,EAAE,aAAa,KAAK,IAAI,CAAC;CAEhD;AAED,MAAM,MAAM,aAAa,GACrB;IAAE,KAAK,EAAE,SAAS,GAAG,UAAU,GAAG,QAAQ,GAAG,MAAM,CAAA;CAAE,GACrD;IAAE,KAAK,EAAE,UAAU,CAAC;IAAC,KAAK,EAAE,MAAM,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC;IAAC,WAAW,CAAC,EAAE,MAAM,CAAA;CAAE,CAAC;AA8BlF,eAAO,MAAM,UAAU,GACrB,WAAW,yBAAyB,EACpC,MAAM,YAAY,KACjB,OAAO,CAAC,iBAAiB,CA+F3B,CAAA"}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import { embedBatch } from '../embedding/embed.js';
|
|
2
|
+
import { segmentImage } from '../segmentation/index.js';
|
|
3
|
+
import { createIndex, loadIndex, EMBEDDING_DIM } from './load-index.js';
|
|
4
|
+
const collectImages = async (dirHandle, path = '') => {
|
|
5
|
+
const images = [];
|
|
6
|
+
const entries = dirHandle.entries();
|
|
7
|
+
for await (const [name, handle] of entries) {
|
|
8
|
+
const fullPath = path ? `${path}/${name}` : name;
|
|
9
|
+
if (handle.kind === 'file') {
|
|
10
|
+
const ext = name.toLowerCase().split('.').pop();
|
|
11
|
+
if (['jpg', 'jpeg', 'png', 'webp'].includes(ext ?? '')) {
|
|
12
|
+
const file = await handle.getFile();
|
|
13
|
+
images.push({ id: fullPath, file });
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
else if (handle.kind === 'directory') {
|
|
17
|
+
images.push(...await collectImages(handle, fullPath));
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return images;
|
|
21
|
+
};
|
|
22
|
+
export const buildIndex = async (dirHandle, opts) => {
|
|
23
|
+
const { onProgress, forceReindex } = opts;
|
|
24
|
+
// 1. Try to load existing index
|
|
25
|
+
onProgress?.({ phase: 'loading' });
|
|
26
|
+
let existingImages = [];
|
|
27
|
+
let existingEmbeddings = new Float32Array(0);
|
|
28
|
+
try {
|
|
29
|
+
const existing = await loadIndex(dirHandle, opts);
|
|
30
|
+
existingImages = [...existing.images];
|
|
31
|
+
existingEmbeddings = existing.embeddings;
|
|
32
|
+
;
|
|
33
|
+
}
|
|
34
|
+
catch {
|
|
35
|
+
// No index yet — start fresh
|
|
36
|
+
}
|
|
37
|
+
// 2. Scan directory
|
|
38
|
+
onProgress?.({ phase: 'scanning' });
|
|
39
|
+
const allImages = await collectImages(dirHandle);
|
|
40
|
+
const allImageIds = new Set(allImages.map(img => img.id));
|
|
41
|
+
// 3. Prune images no longer on disk
|
|
42
|
+
const keptImages = existingImages.filter(img => allImageIds.has(img.imageId));
|
|
43
|
+
// 4. Determine which images need indexing
|
|
44
|
+
const indexedIds = new Set(keptImages.map(img => img.imageId));
|
|
45
|
+
const toIndex = forceReindex
|
|
46
|
+
? allImages
|
|
47
|
+
: allImages.filter(img => !indexedIds.has(img.id));
|
|
48
|
+
// 5. Index new images
|
|
49
|
+
const newImages = [];
|
|
50
|
+
const newEmbeddings = [];
|
|
51
|
+
let nextRow = 0;
|
|
52
|
+
for (let i = 0; i < toIndex.length; i++) {
|
|
53
|
+
const input = toIndex[i];
|
|
54
|
+
onProgress?.({ phase: 'indexing', total: toIndex.length, completed: i, currentFile: input.id });
|
|
55
|
+
const detections = await segmentImage(input.file, opts);
|
|
56
|
+
const bitmap = await createImageBitmap(input.file);
|
|
57
|
+
const embeddings = await embedBatch(bitmap, detections.map(d => d.bbox), opts);
|
|
58
|
+
bitmap.close();
|
|
59
|
+
const segments = detections.map((det, j) => ({
|
|
60
|
+
bbox: det.bbox,
|
|
61
|
+
area: det.area,
|
|
62
|
+
embeddingRow: nextRow++,
|
|
63
|
+
}));
|
|
64
|
+
newImages.push({ imageId: input.id, indexedAt: new Date().toISOString(), segments });
|
|
65
|
+
newEmbeddings.push(...embeddings);
|
|
66
|
+
}
|
|
67
|
+
// 6. Merge kept + new
|
|
68
|
+
// Re-map embeddingRows for kept images, then append new
|
|
69
|
+
const mergedImages = [];
|
|
70
|
+
const mergedEmbeddingVecs = [];
|
|
71
|
+
let row = 0;
|
|
72
|
+
const baseImages = forceReindex ? [] : keptImages;
|
|
73
|
+
for (const img of baseImages) {
|
|
74
|
+
const remappedSegments = img.segments.map(seg => {
|
|
75
|
+
const vec = existingEmbeddings.subarray(seg.embeddingRow * EMBEDDING_DIM, seg.embeddingRow * EMBEDDING_DIM + EMBEDDING_DIM);
|
|
76
|
+
mergedEmbeddingVecs.push(new Float32Array(vec));
|
|
77
|
+
return { ...seg, embeddingRow: row++ };
|
|
78
|
+
});
|
|
79
|
+
mergedImages.push({ ...img, segments: remappedSegments });
|
|
80
|
+
}
|
|
81
|
+
for (const img of newImages) {
|
|
82
|
+
const remappedSegments = img.segments.map(seg => ({
|
|
83
|
+
...seg,
|
|
84
|
+
embeddingRow: row++,
|
|
85
|
+
}));
|
|
86
|
+
mergedImages.push({ ...img, segments: remappedSegments });
|
|
87
|
+
}
|
|
88
|
+
mergedEmbeddingVecs.push(...newEmbeddings);
|
|
89
|
+
// Flatten into a single Float32Array
|
|
90
|
+
const mergedEmbeddings = new Float32Array(mergedEmbeddingVecs.length * EMBEDDING_DIM);
|
|
91
|
+
mergedEmbeddingVecs.forEach((vec, i) => mergedEmbeddings.set(vec, i * EMBEDDING_DIM));
|
|
92
|
+
// 7. Serialize
|
|
93
|
+
onProgress?.({ phase: 'saving' });
|
|
94
|
+
await serialize(mergedImages, mergedEmbeddings, dirHandle);
|
|
95
|
+
// 8. Return index without re-reading from disk
|
|
96
|
+
onProgress?.({ phase: 'done' });
|
|
97
|
+
return createIndex(mergedImages, mergedEmbeddings, dirHandle, opts);
|
|
98
|
+
};
|
|
99
|
+
const serialize = async (images, embeddings, dirHandle) => {
|
|
100
|
+
const vsDir = await dirHandle.getDirectoryHandle('.visual-search', { create: true });
|
|
101
|
+
const jsonHandle = await vsDir.getFileHandle('index.json', { create: true });
|
|
102
|
+
const jsonWriter = await jsonHandle.createWritable();
|
|
103
|
+
await jsonWriter.write(JSON.stringify({ version: 1, updatedAt: new Date().toISOString(), images }, null, 2));
|
|
104
|
+
await jsonWriter.close();
|
|
105
|
+
const binHandle = await vsDir.getFileHandle('embeddings.bin', { create: true });
|
|
106
|
+
const binWriter = await binHandle.createWritable();
|
|
107
|
+
await binWriter.write(embeddings.buffer);
|
|
108
|
+
await binWriter.close();
|
|
109
|
+
};
|
|
110
|
+
//# sourceMappingURL=build-index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"build-index.js","sourceRoot":"","sources":["../../src/search/build-index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/search/index.ts"],"names":[],"mappings":"AAAA,cAAc,kBAAkB,CAAC;AACjC,cAAc,iBAAiB,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/search/index.ts"],"names":[],"mappings":"AAAA,cAAc,kBAAkB,CAAC;AACjC,cAAc,iBAAiB,CAAC"}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { IndexedImage, VisualSearchIndex } from '../types.js';
|
|
2
|
+
export declare const EMBEDDING_DIM = 512;
|
|
3
|
+
export interface LoadIndexOptions {
|
|
4
|
+
embedderUrl: string;
|
|
5
|
+
executionProviders?: string[];
|
|
6
|
+
}
|
|
7
|
+
export declare const createIndex: (images: IndexedImage[], embeddings: Float32Array, dirHandle: FileSystemDirectoryHandle, opts: LoadIndexOptions) => VisualSearchIndex;
|
|
8
|
+
/**
|
|
9
|
+
* Load an existing visual search index from a directory handle.
|
|
10
|
+
* @param dirHandle The directory handle containing index.json and embeddings.bin
|
|
11
|
+
* @returns The loaded index
|
|
12
|
+
*/
|
|
13
|
+
export declare const loadIndex: (dirHandle: FileSystemDirectoryHandle, opts: LoadIndexOptions) => Promise<VisualSearchIndex>;
|
|
14
|
+
//# sourceMappingURL=load-index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"load-index.d.ts","sourceRoot":"","sources":["../../src/search/load-index.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAQ,YAAY,EAA+B,iBAAiB,EAAE,MAAM,aAAa,CAAC;AAMtG,eAAO,MAAM,aAAa,MAAM,CAAC;AAEjC,MAAM,WAAW,gBAAgB;IAE/B,WAAW,EAAE,MAAM,CAAC;IAEpB,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAC;CAE/B;AAmCD,eAAO,MAAM,WAAW,GACtB,QAAQ,YAAY,EAAE,EACtB,YAAY,YAAY,EACxB,WAAW,yBAAyB,EACpC,MAAM,gBAAgB,KACrB,iBAqBF,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,SAAS,GAAU,WAAW,yBAAyB,EAAE,MAAM,gBAAgB,KAAG,OAAO,CAAC,iBAAiB,CAmBvH,CAAA"}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { embedImage } from '../embedding/embed.js';
|
|
2
|
+
const DIR_NAME = '.visual-search';
|
|
3
|
+
const INDEX_FILE = 'index.json';
|
|
4
|
+
const EMBED_FILE = 'embeddings.bin';
|
|
5
|
+
export const EMBEDDING_DIM = 512;
|
|
6
|
+
const nearestNeighbours = (query, embeddings, images, topK = 20) => {
|
|
7
|
+
const results = [];
|
|
8
|
+
for (const img of images) {
|
|
9
|
+
for (const seg of img.segments) {
|
|
10
|
+
const offset = seg.embeddingRow * EMBEDDING_DIM;
|
|
11
|
+
let score = 0;
|
|
12
|
+
for (let k = 0; k < EMBEDDING_DIM; k++)
|
|
13
|
+
score += query[k] * embeddings[offset + k];
|
|
14
|
+
results.push({ imageId: img.imageId, bbox: seg.bbox, area: seg.area, score });
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
return results
|
|
18
|
+
.sort((a, b) => b.score - a.score)
|
|
19
|
+
.slice(0, topK);
|
|
20
|
+
};
|
|
21
|
+
export const createIndex = (images, embeddings, dirHandle, opts) => {
|
|
22
|
+
const imageMap = new Map(images.map(img => [img.imageId, img]));
|
|
23
|
+
return {
|
|
24
|
+
images,
|
|
25
|
+
embeddings,
|
|
26
|
+
dirHandle,
|
|
27
|
+
getImage(imageId) {
|
|
28
|
+
return imageMap.get(imageId);
|
|
29
|
+
},
|
|
30
|
+
async query(file, bbox, options) {
|
|
31
|
+
const queryEmbedding = await embedImage(file, bbox, {
|
|
32
|
+
embedderUrl: opts.embedderUrl,
|
|
33
|
+
executionProviders: opts.executionProviders,
|
|
34
|
+
});
|
|
35
|
+
return nearestNeighbours(queryEmbedding, embeddings, images, options?.topK);
|
|
36
|
+
}
|
|
37
|
+
};
|
|
38
|
+
};
|
|
39
|
+
/**
|
|
40
|
+
* Load an existing visual search index from a directory handle.
|
|
41
|
+
* @param dirHandle The directory handle containing index.json and embeddings.bin
|
|
42
|
+
* @returns The loaded index
|
|
43
|
+
*/
|
|
44
|
+
export const loadIndex = async (dirHandle, opts) => {
|
|
45
|
+
const vsDir = await dirHandle.getDirectoryHandle(DIR_NAME);
|
|
46
|
+
const jsonHandle = await vsDir.getFileHandle(INDEX_FILE);
|
|
47
|
+
const jsonFile = await jsonHandle.getFile();
|
|
48
|
+
const jsonText = await jsonFile.text();
|
|
49
|
+
const { images } = JSON.parse(jsonText);
|
|
50
|
+
const binHandle = await vsDir.getFileHandle(EMBED_FILE);
|
|
51
|
+
const binFile = await binHandle.getFile();
|
|
52
|
+
const binBuffer = await binFile.arrayBuffer();
|
|
53
|
+
const embeddings = new Float32Array(binBuffer);
|
|
54
|
+
// Validate length
|
|
55
|
+
const totalSegments = images.reduce((n, img) => n + img.segments.length, 0);
|
|
56
|
+
if (embeddings.length !== totalSegments * EMBEDDING_DIM)
|
|
57
|
+
throw new Error(`embeddings.bin length mismatch: expected ${totalSegments * EMBEDDING_DIM}, got ${embeddings.length}`);
|
|
58
|
+
return createIndex(images, embeddings, dirHandle, opts);
|
|
59
|
+
};
|
|
60
|
+
//# sourceMappingURL=load-index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"load-index.js","sourceRoot":"","sources":["../../src/search/load-index.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/segmentation/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAC"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/segmentation/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAC"}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Post-processing for FastSAM-s / YOLOv8-seg ONNX output.
|
|
3
|
+
*
|
|
4
|
+
* The model produces two output tensors:
|
|
5
|
+
*
|
|
6
|
+
* output0 [1, 37, N] where N = anchors (8400 for 640px input, 21504 for 1024px input)
|
|
7
|
+
* For each of N anchors:
|
|
8
|
+
* [0..3] cx, cy, w, h (model-input pixel space)
|
|
9
|
+
* [4] objectness confidence
|
|
10
|
+
* [5..36] 32 mask coefficients
|
|
11
|
+
*
|
|
12
|
+
* output1 [1, 32, 256, 256]
|
|
13
|
+
* 32 prototype masks (model-input space, scaled down 4×)
|
|
14
|
+
*
|
|
15
|
+
* Pipeline:
|
|
16
|
+
* 1. Filter proposals by confidence threshold
|
|
17
|
+
* 2. Convert cx/cy/w/h → x1/y1/x2/y2
|
|
18
|
+
* 3. Non-maximum suppression (NMS)
|
|
19
|
+
* 4. For each surviving detection: decode mask = sigmoid(coeffs @ protos)
|
|
20
|
+
* 5. Compute mask area, convert bbox to normalised image coords
|
|
21
|
+
*/
|
|
22
|
+
import type { BBox } from '../types.js';
|
|
23
|
+
export interface RawDetection {
|
|
24
|
+
/** Absolute pixel bbox in original image space, normalised [x,y,w,h] */
|
|
25
|
+
bbox: BBox;
|
|
26
|
+
/** Normalised mask area in [0,1] relative to original image */
|
|
27
|
+
area: number;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Parse the two ONNX output tensors from FastSAM-s into RawDetections.
|
|
31
|
+
*
|
|
32
|
+
* @param output0Data Flat data from output0 tensor [1, 37, 8400]
|
|
33
|
+
* @param output1Data Flat data from output1 tensor [1, 32, 256, 256]
|
|
34
|
+
* @param scale From letterboxToTensor()
|
|
35
|
+
* @param padX From letterboxToTensor()
|
|
36
|
+
* @param padY From letterboxToTensor()
|
|
37
|
+
* @param origW Original image width in px
|
|
38
|
+
* @param origH Original image height in px
|
|
39
|
+
*/
|
|
40
|
+
export declare const decodeDetections: (output0Data: Float32Array, output1Data: Float32Array, scale: number, padX: number, padY: number, origW: number, origH: number) => RawDetection[];
|
|
41
|
+
//# sourceMappingURL=postprocess.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"postprocess.d.ts","sourceRoot":"","sources":["../../src/segmentation/postprocess.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;GAoBG;AAGH,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAExC,MAAM,WAAW,YAAY;IAE3B,wEAAwE;IACxE,IAAI,EAAE,IAAI,CAAC;IAEX,+DAA+D;IAC/D,IAAI,EAAE,MAAM,CAAC;CAEd;AAmID;;;;;;;;;;GAUG;AACH,eAAO,MAAM,gBAAgB,GAC3B,aAAa,YAAY,EACzB,aAAa,YAAY,EACzB,OAAO,MAAM,EACb,MAAM,MAAM,EACZ,MAAM,MAAM,EACZ,OAAO,MAAM,EACb,OAAO,MAAM,KACZ,YAAY,EAwDd,CAAA"}
|
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Post-processing for FastSAM-s / YOLOv8-seg ONNX output.
|
|
3
|
+
*
|
|
4
|
+
* The model produces two output tensors:
|
|
5
|
+
*
|
|
6
|
+
* output0 [1, 37, N] where N = anchors (8400 for 640px input, 21504 for 1024px input)
|
|
7
|
+
* For each of N anchors:
|
|
8
|
+
* [0..3] cx, cy, w, h (model-input pixel space)
|
|
9
|
+
* [4] objectness confidence
|
|
10
|
+
* [5..36] 32 mask coefficients
|
|
11
|
+
*
|
|
12
|
+
* output1 [1, 32, 256, 256]
|
|
13
|
+
* 32 prototype masks (model-input space, scaled down 4×)
|
|
14
|
+
*
|
|
15
|
+
* Pipeline:
|
|
16
|
+
* 1. Filter proposals by confidence threshold
|
|
17
|
+
* 2. Convert cx/cy/w/h → x1/y1/x2/y2
|
|
18
|
+
* 3. Non-maximum suppression (NMS)
|
|
19
|
+
* 4. For each surviving detection: decode mask = sigmoid(coeffs @ protos)
|
|
20
|
+
* 5. Compute mask area, convert bbox to normalised image coords
|
|
21
|
+
*/
|
|
22
|
+
import { modelBoxToNormalisedBBox } from './preprocess.js';
|
|
23
|
+
const CONF_THRESHOLD = 0.01;
|
|
24
|
+
const IOU_THRESHOLD = 0.5;
|
|
25
|
+
const PROTO_SIZE = 256; // output1 spatial dimension
|
|
26
|
+
const NUM_PROTOS = 32;
|
|
27
|
+
// ── Helpers ──────────────────────────────────────────────────────────────────
|
|
28
|
+
const sigmoid = (x) => 1 / (1 + Math.exp(-x));
|
|
29
|
+
const intersectionArea = (ax1, ay1, ax2, ay2, bx1, by1, bx2, by2) => {
|
|
30
|
+
const ix1 = Math.max(ax1, bx1);
|
|
31
|
+
const iy1 = Math.max(ay1, by1);
|
|
32
|
+
const ix2 = Math.min(ax2, bx2);
|
|
33
|
+
const iy2 = Math.min(ay2, by2);
|
|
34
|
+
return Math.max(0, ix2 - ix1) * Math.max(0, iy2 - iy1);
|
|
35
|
+
};
|
|
36
|
+
const iou = (ax1, ay1, ax2, ay2, bx1, by1, bx2, by2) => {
|
|
37
|
+
const inter = intersectionArea(ax1, ay1, ax2, ay2, bx1, by1, bx2, by2);
|
|
38
|
+
if (inter === 0)
|
|
39
|
+
return 0;
|
|
40
|
+
const aArea = (ax2 - ax1) * (ay2 - ay1);
|
|
41
|
+
const bArea = (bx2 - bx1) * (by2 - by1);
|
|
42
|
+
return inter / (aArea + bArea - inter);
|
|
43
|
+
};
|
|
44
|
+
const nms = (proposals, iouThreshold) => {
|
|
45
|
+
// Sort descending by confidence
|
|
46
|
+
proposals.sort((a, b) => b.conf - a.conf);
|
|
47
|
+
const kept = [];
|
|
48
|
+
const suppressed = new Uint8Array(proposals.length);
|
|
49
|
+
for (let i = 0; i < proposals.length; i++) {
|
|
50
|
+
if (suppressed[i])
|
|
51
|
+
continue;
|
|
52
|
+
kept.push(proposals[i]);
|
|
53
|
+
const p = proposals[i];
|
|
54
|
+
for (let j = i + 1; j < proposals.length; j++) {
|
|
55
|
+
if (suppressed[j])
|
|
56
|
+
continue;
|
|
57
|
+
const q = proposals[j];
|
|
58
|
+
if (iou(p.x1, p.y1, p.x2, p.y2, q.x1, q.y1, q.x2, q.y2) > iouThreshold) {
|
|
59
|
+
suppressed[j] = 1;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return kept;
|
|
64
|
+
};
|
|
65
|
+
// ── Mask decoding ─────────────────────────────────────────────────────────────
|
|
66
|
+
/**
|
|
67
|
+
* Decode a single instance mask from 32 coefficients and the prototype tensor.
|
|
68
|
+
*
|
|
69
|
+
* protos: flat Float32Array of shape [32, 256, 256], row-major.
|
|
70
|
+
* Returns a flat Float32Array of shape [256, 256] with values in [0,1].
|
|
71
|
+
*/
|
|
72
|
+
const decodeMask = (coeffs, protos) => {
|
|
73
|
+
const protoSize = PROTO_SIZE * PROTO_SIZE;
|
|
74
|
+
const mask = new Float32Array(protoSize);
|
|
75
|
+
for (let k = 0; k < NUM_PROTOS; k++) {
|
|
76
|
+
const c = coeffs[k];
|
|
77
|
+
const offset = k * protoSize;
|
|
78
|
+
for (let i = 0; i < protoSize; i++) {
|
|
79
|
+
mask[i] += c * protos[offset + i];
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
// Apply sigmoid
|
|
83
|
+
for (let i = 0; i < protoSize; i++) {
|
|
84
|
+
mask[i] = sigmoid(mask[i]);
|
|
85
|
+
}
|
|
86
|
+
return mask;
|
|
87
|
+
};
|
|
88
|
+
/**
|
|
89
|
+
* Count positive mask pixels within a bbox region of the proto-space mask,
|
|
90
|
+
* then normalise to original image area.
|
|
91
|
+
*
|
|
92
|
+
* The prototype mask is 256×256 (model input / 4). The bbox coords are in
|
|
93
|
+
* the 1024×1024 model-input space, so we scale down by 4 to index into the
|
|
94
|
+
* prototype mask.
|
|
95
|
+
*/
|
|
96
|
+
const maskAreaInBBox = (mask, x1, y1, x2, y2, origW, origH, threshold = 0.5) => {
|
|
97
|
+
const scale = PROTO_SIZE / 1024;
|
|
98
|
+
const mx1 = Math.max(0, Math.floor(x1 * scale));
|
|
99
|
+
const my1 = Math.max(0, Math.floor(y1 * scale));
|
|
100
|
+
const mx2 = Math.min(PROTO_SIZE, Math.ceil(x2 * scale));
|
|
101
|
+
const my2 = Math.min(PROTO_SIZE, Math.ceil(y2 * scale));
|
|
102
|
+
let positive = 0;
|
|
103
|
+
for (let row = my1; row < my2; row++) {
|
|
104
|
+
for (let col = mx1; col < mx2; col++) {
|
|
105
|
+
if (mask[row * PROTO_SIZE + col] > threshold)
|
|
106
|
+
positive++;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
// Pixels in proto space → original image pixels → normalised area
|
|
110
|
+
const protoPixelArea = (1024 / PROTO_SIZE) * (1024 / PROTO_SIZE);
|
|
111
|
+
return (positive * protoPixelArea) / (origW * origH);
|
|
112
|
+
};
|
|
113
|
+
// ── Public interface ──────────────────────────────────────────────────────────
|
|
114
|
+
/**
|
|
115
|
+
* Parse the two ONNX output tensors from FastSAM-s into RawDetections.
|
|
116
|
+
*
|
|
117
|
+
* @param output0Data Flat data from output0 tensor [1, 37, 8400]
|
|
118
|
+
* @param output1Data Flat data from output1 tensor [1, 32, 256, 256]
|
|
119
|
+
* @param scale From letterboxToTensor()
|
|
120
|
+
* @param padX From letterboxToTensor()
|
|
121
|
+
* @param padY From letterboxToTensor()
|
|
122
|
+
* @param origW Original image width in px
|
|
123
|
+
* @param origH Original image height in px
|
|
124
|
+
*/
|
|
125
|
+
export const decodeDetections = (output0Data, output1Data, scale, padX, padY, origW, origH) => {
|
|
126
|
+
const NUM_ATTRS = 37; // 4 box + 1 conf + 32 coeffs
|
|
127
|
+
const NUM_ANCHORS = output0Data.length / NUM_ATTRS; // 8400 for 640px input, 21504 for 1024px
|
|
128
|
+
// ── Step 1: collect proposals above confidence threshold ──────────────────
|
|
129
|
+
const proposals = [];
|
|
130
|
+
for (let a = 0; a < NUM_ANCHORS; a++) {
|
|
131
|
+
// output0 is [1, 37, 8400] — iterate over the anchor dimension
|
|
132
|
+
const conf = output0Data[4 * NUM_ANCHORS + a];
|
|
133
|
+
if (conf < CONF_THRESHOLD)
|
|
134
|
+
continue;
|
|
135
|
+
const cx = output0Data[0 * NUM_ANCHORS + a];
|
|
136
|
+
const cy = output0Data[1 * NUM_ANCHORS + a];
|
|
137
|
+
const w = output0Data[2 * NUM_ANCHORS + a];
|
|
138
|
+
const h = output0Data[3 * NUM_ANCHORS + a];
|
|
139
|
+
const x1 = cx - w / 2;
|
|
140
|
+
const y1 = cy - h / 2;
|
|
141
|
+
const x2 = cx + w / 2;
|
|
142
|
+
const y2 = cy + h / 2;
|
|
143
|
+
const coeffs = new Float32Array(NUM_PROTOS);
|
|
144
|
+
for (let k = 0; k < NUM_PROTOS; k++) {
|
|
145
|
+
coeffs[k] = output0Data[(5 + k) * NUM_ANCHORS + a];
|
|
146
|
+
}
|
|
147
|
+
proposals.push({ x1, y1, x2, y2, conf, coeffs });
|
|
148
|
+
}
|
|
149
|
+
if (proposals.length === 0)
|
|
150
|
+
return [];
|
|
151
|
+
// ── Step 2: NMS ───────────────────────────────────────────────────────────
|
|
152
|
+
const kept = nms(proposals, IOU_THRESHOLD);
|
|
153
|
+
// ── Step 3: decode masks + compute areas ─────────────────────────────────
|
|
154
|
+
// output1 is [1, 32, 256, 256] — drop the batch dimension
|
|
155
|
+
const protos = output1Data.subarray(0);
|
|
156
|
+
const results = [];
|
|
157
|
+
for (const p of kept) {
|
|
158
|
+
const mask = decodeMask(p.coeffs, protos);
|
|
159
|
+
const area = maskAreaInBBox(mask, p.x1, p.y1, p.x2, p.y2, origW, origH);
|
|
160
|
+
const bbox = modelBoxToNormalisedBBox(p.x1, p.y1, p.x2, p.y2, scale, padX, padY, origW, origH);
|
|
161
|
+
results.push({ bbox, area });
|
|
162
|
+
}
|
|
163
|
+
// Sort largest-first, matching server-side behaviour
|
|
164
|
+
return results.sort((a, b) => b.area - a.area);
|
|
165
|
+
};
|
|
166
|
+
//# sourceMappingURL=postprocess.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"postprocess.js","sourceRoot":"","sources":["../../src/segmentation/postprocess.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image pre-processing for FastSAM inference.
|
|
3
|
+
*
|
|
4
|
+
* FastSAM-s expects a [1, 3, 1024, 1024] float32 tensor.
|
|
5
|
+
* Pixel values normalised to [0, 1], RGB channel order.
|
|
6
|
+
* The image is letterboxed (aspect ratio preserved, padded with 0.5 grey).
|
|
7
|
+
*/
|
|
8
|
+
export declare const MODEL_INPUT_SIZE = 1280;
|
|
9
|
+
export interface LetterboxResult {
|
|
10
|
+
/** [1, 3, H, W] flat, row-major */
|
|
11
|
+
tensor: Float32Array;
|
|
12
|
+
/** Scale factor applied (same for x and y — uniform scaling). */
|
|
13
|
+
scale: number;
|
|
14
|
+
/** Padding applied to left/right (px, in model-input space). */
|
|
15
|
+
padX: number;
|
|
16
|
+
/** Padding applied to top/bottom (px, in model-input space). */
|
|
17
|
+
padY: number;
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Letterbox an ImageBitmap to MODEL_INPUT_SIZE × MODEL_INPUT_SIZE,
|
|
21
|
+
* then return a normalised float32 tensor and the transform parameters
|
|
22
|
+
* needed to map detections back to the original image coordinate space.
|
|
23
|
+
*/
|
|
24
|
+
export declare const letterboxToTensor: (bitmap: ImageBitmap) => LetterboxResult;
|
|
25
|
+
/**
|
|
26
|
+
* Map a model-space bbox [x1, y1, x2, y2] (absolute pixels in the
|
|
27
|
+
* letterboxed 1024×1024 space) back to normalised [x, y, w, h]
|
|
28
|
+
* coordinates in the original image space.
|
|
29
|
+
*/
|
|
30
|
+
export declare const modelBoxToNormalisedBBox: (x1: number, y1: number, x2: number, y2: number, scale: number, padX: number, padY: number, origW: number, origH: number) => [number, number, number, number];
|
|
31
|
+
//# sourceMappingURL=preprocess.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"preprocess.d.ts","sourceRoot":"","sources":["../../src/segmentation/preprocess.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,eAAO,MAAM,gBAAgB,OAAO,CAAC;AAErC,MAAM,WAAW,eAAe;IAE9B,mCAAmC;IACnC,MAAM,EAAE,YAAY,CAAC;IAErB,iEAAiE;IACjE,KAAK,EAAE,MAAM,CAAC;IAEd,gEAAgE;IAChE,IAAI,EAAE,MAAM,CAAC;IAEb,gEAAgE;IAChE,IAAI,EAAE,MAAM,CAAC;CAEd;AAED;;;;GAIG;AACH,eAAO,MAAM,iBAAiB,GAAI,QAAQ,WAAW,KAAG,eAoCvD,CAAA;AAED;;;;GAIG;AACH,eAAO,MAAM,wBAAwB,GACnC,IAAI,MAAM,EAAE,IAAI,MAAM,EAAE,IAAI,MAAM,EAAE,IAAI,MAAM,EAC9C,OAAO,MAAM,EAAE,MAAM,MAAM,EAAE,MAAM,MAAM,EACzC,OAAO,MAAM,EAAE,OAAO,MAAM,KAC3B,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAajC,CAAA"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Image pre-processing for FastSAM inference.
|
|
3
|
+
*
|
|
4
|
+
* FastSAM-s expects a [1, 3, 1024, 1024] float32 tensor.
|
|
5
|
+
* Pixel values normalised to [0, 1], RGB channel order.
|
|
6
|
+
* The image is letterboxed (aspect ratio preserved, padded with 0.5 grey).
|
|
7
|
+
*/
|
|
8
|
+
export const MODEL_INPUT_SIZE = 1280;
|
|
9
|
+
/**
|
|
10
|
+
* Letterbox an ImageBitmap to MODEL_INPUT_SIZE × MODEL_INPUT_SIZE,
|
|
11
|
+
* then return a normalised float32 tensor and the transform parameters
|
|
12
|
+
* needed to map detections back to the original image coordinate space.
|
|
13
|
+
*/
|
|
14
|
+
export const letterboxToTensor = (bitmap) => {
|
|
15
|
+
const S = MODEL_INPUT_SIZE;
|
|
16
|
+
const { width: srcW, height: srcH } = bitmap;
|
|
17
|
+
// Uniform scale so the longest side fits in S
|
|
18
|
+
const scale = Math.min(S / srcW, S / srcH);
|
|
19
|
+
const scaledW = Math.round(srcW * scale);
|
|
20
|
+
const scaledH = Math.round(srcH * scale);
|
|
21
|
+
// Padding to centre the scaled image
|
|
22
|
+
const padX = Math.floor((S - scaledW) / 2);
|
|
23
|
+
const padY = Math.floor((S - scaledH) / 2);
|
|
24
|
+
// Draw into an off-screen canvas
|
|
25
|
+
const canvas = new OffscreenCanvas(S, S);
|
|
26
|
+
const ctx = canvas.getContext('2d');
|
|
27
|
+
// Fill with mid-grey (matching Ultralytics letterbox default: 114/255 ≈ 0.447)
|
|
28
|
+
ctx.fillStyle = `rgb(114,114,114)`;
|
|
29
|
+
ctx.fillRect(0, 0, S, S);
|
|
30
|
+
ctx.drawImage(bitmap, padX, padY, scaledW, scaledH);
|
|
31
|
+
const imageData = ctx.getImageData(0, 0, S, S);
|
|
32
|
+
const { data } = imageData; // Uint8ClampedArray, RGBA interleaved
|
|
33
|
+
// Convert RGBA interleaved → RGB planar, normalised to [0,1]
|
|
34
|
+
const tensor = new Float32Array(3 * S * S);
|
|
35
|
+
const planeSize = S * S;
|
|
36
|
+
for (let i = 0; i < planeSize; i++) {
|
|
37
|
+
tensor[i] = data[i * 4] / 255; // R
|
|
38
|
+
tensor[planeSize + i] = data[i * 4 + 1] / 255; // G
|
|
39
|
+
tensor[planeSize * 2 + i] = data[i * 4 + 2] / 255; // B
|
|
40
|
+
}
|
|
41
|
+
return { tensor, scale, padX, padY };
|
|
42
|
+
};
|
|
43
|
+
/**
|
|
44
|
+
* Map a model-space bbox [x1, y1, x2, y2] (absolute pixels in the
|
|
45
|
+
* letterboxed 1024×1024 space) back to normalised [x, y, w, h]
|
|
46
|
+
* coordinates in the original image space.
|
|
47
|
+
*/
|
|
48
|
+
export const modelBoxToNormalisedBBox = (x1, y1, x2, y2, scale, padX, padY, origW, origH) => {
|
|
49
|
+
// Remove padding, undo scale
|
|
50
|
+
const ox1 = Math.max(0, (x1 - padX) / scale);
|
|
51
|
+
const oy1 = Math.max(0, (y1 - padY) / scale);
|
|
52
|
+
const ox2 = Math.min(origW, (x2 - padX) / scale);
|
|
53
|
+
const oy2 = Math.min(origH, (y2 - padY) / scale);
|
|
54
|
+
return [
|
|
55
|
+
ox1 / origW,
|
|
56
|
+
oy1 / origH,
|
|
57
|
+
(ox2 - ox1) / origW,
|
|
58
|
+
(oy2 - oy1) / origH,
|
|
59
|
+
];
|
|
60
|
+
};
|
|
61
|
+
//# sourceMappingURL=preprocess.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"preprocess.js","sourceRoot":"","sources":["../../src/segmentation/preprocess.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,gBAAgB,GAAG,IAAI,CAAC;AAkBrC;;;;GAIG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,CAAC,MAAmB,EAAmB,EAAE;IACxE,MAAM,CAAC,GAAG,gBAAgB,CAAC;IAC3B,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,CAAC;IAE7C,8CAA8C;IAC9C,MAAM,KAAK,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,IAAI,EAAE,CAAC,GAAG,IAAI,CAAC,CAAC;IAC3C,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,GAAG,KAAK,CAAC,CAAC;IACzC,MAAM,OAAO,GAAG,IAAI,CAAC,KAAK,CAAC,IAAI,GAAG,KAAK,CAAC,CAAC;IAEzC,qCAAqC;IACrC,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC;IAC3C,MAAM,IAAI,GAAG,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC;IAE3C,iCAAiC;IACjC,MAAM,MAAM,GAAG,IAAI,eAAe,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC;IACzC,MAAM,GAAG,GAAG,MAAM,CAAC,UAAU,CAAC,IAAI,CAAE,CAAC;IAErC,+EAA+E;IAC/E,GAAG,CAAC,SAAS,GAAG,kBAAkB,CAAC;IACnC,GAAG,CAAC,QAAQ,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;IACzB,GAAG,CAAC,SAAS,CAAC,MAAM,EAAE,IAAI,EAAE,IAAI,EAAE,OAAO,EAAE,OAAO,CAAC,CAAC;IAEpD,MAAM,SAAS,GAAG,GAAG,CAAC,YAAY,CAAC,CAAC,EAAE,CAAC,EAAE,CAAC,EAAE,CAAC,CAAC,CAAC;IAC/C,MAAM,EAAE,IAAI,EAAE,GAAG,SAAS,CAAC,CAAC,sCAAsC;IAElE,6DAA6D;IAC7D,MAAM,MAAM,GAAG,IAAI,YAAY,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;IAC3C,MAAM,SAAS,GAAG,CAAC,GAAG,CAAC,CAAC;IAExB,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,SAAS,EAAE,CAAC,EAAE,EAAE,CAAC;QACnC,MAAM,CAAC,CAAC,CAAC,GAAqB,IAAI,CAAC,CAAC,GAAG,CAAC,CAAC,GAAO,GAAG,CAAC,CAAC,IAAI;QACzD,MAAM,CAAC,SAAS,GAAG,CAAC,CAAC,GAAS,IAAI,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,IAAI;QACzD,MAAM,CAAC,SAAS,GAAG,CAAC,GAAG,CAAC,CAAC,GAAK,IAAI,CAAC,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,GAAG,GAAG,CAAC,CAAC,IAAI;IAC3D,CAAC;IAED,OAAO,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,CAAC;AACvC,CAAC,CAAA;AAED;;;;GAIG;AACH,MAAM,CAAC,MAAM,wBAAwB,GAAG,CACtC,EAAU,EAAE,EAAU,EAAE,EAAU,EAAE,EAAU,EAC9C,KAAa,EAAE,IAAY,EAAE,IAAY,EACzC,KAAa,EAAE,KAAa,EACM,EAAE;IACpC,6BAA6B;IAC7B,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,EAAE,GAAG,IAAI,CAAC,GAAG,KAAK,CAAC,CAAC;IAC7C,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,CAAC,EAAE,GAAG,IAAI,CAAC,GAAG,KAAK,CAAC,CAAC;IAC7C,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC,EAAE,GAAG,IAAI,CAAC,GAAG,KAAK,CAAC,CAAC;IACjD,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC,EAAE,GAAG,IAAI,CAAC,GAAG,KAAK,CAAC,CAAC;IAEjD,OAAO;QACL,GAAG,GAAG,KAAK;QACX,GAAG,GAAG,KAAK;QACX,CAAC,GAAG,GAAG,GAAG,CAAC,GAAG,KAAK;QACnB,CAAC,GAAG,GAAG,GAAG,CAAC,GAAG,KAAK;KACpB,CAAC;AACJ,CAAC,CAAA"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"segment.d.ts","sourceRoot":"","sources":["../../src/segmentation/segment.ts"],"names":[],"mappings":"AAGA,OAAO,KAAK,EAAE,IAAI,EAAE,MAAM,aAAa,CAAC;AAUxC,eAAO,MAAM,YAAY,GACvB,MAAM,IAAI,EACV,UAAS;IAAE,YAAY,EAAE,MAAM,CAAC;IAAC,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAA;CAAyB,KACtF,OAAO,CAAC,KAAK,CAAC;IAAE,IAAI,EAAE,IAAI,CAAC;IAAC,IAAI,EAAE,MAAM,CAAA;CAAE,CAAC,CAyB7C,CAAA"}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import * as ort from 'onnxruntime-web';
|
|
2
|
+
import { letterboxToTensor, MODEL_INPUT_SIZE } from './preprocess.js';
|
|
3
|
+
import { decodeDetections } from './postprocess.js';
|
|
4
|
+
let segmenterSession = null;
|
|
5
|
+
const loadSegmenter = async (url, providers = ['wasm']) => {
|
|
6
|
+
if (!segmenterSession)
|
|
7
|
+
segmenterSession = await ort.InferenceSession.create(url, { executionProviders: providers });
|
|
8
|
+
return segmenterSession;
|
|
9
|
+
};
|
|
10
|
+
export const segmentImage = async (file, options = { segmenterUrl: '' }) => {
|
|
11
|
+
if (!options.segmenterUrl)
|
|
12
|
+
throw new Error('segmenterUrl is required');
|
|
13
|
+
const bitmap = await createImageBitmap(file);
|
|
14
|
+
const segmenter = await loadSegmenter(options.segmenterUrl, options.executionProviders);
|
|
15
|
+
const { tensor, scale, padX, padY } = letterboxToTensor(bitmap);
|
|
16
|
+
const { width: origW, height: origH } = bitmap;
|
|
17
|
+
const inputTensor = new ort.Tensor('float32', tensor, [1, 3, MODEL_INPUT_SIZE, MODEL_INPUT_SIZE]);
|
|
18
|
+
const feeds = { [segmenter.inputNames[0]]: inputTensor };
|
|
19
|
+
const output = await segmenter.run(feeds);
|
|
20
|
+
const output0 = output[segmenter.outputNames[0]];
|
|
21
|
+
const output1 = output[segmenter.outputNames[1]];
|
|
22
|
+
const detections = decodeDetections(output0.data, output1.data, scale, padX, padY, origW, origH);
|
|
23
|
+
bitmap.close();
|
|
24
|
+
return detections;
|
|
25
|
+
};
|
|
26
|
+
//# sourceMappingURL=segment.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"segment.js","sourceRoot":"","sources":["../../src/segmentation/segment.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,GAAG,MAAM,iBAAiB,CAAC;AACvC,OAAO,EAAE,iBAAiB,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AACtE,OAAO,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AAGpD,IAAI,gBAAgB,GAAgC,IAAI,CAAC;AAEzD,MAAM,aAAa,GAAG,KAAK,EAAE,GAAW,EAAE,YAAsB,CAAC,MAAM,CAAC,EAAiC,EAAE;IACzG,IAAI,CAAC,gBAAgB;QACnB,gBAAgB,GAAG,MAAM,GAAG,CAAC,gBAAgB,CAAC,MAAM,CAAC,GAAG,EAAE,EAAE,kBAAkB,EAAE,SAAS,EAAE,CAAC,CAAC;IAC/F,OAAO,gBAAgB,CAAC;AAC1B,CAAC,CAAA;AAED,MAAM,CAAC,MAAM,YAAY,GAAG,KAAK,EAC/B,IAAU,EACV,UAAmE,EAAE,YAAY,EAAE,EAAE,EAAE,EACzC,EAAE;IAChD,IAAI,CAAC,OAAO,CAAC,YAAY;QAAE,MAAM,IAAI,KAAK,CAAC,0BAA0B,CAAC,CAAC;IAEvE,MAAM,MAAM,GAAG,MAAM,iBAAiB,CAAC,IAAI,CAAC,CAAC;IAC7C,MAAM,SAAS,GAAG,MAAM,aAAa,CAAC,OAAO,CAAC,YAAY,EAAE,OAAO,CAAC,kBAAkB,CAAC,CAAC;IAExF,MAAM,EAAE,MAAM,EAAE,KAAK,EAAE,IAAI,EAAE,IAAI,EAAE,GAAG,iBAAiB,CAAC,MAAM,CAAC,CAAC;IAChE,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,MAAM,CAAC;IAE/C,MAAM,WAAW,GAAG,IAAI,GAAG,CAAC,MAAM,CAAC,SAAS,EAAE,MAAM,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,gBAAgB,EAAE,gBAAgB,CAAC,CAAC,CAAC;IAClG,MAAM,KAAK,GAAG,EAAE,CAAC,SAAS,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,CAAC;IACzD,MAAM,MAAM,GAAG,MAAM,SAAS,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;IAE1C,MAAM,OAAO,GAAG,MAAM,CAAC,SAAS,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC;IACjD,MAAM,OAAO,GAAG,MAAM,CAAC,SAAS,CAAC,WAAW,CAAC,CAAC,CAAC,CAAC,CAAC;IAEjD,MAAM,UAAU,GAAG,gBAAgB,CACjC,OAAO,CAAC,IAAoB,EAC5B,OAAO,CAAC,IAAoB,EAC5B,KAAK,EAAE,IAAI,EAAE,IAAI,EACjB,KAAK,EAAE,KAAK,CACb,CAAC;IAEF,MAAM,CAAC,KAAK,EAAE,CAAC;IACf,OAAO,UAAU,CAAC;AACpB,CAAC,CAAA"}
|
package/dist/types.d.ts
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export type BBox = [number, number, number, number];
|
|
2
|
+
export interface Segment {
|
|
3
|
+
bbox: BBox;
|
|
4
|
+
area: number;
|
|
5
|
+
embedding: Float32Array;
|
|
6
|
+
}
|
|
7
|
+
export interface IndexedImageSegment {
|
|
8
|
+
bbox: BBox;
|
|
9
|
+
area: number;
|
|
10
|
+
embeddingRow: number;
|
|
11
|
+
}
|
|
12
|
+
export interface IndexedImage {
|
|
13
|
+
imageId: string;
|
|
14
|
+
indexedAt: string;
|
|
15
|
+
segments: IndexedImageSegment[];
|
|
16
|
+
}
|
|
17
|
+
export interface VisualSearchIndex {
|
|
18
|
+
readonly images: ReadonlyArray<IndexedImage>;
|
|
19
|
+
readonly embeddings: Float32Array;
|
|
20
|
+
readonly dirHandle: FileSystemDirectoryHandle;
|
|
21
|
+
query(file: File, bbox?: BBox, options?: SearchOptions): Promise<SearchResult[]>;
|
|
22
|
+
getImage(imageId: string): IndexedImage | undefined;
|
|
23
|
+
}
|
|
24
|
+
export interface SearchOptions {
|
|
25
|
+
topK?: number;
|
|
26
|
+
}
|
|
27
|
+
export interface SearchResult {
|
|
28
|
+
imageId: string;
|
|
29
|
+
bbox: BBox;
|
|
30
|
+
area: number;
|
|
31
|
+
score: number;
|
|
32
|
+
}
|
|
33
|
+
//# sourceMappingURL=types.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,IAAI,GAAG,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC;AAEpD,MAAM,WAAW,OAAO;IAEtB,IAAI,EAAE,IAAI,CAAC;IAEX,IAAI,EAAE,MAAM,CAAC;IAEb,SAAS,EAAE,YAAY,CAAC;CAEzB;AAED,MAAM,WAAW,mBAAmB;IAElC,IAAI,EAAE,IAAI,CAAC;IAEX,IAAI,EAAE,MAAM,CAAC;IAEb,YAAY,EAAE,MAAM,CAAC;CAEtB;AAED,MAAM,WAAW,YAAY;IAE3B,OAAO,EAAE,MAAM,CAAC;IAEhB,SAAS,EAAE,MAAM,CAAC;IAElB,QAAQ,EAAE,mBAAmB,EAAE,CAAC;CAEjC;AAED,MAAM,WAAW,iBAAiB;IAEhC,QAAQ,CAAC,MAAM,EAAE,aAAa,CAAC,YAAY,CAAC,CAAC;IAE7C,QAAQ,CAAC,UAAU,EAAE,YAAY,CAAC;IAElC,QAAQ,CAAC,SAAS,EAAE,yBAAyB,CAAC;IAE9C,KAAK,CAAC,IAAI,EAAE,IAAI,EAAE,IAAI,CAAC,EAAE,IAAI,EAAE,OAAO,CAAC,EAAE,aAAa,GAAG,OAAO,CAAC,YAAY,EAAE,CAAC,CAAC;IAEjF,QAAQ,CAAC,OAAO,EAAE,MAAM,GAAG,YAAY,GAAG,SAAS,CAAC;CAErD;AAED,MAAM,WAAW,aAAa;IAE5B,IAAI,CAAC,EAAE,MAAM,CAAC;CAEf;AAED,MAAM,WAAW,YAAY;IAE3B,OAAO,EAAE,MAAM,CAAC;IAEhB,IAAI,EAAE,IAAI,CAAC;IAEX,IAAI,EAAE,MAAM,CAAC;IAEb,KAAK,EAAE,MAAM,CAAC;CAEf"}
|
package/dist/types.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
package/package.json
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "browser-visual-search",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "In-browser visual search powered by FastSAM, CLIP and onnxruntime-web.",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"sideEffects": false,
|
|
7
|
+
"main": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"import": "./dist/index.js",
|
|
12
|
+
"types": "./dist/index.d.ts"
|
|
13
|
+
}
|
|
14
|
+
},
|
|
15
|
+
"files": [
|
|
16
|
+
"dist"
|
|
17
|
+
],
|
|
18
|
+
"scripts": {
|
|
19
|
+
"start": "vite",
|
|
20
|
+
"build": "tsc",
|
|
21
|
+
"test": "vitest"
|
|
22
|
+
},
|
|
23
|
+
"peerDependencies": {
|
|
24
|
+
"onnxruntime-web": "^1.24.3"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@vitest/browser": "^4.1.0",
|
|
28
|
+
"@vitest/browser-playwright": "^4.1.0",
|
|
29
|
+
"onnxruntime-web": "^1.24.3",
|
|
30
|
+
"playwright": "^1.58.2",
|
|
31
|
+
"typescript": "^5.9.3",
|
|
32
|
+
"vite-plugin-static-copy": "^3.3.0",
|
|
33
|
+
"vitest": "^4.1.0"
|
|
34
|
+
},
|
|
35
|
+
"keywords": [
|
|
36
|
+
"visual-search",
|
|
37
|
+
"clip",
|
|
38
|
+
"fastsam",
|
|
39
|
+
"onnx",
|
|
40
|
+
"browser",
|
|
41
|
+
"image-segmentation",
|
|
42
|
+
"embeddings"
|
|
43
|
+
],
|
|
44
|
+
"license": "MIT"
|
|
45
|
+
}
|