From 5b8d5c047bdd0a599c93df70deee8d007a21a17b Mon Sep 17 00:00:00 2001 From: AIWintermuteAI Date: Fri, 19 Dec 2025 10:51:54 +0100 Subject: [PATCH] added example for dynamic image input size --- edge_impulse_linux/image.py | 8 +- edge_impulse_linux/runner.py | 38 ++++++- .../classify-image-dynamic-input-size.py | 107 ++++++++++++++++++ 3 files changed, 147 insertions(+), 6 deletions(-) create mode 100644 examples/image/classify-image-dynamic-input-size.py diff --git a/edge_impulse_linux/image.py b/edge_impulse_linux/image.py index 07fc883..a2304f4 100644 --- a/edge_impulse_linux/image.py +++ b/edge_impulse_linux/image.py @@ -21,10 +21,10 @@ def __init__(self, model_path: str): self.isGrayscale = False self.resizeMode = '' - def init(self, debug=False): - model_info = super(ImageImpulseRunner, self).init(debug) - width = model_info['model_parameters']['image_input_width'] - height = model_info['model_parameters']['image_input_height'] + def init(self, debug=False, dynamic_input_size: tuple = False): + model_info = super(ImageImpulseRunner, self).init(debug, dynamic_input_size) + width = model_info['model_parameters']['image_input_width'] if not dynamic_input_size else dynamic_input_size[0] + height = model_info['model_parameters']['image_input_height'] if not dynamic_input_size else dynamic_input_size[1] if width == 0 or height == 0: raise Exception('Model file "' + self._model_path + '" is not suitable for image recognition') diff --git a/edge_impulse_linux/runner.py b/edge_impulse_linux/runner.py index d5bea3a..1925b0a 100755 --- a/edge_impulse_linux/runner.py +++ b/edge_impulse_linux/runner.py @@ -24,9 +24,11 @@ def __init__(self, model_path: str, timeout: int = 30, allow_shm = True): self._allow_shm = allow_shm self._input_shm = None self._freeform_output_shm = [] - self._timeout = timeout + self._timeout = timeout if not allow_shm else None - def init(self, debug=False): + def init(self, debug=False, dynamic_input_size: tuple = False): + self._dynamic_input_size = dynamic_input_size + self._allow_shm = self._allow_shm and (dynamic_input_size == False) if not os.path.exists(self._model_path): raise Exception("Model file does not exist: " + self._model_path) @@ -59,6 +61,15 @@ def init(self, debug=False): hello_resp = self._hello_resp = self.hello() + if self._dynamic_input_size: + width, height, channels = self._dynamic_input_size + resp = self.set_parameters({ + 'image_width': width, + 'image_height': height, + 'image_channels': channels + }) + hello_resp['features_shm']['elements'] = width * height + if self._allow_shm: if ('features_shm' in hello_resp.keys()): shm_name = hello_resp['features_shm']['name'] @@ -151,6 +162,29 @@ def set_threshold(self, obj): msg = { 'set_threshold': obj } return self.send_msg(msg) + def set_parameter(self, obj): + if not 'id' in obj: + raise Exception('set_threshold requires an object with an "id" field') + + msg = { 'set_parameter': obj } + return self.send_msg(msg) + + def set_parameters(self, *kwargs): + msg = { 'set_parameter': kwargs[0] } + return self.send_msg(msg) + + def set_image_input_parameters(self, width, height, channels): + msg = { + 'set_parameter': { + 'width': width, + 'height': height, + 'channels': channels + } + } + self.dim = (width, height) + self.isGrayscale = (channels == 1) + return self.send_msg(msg) + def send_msg(self, msg): t_send_msg = now() diff --git a/examples/image/classify-image-dynamic-input-size.py b/examples/image/classify-image-dynamic-input-size.py new file mode 100644 index 0000000..79dc93a --- /dev/null +++ b/examples/image/classify-image-dynamic-input-size.py @@ -0,0 +1,107 @@ +#!/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 width height') + +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) != 4: + 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(debug=True) + model_info = runner.init(debug=True, dynamic_input_size = (96, 96, 3)) + # set dynamic input size, only RGB for now + 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) + + elif "freeform" in res['result'].keys(): + print('Result (%d ms.)' % (res['timing']['dsp'] + res['timing']['classification'])) + for i in range(0, len(res['result']['freeform'])): + print(f' Freeform output {i}:', ", ".join(f"{x:.4f}" for x in res['result']['freeform'][i])) + + 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:])