Skip to content

feat: Add native YOLO dataset import and in-place editing#239

Open
LeonKraim wants to merge 6 commits into
vietanhdev:mainfrom
LeonKraim:main
Open

feat: Add native YOLO dataset import and in-place editing#239
LeonKraim wants to merge 6 commits into
vietanhdev:mainfrom
LeonKraim:main

Conversation

@LeonKraim

Copy link
Copy Markdown
Contributor

Allows opening a YOLO-format dataset directory via \Open Dir\ (Ctrl+U) and editing annotations in-place without creating AnyLabeling .json files.

Changes

New file: \�nylabeling/views/labeling/yolo_io.py\ with utilities:

  • \ ind_dataset_yaml()\ — walks up 5 levels looking for \data.yaml\

  • ead_dataset_yaml()\ — parses
    c\ +
    ames\ into id-to-label mapping

  • ead_yolo_label()\ — parses YOLO .txt into AnyLabeling shape dicts
  • \write_yolo_label()\ — writes shapes back to YOLO format

  • esolve_yolo_label_path()\ — finds sibling \labels/\ directory

Modified: \�nylabeling/views/labeling/label_widget.py\

  • \import_image_folder()\ detects \data.yaml, populates class name map
  • \load_file()\ falls back to YOLO .txt when no .json exists
  • \save_labels()\ writes .txt when source was YOLO format

  • eset_state()\ clears YOLO path
  • \get_label_file()\ returns .txt path in YOLO mode
  • \import_dropped_image_files()\ checks for .txt checkmarks

Priority

.json\ > .txt\ (same dir) > .txt\ (sibling \labels/)

Testing

All 92 existing unit tests pass. No regression on JSON-only workflows.

Allows opening a YOLO-format dataset directory via Open Dir (Ctrl+U)
and editing annotations in-place without creating AnyLabeling .json
files.

- New anylabeling/views/labeling/yolo_io.py with utilities:
  - find_dataset_yaml() walks up 5 levels looking for data.yaml
  - read_dataset_yaml() parses nc + names into id-to-label mapping
  - read_yolo_label() parses .txt files into AnyLabeling shapes
  - write_yolo_label() writes shapes back to YOLO .txt format
  - resolve_yolo_label_path() finds sibling labels/ directory

- Modified label_widget.py:
  - import_image_folder() detects data.yaml, populates class map
  - load_file() falls back to .txt when no .json exists
  - save_labels() writes .txt when source was YOLO format
  - reset_state() clears YOLO path
  - get_label_file() returns .txt path in YOLO mode
  - import_dropped_image_files() checks for .txt checkmarks

Priority: .json > .txt (same dir) > .txt (sibling labels/)
No regression on existing JSON-only workflows.
@LeonKraim LeonKraim changed the title Add native YOLO dataset import and in-place editing feat: Add native YOLO dataset import and in-place editing Jul 9, 2026
LeonKraim added 5 commits July 9, 2026 17:43
…sses.txt

Fixes a bug where _yolo_label_path was set once and never reset,
causing YOLO labels from the first image to persist when navigating
to other images, and saving to overwrite the wrong .txt file.
Now resolves the path per-image in load_file().

Adds data.yaml / classes.txt sync:
- find_dataset_config() discovers both data.yaml and classes.txt
- update_dataset_config() keeps config in sync when new classes
  are added in the UI (targeted regex for YAML, plain rewrite for txt)
- Preserves all other YAML content (comments, train/val, roboflow)

Adds error handling with try/except + logging on all YOLO I/O
paths to prevent crashes from corrupt label files.
When save_labels() writes data.yaml with a new class via
update_dataset_config(), the in-memory _yolo_id_to_label was never
updated.  On the next image switch + return, read_yolo_label() would
look up the new class_id in the stale _yolo_id_to_label, find None,
and fall back to class_N.
During YOLO dataset import, class names were only added to the
label dialog's autocomplete history (add_label_history), but not
to the unique_label_list sidebar.  This made the label popup always
start blank on the first shape, and forced the user to type or
scroll the dropdown every time — unlike the non-YOLO workflow where
clicking a label in the sidebar pre-fills it for the next shape.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR adds first-class support for opening YOLO-format datasets via “Open Dir” and editing YOLO annotations in-place, avoiding AnyLabeling JSON sidecar creation when the source dataset is YOLO.

Changes:

  • Introduces yolo_io.py utilities for reading/writing YOLO detection and segmentation labels, plus dataset config discovery/update helpers.
  • Extends LabelingWidget to detect YOLO datasets (data.yaml / classes.txt), load .txt labels when .json is absent, and save back to YOLO format.
  • Adds unit tests covering YOLO label read/write and round-trip behavior.

Reviewed changes

Copilot reviewed 3 out of 3 changed files in this pull request and generated 5 comments.

File Description
tests/test_yolo_io.py Adds unit tests for YOLO label parsing/writing (detection + polygon segmentation).
anylabeling/views/labeling/yolo_io.py New YOLO dataset and label I/O utilities, including config updating logic.
anylabeling/views/labeling/label_widget.py Integrates YOLO dataset detection, .txt loading fallback, and .txt saving path into the labeling UI flow.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines 1412 to 1420
def reset_state(self):
self.label_list.clear()
self.filename = None
self.image_path = None
self.image_data = None
self.label_file = None
self.other_data = {}
self._yolo_label_path = None
self.canvas.reset_state()
Comment on lines +34 to +39
with open(yaml_path, "r", encoding="utf-8") as f:
data = yaml.safe_load(f)

nc = data.get("nc", 0)
raw_names = data.get("names", [])

Comment on lines +275 to +287
content = re.sub(
r"^nc:\s*\d+",
f"nc: {len(ordered_names)}",
content,
flags=re.MULTILINE,
)
names_str = repr(ordered_names)
content = re.sub(
r"^names:\s*\[.*?\]|^names:\s*\n(\s+- .*\n?)*",
f"names: {names_str}",
content,
flags=re.MULTILINE | re.DOTALL,
)
Comment on lines +1862 to +1886
write_yolo_label(
self._yolo_label_path,
shapes,
self.image.width(),
self.image.height(),
self._yolo_label_to_id,
)
self.label_file = None
if (
self._yolo_config_path
and self._yolo_label_to_id.keys()
!= self._yolo_original_label_to_id.keys()
):
update_dataset_config(
self._yolo_config_path,
self._yolo_config_type,
self._yolo_label_to_id,
)
self._yolo_original_label_to_id = dict(
self._yolo_label_to_id
)
self._yolo_id_to_label = {
v: k for k, v in self._yolo_label_to_id.items()
}
except Exception:
Comment on lines +79 to +84
parts = line.split()
if len(parts) < 5:
continue

class_id = int(parts[0])

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants