From 9a7c2e3a0660e2d3f2d4eb1f4fe72b1689b3245c Mon Sep 17 00:00:00 2001 From: rockleona <34214497+rockleona@users.noreply.github.com> Date: Tue, 18 Nov 2025 21:43:04 +0800 Subject: [PATCH 1/3] feat: add object detector feature in modmesh --- modmesh/pilot/_gui.py | 5 +- modmesh/pilot/vision/__init__.py | 37 +++++ modmesh/pilot/vision/_vision_gui.py | 200 +++++++++++++++++++++++++++ modmesh/pilot/vision/_vision_yolo.py | 170 +++++++++++++++++++++++ 4 files changed, 411 insertions(+), 1 deletion(-) create mode 100644 modmesh/pilot/vision/__init__.py create mode 100644 modmesh/pilot/vision/_vision_gui.py create mode 100644 modmesh/pilot/vision/_vision_yolo.py diff --git a/modmesh/pilot/_gui.py b/modmesh/pilot/_gui.py index 74a24a9cf..56439fd81 100644 --- a/modmesh/pilot/_gui.py +++ b/modmesh/pilot/_gui.py @@ -37,6 +37,7 @@ from . import _pilot_core as _pcore from . import airfoil +from . import vision if _pcore.enable: from PySide6.QtGui import QAction @@ -80,6 +81,7 @@ def __init__(self): self.burgers = None self.openprofiledata = None self.runprofiling = None + self.vision = None def __getattr__(self, name): return None if self._rmgr is None else getattr(self._rmgr, name) @@ -99,6 +101,7 @@ def launch(self, name="pilot", size=(1000, 600)): self.linear_wave = _linear_wave.LinearWave1DApp(mgr=self._rmgr) self.openprofiledata = _profiling.Profiling(mgr=self._rmgr) self.runprofiling = _profiling.RunProfiling(mgr=self._rmgr) + self.vision = vision.VisionGui(mgr=self._rmgr) self.populate_menu() self._rmgr.show() return self._rmgr.exec() @@ -131,6 +134,7 @@ def _addAction(menu, text, tip, func, checkable=False, checked=False): self.linear_wave.populate_menu() self.openprofiledata.populate_menu() self.runprofiling.populate_menu() + self.vision.populate_menu() if sys.platform != 'darwin': _addAction( @@ -149,7 +153,6 @@ def _addAction(menu, text, tip, func, checkable=False, checked=False): checked=True, ) - controller = _Controller() # vim: set ff=unix fenc=utf8 et sw=4 ts=4 sts=4: diff --git a/modmesh/pilot/vision/__init__.py b/modmesh/pilot/vision/__init__.py new file mode 100644 index 000000000..e02f33dd4 --- /dev/null +++ b/modmesh/pilot/vision/__init__.py @@ -0,0 +1,37 @@ +# Copyright (c) 2025, Li-Hung Wang +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# - Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# - Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# - Neither the name of the copyright holder nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + + +""" +Vision features modules +""" + +from ._vision_gui import VisionGui + +__all__ = [ + 'VisionGui', +] + diff --git a/modmesh/pilot/vision/_vision_gui.py b/modmesh/pilot/vision/_vision_gui.py new file mode 100644 index 000000000..1fe1f3ee9 --- /dev/null +++ b/modmesh/pilot/vision/_vision_gui.py @@ -0,0 +1,200 @@ +# Copyright (c) 2025, Li-Hung Wang +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# - Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# - Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# - Neither the name of the copyright holder nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +""" +GUI for Vision features +""" + +import numpy as np + +from PySide6.QtCore import Qt +from PySide6.QtGui import QIcon, QImage, QPixmap +from PySide6.QtWidgets import QDockWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QFileDialog, QWidget +from PySide6.QtGui import QPainter, QPen, QFont + +from .._gui_common import PilotFeature +from ._vision_yolo import _yolo_detector + +class VisionGui(PilotFeature): + + def __init__(self, *args, **kw): + super().__init__(*args, **kw) + # Initialize Vision GUI components here + self.widget = QDockWidget("Vision", self._mainWindow) + self.widget.resize(400, 300) + + # Create central widget for the dock widget + self.central_widget = QWidget() + self.widget.setWidget(self.central_widget) + + self.layout = QVBoxLayout() + self.central_widget.setLayout(self.layout) + + self._status_layout = QHBoxLayout() + self._status_layout.setSpacing(10) # Set spacing between items + self._status_layout.setContentsMargins(0, 0, 0, 0) # Remove margins + self._status_layout.setAlignment(Qt.AlignmentFlag.AlignLeft) # Align items to left + self.layout.addLayout(self._status_layout) + + self.status_light_icon = QLabel() + red_icon = QIcon.fromTheme("media-record") + self.status_light_icon.setPixmap(red_icon.pixmap(16, 16)) + + self._status_layout.addWidget(self.status_light_icon) + self.status_label = QLabel("Not Activated") + self._status_layout.addWidget(self.status_label) + + self.image_instance = QImage() + self.image_label = QLabel() + self.layout.addWidget(self.image_label, 1) # Add stretch factor of 1 + + self.load_image_button = QPushButton("Load Image") + self.load_image_button.clicked.connect(self.click_load_image) + self.layout.addWidget(self.load_image_button) + + self.is_vision_active = False + self.vision_button = QPushButton("Activate Vision") + self.vision_button.clicked.connect(self.toggle_activation) + self.layout.addWidget(self.vision_button) + + self._mainWindow.addDockWidget(Qt.DockWidgetArea.BottomDockWidgetArea, self.widget) + + + def populate_menu(self): + # Code to populate the menu for Vision GUI + self._add_menu_item( + menu=self._mgr.windowMenu, + text="Computer Vision", + tip="Open / Close Computer Vision Window", + func=self.toggle_visibility, + ) + + def click_load_image(self): + # Code to handle image loading + file_name, _ = QFileDialog.getOpenFileName( + self.widget, + "Open Image", + "", + "Image Files (*.png *.jpg *.bmp);;All Files (*)" + ) + + if file_name: + self.image_instance.load(file_name) + scaled_image = self.image_instance.scaled( + self.image_label.size(), + Qt.AspectRatioMode.KeepAspectRatio, + Qt.TransformationMode.SmoothTransformation + ) + self.image_label.setPixmap(QPixmap.fromImage(scaled_image)) + + if not self.is_vision_active: + return + + image_array = self.qimage_to_numpy(self.image_instance) + detections = _yolo_detector.detect(image_array) + + self.draw_bboxes(image_array, detections) + + def draw_bboxes(self, image, detections): + # Code to draw bounding boxes on the image based on detections + + for det in detections: + x1, y1, w, h = det['bbox'] + label = det['label'] + score = det['score'] + + # Convert numpy array to QImage for drawing + height, width, channel = image.shape + bytes_per_line = 3 * width + q_image = QImage(image.data, width, height, bytes_per_line, QImage.Format.Format_RGB888) + + # Create QPainter to draw on the image + + painter = QPainter(q_image) + + # Set pen for bounding box + pen = QPen(Qt.GlobalColor.red) + pen.setWidth(3) + painter.setPen(pen) + + # Draw rectangle (bounding box) + painter.drawRect(x1, y1, w, h) + + # Set font and draw label text + font = QFont() + font.setPointSize(20) + painter.setFont(font) + + text = f"{label}: {score:.2f}" + painter.drawText(x1, y1 - 5, text) + + painter.end() + + # Update the displayed image + scaled_image = q_image.scaled( + self.image_label.size(), + Qt.AspectRatioMode.KeepAspectRatio, + Qt.TransformationMode.SmoothTransformation + ) + self.image_label.setPixmap(QPixmap.fromImage(scaled_image)) + + def qimage_to_numpy(self, qimage): + # Convert QImage to numpy array + qimage = qimage.convertToFormat(QImage.Format.Format_RGB888) + width = qimage.width() + height = qimage.height() + + ptr = qimage.bits() + arr = np.array(ptr).reshape((height, width, 3)) + return arr + + def toggle_activation(self): + # Code to toggle activation of Vision features + if self.is_vision_active: + self.is_vision_active = False + _yolo_detector.deactivate() + + self.vision_button.setText("Activate Vision") + self.status_label.setText("Vision Module Deactivated") + + red_icon = QIcon.fromTheme("media-record") + self.status_light_icon.setPixmap(red_icon.pixmap(16, 16)) + else: + self.is_vision_active = True + _yolo_detector.activate() + + self.vision_button.setText("Deactivate Vision") + self.status_label.setText("Vision Module Activated") + + green_icon = QIcon.fromTheme("media-playback-start") + self.status_light_icon.setPixmap(green_icon.pixmap(16, 16)) + + def toggle_visibility(self): + # Code to toggle visibility of Vision GUI + if self.widget.isVisible(): + self.widget.hide() + else: + self.widget.show() \ No newline at end of file diff --git a/modmesh/pilot/vision/_vision_yolo.py b/modmesh/pilot/vision/_vision_yolo.py new file mode 100644 index 000000000..33efe0db2 --- /dev/null +++ b/modmesh/pilot/vision/_vision_yolo.py @@ -0,0 +1,170 @@ +# Copyright (c) 2025, Li-Hung Wang +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# - Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# - Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# - Neither the name of the copyright holder nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + +""" +Vision feature: YOLO object detection integration +""" + +from ultralytics import YOLO +from ultralytics.utils import set_logging +import io +import logging + +__all__ = [ + "_yolo_detector", +] + + +class ConsoleHandler(logging.Handler): + """ + Custom logging handler for sending logs to python console widget + """ + + def __init__(self, console_widget): + super().__init__() + self.console_widget = console_widget + + def emit(self, record): + """ + Send log record to console widget + """ + try: + msg = self.format(record) + if self.console_widget: + self.console_widget.writeToHistory(msg + "\n") + except Exception: + self.handleError(record) + + +def set_up_logger(): + global logger + if "logger" not in globals(): + set_logging(name="ultralytics", verbose=True) + logger = logging.getLogger("ultralytics") + + try: + from .. import mgr + + console_widget = mgr.pycon + + if console_widget is None: + raise RuntimeError("Python console widget is not available") + + # Create custom handler + console_handler = ConsoleHandler(console_widget) + console_handler.setLevel(logging.DEBUG) + + # Set formatter + formatter = logging.Formatter("[%(name)s] %(levelname)s: %(message)s") + console_handler.setFormatter(formatter) + + # Clear existing handlers and add the custom handler + logger.handlers.clear() + logger.addHandler(console_handler) + logger.setLevel(logging.DEBUG) + logger.propagate = False # Avoid duplicate output + + # Test the console widget by writing a message + if console_widget: + console_widget.writeToHistory( + "[Vision] Logger initialized successfully\n" + ) + + except Exception as e: + # If unable to get console widget, fall back to original method + # Print the error for debugging + print(f"[Vision] Failed to set up console widget logger: {e}") + + log_stream = io.StringIO() + handler = logging.StreamHandler(log_stream) + logger.addHandler(handler) + logger.setLevel(logging.DEBUG) + + # Also add a console handler for fallback + console_handler = logging.StreamHandler() + console_handler.setLevel(logging.DEBUG) + formatter = logging.Formatter("[%(name)s] %(levelname)s: %(message)s") + console_handler.setFormatter(formatter) + logger.addHandler(console_handler) + + +class YoloDetector: + """ + YOLO Detector class to manage model loading and detection + """ + + def __init__(self): + self.model = None + self.logger = None + self.is_active = False + + def activate(self, model_path="./thirdparty/yolo11n.pt"): + if self.model is None: + self.model = YOLO(model_path) + set_up_logger() + self.logger = logging.getLogger("ultralytics") + self.is_active = True + + def deactivate(self): + self.is_active = False + + def detect(self, np_img): + """ + Perform detection on the input image + + Args: + np_img (numpy.ndarray): Input image as a numpy array. + + Returns: + list of dict: Each dict contains 'bbox', 'label', and 'score'. + """ + if self.model is None: + raise RuntimeError( + "YOLO model is not loaded. Please activate the detector first." + ) + + results = self.model(np_img) + boxes = [] + for box in results[0].boxes: + x1, y1, x2, y2 = box.xyxy[0].tolist() + label_idx = int(box.cls[0]) + label = results[0].names[label_idx] + score = float(box.conf[0]) + + box_obj = { + "bbox": [int(x1), int(y1), int(x2 - x1), int(y2 - y1)], + "label": label, + "score": score, + } + self.logger.debug( + f"Detected {label} with confidence {score:.2f} at [{x1}, {y1}, {x2}, {y2}]" + ) + boxes.append(box_obj) + + return boxes + + +_yolo_detector = YoloDetector() From c069cd70b6c8a5a9fd338852d35878c11db12f38 Mon Sep 17 00:00:00 2001 From: rockleona <34214497+rockleona@users.noreply.github.com> Date: Mon, 24 Nov 2025 21:54:59 +0800 Subject: [PATCH 2/3] test: adding test files for vision --- tests/data/jpg/cat.jpg | Bin 0 -> 34429 bytes tests/test_pilot_vision.py | 89 +++++++++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+) create mode 100644 tests/data/jpg/cat.jpg create mode 100644 tests/test_pilot_vision.py diff --git a/tests/data/jpg/cat.jpg b/tests/data/jpg/cat.jpg new file mode 100644 index 0000000000000000000000000000000000000000..70b8ee4f8fa82a7247f913ce3d2af3a45fe9be2f GIT binary patch literal 34429 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 If;ij%*?M~Y6aWAK literal 0 HcmV?d00001 diff --git a/tests/test_pilot_vision.py b/tests/test_pilot_vision.py new file mode 100644 index 000000000..135d2eefe --- /dev/null +++ b/tests/test_pilot_vision.py @@ -0,0 +1,89 @@ +# Copyright (c) 2025, Li-Hung Wang +# +# Redistribution and use in source and binary forms, with or without +# modification, are permitted provided that the following conditions are met: +# +# - Redistributions of source code must retain the above copyright notice, this +# list of conditions and the following disclaimer. +# - Redistributions in binary form must reproduce the above copyright notice, +# this list of conditions and the following disclaimer in the documentation +# and/or other materials provided with the distribution. +# - Neither the name of the copyright holder nor the names of its contributors +# may be used to endorse or promote products derived from this software +# without specific prior written permission. +# +# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +# AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +# IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE +# ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE +# LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR +# CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF +# SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS +# INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN +# CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) +# ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE +# POSSIBILITY OF SUCH DAMAGE. + + +import io +import os +import logging +import unittest + +import numpy as np +import requests +from PIL import Image +from ultralytics import YOLO + +import modmesh +from modmesh.pilot.vision._vision_yolo import _yolo_detector + +try: + from modmesh import pilot +except ImportError: + pilot = None + + +@unittest.skipUnless(modmesh.HAS_PILOT, "Qt pilot is not built") +class VisionTC(unittest.TestCase): + + def test_yolo_detector(self): + self.assertIsNotNone(_yolo_detector) + + self.assertFalse(_yolo_detector.model) + self.assertFalse(_yolo_detector.logger) + self.assertFalse(_yolo_detector.is_active) + + _yolo_detector.activate() + + self.assertTrue(_yolo_detector.is_active) + self.assertIsInstance(_yolo_detector.logger, logging.Logger) + self.assertIsInstance(_yolo_detector.model, YOLO) + + _yolo_detector.deactivate() + + def test_yolo_inference(self): + + TESTDIR = os.path.abspath(os.path.dirname(__file__)) + DATADIR = os.path.join(TESTDIR, "data/jpg") + + _yolo_detector.activate() + + # Convert to numpy array + image = Image.open(os.path.join(DATADIR, "cat.jpg")).convert("RGB") + image_array = np.array(image) + + results = _yolo_detector.detect(image_array) + + self.assertIsNotNone(results) + self.assertGreater(len(results), 0) + + first_result = results[0] + self.assertTrue("bbox" in first_result) + self.assertTrue("label" in first_result) + self.assertTrue("score" in first_result) + + _yolo_detector.deactivate() + + +# vim: set ff=unix fenc=utf8 et sw=4 ts=4 sts=4: From 069a67f8a49242a38cbb1cd1bcbe4be47918eb88 Mon Sep 17 00:00:00 2001 From: rockleona <34214497+rockleona@users.noreply.github.com> Date: Mon, 24 Nov 2025 22:00:32 +0800 Subject: [PATCH 3/3] docs: adding license file for test jpg file --- tests/data/jpg/COPYING | 12 ++++++++++++ tests/data/jpg/README.rst | 15 +++++++++++++++ 2 files changed, 27 insertions(+) create mode 100644 tests/data/jpg/COPYING create mode 100644 tests/data/jpg/README.rst diff --git a/tests/data/jpg/COPYING b/tests/data/jpg/COPYING new file mode 100644 index 000000000..7ab100b73 --- /dev/null +++ b/tests/data/jpg/COPYING @@ -0,0 +1,12 @@ +The JPG file (cat.jpg) is licensed under +the Unsplash License (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + https://unsplash.com/license + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. diff --git a/tests/data/jpg/README.rst b/tests/data/jpg/README.rst new file mode 100644 index 000000000..98faf6041 --- /dev/null +++ b/tests/data/jpg/README.rst @@ -0,0 +1,15 @@ +============== +JPG Test Files +============== + +This directory contains JPG files used for testing purposes in modmesh. + +Test Files +========== + +- cat.jpg (original source: https://unsplash.com/photos/orange-and-white-cat-on-yellow-surface-sR0cTmQHPug?utm_source=unsplash&utm_medium=referral&utm_content=creditShareLink) + +License +======= + +See the COPYING file in the root directory for license information. \ No newline at end of file