Skip to content

Commit a706492

Browse files
committed
added extension codebase
1 parent 271d28c commit a706492

File tree

9 files changed

+1332
-0
lines changed

9 files changed

+1332
-0
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
node_modules
2+
package-lock.json
3+
dist

README.md

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,26 @@
11
# ImageClassifier
22
A browser extension to classify images, runs locally within browser.
3+
4+
## Installation
5+
6+
### Building the extension
7+
1. Clone the repository:
8+
```bash
9+
git clone https://github.com/zweack/ImageClassifier.git
10+
cd ImageClassifier
11+
```
12+
2. Install dependencies:
13+
```bash
14+
npm install
15+
```
16+
3. Build the extension:
17+
```bash
18+
npm run build
19+
```
20+
4. Load the extension in your browser:
21+
- For Chrome / Edge:
22+
1. Open `chrome://extensions/`
23+
2. Enable "Developer mode"
24+
3. Click "Load unpacked" and select the `dist` directory
25+
- For Firefox:
26+
Coming soon...

ext/package.json

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"name": "image-classifier",
3+
"version": "1.0.0",
4+
"description": "Classify images in browser",
5+
"scripts": {
6+
"build": "webpack --config webpack.config.js"
7+
},
8+
"dependencies": {
9+
"onnxruntime-web": "^1.20.1"
10+
},
11+
"devDependencies": {
12+
"ts-loader": "^9.5.0",
13+
"typescript": "^5.3.3",
14+
"webpack": "^5.99.9",
15+
"webpack-cli": "^6.0.1",
16+
"webpack-obfuscator": "^3.5.1",
17+
"copy-webpack-plugin": "^13.0.0"
18+
}
19+
}

ext/src/background.js

Lines changed: 90 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,90 @@
1+
import * as ort from 'onnxruntime-web';
2+
3+
class ImageClassifierBackground {
4+
constructor() {
5+
this.labels = null;
6+
this.session = null;
7+
this.init();
8+
}
9+
10+
async init() {
11+
await this.loadModelAndLabels();
12+
this.setupContextMenu();
13+
this.setupListeners();
14+
}
15+
16+
async loadModelAndLabels() {
17+
if (!this.session) {
18+
const modelUrl = chrome.runtime.getURL('model.onnx');
19+
this.session = await ort.InferenceSession.create(modelUrl);
20+
}
21+
if (!this.labels) {
22+
const labelsUrl = chrome.runtime.getURL('labels.json');
23+
this.labels = await fetch(labelsUrl).then(r => r.json());
24+
}
25+
}
26+
27+
setupContextMenu() {
28+
chrome.contextMenus.create({
29+
id: 'classify-image',
30+
title: 'Classify this image',
31+
contexts: ['image']
32+
});
33+
chrome.runtime.onInstalled.addListener(() => {
34+
chrome.contextMenus.create({
35+
id: 'classify-image',
36+
title: 'Classify this image',
37+
contexts: ['image']
38+
});
39+
});
40+
41+
chrome.contextMenus.onClicked.addListener((info, tab) => {
42+
if (info.menuItemId === 'classify-image' && info.srcUrl) {
43+
chrome.tabs.sendMessage(tab.id, {
44+
action: 'classify-image',
45+
imageUrl: info.srcUrl
46+
});
47+
}
48+
});
49+
}
50+
51+
setupListeners() {
52+
chrome.runtime.onMessage.addListener(async (message, sender, sendResponse) => {
53+
if (message.action === 'run-inference' && message.tensorData && message.tensorShape) {
54+
try {
55+
await this.loadModelAndLabels();
56+
const outputNames = this.session.outputNames;
57+
const tensor = new ort.Tensor('float32', new Float32Array(message.tensorData), message.tensorShape);
58+
const feeds = {};
59+
feeds[this.session.inputNames[0]] = tensor;
60+
const results = await this.session.run(feeds);
61+
const output = results[outputNames[0]].data;
62+
let maxScore = -Infinity;
63+
let maxIndex = -1;
64+
for (let i = 0; i < output.length; i++) {
65+
if (output[i] > maxScore) {
66+
maxScore = output[i];
67+
maxIndex = i;
68+
}
69+
}
70+
let labelRes = {
71+
label: this.labels[maxIndex],
72+
confidence: (1 / (1 + Math.exp(-maxScore)))
73+
};
74+
chrome.tabs.sendMessage(sender.tab.id, {
75+
action: 'inference-result',
76+
result: labelRes
77+
});
78+
} catch (e) {
79+
chrome.tabs.sendMessage(sender.tab.id, {
80+
action: 'inference-result',
81+
error: e.toString()
82+
});
83+
}
84+
}
85+
});
86+
}
87+
}
88+
89+
new ImageClassifierBackground();
90+

ext/src/content.js

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
class ImageClassifierContent {
2+
constructor() {
3+
this.listenForMessages();
4+
}
5+
6+
listenForMessages() {
7+
chrome.runtime.onMessage.addListener(async (request, sender, sendResponse) => {
8+
if (request.action === 'classify-image' && request.imageUrl) {
9+
try {
10+
this.showNotification('Classifying image...', 'info');
11+
// Fetch the image as a blob
12+
const response = await fetch(request.imageUrl);
13+
const blob = await response.blob();
14+
const img = await createImageBitmap(blob);
15+
16+
// Preprocess image to [1, 3, 224, 224] Float32Array
17+
const floatData = await this.preprocessImage(img);
18+
// Send tensor data to background for inference
19+
chrome.runtime.sendMessage({
20+
action: 'run-inference',
21+
tensorData: Array.from(floatData),
22+
tensorShape: [1, 3, 224, 224]
23+
});
24+
} catch (e) {
25+
this.showNotification('Error preparing image: ' + e, 'error');
26+
}
27+
} else if (request.action === 'inference-result') {
28+
if (request.error) {
29+
this.showNotification('Inference error: ' + request.error, 'error');
30+
} else {
31+
// Expecting result: { label: string, confidence: number }
32+
const result = request.result;
33+
if (result && result.label && typeof result.confidence === 'number') {
34+
this.showNotification(`This appears to be: ${result.label} (${(result.confidence * 100).toFixed(2)}% confidence)`, 'success');
35+
} else {
36+
this.showNotification('Classification result: ' + JSON.stringify(result), 'success');
37+
}
38+
}
39+
}
40+
});
41+
}
42+
43+
async preprocessImage(img) {
44+
const canvas = new OffscreenCanvas(224, 224);
45+
const ctx = canvas.getContext('2d');
46+
ctx.drawImage(img, 0, 0, 224, 224);
47+
const imageData = ctx.getImageData(0, 0, 224, 224).data;
48+
const floatData = new Float32Array(1 * 3 * 224 * 224);
49+
for (let i = 0; i < 224 * 224; i++) {
50+
floatData[i] = imageData[i * 4] / 255.0; // R
51+
floatData[i + 224 * 224] = imageData[i * 4 + 1] / 255.0; // G
52+
floatData[i + 2 * 224 * 224] = imageData[i * 4 + 2] / 255.0; // B
53+
}
54+
return floatData;
55+
}
56+
57+
showNotification(message, type) {
58+
const old = document.getElementById('image-classifier-notification');
59+
if (old) old.remove();
60+
const div = document.createElement('div');
61+
div.id = 'image-classifier-notification';
62+
div.textContent = message;
63+
div.style.position = 'fixed';
64+
div.style.top = '32px';
65+
div.style.right = '32px';
66+
div.style.zIndex = 99999;
67+
if (type === 'success') {
68+
div.style.background = '#27ae60';
69+
} else if (type === 'error') {
70+
div.style.background = '#ff4d4f';
71+
} else {
72+
div.style.background = '#222';
73+
}
74+
div.style.color = '#fff';
75+
div.style.padding = '16px 24px';
76+
div.style.borderRadius = '8px';
77+
div.style.boxShadow = '0 2px 12px rgba(0,0,0,0.2)';
78+
div.style.fontSize = '16px';
79+
div.style.fontFamily = 'sans-serif';
80+
div.style.cursor = 'pointer';
81+
div.style.transition = 'opacity 0.3s';
82+
div.onclick = () => div.remove();
83+
document.body.appendChild(div);
84+
setTimeout(() => {
85+
div.style.opacity = '0';
86+
setTimeout(() => div.remove(), 300);
87+
}, 4000);
88+
}
89+
}
90+
91+
new ImageClassifierContent();

0 commit comments

Comments
 (0)