From 953f5d015bda69284ebfc22209494d62d3a6fa3d Mon Sep 17 00:00:00 2001 From: mrpositron <42044624+mrpositron@users.noreply.github.com> Date: Tue, 11 Aug 2026 19:38:13 +0200 Subject: [PATCH 1/5] Fix mypy errors in kitti and yolo plugins lightly_studio 1.0.4 added two required parameters to LightlyStudioInputBase.__init__. Pass annotation_collection_id and sample_to_image through from KittiObjectDetectionInput, reusing the existing image_sample_to_image strategy. ultralytics now ships py.typed, so mypy sees its real annotations: predict() is declared to return an iterator or tensors, and Boxes has no __iter__. Narrow the result with isinstance and iterate boxes by index instead. No behavior change. Co-Authored-By: Claude Opus 5 (1M context) --- .../operator.py | 12 +++++++++- .../operator.py | 24 +++++++++++++------ 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/plugins/kitti_export_object_detection/src/lightly_plugins_kitti_export_object_detection/operator.py b/plugins/kitti_export_object_detection/src/lightly_plugins_kitti_export_object_detection/operator.py index d01c2ea..ef960c9 100644 --- a/plugins/kitti_export_object_detection/src/lightly_plugins_kitti_export_object_detection/operator.py +++ b/plugins/kitti_export_object_detection/src/lightly_plugins_kitti_export_object_detection/operator.py @@ -15,6 +15,7 @@ from sqlmodel import Session from lightly_studio.core.image.image_sample import ImageSample +from lightly_studio.export.image_dataset_export import image_sample_to_image from lightly_studio.export.lightly_studio_label_input import ( LightlyStudioObjectDetectionInput, ) @@ -40,6 +41,7 @@ def __init__( session: Session, dataset_id: UUID, samples: Iterable[ImageSample], + annotation_collection_id: UUID | None = None, images_root: Path | None = None, ) -> None: """Initialize the input. @@ -48,10 +50,18 @@ def __init__( session: The database session. dataset_id: The dataset ID for label retrieval. samples: The samples to export. + annotation_collection_id: If provided, only annotations belonging to this + annotation collection are exported. If None, all annotations are exported. images_root: Common root path used to preserve nested image folders. """ self._images_root = images_root - super().__init__(session=session, dataset_id=dataset_id, samples=samples) + super().__init__( + session=session, + dataset_id=dataset_id, + samples=samples, + annotation_collection_id=annotation_collection_id, + sample_to_image=image_sample_to_image, + ) def get_images(self) -> list[Image]: """Return images with filenames relative to the KITTI output folder.""" diff --git a/plugins/yolo_object_detection/src/lightly_plugins_yolo_object_detection/operator.py b/plugins/yolo_object_detection/src/lightly_plugins_yolo_object_detection/operator.py index e2a532f..b976ab5 100644 --- a/plugins/yolo_object_detection/src/lightly_plugins_yolo_object_detection/operator.py +++ b/plugins/yolo_object_detection/src/lightly_plugins_yolo_object_detection/operator.py @@ -9,6 +9,7 @@ from sqlmodel import Session from ultralytics import YOLO # type: ignore[attr-defined] +from ultralytics.engine.results import Boxes, Results from lightly_studio.models.annotation.annotation_base import ( AnnotationCreate, @@ -140,9 +141,11 @@ def execute( total_annotations_created = 0 for i, image_entry in enumerate(samples, start=1): try: - results = model( - image_entry.file_path_abs, conf=confidence, verbose=False - )[0] + results = list( + model.predict( + image_entry.file_path_abs, conf=confidence, verbose=False + ) + ) except Exception as e: logger.error( "Failed to run inference on '%s': %s", @@ -153,12 +156,19 @@ def execute( success=False, message=f"Failed to run inference on '{image_entry.file_path_abs}': {e}", ) - for box in results.boxes: - category_id = int(box.cls) + # Detection on a single image returns exactly one `Results`. + result = results[0] + if not isinstance(result, Results): + continue + boxes: Boxes | None = result.boxes + if boxes is None: + continue + for box_index in range(len(boxes)): + category_id = int(boxes.cls[box_index]) label_id = label_map.get(category_id) if label_id is None: continue - x_center, y_center, w, h = box.xywh[0].tolist() + x_center, y_center, w, h = boxes.xywh[box_index].tolist() annotations_to_create.append( AnnotationCreate( annotation_label_id=label_id, @@ -168,7 +178,7 @@ def execute( y=round(y_center - h / 2), width=max(1, round(w)), height=max(1, round(h)), - confidence=float(box.conf), + confidence=float(boxes.conf[box_index]), ) ) From 9040bc3975115779b19ff4dd22bb0faac93e1b77 Mon Sep 17 00:00:00 2001 From: mrpositron <42044624+mrpositron@users.noreply.github.com> Date: Tue, 11 Aug 2026 19:55:11 +0200 Subject: [PATCH 2/5] Address review findings on the kitti and yolo fixes Bump lightly_studio floor to >=1.0.4 in both plugins. The kitti plugin imports image_sample_to_image, which does not exist before 1.0.4, so 1.0.0-1.0.3 installed cleanly and then failed with an ImportError when the entry point loaded. yolo: move results[0] back inside the try so an empty result list returns success=False instead of raising IndexError out of execute(), and log a warning on both skip branches. Pointing model_path at a classification checkpoint previously raised a visible TypeError; it now skips every image, so without a log the operator reports success with zero annotations and no way to diagnose it. Also drop the unused annotation_collection_id parameter from KittiObjectDetectionInput (no caller passes it) and the redundant Boxes annotation. Co-Authored-By: Claude Opus 5 (1M context) --- .../kitti_export_object_detection/pyproject.toml | 2 +- .../operator.py | 5 +---- plugins/yolo_object_detection/pyproject.toml | 2 +- .../operator.py | 16 ++++++++++++---- 4 files changed, 15 insertions(+), 10 deletions(-) diff --git a/plugins/kitti_export_object_detection/pyproject.toml b/plugins/kitti_export_object_detection/pyproject.toml index 67cf4e8..ba2caaa 100644 --- a/plugins/kitti_export_object_detection/pyproject.toml +++ b/plugins/kitti_export_object_detection/pyproject.toml @@ -4,7 +4,7 @@ version = "0.1.0" description = "KITTI object detection export plugin for Lightly Studio" requires-python = ">=3.9" dependencies = [ - "lightly_studio>=1.0.0", + "lightly_studio>=1.0.4", "labelformat", "sqlmodel", ] diff --git a/plugins/kitti_export_object_detection/src/lightly_plugins_kitti_export_object_detection/operator.py b/plugins/kitti_export_object_detection/src/lightly_plugins_kitti_export_object_detection/operator.py index ef960c9..77deb11 100644 --- a/plugins/kitti_export_object_detection/src/lightly_plugins_kitti_export_object_detection/operator.py +++ b/plugins/kitti_export_object_detection/src/lightly_plugins_kitti_export_object_detection/operator.py @@ -41,7 +41,6 @@ def __init__( session: Session, dataset_id: UUID, samples: Iterable[ImageSample], - annotation_collection_id: UUID | None = None, images_root: Path | None = None, ) -> None: """Initialize the input. @@ -50,8 +49,6 @@ def __init__( session: The database session. dataset_id: The dataset ID for label retrieval. samples: The samples to export. - annotation_collection_id: If provided, only annotations belonging to this - annotation collection are exported. If None, all annotations are exported. images_root: Common root path used to preserve nested image folders. """ self._images_root = images_root @@ -59,7 +56,7 @@ def __init__( session=session, dataset_id=dataset_id, samples=samples, - annotation_collection_id=annotation_collection_id, + annotation_collection_id=None, sample_to_image=image_sample_to_image, ) diff --git a/plugins/yolo_object_detection/pyproject.toml b/plugins/yolo_object_detection/pyproject.toml index e8f7ef8..b2087e6 100644 --- a/plugins/yolo_object_detection/pyproject.toml +++ b/plugins/yolo_object_detection/pyproject.toml @@ -4,7 +4,7 @@ version = "0.1.0" description = "YOLO inference operator for object detection auto-labeling" requires-python = ">=3.9" dependencies = [ - "lightly_studio>=1.0.0", + "lightly_studio>=1.0.4", "sqlmodel", "ultralytics", ] diff --git a/plugins/yolo_object_detection/src/lightly_plugins_yolo_object_detection/operator.py b/plugins/yolo_object_detection/src/lightly_plugins_yolo_object_detection/operator.py index b976ab5..5c467cf 100644 --- a/plugins/yolo_object_detection/src/lightly_plugins_yolo_object_detection/operator.py +++ b/plugins/yolo_object_detection/src/lightly_plugins_yolo_object_detection/operator.py @@ -9,7 +9,7 @@ from sqlmodel import Session from ultralytics import YOLO # type: ignore[attr-defined] -from ultralytics.engine.results import Boxes, Results +from ultralytics.engine.results import Results from lightly_studio.models.annotation.annotation_base import ( AnnotationCreate, @@ -146,6 +146,7 @@ def execute( image_entry.file_path_abs, conf=confidence, verbose=False ) ) + result = results[0] except Exception as e: logger.error( "Failed to run inference on '%s': %s", @@ -156,12 +157,19 @@ def execute( success=False, message=f"Failed to run inference on '{image_entry.file_path_abs}': {e}", ) - # Detection on a single image returns exactly one `Results`. - result = results[0] if not isinstance(result, Results): + logger.warning( + "Unexpected result type for '%s'; skipping.", + image_entry.file_path_abs, + ) continue - boxes: Boxes | None = result.boxes + boxes = result.boxes if boxes is None: + logger.warning( + "No boxes returned for '%s'; is '%s' a detection model?", + image_entry.file_path_abs, + model_path, + ) continue for box_index in range(len(boxes)): category_id = int(boxes.cls[box_index]) From e67e5d9197272dd0a2c0c0d6e38034bdb78e1c52 Mon Sep 17 00:00:00 2001 From: mrpositron <42044624+mrpositron@users.noreply.github.com> Date: Tue, 11 Aug 2026 20:37:50 +0200 Subject: [PATCH 3/5] Revert unmotivated lightly_studio floor bump in yolo plugin The yolo plugin does not use the export API; all of its lightly_studio imports resolve on 1.0.3. Only kitti imports image_sample_to_image, so only kitti needs the >=1.0.4 floor. Bumping yolo would force an upgrade on users of a plugin that works fine on 1.0.0-1.0.3. Co-Authored-By: Claude Opus 5 (1M context) --- plugins/yolo_object_detection/pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/yolo_object_detection/pyproject.toml b/plugins/yolo_object_detection/pyproject.toml index b2087e6..e8f7ef8 100644 --- a/plugins/yolo_object_detection/pyproject.toml +++ b/plugins/yolo_object_detection/pyproject.toml @@ -4,7 +4,7 @@ version = "0.1.0" description = "YOLO inference operator for object detection auto-labeling" requires-python = ">=3.9" dependencies = [ - "lightly_studio>=1.0.4", + "lightly_studio>=1.0.0", "sqlmodel", "ultralytics", ] From 01dcc3a6b4a7a3ed620aef08b48683256380f3bf Mon Sep 17 00:00:00 2001 From: mrpositron <42044624+mrpositron@users.noreply.github.com> Date: Tue, 11 Aug 2026 20:42:55 +0200 Subject: [PATCH 4/5] Collapse unreachable isinstance branch to an assert predict() only returns tensors when embed= is passed, which this operator never does, so the non-Results branch cannot be reached. An assert documents the expectation and still narrows the type. Co-Authored-By: Claude Opus 5 (1M context) --- .../src/lightly_plugins_yolo_object_detection/operator.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/plugins/yolo_object_detection/src/lightly_plugins_yolo_object_detection/operator.py b/plugins/yolo_object_detection/src/lightly_plugins_yolo_object_detection/operator.py index 5c467cf..af6a6e6 100644 --- a/plugins/yolo_object_detection/src/lightly_plugins_yolo_object_detection/operator.py +++ b/plugins/yolo_object_detection/src/lightly_plugins_yolo_object_detection/operator.py @@ -157,12 +157,8 @@ def execute( success=False, message=f"Failed to run inference on '{image_entry.file_path_abs}': {e}", ) - if not isinstance(result, Results): - logger.warning( - "Unexpected result type for '%s'; skipping.", - image_entry.file_path_abs, - ) - continue + # A single image always yields one `Results`; `embed=` is never passed. + assert isinstance(result, Results) boxes = result.boxes if boxes is None: logger.warning( From d0b5ba89a63b0e652e4cde1bee8c61ac728497cb Mon Sep 17 00:00:00 2001 From: mrpositron <42044624+mrpositron@users.noreply.github.com> Date: Tue, 11 Aug 2026 20:45:08 +0200 Subject: [PATCH 5/5] Keep model() call form instead of model.predict() __call__ forwards to predict() with the same signature and return type, so the rename was a no-op that enlarged the diff. Reverting keeps )[0] on an unchanged context line and drops the intermediate variable. Co-Authored-By: Claude Opus 5 (1M context) --- .../lightly_plugins_yolo_object_detection/operator.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/plugins/yolo_object_detection/src/lightly_plugins_yolo_object_detection/operator.py b/plugins/yolo_object_detection/src/lightly_plugins_yolo_object_detection/operator.py index af6a6e6..8c8bb91 100644 --- a/plugins/yolo_object_detection/src/lightly_plugins_yolo_object_detection/operator.py +++ b/plugins/yolo_object_detection/src/lightly_plugins_yolo_object_detection/operator.py @@ -141,12 +141,9 @@ def execute( total_annotations_created = 0 for i, image_entry in enumerate(samples, start=1): try: - results = list( - model.predict( - image_entry.file_path_abs, conf=confidence, verbose=False - ) - ) - result = results[0] + result = list( + model(image_entry.file_path_abs, conf=confidence, verbose=False) + )[0] except Exception as e: logger.error( "Failed to run inference on '%s': %s",