@ruvector/cnn 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/README.md +301 -0
- package/index.d.ts +164 -0
- package/index.js +266 -0
- package/package.json +50 -0
- package/ruvector_cnn_wasm.d.ts +204 -0
- package/ruvector_cnn_wasm.js +804 -0
- package/ruvector_cnn_wasm_bg.wasm +0 -0
- package/ruvector_cnn_wasm_bg.wasm.d.ts +39 -0
package/README.md
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
# @ruvector/cnn
|
|
2
|
+
|
|
3
|
+
[](https://www.npmjs.com/package/@ruvector/cnn)
|
|
4
|
+
[](https://opensource.org/licenses/MIT)
|
|
5
|
+
|
|
6
|
+
**Turn images into searchable vectors** — runs in browsers, no backend needed.
|
|
7
|
+
|
|
8
|
+
## What Does This Do?
|
|
9
|
+
|
|
10
|
+
This package converts images into numbers (called "embeddings") that describe what's in the picture. Similar images produce similar numbers.
|
|
11
|
+
|
|
12
|
+
**Use it to:**
|
|
13
|
+
- Build "find similar images" features
|
|
14
|
+
- Group photos by what they show
|
|
15
|
+
- Create visual search for products
|
|
16
|
+
- Compare images without AI APIs
|
|
17
|
+
|
|
18
|
+
```javascript
|
|
19
|
+
import { init, CnnEmbedder } from '@ruvector/cnn';
|
|
20
|
+
|
|
21
|
+
await init();
|
|
22
|
+
const embedder = new CnnEmbedder();
|
|
23
|
+
|
|
24
|
+
// Turn an image into numbers
|
|
25
|
+
const numbers = embedder.extract(imagePixels, 224, 224);
|
|
26
|
+
|
|
27
|
+
// Compare two images (1.0 = identical, 0 = unrelated)
|
|
28
|
+
const similarity = embedder.cosineSimilarity(numbers1, numbers2);
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Why Use This?
|
|
32
|
+
|
|
33
|
+
| What You Get | Why It Matters |
|
|
34
|
+
|--------------|----------------|
|
|
35
|
+
| Runs in the browser | No server costs, instant results |
|
|
36
|
+
| ~5ms per image | Fast enough for real-time |
|
|
37
|
+
| ~900KB download | Small enough for any website |
|
|
38
|
+
| No API calls | Works offline, no per-image fees |
|
|
39
|
+
| Training included | Teach it your own categories |
|
|
40
|
+
|
|
41
|
+
## Installation
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
npm install @ruvector/cnn
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
## How to Use It
|
|
48
|
+
|
|
49
|
+
### 1. Extract Image Features
|
|
50
|
+
|
|
51
|
+
```javascript
|
|
52
|
+
import { init, CnnEmbedder } from '@ruvector/cnn';
|
|
53
|
+
|
|
54
|
+
// Start the engine (do this once)
|
|
55
|
+
await init();
|
|
56
|
+
|
|
57
|
+
// Create the feature extractor
|
|
58
|
+
const embedder = new CnnEmbedder({
|
|
59
|
+
embeddingDim: 512, // How many numbers per image
|
|
60
|
+
normalize: true // Make comparisons easier
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// Get pixels from an image (RGB, no transparency)
|
|
64
|
+
// Each pixel has 3 values: red, green, blue (0-255)
|
|
65
|
+
const pixels = new Uint8Array(224 * 224 * 3);
|
|
66
|
+
|
|
67
|
+
// Turn pixels into 512 numbers that describe the image
|
|
68
|
+
const features = embedder.extract(pixels, 224, 224);
|
|
69
|
+
console.log('Got', features.length, 'numbers'); // 512
|
|
70
|
+
```
|
|
71
|
+
|
|
72
|
+
### 2. Compare Two Images
|
|
73
|
+
|
|
74
|
+
```javascript
|
|
75
|
+
const features1 = embedder.extract(image1Pixels, 224, 224);
|
|
76
|
+
const features2 = embedder.extract(image2Pixels, 224, 224);
|
|
77
|
+
|
|
78
|
+
// How similar are they? (1.0 = same, 0 = different, -1 = opposite)
|
|
79
|
+
const score = embedder.cosineSimilarity(features1, features2);
|
|
80
|
+
|
|
81
|
+
if (score > 0.8) {
|
|
82
|
+
console.log('These images are very similar!');
|
|
83
|
+
} else if (score > 0.5) {
|
|
84
|
+
console.log('These images have some things in common');
|
|
85
|
+
} else {
|
|
86
|
+
console.log('These images are different');
|
|
87
|
+
}
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
### 3. Find the Most Similar Image
|
|
91
|
+
|
|
92
|
+
```javascript
|
|
93
|
+
// Your collection of images (already converted to features)
|
|
94
|
+
const catalog = [
|
|
95
|
+
{ name: 'red-shoe.jpg', features: embedder.extract(redShoePixels, 224, 224) },
|
|
96
|
+
{ name: 'blue-bag.jpg', features: embedder.extract(blueBagPixels, 224, 224) },
|
|
97
|
+
{ name: 'red-dress.jpg', features: embedder.extract(redDressPixels, 224, 224) },
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
// User uploads a photo
|
|
101
|
+
const userPhoto = embedder.extract(uploadedPixels, 224, 224);
|
|
102
|
+
|
|
103
|
+
// Find the best match
|
|
104
|
+
let bestMatch = null;
|
|
105
|
+
let bestScore = -1;
|
|
106
|
+
|
|
107
|
+
for (const item of catalog) {
|
|
108
|
+
const score = embedder.cosineSimilarity(userPhoto, item.features);
|
|
109
|
+
if (score > bestScore) {
|
|
110
|
+
bestScore = score;
|
|
111
|
+
bestMatch = item.name;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
console.log('Best match:', bestMatch, 'Score:', bestScore);
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
### 4. Get Pixels from a Canvas
|
|
119
|
+
|
|
120
|
+
```javascript
|
|
121
|
+
// If you have an image in a canvas element
|
|
122
|
+
const canvas = document.getElementById('myCanvas');
|
|
123
|
+
const ctx = canvas.getContext('2d');
|
|
124
|
+
|
|
125
|
+
// Get the pixel data
|
|
126
|
+
const imageData = ctx.getImageData(0, 0, 224, 224);
|
|
127
|
+
|
|
128
|
+
// Canvas gives RGBA (4 values per pixel), we need RGB (3 values)
|
|
129
|
+
const rgb = new Uint8Array(224 * 224 * 3);
|
|
130
|
+
for (let i = 0, j = 0; i < imageData.data.length; i += 4, j += 3) {
|
|
131
|
+
rgb[j] = imageData.data[i]; // Red
|
|
132
|
+
rgb[j + 1] = imageData.data[i + 1]; // Green
|
|
133
|
+
rgb[j + 2] = imageData.data[i + 2]; // Blue
|
|
134
|
+
// Skip alpha (imageData.data[i + 3])
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const features = embedder.extract(rgb, 224, 224);
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
## Training (Teaching It Your Categories)
|
|
141
|
+
|
|
142
|
+
You can train the model to be better at recognizing your specific images.
|
|
143
|
+
|
|
144
|
+
### Contrastive Training (SimCLR style)
|
|
145
|
+
|
|
146
|
+
Show it pairs of images that should match:
|
|
147
|
+
|
|
148
|
+
```javascript
|
|
149
|
+
import { init, InfoNCELoss, CnnEmbedder } from '@ruvector/cnn';
|
|
150
|
+
|
|
151
|
+
await init();
|
|
152
|
+
|
|
153
|
+
const embedder = new CnnEmbedder();
|
|
154
|
+
const trainer = new InfoNCELoss(0.1); // Lower = stricter matching
|
|
155
|
+
|
|
156
|
+
// Get features for your training pairs
|
|
157
|
+
// Pairs: image1 and image1_different_angle should match
|
|
158
|
+
const image1 = embedder.extract(photo1, 224, 224);
|
|
159
|
+
const image1_alt = embedder.extract(photo1_rotated, 224, 224);
|
|
160
|
+
|
|
161
|
+
// Pack into one array: [view1s..., view2s...]
|
|
162
|
+
const batch = new Float32Array(2 * 512);
|
|
163
|
+
batch.set(image1, 0);
|
|
164
|
+
batch.set(image1_alt, 512);
|
|
165
|
+
|
|
166
|
+
const loss = trainer.forward(batch, 1, 512);
|
|
167
|
+
console.log('Loss:', loss); // Lower is better
|
|
168
|
+
```
|
|
169
|
+
|
|
170
|
+
### Triplet Training
|
|
171
|
+
|
|
172
|
+
Show it: "A is similar to B, but different from C"
|
|
173
|
+
|
|
174
|
+
```javascript
|
|
175
|
+
import { init, TripletLoss, CnnEmbedder } from '@ruvector/cnn';
|
|
176
|
+
|
|
177
|
+
await init();
|
|
178
|
+
|
|
179
|
+
const embedder = new CnnEmbedder();
|
|
180
|
+
const trainer = new TripletLoss(1.0); // margin
|
|
181
|
+
|
|
182
|
+
// Anchor: the reference image
|
|
183
|
+
// Positive: should be similar to anchor
|
|
184
|
+
// Negative: should be different from anchor
|
|
185
|
+
const anchor = embedder.extract(redShoePhoto, 224, 224);
|
|
186
|
+
const positive = embedder.extract(redShoePhoto2, 224, 224); // Same shoe
|
|
187
|
+
const negative = embedder.extract(blueBagPhoto, 224, 224); // Different item
|
|
188
|
+
|
|
189
|
+
const loss = trainer.forward(
|
|
190
|
+
new Float32Array(anchor),
|
|
191
|
+
new Float32Array(positive),
|
|
192
|
+
new Float32Array(negative),
|
|
193
|
+
512
|
|
194
|
+
);
|
|
195
|
+
console.log('Loss:', loss);
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
## Fast Math Operations
|
|
199
|
+
|
|
200
|
+
If you're building custom features, these are optimized:
|
|
201
|
+
|
|
202
|
+
```javascript
|
|
203
|
+
import { init, SimdOps, LayerOps } from '@ruvector/cnn';
|
|
204
|
+
|
|
205
|
+
await init();
|
|
206
|
+
|
|
207
|
+
// Dot product (sum of element-wise multiplication)
|
|
208
|
+
const a = new Float32Array([1, 2, 3, 4]);
|
|
209
|
+
const b = new Float32Array([5, 6, 7, 8]);
|
|
210
|
+
const result = SimdOps.dotProduct(a, b); // 70
|
|
211
|
+
|
|
212
|
+
// ReLU: set negative values to 0
|
|
213
|
+
const data = new Float32Array([-1, 0, 1, 7]);
|
|
214
|
+
SimdOps.relu(data); // [0, 0, 1, 7]
|
|
215
|
+
|
|
216
|
+
// ReLU6: clamp between 0 and 6
|
|
217
|
+
SimdOps.relu6(data); // [0, 0, 1, 6]
|
|
218
|
+
|
|
219
|
+
// L2 normalize (make length = 1)
|
|
220
|
+
SimdOps.l2Normalize(data);
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
## Performance
|
|
224
|
+
|
|
225
|
+
| What | How Long | Notes |
|
|
226
|
+
|------|----------|-------|
|
|
227
|
+
| Extract features (224×224 image) | ~5ms | With SIMD |
|
|
228
|
+
| Compare two images | ~0.01ms | Just math |
|
|
229
|
+
| Training step | ~1ms | Per batch |
|
|
230
|
+
| First load | ~100ms | Downloads WASM |
|
|
231
|
+
|
|
232
|
+
## Browser Support
|
|
233
|
+
|
|
234
|
+
Works in all modern browsers with WebAssembly:
|
|
235
|
+
- Chrome 57+
|
|
236
|
+
- Firefox 52+
|
|
237
|
+
- Safari 11+
|
|
238
|
+
- Edge 16+
|
|
239
|
+
|
|
240
|
+
For best speed, use browsers with SIMD128 support:
|
|
241
|
+
- Chrome 91+
|
|
242
|
+
- Firefox 89+
|
|
243
|
+
- Safari 16.4+
|
|
244
|
+
|
|
245
|
+
## Troubleshooting
|
|
246
|
+
|
|
247
|
+
**"init() takes too long"**
|
|
248
|
+
- Normal: First call downloads ~900KB WASM file
|
|
249
|
+
- Fix: Call init() early, before user needs results
|
|
250
|
+
|
|
251
|
+
**"Images look wrong"**
|
|
252
|
+
- Check: Images must be 224×224 pixels
|
|
253
|
+
- Check: Pixel format is RGB (3 values per pixel, not RGBA)
|
|
254
|
+
- Check: Values are 0-255, not 0-1
|
|
255
|
+
|
|
256
|
+
**"Similarity scores are all low"**
|
|
257
|
+
- Try: Set `normalize: true` in CnnEmbedder options
|
|
258
|
+
- Check: Are your images actually similar?
|
|
259
|
+
|
|
260
|
+
## API Reference
|
|
261
|
+
|
|
262
|
+
### CnnEmbedder
|
|
263
|
+
|
|
264
|
+
```typescript
|
|
265
|
+
new CnnEmbedder(options?: {
|
|
266
|
+
embeddingDim?: number; // Default: 512
|
|
267
|
+
normalize?: boolean; // Default: true
|
|
268
|
+
})
|
|
269
|
+
|
|
270
|
+
.extract(pixels: Uint8Array, width: number, height: number): Float32Array
|
|
271
|
+
.cosineSimilarity(a: Float32Array, b: Float32Array): number
|
|
272
|
+
.embeddingDim: number
|
|
273
|
+
```
|
|
274
|
+
|
|
275
|
+
### InfoNCELoss
|
|
276
|
+
|
|
277
|
+
```typescript
|
|
278
|
+
new InfoNCELoss(temperature?: number) // Default: 0.1
|
|
279
|
+
|
|
280
|
+
.forward(embeddings: Float32Array, batchSize: number, dim: number): number
|
|
281
|
+
.temperature: number
|
|
282
|
+
```
|
|
283
|
+
|
|
284
|
+
### TripletLoss
|
|
285
|
+
|
|
286
|
+
```typescript
|
|
287
|
+
new TripletLoss(margin?: number) // Default: 1.0
|
|
288
|
+
|
|
289
|
+
.forward(anchors, positives, negatives: Float32Array, dim: number): number
|
|
290
|
+
.margin: number
|
|
291
|
+
```
|
|
292
|
+
|
|
293
|
+
## Related Packages
|
|
294
|
+
|
|
295
|
+
- [`ruvector`](https://www.npmjs.com/package/ruvector) — Core vector database
|
|
296
|
+
- [`@ruvector/attention`](https://www.npmjs.com/package/@ruvector/attention) — AI attention layers
|
|
297
|
+
- [`@ruvector/gnn`](https://www.npmjs.com/package/@ruvector/gnn) — Graph neural networks
|
|
298
|
+
|
|
299
|
+
## License
|
|
300
|
+
|
|
301
|
+
MIT OR Apache-2.0
|
package/index.d.ts
ADDED
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @ruvector/cnn - CNN feature extraction for image embeddings
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Initialize the WASM module
|
|
7
|
+
*/
|
|
8
|
+
export function init(): Promise<void>;
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Configuration for CNN embedder
|
|
12
|
+
*/
|
|
13
|
+
export interface EmbedderConfig {
|
|
14
|
+
/** Input image size (square), default: 224 */
|
|
15
|
+
inputSize?: number;
|
|
16
|
+
/** Output embedding dimension, default: 512 */
|
|
17
|
+
embeddingDim?: number;
|
|
18
|
+
/** L2 normalize embeddings, default: true */
|
|
19
|
+
normalize?: boolean;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* CNN Embedder for extracting image features
|
|
24
|
+
*/
|
|
25
|
+
export class CnnEmbedder {
|
|
26
|
+
/**
|
|
27
|
+
* Create a new CNN embedder
|
|
28
|
+
* @param config - Configuration options
|
|
29
|
+
*/
|
|
30
|
+
constructor(config?: EmbedderConfig);
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Extract embedding from image data
|
|
34
|
+
* @param imageData - RGB image data (row-major, no alpha)
|
|
35
|
+
* @param width - Image width
|
|
36
|
+
* @param height - Image height
|
|
37
|
+
* @returns Embedding vector
|
|
38
|
+
*/
|
|
39
|
+
extract(imageData: Uint8Array, width: number, height: number): Float32Array;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Compute cosine similarity between two embeddings
|
|
43
|
+
* @param a - First embedding
|
|
44
|
+
* @param b - Second embedding
|
|
45
|
+
* @returns Similarity in [-1, 1]
|
|
46
|
+
*/
|
|
47
|
+
cosineSimilarity(a: Float32Array, b: Float32Array): number;
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Get the embedding dimension
|
|
51
|
+
*/
|
|
52
|
+
readonly embeddingDim: number;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* InfoNCE loss for contrastive learning (SimCLR style)
|
|
57
|
+
*/
|
|
58
|
+
export class InfoNCELoss {
|
|
59
|
+
/**
|
|
60
|
+
* Create InfoNCE loss
|
|
61
|
+
* @param temperature - Temperature parameter (default: 0.1)
|
|
62
|
+
*/
|
|
63
|
+
constructor(temperature?: number);
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Compute loss for embedding pairs
|
|
67
|
+
* @param embeddings - Flattened [2N, D] array where (i, i+N) are positive pairs
|
|
68
|
+
* @param batchSize - N (number of pairs)
|
|
69
|
+
* @param dim - D (embedding dimension)
|
|
70
|
+
* @returns Loss value
|
|
71
|
+
*/
|
|
72
|
+
forward(embeddings: Float32Array, batchSize: number, dim: number): number;
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Get temperature parameter
|
|
76
|
+
*/
|
|
77
|
+
readonly temperature: number;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* Triplet loss for metric learning
|
|
82
|
+
*/
|
|
83
|
+
export class TripletLoss {
|
|
84
|
+
/**
|
|
85
|
+
* Create triplet loss
|
|
86
|
+
* @param margin - Margin parameter (default: 1.0)
|
|
87
|
+
*/
|
|
88
|
+
constructor(margin?: number);
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Compute triplet loss
|
|
92
|
+
* @param anchors - Anchor embeddings [N, D]
|
|
93
|
+
* @param positives - Positive embeddings [N, D]
|
|
94
|
+
* @param negatives - Negative embeddings [N, D]
|
|
95
|
+
* @param dim - Embedding dimension D
|
|
96
|
+
* @returns Loss value
|
|
97
|
+
*/
|
|
98
|
+
forward(
|
|
99
|
+
anchors: Float32Array,
|
|
100
|
+
positives: Float32Array,
|
|
101
|
+
negatives: Float32Array,
|
|
102
|
+
dim: number
|
|
103
|
+
): number;
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Get margin parameter
|
|
107
|
+
*/
|
|
108
|
+
readonly margin: number;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* SIMD-optimized operations
|
|
113
|
+
*/
|
|
114
|
+
export namespace SimdOps {
|
|
115
|
+
/**
|
|
116
|
+
* Compute dot product of two vectors
|
|
117
|
+
*/
|
|
118
|
+
function dotProduct(a: Float32Array, b: Float32Array): number;
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Apply ReLU activation in-place
|
|
122
|
+
*/
|
|
123
|
+
function relu(data: Float32Array): void;
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* Apply ReLU6 activation in-place
|
|
127
|
+
*/
|
|
128
|
+
function relu6(data: Float32Array): void;
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* L2 normalize a vector in-place
|
|
132
|
+
*/
|
|
133
|
+
function l2Normalize(data: Float32Array): void;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Layer operations for building custom networks
|
|
138
|
+
*/
|
|
139
|
+
export namespace LayerOps {
|
|
140
|
+
/**
|
|
141
|
+
* Apply batch normalization
|
|
142
|
+
*/
|
|
143
|
+
function batchNorm(
|
|
144
|
+
input: Float32Array,
|
|
145
|
+
gamma: Float32Array,
|
|
146
|
+
beta: Float32Array,
|
|
147
|
+
mean: Float32Array,
|
|
148
|
+
variance: Float32Array,
|
|
149
|
+
epsilon?: number
|
|
150
|
+
): void;
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* Apply global average pooling
|
|
154
|
+
* @param input - Input tensor [C, H, W]
|
|
155
|
+
* @param channels - Number of channels
|
|
156
|
+
* @param spatialSize - H * W
|
|
157
|
+
* @returns Output [C]
|
|
158
|
+
*/
|
|
159
|
+
function globalAvgPool(
|
|
160
|
+
input: Float32Array,
|
|
161
|
+
channels: number,
|
|
162
|
+
spatialSize: number
|
|
163
|
+
): Float32Array;
|
|
164
|
+
}
|