From 0ed07dbeda5522436d48e4fa071fe362606d7ebb Mon Sep 17 00:00:00 2001 From: Jan Jongboom Date: Mon, 4 Aug 2025 10:59:29 +0200 Subject: [PATCH 1/9] Add support for shared memory inference --- .gitignore | 1 + README.md | 2 +- edge_impulse_linux/image.py | 12 ++- edge_impulse_linux/runner.py | 61 +++++++++++++-- examples/image/classify-full-frame.py | 6 +- examples/image/classify-image.py | 6 +- examples/image/classify-video.py | 7 +- examples/image/classify.py | 6 +- examples/image/resize_demo.py | 6 +- examples/image/set-thresholds.py | 103 ++++++++++++++++++++++++++ requirements.txt | 8 +- setup.cfg | 4 +- 12 files changed, 196 insertions(+), 26 deletions(-) create mode 100644 examples/image/set-thresholds.py diff --git a/.gitignore b/.gitignore index 09116f0..02df1b9 100644 --- a/.gitignore +++ b/.gitignore @@ -8,3 +8,4 @@ build .vscode *.eim .act-secrets +*.png diff --git a/README.md b/README.md index ec78a18..a7202b5 100644 --- a/README.md +++ b/README.md @@ -32,7 +32,7 @@ This library lets you run machine learning models and collect sensor data on Lin $ pip3 install -r requirements.txt ``` - For the computer vision examples you'll want `opencv-python>=4.5.1.48` + For the computer vision examples you'll want `opencv-python>=4.5.1.48,<5` Note on macOS on apple silicon, you will need to use a later version, 4.10.0.84 tested and installs cleanly diff --git a/edge_impulse_linux/image.py b/edge_impulse_linux/image.py index a688b76..52ae0f1 100644 --- a/edge_impulse_linux/image.py +++ b/edge_impulse_linux/image.py @@ -1,10 +1,14 @@ #!/usr/bin/env python import numpy as np -import cv2 +try: + import cv2 +except ImportError: + print('Missing OpenCV, install via `pip3 install "opencv-python>=4.5.1.48,<5"`') + exit(1) + from edge_impulse_linux.runner import ImpulseRunner import math -import psutil class ImageImpulseRunner(ImpulseRunner): def __init__(self, model_path: str): @@ -44,7 +48,7 @@ def classify(self, data): # This returns images in RGB format (not BGR) def get_frames(self, videoDeviceId = 0): - if psutil.OSX or psutil.MACOS: + if sys.platform == "darwin": print('Make sure to grant the this script access to your webcam.') print('If your webcam is not responding, try running "tccutil reset Camera" to reset the camera access privileges.') @@ -57,7 +61,7 @@ def get_frames(self, videoDeviceId = 0): # This returns images in RGB format (not BGR) def classifier(self, videoDeviceId = 0): - if psutil.OSX or psutil.MACOS: + if sys.platform == "darwin": print('Make sure to grant the this script access to your webcam.') print('If your webcam is not responding, try running "tccutil reset Camera" to reset the camera access privileges.') diff --git a/edge_impulse_linux/runner.py b/edge_impulse_linux/runner.py index 5e33863..caf80a5 100755 --- a/edge_impulse_linux/runner.py +++ b/edge_impulse_linux/runner.py @@ -6,12 +6,12 @@ import signal import socket import json - +from multiprocessing import shared_memory, resource_tracker +import numpy as np def now(): return round(time.time() * 1000) - class ImpulseRunner: def __init__(self, model_path: str): self._model_path = model_path @@ -20,6 +20,8 @@ def __init__(self, model_path: str): self._client = None self._ix = 0 self._debug = False + self._hello_resp = None + self._shm = None def init(self, debug=False): if not os.path.exists(self._model_path): @@ -50,28 +52,71 @@ def init(self, debug=False): self._client = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM) self._client.connect(socket_path) - return self.hello() + hello_resp = self._hello_resp = self.hello() + + if ('features_shm' in hello_resp.keys()): + shm_name = hello_resp['features_shm']['name'] + # python does not want the leading slash + if (shm_name.startswith('/')): + shm_name = shm_name[1:] + shm = shared_memory.SharedMemory(name=shm_name) + self._shm = { + 'shm': shm, + 'type': hello_resp['features_shm']['type'], + 'elements': hello_resp['features_shm']['elements'], + 'array': np.ndarray((hello_resp['features_shm']['elements'],), dtype=np.float32, buffer=shm.buf) + } + + return self._hello_resp + + def __del__(self): + self.stop() def stop(self): - if self._tempdir: + if self._tempdir is not None: shutil.rmtree(self._tempdir) + self._tempdir = None - if self._client: + if self._client is not None: self._client.close() + self._client = None - if self._runner: + if self._runner is not None: os.kill(self._runner.pid, signal.SIGINT) # todo: in Node we send a SIGHUP after 0.5sec if process has not died, can we do this somehow here too? + self._runner = None + + if self._shm is not None: + self._shm['shm'].close() + resource_tracker.unregister(self._shm['shm']._name, "shared_memory") + self._shm = None def hello(self): msg = {"hello": 1} return self.send_msg(msg) def classify(self, data): - msg = {"classify": data} + start = now() + + if self._shm: + self._shm['array'][:] = data + + msg = { + "classify_shm": { + "elements": len(data), + } + } + else: + msg = {"classify": data} + if self._debug: msg["debug"] = True - return self.send_msg(msg) + send_resp = self.send_msg(msg) + end = now() + + print('send_resp', str(end - start) + 'ms.') + + return send_resp def send_msg(self, msg): t_send_msg = now() diff --git a/examples/image/classify-full-frame.py b/examples/image/classify-full-frame.py index 43b6156..85bde9b 100644 --- a/examples/image/classify-full-frame.py +++ b/examples/image/classify-full-frame.py @@ -2,7 +2,11 @@ import device_patches # Device specific patches for Jetson Nano (needs to be before importing cv2) -import cv2 +try: + import cv2 +except ImportError: + print('Missing OpenCV, install via `pip3 install "opencv-python>=4.5.1.48,<5"`') + exit(1) import os import sys, getopt import signal diff --git a/examples/image/classify-image.py b/examples/image/classify-image.py index 79bd82c..0a5cba5 100644 --- a/examples/image/classify-image.py +++ b/examples/image/classify-image.py @@ -2,7 +2,11 @@ import device_patches # Device specific patches for Jetson Nano (needs to be before importing cv2) # noqa: F401 -import cv2 +try: + import cv2 +except ImportError: + print('Missing OpenCV, install via `pip3 install "opencv-python>=4.5.1.48,<5"`') + exit(1) import os import sys import getopt diff --git a/examples/image/classify-video.py b/examples/image/classify-video.py index 7333394..aa1b8bd 100644 --- a/examples/image/classify-video.py +++ b/examples/image/classify-video.py @@ -2,11 +2,14 @@ import device_patches # Device specific patches for Jetson Nano (needs to be before importing cv2) -import cv2 +try: + import cv2 +except ImportError: + print('Missing OpenCV, install via `pip3 install "opencv-python>=4.5.1.48,<5"`') + exit(1) import os import time import sys, getopt -import numpy as np from edge_impulse_linux.image import ImageImpulseRunner runner = None diff --git a/examples/image/classify.py b/examples/image/classify.py index d9b7a60..c35d9da 100755 --- a/examples/image/classify.py +++ b/examples/image/classify.py @@ -2,7 +2,11 @@ import device_patches # Device specific patches for Jetson Nano (needs to be before importing cv2) -import cv2 +try: + import cv2 +except ImportError: + print('Missing OpenCV, install via `pip3 install "opencv-python>=4.5.1.48,<5"`') + exit(1) import os import sys, getopt import signal diff --git a/examples/image/resize_demo.py b/examples/image/resize_demo.py index c1e4d26..bbd677d 100644 --- a/examples/image/resize_demo.py +++ b/examples/image/resize_demo.py @@ -1,5 +1,9 @@ import numpy as np -import cv2 +try: + import cv2 +except ImportError: + print('Missing OpenCV, install via `pip3 install "opencv-python>=4.5.1.48,<5"`') + exit(1) from edge_impulse_linux.image import get_features_from_image_with_studio_mode diff --git a/examples/image/set-thresholds.py b/examples/image/set-thresholds.py new file mode 100644 index 0000000..9bd15a5 --- /dev/null +++ b/examples/image/set-thresholds.py @@ -0,0 +1,103 @@ +#!/usr/bin/env python + +import device_patches # Device specific patches for Jetson Nano (needs to be before importing cv2) # noqa: F401 + +try: + import cv2 +except ImportError: + print('Missing OpenCV, install via `pip3 install "opencv-python>=4.5.1.48,<5"`') + exit(1) +import os +import sys +import getopt +from edge_impulse_linux.image import ImageImpulseRunner + +runner = None + +def help(): + print('python classify-image.py ') + +def main(argv): + try: + opts, args = getopt.getopt(argv, "h", ["--help"]) + except getopt.GetoptError: + help() + sys.exit(2) + + for opt, arg in opts: + if opt in ('-h', '--help'): + help() + sys.exit() + + if len(args) != 2: + help() + sys.exit(2) + + model = args[0] + + dir_path = os.path.dirname(os.path.realpath(__file__)) + modelfile = os.path.join(dir_path, model) + + print('MODEL: ' + modelfile) + + with ImageImpulseRunner(modelfile) as runner: + try: + model_info = runner.init() + print('model_info', model_info) + # model_info = runner.init(debug=True) # to get debug print out + + print('Loaded runner for "' + model_info['project']['owner'] + ' / ' + model_info['project']['name'] + '"') + labels = model_info['model_parameters']['labels'] + + img = cv2.imread(args[1]) + if img is None: + print('Failed to load image', args[1]) + exit(1) + + # imread returns images in BGR format, so we need to convert to RGB + img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) + + # get_features_from_image also takes a crop direction arguments in case you don't have square images + # features, cropped = runner.get_features_from_image(img) + + # this mode uses the same settings used in studio to crop and resize the input + features, cropped = runner.get_features_from_image_auto_studio_settings(img) + + res = runner.classify(features) + + if "classification" in res["result"].keys(): + print('Result (%d ms.) ' % (res['timing']['dsp'] + res['timing']['classification']), end='') + for label in labels: + score = res['result']['classification'][label] + print('%s: %.2f\t' % (label, score), end='') + print('', flush=True) + + elif "bounding_boxes" in res["result"].keys(): + print('Found %d bounding boxes (%d ms.)' % (len(res["result"]["bounding_boxes"]), res['timing']['dsp'] + res['timing']['classification'])) + for bb in res["result"]["bounding_boxes"]: + print('\t%s (%.2f): x=%d y=%d w=%d h=%d' % (bb['label'], bb['value'], bb['x'], bb['y'], bb['width'], bb['height'])) + cropped = cv2.rectangle(cropped, (bb['x'], bb['y']), (bb['x'] + bb['width'], bb['y'] + bb['height']), (255, 0, 0), 1) + + if "visual_anomaly_grid" in res["result"].keys(): + print('Found %d visual anomalies (%d ms.)' % (len(res["result"]["visual_anomaly_grid"]), res['timing']['dsp'] + + res['timing']['classification'] + + res['timing']['anomaly'])) + for grid_cell in res["result"]["visual_anomaly_grid"]: + print('\t%s (%.2f): x=%d y=%d w=%d h=%d' % (grid_cell['label'], grid_cell['value'], grid_cell['x'], grid_cell['y'], grid_cell['width'], grid_cell['height'])) + cropped = cv2.rectangle(cropped, (grid_cell['x'], grid_cell['y']), (grid_cell['x'] + grid_cell['width'], grid_cell['y'] + grid_cell['height']), (255, 125, 0), 1) + values = [grid_cell['value'] for grid_cell in res["result"]["visual_anomaly_grid"]] + mean_value = sum(values) / len(values) + max_value = max(values) + print('Max value: %.2f' % max_value) + print('Mean value: %.2f' % mean_value) + + # the image will be resized and cropped, save a copy of the picture here + # so you can see what's being passed into the classifier + cv2.imwrite('debug.jpg', cv2.cvtColor(cropped, cv2.COLOR_RGB2BGR)) + + finally: + if (runner): + runner.stop() + +if __name__ == "__main__": + main(sys.argv[1:]) diff --git a/requirements.txt b/requirements.txt index 2538bf3..48fe1f7 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,5 +1,3 @@ -numpy>=1.19 -PyAudio==0.2.11 -psutil>=5.8.0 -edge_impulse_linux -six==1.16.0 \ No newline at end of file +numpy>=1.19,<3 +PyAudio>=0.2.11,<0.3 +six>=1.16.0,<2 diff --git a/setup.cfg b/setup.cfg index e0c1c4e..626ac98 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,6 @@ [metadata] name = edge_impulse_linux -version = 1.0.12 +version = 1.1.0 author = EdgeImpulse Inc. author_email = hello@edgeimpulse.com description = Python runner for real-time ML classification @@ -15,5 +15,5 @@ classifiers = license_files = LICENSE [options] packages = find: -python_requires = >=3.6 +python_requires = >=3.8 install_requires = file: requirements.txt From ce449c241e302538d0e514cb182025a06de9fa59 Mon Sep 17 00:00:00 2001 From: Jan Jongboom Date: Mon, 4 Aug 2025 11:01:19 +0200 Subject: [PATCH 2/9] Cleanup --- edge_impulse_linux/image.py | 4 +--- edge_impulse_linux/runner.py | 7 +------ 2 files changed, 2 insertions(+), 9 deletions(-) diff --git a/edge_impulse_linux/image.py b/edge_impulse_linux/image.py index 52ae0f1..da069e4 100644 --- a/edge_impulse_linux/image.py +++ b/edge_impulse_linux/image.py @@ -141,9 +141,7 @@ def get_features_from_image_auto_studio_settings(self, img): raise Exception( 'Runner has not initialized, please call init() first') if self.resizeMode == 'not-reported': - raise Exception( - 'Model file "' + self._model_path + '" does not report the image resize mode\n' - 'Please update the model file via edge-impulse-linux-runner --download') + self.resizeMode = 'squash' return get_features_from_image_with_studio_mode(img, self.resizeMode, self.dim[0], self.dim[1], self.isGrayscale) diff --git a/edge_impulse_linux/runner.py b/edge_impulse_linux/runner.py index caf80a5..bd77ed1 100755 --- a/edge_impulse_linux/runner.py +++ b/edge_impulse_linux/runner.py @@ -96,8 +96,6 @@ def hello(self): return self.send_msg(msg) def classify(self, data): - start = now() - if self._shm: self._shm['array'][:] = data @@ -111,11 +109,8 @@ def classify(self, data): if self._debug: msg["debug"] = True - send_resp = self.send_msg(msg) - end = now() - - print('send_resp', str(end - start) + 'ms.') + send_resp = self.send_msg(msg) return send_resp def send_msg(self, msg): From 38862c344a6fe0c599e71f629e77fe62ff4285f9 Mon Sep 17 00:00:00 2001 From: Jan Jongboom Date: Mon, 4 Aug 2025 11:02:26 +0200 Subject: [PATCH 3/9] Remove set-thresholds.py --- examples/image/set-thresholds.py | 103 ------------------------------- 1 file changed, 103 deletions(-) delete mode 100644 examples/image/set-thresholds.py diff --git a/examples/image/set-thresholds.py b/examples/image/set-thresholds.py deleted file mode 100644 index 9bd15a5..0000000 --- a/examples/image/set-thresholds.py +++ /dev/null @@ -1,103 +0,0 @@ -#!/usr/bin/env python - -import device_patches # Device specific patches for Jetson Nano (needs to be before importing cv2) # noqa: F401 - -try: - import cv2 -except ImportError: - print('Missing OpenCV, install via `pip3 install "opencv-python>=4.5.1.48,<5"`') - exit(1) -import os -import sys -import getopt -from edge_impulse_linux.image import ImageImpulseRunner - -runner = None - -def help(): - print('python classify-image.py ') - -def main(argv): - try: - opts, args = getopt.getopt(argv, "h", ["--help"]) - except getopt.GetoptError: - help() - sys.exit(2) - - for opt, arg in opts: - if opt in ('-h', '--help'): - help() - sys.exit() - - if len(args) != 2: - help() - sys.exit(2) - - model = args[0] - - dir_path = os.path.dirname(os.path.realpath(__file__)) - modelfile = os.path.join(dir_path, model) - - print('MODEL: ' + modelfile) - - with ImageImpulseRunner(modelfile) as runner: - try: - model_info = runner.init() - print('model_info', model_info) - # model_info = runner.init(debug=True) # to get debug print out - - print('Loaded runner for "' + model_info['project']['owner'] + ' / ' + model_info['project']['name'] + '"') - labels = model_info['model_parameters']['labels'] - - img = cv2.imread(args[1]) - if img is None: - print('Failed to load image', args[1]) - exit(1) - - # imread returns images in BGR format, so we need to convert to RGB - img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) - - # get_features_from_image also takes a crop direction arguments in case you don't have square images - # features, cropped = runner.get_features_from_image(img) - - # this mode uses the same settings used in studio to crop and resize the input - features, cropped = runner.get_features_from_image_auto_studio_settings(img) - - res = runner.classify(features) - - if "classification" in res["result"].keys(): - print('Result (%d ms.) ' % (res['timing']['dsp'] + res['timing']['classification']), end='') - for label in labels: - score = res['result']['classification'][label] - print('%s: %.2f\t' % (label, score), end='') - print('', flush=True) - - elif "bounding_boxes" in res["result"].keys(): - print('Found %d bounding boxes (%d ms.)' % (len(res["result"]["bounding_boxes"]), res['timing']['dsp'] + res['timing']['classification'])) - for bb in res["result"]["bounding_boxes"]: - print('\t%s (%.2f): x=%d y=%d w=%d h=%d' % (bb['label'], bb['value'], bb['x'], bb['y'], bb['width'], bb['height'])) - cropped = cv2.rectangle(cropped, (bb['x'], bb['y']), (bb['x'] + bb['width'], bb['y'] + bb['height']), (255, 0, 0), 1) - - if "visual_anomaly_grid" in res["result"].keys(): - print('Found %d visual anomalies (%d ms.)' % (len(res["result"]["visual_anomaly_grid"]), res['timing']['dsp'] + - res['timing']['classification'] + - res['timing']['anomaly'])) - for grid_cell in res["result"]["visual_anomaly_grid"]: - print('\t%s (%.2f): x=%d y=%d w=%d h=%d' % (grid_cell['label'], grid_cell['value'], grid_cell['x'], grid_cell['y'], grid_cell['width'], grid_cell['height'])) - cropped = cv2.rectangle(cropped, (grid_cell['x'], grid_cell['y']), (grid_cell['x'] + grid_cell['width'], grid_cell['y'] + grid_cell['height']), (255, 125, 0), 1) - values = [grid_cell['value'] for grid_cell in res["result"]["visual_anomaly_grid"]] - mean_value = sum(values) / len(values) - max_value = max(values) - print('Max value: %.2f' % max_value) - print('Mean value: %.2f' % mean_value) - - # the image will be resized and cropped, save a copy of the picture here - # so you can see what's being passed into the classifier - cv2.imwrite('debug.jpg', cv2.cvtColor(cropped, cv2.COLOR_RGB2BGR)) - - finally: - if (runner): - runner.stop() - -if __name__ == "__main__": - main(sys.argv[1:]) From 9709116eb2e2f9260047fe401776064ee000ba88 Mon Sep 17 00:00:00 2001 From: Jan Jongboom Date: Mon, 4 Aug 2025 11:12:28 +0200 Subject: [PATCH 4/9] Add set_threshold example --- edge_impulse_linux/runner.py | 7 +++ examples/image/classify-image.py | 1 + examples/image/set-thresholds.py | 86 ++++++++++++++++++++++++++++++++ 3 files changed, 94 insertions(+) create mode 100644 examples/image/set-thresholds.py diff --git a/edge_impulse_linux/runner.py b/edge_impulse_linux/runner.py index bd77ed1..7c1b1f1 100755 --- a/edge_impulse_linux/runner.py +++ b/edge_impulse_linux/runner.py @@ -113,6 +113,13 @@ def classify(self, data): send_resp = self.send_msg(msg) return send_resp + def set_threshold(self, obj): + if not 'id' in obj: + raise Exception('set_threshold requires an object with an "id" field') + + msg = { 'set_threshold': obj } + return self.send_msg(msg) + def send_msg(self, msg): t_send_msg = now() diff --git a/examples/image/classify-image.py b/examples/image/classify-image.py index 0a5cba5..9bd15a5 100644 --- a/examples/image/classify-image.py +++ b/examples/image/classify-image.py @@ -43,6 +43,7 @@ def main(argv): with ImageImpulseRunner(modelfile) as runner: try: model_info = runner.init() + print('model_info', model_info) # model_info = runner.init(debug=True) # to get debug print out print('Loaded runner for "' + model_info['project']['owner'] + ' / ' + model_info['project']['name'] + '"') diff --git a/examples/image/set-thresholds.py b/examples/image/set-thresholds.py new file mode 100644 index 0000000..79c127f --- /dev/null +++ b/examples/image/set-thresholds.py @@ -0,0 +1,86 @@ +#!/usr/bin/env python + +import device_patches # Device specific patches for Jetson Nano (needs to be before importing cv2) # noqa: F401 + +try: + import cv2 +except ImportError: + print('Missing OpenCV, install via `pip3 install "opencv-python>=4.5.1.48,<5"`') + exit(1) +import os +import sys +import getopt +import json +from edge_impulse_linux.image import ImageImpulseRunner + +runner = None + +def help(): + print('python set-thresholds.py ') + +def main(argv): + try: + opts, args = getopt.getopt(argv, "h", ["--help"]) + except getopt.GetoptError: + help() + sys.exit(2) + + for opt, arg in opts: + if opt in ('-h', '--help'): + help() + sys.exit() + + if len(args) != 2: + help() + sys.exit(2) + + model = args[0] + + dir_path = os.path.dirname(os.path.realpath(__file__)) + modelfile = os.path.join(dir_path, model) + + print('MODEL: ' + modelfile) + + with ImageImpulseRunner(modelfile) as runner: + try: + model_info = runner.init() + # model_info = runner.init(debug=True) # to get debug print out + + print('Loaded runner for "' + model_info['project']['owner'] + ' / ' + model_info['project']['name'] + '"') + if not 'thresholds' in model_info['model_parameters']: + print('This model does not expose any thresholds, build a new Linux deployment (.eim file) to get configurable thresholds') + exit(1) + + print('Thresholds:') + for threshold in model_info['model_parameters']['thresholds']: + print(' -', json.dumps(threshold)) + + # Example output for an object detection model: + # Thresholds: + # - {"id": 3, "min_score": 0.20000000298023224, "type": "object_detection"} + + img = cv2.imread(args[1]) + if img is None: + print('Failed to load image', args[1]) + exit(1) + + # imread returns images in BGR format, so we need to convert to RGB + img = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) + # this mode uses the same settings used in studio to crop and resize the input + features, cropped = runner.get_features_from_image_auto_studio_settings(img) + + # dynamically override the thresold from 0.2 -> 0.8 + runner.set_threshold({ + 'id': 3, + 'min_score': 0.8, + }) + + res = runner.classify(features) + print('classify response', json.dumps(res, indent=4)) + + finally: + if (runner): + runner.stop() + +if __name__ == "__main__": + main(sys.argv[1:]) From 162eb4badf4d2dca7d8630ec41af3642071081a1 Mon Sep 17 00:00:00 2001 From: Jan Jongboom Date: Tue, 5 Aug 2025 09:57:29 +0200 Subject: [PATCH 5/9] Remove debug statement --- examples/image/classify-image.py | 1 - 1 file changed, 1 deletion(-) diff --git a/examples/image/classify-image.py b/examples/image/classify-image.py index 9bd15a5..0a5cba5 100644 --- a/examples/image/classify-image.py +++ b/examples/image/classify-image.py @@ -43,7 +43,6 @@ def main(argv): with ImageImpulseRunner(modelfile) as runner: try: model_info = runner.init() - print('model_info', model_info) # model_info = runner.init(debug=True) # to get debug print out print('Loaded runner for "' + model_info['project']['owner'] + ' / ' + model_info['project']['name'] + '"') From 1fcafd8d3726eb240e1cc834e26b89b28114e2a1 Mon Sep 17 00:00:00 2001 From: Mateusz Majchrzycki Date: Wed, 6 Aug 2025 13:32:18 +0200 Subject: [PATCH 6/9] image: missing importing sys package --- edge_impulse_linux/image.py | 1 + 1 file changed, 1 insertion(+) diff --git a/edge_impulse_linux/image.py b/edge_impulse_linux/image.py index da069e4..30d8151 100644 --- a/edge_impulse_linux/image.py +++ b/edge_impulse_linux/image.py @@ -1,6 +1,7 @@ #!/usr/bin/env python import numpy as np +import sys try: import cv2 except ImportError: From 562c56689eb00d994f1421c7bb8d825a0231c03c Mon Sep 17 00:00:00 2001 From: Mateusz Majchrzycki Date: Wed, 6 Aug 2025 13:32:30 +0200 Subject: [PATCH 7/9] runner: small simplification --- edge_impulse_linux/runner.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/edge_impulse_linux/runner.py b/edge_impulse_linux/runner.py index 7c1b1f1..fecaa40 100755 --- a/edge_impulse_linux/runner.py +++ b/edge_impulse_linux/runner.py @@ -57,8 +57,7 @@ def init(self, debug=False): if ('features_shm' in hello_resp.keys()): shm_name = hello_resp['features_shm']['name'] # python does not want the leading slash - if (shm_name.startswith('/')): - shm_name = shm_name[1:] + shm_name = shm_name.lstrip('/') shm = shared_memory.SharedMemory(name=shm_name) self._shm = { 'shm': shm, From 54a146fc9fa4719b69e2956d87a5b902efd1dd42 Mon Sep 17 00:00:00 2001 From: Mateusz Majchrzycki Date: Wed, 6 Aug 2025 13:49:12 +0200 Subject: [PATCH 8/9] set-thresholds: make the example interactive --- examples/image/set-thresholds.py | 26 ++++++++++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) mode change 100644 => 100755 examples/image/set-thresholds.py diff --git a/examples/image/set-thresholds.py b/examples/image/set-thresholds.py old mode 100644 new mode 100755 index 79c127f..907dee6 --- a/examples/image/set-thresholds.py +++ b/examples/image/set-thresholds.py @@ -69,10 +69,32 @@ def main(argv): # this mode uses the same settings used in studio to crop and resize the input features, cropped = runner.get_features_from_image_auto_studio_settings(img) + print("Which threshold would you like to change? (id)") + while True: + try: + threshold_id = int(input('Enter threshold ID: ')) + if threshold_id not in [t['id'] for t in model_info['model_parameters']['thresholds']]: + print('Invalid threshold ID, try again') + continue + break + except ValueError: + print('Invalid input, please enter a number') + + print("Enter a new threshold value (between 0.0 and 1.0):") + while True: + try: + new_threshold = float(input('New threshold value: ')) + if new_threshold < 0.0 or new_threshold > 1.0: + print('Invalid threshold value, must be between 0.0 and 1.0') + continue + break + except ValueError: + print('Invalid input, please enter a number') + # dynamically override the thresold from 0.2 -> 0.8 runner.set_threshold({ - 'id': 3, - 'min_score': 0.8, + 'id': threshold_id, + 'min_score': new_threshold, }) res = runner.classify(features) From 5c18fbc7ded2896971228df1067ab07729f859e7 Mon Sep 17 00:00:00 2001 From: Mateusz Majchrzycki Date: Wed, 6 Aug 2025 16:43:18 +0200 Subject: [PATCH 9/9] image: more optimized image conversion Previous versions were ~10 slower than the new one. --- edge_impulse_linux/image.py | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/edge_impulse_linux/image.py b/edge_impulse_linux/image.py index 30d8151..07fc883 100644 --- a/edge_impulse_linux/image.py +++ b/edge_impulse_linux/image.py @@ -236,17 +236,10 @@ def get_features_from_image_with_studio_mode(img, mode, output_width, output_hei if is_grayscale: resized_img = cv2.cvtColor(resized_img, cv2.COLOR_BGR2GRAY) - pixels = np.array(resized_img).flatten().tolist() - - for p in pixels: - features.append((p << 16) + (p << 8) + p) + features = (resized_img.astype(np.uint32) * 0x010101).flatten().tolist() else: - pixels = np.array(resized_img).flatten().tolist() - - for ix in range(0, len(pixels), 3): - r = pixels[ix + 0] - g = pixels[ix + 1] - b = pixels[ix + 2] - features.append((r << 16) + (g << 8) + b) + # Use numpy's vectorized operations for RGB feature encoding + pixels = resized_img.astype(np.uint32) + features = ((pixels[..., 0] << 16) | (pixels[..., 1] << 8) | pixels[..., 2]).flatten().tolist() return features, resized_img \ No newline at end of file