Skip to content

Commit cf8d164

Browse files
authored
feat: add ai object detection extension (#24)
* feat: implement Arduino object detection extension and integrate video processing * fix: update enableVideo method to capture and emit video frames in base64 format * fix: streamline base64 conversion in enableVideo method for object detection * fix: improve formatting and add detection result logging in Arduino object detection extension * fix: add detection results to response in object detection callback * fix: enhance detection result logging and add model labels to Arduino object detection extension * fix: implement drawing and clearing of bounding boxes for detected objects in Arduino object detection extension * fix: implement toggle functionality for bounding boxes and enhance detection result handling in Arduino object detection extension * fix: update bounding box visibility logic and enhance detection handling in Arduino object detection extension * fix: update WebSocket server URL to use a static IP address in Arduino extensions * fix: implement Detection class for object properties and enhance bounding box handling in Arduino object detection extension * fix: update clearBoundingBoxes command text and improve getColorByConfidence return type in Arduino object detection extension * fix: refactor bounding box handling and improve visibility logic in Arduino object detection extension * feat: add model labels constants and update model labels menu in Arduino object detection extension * refactor: move model labels and Detection class to a separate file for better organization in Arduino object detection extension * fix: correct class name casing for ArduinoObjectDetection and update prototype references in Arduino object detection extension * fix: update color handling in _getColorByConfidence method for improved confidence visualization * fix: update wsServerURL to use dynamic protocol and hostname for better compatibility * fix: correct spacing in wsServerURL declaration for consistency * fix: restore asset copying in app:build task for complete build process * fix: add person detection functionality and update bounding box commands * fix: update wsServerURL to use dynamic protocol and hostname for better compatibility; clean up argument declarations and improve person detection logic * fix: add "when person detected" block and corresponding functionality to ArduinoObjectDetection * fix: correct indentation in whenPersonDetected method for consistency * fix: update object detection to decode image data directly from base64 * fix: refactor person detection logic and rename related methods * feat: implement automatic detection loop and bounding box controls * feat: implement start and stop methods for automatic detection loop * fix: improve code formatting and consistency in object detection extension * fix: remove unused imports from main.py * fix: handle missing image data in object detection callback
1 parent d438ee2 commit cf8d164

File tree

5 files changed

+474
-2
lines changed

5 files changed

+474
-2
lines changed

app.yaml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,4 +4,5 @@ ports:
44
- 7000
55
bricks:
66
- arduino:web_ui
7+
- arduino:object_detection
78
icon: 🐱

python/main.py

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
from arduino.app_utils import App, Bridge
22
from arduino.app_bricks.web_ui import WebUI
3+
from arduino.app_bricks.object_detection import ObjectDetection
34
import time
5+
import base64
46

5-
ui = WebUI()
6-
ui.on_connect(lambda sid: (print(f"Client connected: {sid} "),))
7+
object_detection = ObjectDetection()
78

89

910
def on_matrix_draw(_, data):
@@ -45,8 +46,40 @@ def on_set_led_rgb(_, data):
4546
Bridge.call("set_led_rgb", led, r_digital, g_digital, b_digital)
4647

4748

49+
def on_detect_objects(client_id, data):
50+
"""Callback function to handle object detection requests."""
51+
try:
52+
image_data = data.get("image")
53+
confidence = data.get("confidence", 0.5)
54+
if not image_data:
55+
# TODO: implement the 'detection_error` in the extension
56+
ui.send_message("detection_error", {"error": "No image data"})
57+
return
58+
59+
start_time = time.time() * 1000
60+
results = object_detection.detect(base64.b64decode(image_data), confidence=confidence)
61+
diff = time.time() * 1000 - start_time
62+
63+
if results is None:
64+
ui.send_message("detection_error", {"error": "No results returned"})
65+
return
66+
67+
response = {
68+
"detection": results.get("detection", []),
69+
"detection_count": len(results.get("detection", [])) if results else 0,
70+
"processing_time": f"{diff:.2f} ms",
71+
}
72+
ui.send_message("detection_result", response)
73+
74+
except Exception as e:
75+
ui.send_message("detection_error", {"error": str(e)})
76+
77+
78+
ui = WebUI()
79+
ui.on_connect(lambda sid: (print(f"Client connected: {sid} "),))
4880
ui.on_message("matrix_draw", on_matrix_draw)
4981
ui.on_message("set_led_rgb", on_set_led_rgb)
82+
ui.on_message("detect_objects", on_detect_objects)
5083

5184

5285
def on_modulino_button_pressed(btn):
Lines changed: 328 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,328 @@
1+
const BlockType = require("../../../../../../scratch-editor/packages/scratch-vm/src/extension-support/block-type");
2+
const ArgumentType = require(
3+
"../../../../../../scratch-editor/packages/scratch-vm/src/extension-support/argument-type",
4+
);
5+
const io = require("../socket.io.min.js");
6+
const Video = require("../../../../../../scratch-editor/packages/scratch-vm/src/io/video");
7+
const Rectangle = require("../../../../../../scratch-editor/packages/scratch-render/src/Rectangle.js");
8+
const StageLayering = require("../../../../../../scratch-editor/packages/scratch-vm/src/engine/stage-layering.js");
9+
const { Detection, MODEL_LABELS } = require("./object_detection");
10+
11+
/**
12+
* Url of icon to be displayed at the left edge of each extension block.
13+
* @type {string}
14+
*/
15+
// eslint-disable-next-line max-len
16+
const iconURI = "";
17+
18+
/**
19+
* Url of icon to be displayed in the toolbox menu for the extension category.
20+
* @type {string}
21+
*/
22+
// eslint-disable-next-line max-len
23+
const menuIconURI = "";
24+
25+
const wsServerURL = `${window.location.protocol}//${window.location.hostname}:7000`;
26+
27+
/**
28+
* RGB color constants for confidence visualization
29+
*/
30+
const RGB_COLORS = {
31+
RED: { r: 1.0, g: 0.0, b: 0.0 },
32+
ORANGE: { r: 1.0, g: 0.5, b: 0.0 },
33+
GREEN: { r: 0.0, g: 1.0, b: 0.0 },
34+
};
35+
36+
class ArduinoObjectDetection {
37+
constructor(runtime) {
38+
this.runtime = runtime;
39+
40+
/** @type {Array<Detection>} */
41+
this.detectedObjects = [];
42+
43+
this._penSkinId = null;
44+
45+
this._isfaceDetected = false;
46+
47+
/** @type {number|null} */
48+
this._loopIntervalId = null;
49+
50+
/** @type {boolean} */
51+
this._isLoopRunning = false;
52+
53+
/** @type {boolean} */
54+
this._enableBoundingBoxes = true;
55+
56+
this.runtime.on("PROJECT_LOADED", () => {
57+
if (!this.runtime.renderer) {
58+
console.log("Renderer is NOT available in runtime.");
59+
return;
60+
}
61+
if (!this._penSkinId) {
62+
this._penSkinId = this.runtime.renderer.createPenSkin();
63+
this.penDrawableId = this.runtime.renderer.createDrawable(StageLayering.PEN_LAYER);
64+
this.runtime.renderer.updateDrawableSkinId(this.penDrawableId, this._penSkinId);
65+
}
66+
});
67+
68+
this.io = io(wsServerURL, {
69+
path: "/socket.io",
70+
transports: ["polling", "websocket"],
71+
autoConnect: true,
72+
});
73+
74+
this.io.on("detection_result", (data) => {
75+
this.detectedObjects = [];
76+
77+
this._clearBoundingBoxes();
78+
79+
data.detection.forEach((detection) => {
80+
const [x1, y1, x2, y2] = detection.bounding_box_xyxy;
81+
82+
const detectionObject = new Detection(
83+
detection.class_name,
84+
this._createRectangleFromBoundingBox(x1, y1, x2, y2),
85+
parseFloat(detection.confidence),
86+
);
87+
this.detectedObjects.push(detectionObject);
88+
89+
console.log(
90+
`Detected ${detectionObject.label} with confidence ${
91+
detectionObject.confidence.toFixed(2)
92+
} took ${data.processing_time}`,
93+
);
94+
});
95+
96+
const personDetected = this.detectedObjects.some(detectionObject =>
97+
detectionObject.label === MODEL_LABELS.PERSON
98+
);
99+
this._isfaceDetected = personDetected;
100+
101+
if (this._enableBoundingBoxes) {
102+
this._drawBoundingBoxes();
103+
} else {
104+
this._clearBoundingBoxes();
105+
}
106+
});
107+
}
108+
}
109+
110+
ArduinoObjectDetection.prototype.getInfo = function() {
111+
return {
112+
id: "ArduinoObjectDetection",
113+
name: "Arduino Object Detection",
114+
menuIconURI: menuIconURI,
115+
blockIconURI: iconURI,
116+
blocks: [
117+
{
118+
opcode: "whenPersonDetected",
119+
blockType: BlockType.HAT,
120+
text: "when person detected",
121+
func: "whenPersonDetected",
122+
arguments: {},
123+
},
124+
{
125+
opcode: "startDetectionLoop",
126+
blockType: BlockType.COMMAND,
127+
text: "start detection",
128+
func: "startDetectionLoop",
129+
arguments: {},
130+
},
131+
{
132+
opcode: "stopDetectionLoop",
133+
blockType: BlockType.COMMAND,
134+
text: "stop detection",
135+
func: "stopDetectionLoop",
136+
arguments: {},
137+
},
138+
{
139+
opcode: "isPersonDetected",
140+
blockType: BlockType.BOOLEAN,
141+
text: "is person detected",
142+
func: "isPersonDetected",
143+
arguments: {},
144+
},
145+
{
146+
opcode: "showBoundingBoxes",
147+
blockType: BlockType.COMMAND,
148+
text: "show bounding boxes",
149+
func: "showBoundingBoxes",
150+
arguments: {},
151+
},
152+
{
153+
opcode: "hideBoundingBoxes",
154+
blockType: BlockType.COMMAND,
155+
text: "hide bounding boxes",
156+
func: "hideBoundingBoxes",
157+
arguments: {},
158+
},
159+
],
160+
menus: {
161+
modelsLabels: Object.values(MODEL_LABELS).sort(),
162+
},
163+
};
164+
};
165+
166+
ArduinoObjectDetection.prototype.startDetectionLoop = function(args) {
167+
if (this._isLoopRunning) {
168+
console.log("Detection loop is already running");
169+
return;
170+
}
171+
172+
this._isLoopRunning = true;
173+
this.runtime.ioDevices.video.enableVideo();
174+
this._loop();
175+
176+
this._loopIntervalId = setInterval(() => {
177+
this._loop();
178+
}, 1000); // 1000ms = 1s
179+
};
180+
181+
ArduinoObjectDetection.prototype.stopDetectionLoop = function(args) {
182+
this.runtime.ioDevices.video.disableVideo();
183+
this.hideBoundingBoxes();
184+
185+
if (!this._isLoopRunning) {
186+
console.log("Detection loop is not running");
187+
return;
188+
}
189+
190+
console.log("Stopping detection loop");
191+
this._isLoopRunning = false;
192+
if (this._loopIntervalId) {
193+
clearInterval(this._loopIntervalId);
194+
this._loopIntervalId = null;
195+
}
196+
};
197+
198+
ArduinoObjectDetection.prototype._loop = function() {
199+
if (!this._isLoopRunning) {
200+
return;
201+
}
202+
this._detectObjects();
203+
204+
// Note: The face detection state (_isfaceDetected) will be updated
205+
// automatically when the detection_result event is received
206+
};
207+
208+
ArduinoObjectDetection.prototype.whenPersonDetected = function(args) {
209+
return this.isPersonDetected();
210+
};
211+
212+
ArduinoObjectDetection.prototype.isPersonDetected = function(args) {
213+
return this._isfaceDetected;
214+
};
215+
216+
ArduinoObjectDetection.prototype.hideBoundingBoxes = function(args) {
217+
this._enableBoundingBoxes = false;
218+
this._clearBoundingBoxes();
219+
};
220+
221+
ArduinoObjectDetection.prototype.showBoundingBoxes = function(args) {
222+
this._enableBoundingBoxes = true;
223+
this._drawBoundingBoxes();
224+
};
225+
226+
ArduinoObjectDetection.prototype._detectObjects = function(args) {
227+
if (!this.runtime.ioDevices) {
228+
console.log("No ioDevices available.");
229+
return;
230+
}
231+
const canvas = this.runtime.ioDevices.video.getFrame({
232+
format: Video.FORMAT_CANVAS,
233+
dimensions: [480, 360], // the same as the stage resolution
234+
});
235+
if (!canvas) {
236+
console.log("No canvas available from video frame.");
237+
return;
238+
}
239+
const dataUrl = canvas.toDataURL("image/png");
240+
const base64Frame = dataUrl.split(",")[1];
241+
this.io.emit("detect_objects", { image: base64Frame });
242+
};
243+
244+
ArduinoObjectDetection.prototype._clearBoundingBoxes = function(args) {
245+
if (!this.runtime.renderer || !this._penSkinId) {
246+
console.log("Renderer or pen skin not available for clearing");
247+
return;
248+
}
249+
const penSkin = this.runtime.renderer._allSkins[this._penSkinId];
250+
if (penSkin && penSkin.clear) {
251+
penSkin.clear();
252+
} else {
253+
console.log("Could not clear pen skin");
254+
}
255+
};
256+
257+
ArduinoObjectDetection.prototype._drawBoundingBoxes = function(args) {
258+
this.detectedObjects.forEach(detectionObject => {
259+
const { r, g, b } = this._getColorByConfidence(detectionObject.confidence);
260+
const penAttributes = {
261+
color4f: [r, g, b, 1.0],
262+
diameter: 3,
263+
};
264+
this._drawRectangleWithPen(detectionObject.rectangle, penAttributes);
265+
});
266+
};
267+
268+
/**
269+
* Get pen color based on confidence level
270+
* @param {number} confidence - Confidence score (0 to 100)
271+
* @returns {Object} RGB color object {r, g, b} in 0-1 range
272+
*/
273+
ArduinoObjectDetection.prototype._getColorByConfidence = function(confidence) {
274+
if (confidence >= 90) {
275+
return RGB_COLORS.GREEN;
276+
}
277+
if (confidence >= 75 && confidence < 90) {
278+
return RGB_COLORS.ORANGE;
279+
}
280+
return RGB_COLORS.RED;
281+
};
282+
283+
/**
284+
* Draw a rectangle using the Rectangle class and pen system
285+
* @param {Rectangle} rectangle - Rectangle object defining the bounds
286+
* @param {Object} penAttributes - Pen drawing attributes (color, thickness)
287+
*/
288+
ArduinoObjectDetection.prototype._drawRectangleWithPen = function(rectangle, penAttributes) {
289+
if (!this.runtime.renderer || !this._penSkinId) {
290+
console.log("Renderer or pen skin not available");
291+
return;
292+
}
293+
294+
// TODO: Get the pen skin object in a better way
295+
const penSkin = this.runtime.renderer._allSkins[this._penSkinId];
296+
if (!penSkin) {
297+
console.log("Pen skin not found");
298+
return;
299+
}
300+
301+
const left = rectangle.left;
302+
const right = rectangle.right;
303+
const bottom = rectangle.bottom;
304+
const top = rectangle.top;
305+
306+
penSkin.drawLine(penAttributes, left, top, right, top);
307+
penSkin.drawLine(penAttributes, right, top, right, bottom);
308+
penSkin.drawLine(penAttributes, right, bottom, left, bottom);
309+
penSkin.drawLine(penAttributes, left, bottom, left, top);
310+
};
311+
312+
ArduinoObjectDetection.prototype._createRectangleFromBoundingBox = function(x1, y1, x2, y2) {
313+
x1 = x1 - 240; // 0-480 -> -240 to +240
314+
y1 = -(y1 - 180); // 0-360 -> -180 to +180
315+
x2 = x2 - 240;
316+
y2 = -(y2 - 180);
317+
318+
const left = Math.min(x1, x2);
319+
const right = Math.max(x1, x2);
320+
const bottom = Math.min(y1, y2);
321+
const top = Math.max(y1, y2);
322+
323+
const rectangle = new Rectangle();
324+
rectangle.initFromBounds(left, right, bottom, top);
325+
return rectangle;
326+
};
327+
328+
module.exports = ArduinoObjectDetection;

0 commit comments

Comments
 (0)