dataflow-cv 0.3.0__tar.gz → 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (30) hide show
  1. {dataflow_cv-0.3.0/dataflow_cv.egg-info → dataflow_cv-0.4.0}/PKG-INFO +31 -6
  2. {dataflow_cv-0.3.0 → dataflow_cv-0.4.0}/README.md +30 -5
  3. {dataflow_cv-0.3.0 → dataflow_cv-0.4.0}/dataflow/__init__.py +8 -6
  4. {dataflow_cv-0.3.0 → dataflow_cv-0.4.0}/dataflow/cli.py +20 -11
  5. {dataflow_cv-0.3.0 → dataflow_cv-0.4.0}/dataflow/convert/coco_and_labelme.py +6 -2
  6. {dataflow_cv-0.3.0 → dataflow_cv-0.4.0}/dataflow/convert/coco_and_yolo.py +53 -20
  7. {dataflow_cv-0.3.0 → dataflow_cv-0.4.0}/dataflow/convert/yolo_and_labelme.py +32 -22
  8. {dataflow_cv-0.3.0 → dataflow_cv-0.4.0}/dataflow/label/labelme.py +36 -16
  9. {dataflow_cv-0.3.0 → dataflow_cv-0.4.0}/dataflow/label/yolo.py +29 -19
  10. {dataflow_cv-0.3.0 → dataflow_cv-0.4.0}/dataflow/visualize/base.py +17 -3
  11. {dataflow_cv-0.3.0 → dataflow_cv-0.4.0}/dataflow/visualize/generic.py +57 -1
  12. {dataflow_cv-0.3.0 → dataflow_cv-0.4.0}/dataflow/visualize/labelme.py +4 -0
  13. {dataflow_cv-0.3.0 → dataflow_cv-0.4.0}/dataflow/visualize/yolo.py +101 -0
  14. {dataflow_cv-0.3.0 → dataflow_cv-0.4.0/dataflow_cv.egg-info}/PKG-INFO +31 -6
  15. {dataflow_cv-0.3.0 → dataflow_cv-0.4.0}/pyproject.toml +1 -1
  16. {dataflow_cv-0.3.0 → dataflow_cv-0.4.0}/setup.py +1 -1
  17. {dataflow_cv-0.3.0 → dataflow_cv-0.4.0}/LICENSE +0 -0
  18. {dataflow_cv-0.3.0 → dataflow_cv-0.4.0}/dataflow/config.py +0 -0
  19. {dataflow_cv-0.3.0 → dataflow_cv-0.4.0}/dataflow/convert/__init__.py +0 -0
  20. {dataflow_cv-0.3.0 → dataflow_cv-0.4.0}/dataflow/convert/base.py +0 -0
  21. {dataflow_cv-0.3.0 → dataflow_cv-0.4.0}/dataflow/label/__init__.py +0 -0
  22. {dataflow_cv-0.3.0 → dataflow_cv-0.4.0}/dataflow/label/coco.py +0 -0
  23. {dataflow_cv-0.3.0 → dataflow_cv-0.4.0}/dataflow/visualize/__init__.py +0 -0
  24. {dataflow_cv-0.3.0 → dataflow_cv-0.4.0}/dataflow/visualize/coco.py +0 -0
  25. {dataflow_cv-0.3.0 → dataflow_cv-0.4.0}/dataflow_cv.egg-info/SOURCES.txt +0 -0
  26. {dataflow_cv-0.3.0 → dataflow_cv-0.4.0}/dataflow_cv.egg-info/dependency_links.txt +0 -0
  27. {dataflow_cv-0.3.0 → dataflow_cv-0.4.0}/dataflow_cv.egg-info/entry_points.txt +0 -0
  28. {dataflow_cv-0.3.0 → dataflow_cv-0.4.0}/dataflow_cv.egg-info/requires.txt +0 -0
  29. {dataflow_cv-0.3.0 → dataflow_cv-0.4.0}/dataflow_cv.egg-info/top_level.txt +0 -0
  30. {dataflow_cv-0.3.0 → dataflow_cv-0.4.0}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dataflow-cv
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: A data processing library for computer vision datasets
5
5
  Home-page: https://github.com/zjykzj/DataFlow-CV
6
6
  Author: DataFlow Team
@@ -34,10 +34,8 @@ Dynamic: requires-python
34
34
  > **Where Vibe Coding meets CV data.** 🌊
35
35
  > Convert & visualize datasets. Built with the flow of Claude Code.
36
36
 
37
- ![Python Version](https://img.shields.io/badge/python-3.8%20|%203.9%20|%203.10%20|%203.11%20|%203.12-blue)
38
- ![License](https://img.shields.io/badge/license-MIT-green)
39
- ![Version](https://img.shields.io/badge/version-0.3.0-orange)
40
- ![Development Status](https://img.shields.io/badge/status-alpha-yellow)
37
+ ![Python Version](https://img.shields.io/badge/python-3.8%20|%203.9%20|%203.10%20|%203.11%20|%203.12-blue) ![License](https://img.shields.io/badge/license-MIT-green) [![PyPI](https://img.shields.io/pypi/v/dataflow-cv.svg)](https://pypi.org/project/dataflow-cv/) ![Development Status](https://img.shields.io/badge/status-alpha-yellow) [![GitHub Actions](https://github.com/zjykzj/DataFlow-CV/actions/workflows/python-publish.yml/badge.svg)](https://github.com/zjykzj/DataFlow-CV/actions/workflows/python-publish.yml)
38
+
41
39
 
42
40
  A data processing library for computer vision datasets, focusing on format conversion and visualization between LabelMe, COCO, and YOLO formats. Provides both a CLI and Python API.
43
41
 
@@ -50,6 +48,8 @@ A data processing library for computer vision datasets, focusing on format conve
50
48
  - [Core Dependencies](#core-dependencies)
51
49
  - [Quick Start](#quick-start)
52
50
  - [Installation](#installation)
51
+ - [Editable Installation (Development Mode)](#editable-installation-development-mode)
52
+ - [Build System](#build-system)
53
53
  - [Command Line Usage](#command-line-usage)
54
54
  - [Python API Usage](#python-api-usage)
55
55
  - [CLI Reference](#cli-reference)
@@ -61,6 +61,7 @@ A data processing library for computer vision datasets, focusing on format conve
61
61
  - [Segmentation Support](#segmentation-support)
62
62
  - [Running Tests](#running-tests)
63
63
  - [Examples](#examples)
64
+ - [Documentation](#documentation)
64
65
  - [License](#license)
65
66
 
66
67
  ## Project Structure
@@ -79,6 +80,7 @@ dataflow/
79
80
  ├── visualize/ # Annotation visualization module
80
81
  │ ├── __init__.py
81
82
  │ ├── base.py # Visualizer base class
83
+ │ ├── generic.py # Generic visualizer base class using label handlers
82
84
  │ ├── yolo.py # YOLO annotation visualizer
83
85
  │ ├── coco.py # COCO annotation visualizer
84
86
  │ └── labelme.py # LabelMe annotation visualizer
@@ -92,7 +94,11 @@ tests/
92
94
  ├── convert/ # Conversion tests
93
95
  │ ├── __init__.py
94
96
  │ ├── test_coco_to_yolo.py
95
- └── test_yolo_to_coco.py
97
+ ├── test_yolo_to_coco.py
98
+ │ ├── test_coco_to_labelme.py
99
+ │ ├── test_labelme_to_coco.py
100
+ │ ├── test_labelme_to_yolo.py
101
+ │ └── test_yolo_to_labelme.py
96
102
  ├── visualize/ # Visualization tests
97
103
  │ ├── __init__.py
98
104
  │ ├── test_yolo.py
@@ -130,6 +136,11 @@ samples/
130
136
  ├── api_yolo.py
131
137
  ├── api_coco.py
132
138
  └── api_labelme.py
139
+ docs/ # Data format documentation
140
+ ├── README.md # Documentation index
141
+ ├── yolo.md # YOLO format specification
142
+ ├── labelme.md # LabelMe format specification
143
+ └── coco.md # COCO format specification
133
144
  ```
134
145
 
135
146
  ## Requirements
@@ -468,6 +479,20 @@ Check the `samples/` directory for detailed usage examples:
468
479
  - `samples/api/convert/` - Python API conversion examples
469
480
  - `samples/api/visualize/` - Python API visualization examples
470
481
 
482
+ ### Documentation
483
+
484
+ Detailed data format specifications are available in the `docs/` directory:
485
+
486
+ - [`docs/README.md`](docs/README.md) - Documentation index
487
+ - [`docs/yolo.md`](docs/yolo.md) - YOLO format specification
488
+ - [`docs/labelme.md`](docs/labelme.md) - LabelMe format specification
489
+ - [`docs/coco.md`](docs/coco.md) - COCO format specification
490
+
491
+ These documents describe the annotation formats supported by DataFlow-CV, without covering tool usage.
492
+ ## Development
493
+
494
+ For development guidelines, architecture details, and contribution instructions, see [CLAUDE.md](CLAUDE.md). This file provides guidance for working with the codebase, including common development commands, architectural patterns, and writing principles.
495
+
471
496
  ## License
472
497
 
473
498
  [MIT License](LICENSE) © 2026 zjykzj
@@ -3,10 +3,8 @@
3
3
  > **Where Vibe Coding meets CV data.** 🌊
4
4
  > Convert & visualize datasets. Built with the flow of Claude Code.
5
5
 
6
- ![Python Version](https://img.shields.io/badge/python-3.8%20|%203.9%20|%203.10%20|%203.11%20|%203.12-blue)
7
- ![License](https://img.shields.io/badge/license-MIT-green)
8
- ![Version](https://img.shields.io/badge/version-0.3.0-orange)
9
- ![Development Status](https://img.shields.io/badge/status-alpha-yellow)
6
+ ![Python Version](https://img.shields.io/badge/python-3.8%20|%203.9%20|%203.10%20|%203.11%20|%203.12-blue) ![License](https://img.shields.io/badge/license-MIT-green) [![PyPI](https://img.shields.io/pypi/v/dataflow-cv.svg)](https://pypi.org/project/dataflow-cv/) ![Development Status](https://img.shields.io/badge/status-alpha-yellow) [![GitHub Actions](https://github.com/zjykzj/DataFlow-CV/actions/workflows/python-publish.yml/badge.svg)](https://github.com/zjykzj/DataFlow-CV/actions/workflows/python-publish.yml)
7
+
10
8
 
11
9
  A data processing library for computer vision datasets, focusing on format conversion and visualization between LabelMe, COCO, and YOLO formats. Provides both a CLI and Python API.
12
10
 
@@ -19,6 +17,8 @@ A data processing library for computer vision datasets, focusing on format conve
19
17
  - [Core Dependencies](#core-dependencies)
20
18
  - [Quick Start](#quick-start)
21
19
  - [Installation](#installation)
20
+ - [Editable Installation (Development Mode)](#editable-installation-development-mode)
21
+ - [Build System](#build-system)
22
22
  - [Command Line Usage](#command-line-usage)
23
23
  - [Python API Usage](#python-api-usage)
24
24
  - [CLI Reference](#cli-reference)
@@ -30,6 +30,7 @@ A data processing library for computer vision datasets, focusing on format conve
30
30
  - [Segmentation Support](#segmentation-support)
31
31
  - [Running Tests](#running-tests)
32
32
  - [Examples](#examples)
33
+ - [Documentation](#documentation)
33
34
  - [License](#license)
34
35
 
35
36
  ## Project Structure
@@ -48,6 +49,7 @@ dataflow/
48
49
  ├── visualize/ # Annotation visualization module
49
50
  │ ├── __init__.py
50
51
  │ ├── base.py # Visualizer base class
52
+ │ ├── generic.py # Generic visualizer base class using label handlers
51
53
  │ ├── yolo.py # YOLO annotation visualizer
52
54
  │ ├── coco.py # COCO annotation visualizer
53
55
  │ └── labelme.py # LabelMe annotation visualizer
@@ -61,7 +63,11 @@ tests/
61
63
  ├── convert/ # Conversion tests
62
64
  │ ├── __init__.py
63
65
  │ ├── test_coco_to_yolo.py
64
- └── test_yolo_to_coco.py
66
+ ├── test_yolo_to_coco.py
67
+ │ ├── test_coco_to_labelme.py
68
+ │ ├── test_labelme_to_coco.py
69
+ │ ├── test_labelme_to_yolo.py
70
+ │ └── test_yolo_to_labelme.py
65
71
  ├── visualize/ # Visualization tests
66
72
  │ ├── __init__.py
67
73
  │ ├── test_yolo.py
@@ -99,6 +105,11 @@ samples/
99
105
  ├── api_yolo.py
100
106
  ├── api_coco.py
101
107
  └── api_labelme.py
108
+ docs/ # Data format documentation
109
+ ├── README.md # Documentation index
110
+ ├── yolo.md # YOLO format specification
111
+ ├── labelme.md # LabelMe format specification
112
+ └── coco.md # COCO format specification
102
113
  ```
103
114
 
104
115
  ## Requirements
@@ -437,6 +448,20 @@ Check the `samples/` directory for detailed usage examples:
437
448
  - `samples/api/convert/` - Python API conversion examples
438
449
  - `samples/api/visualize/` - Python API visualization examples
439
450
 
451
+ ### Documentation
452
+
453
+ Detailed data format specifications are available in the `docs/` directory:
454
+
455
+ - [`docs/README.md`](docs/README.md) - Documentation index
456
+ - [`docs/yolo.md`](docs/yolo.md) - YOLO format specification
457
+ - [`docs/labelme.md`](docs/labelme.md) - LabelMe format specification
458
+ - [`docs/coco.md`](docs/coco.md) - COCO format specification
459
+
460
+ These documents describe the annotation formats supported by DataFlow-CV, without covering tool usage.
461
+ ## Development
462
+
463
+ For development guidelines, architecture details, and contribution instructions, see [CLAUDE.md](CLAUDE.md). This file provides guidance for working with the codebase, including common development commands, architectural patterns, and writing principles.
464
+
440
465
  ## License
441
466
 
442
467
  [MIT License](LICENSE) © 2026 zjykzj
@@ -7,7 +7,7 @@
7
7
  @Description: DataFlow-CV: A data processing library for computer vision datasets
8
8
  """
9
9
 
10
- __version__ = "0.3.0"
10
+ __version__ = "0.4.0"
11
11
  __author__ = "DataFlow Team"
12
12
  __description__ = "A data processing library for computer vision datasets"
13
13
 
@@ -42,14 +42,15 @@ def coco_to_yolo(coco_json_path: str, output_dir: str, **kwargs):
42
42
 
43
43
  Args:
44
44
  coco_json_path: Path to COCO JSON file
45
- output_dir: Output directory where labels/ and class.names will be created
45
+ output_dir: Output directory where YOLO label files will be created
46
+ (class.names will be auto-generated in output_dir)
46
47
  **kwargs: Additional options passed to CocoToYoloConverter.convert()
47
48
 
48
49
  Returns:
49
50
  Dictionary with conversion statistics
50
51
  """
51
52
  converter = CocoToYoloConverter()
52
- return converter.convert(coco_json_path, output_dir, **kwargs)
53
+ return converter.convert(coco_json_path, output_dir, classes_path=None, **kwargs)
53
54
 
54
55
 
55
56
  def yolo_to_coco(
@@ -130,20 +131,21 @@ def yolo_to_labelme(image_dir: str, label_dir: str, classes_path: str, output_di
130
131
  return converter.convert(image_dir, label_dir, classes_path, output_dir, **kwargs)
131
132
 
132
133
 
133
- def labelme_to_yolo(label_dir: str, output_dir: str, **kwargs):
134
+ def labelme_to_yolo(label_dir: str, classes_path: str, output_dir: str, **kwargs):
134
135
  """
135
136
  Convert LabelMe format to YOLO format.
136
137
 
137
138
  Args:
138
139
  label_dir: Directory containing LabelMe JSON files
139
- output_dir: Output directory where labels/ and class.names will be created
140
+ classes_path: Path to class names file (e.g., class.names)
141
+ output_dir: Output directory where YOLO label files will be created
140
142
  **kwargs: Additional options passed to LabelMeToYoloConverter.convert()
141
143
 
142
144
  Returns:
143
145
  Dictionary with conversion statistics
144
146
  """
145
147
  converter = LabelMeToYoloConverter()
146
- return converter.convert(label_dir, output_dir, **kwargs)
148
+ return converter.convert(label_dir, classes_path, output_dir, **kwargs)
147
149
 
148
150
 
149
151
  # Convenience functions for visualization
@@ -31,6 +31,11 @@ from dataflow import __version__
31
31
  @click.pass_context
32
32
  def cli(ctx, verbose, overwrite):
33
33
  """DataFlow-CV: Computer vision dataset processing tool."""
34
+ # If -v is used alone (no subcommand), show version and exit
35
+ if verbose and ctx.invoked_subcommand is None:
36
+ click.echo(f"DataFlow-CV, version {__version__}")
37
+ ctx.exit()
38
+
34
39
  # Store configuration in context
35
40
  ctx.ensure_object(dict)
36
41
  ctx.obj['verbose'] = verbose
@@ -70,29 +75,30 @@ def coco2yolo(ctx, coco_json_path, output_dir, segmentation):
70
75
 
71
76
  \b
72
77
  COCO_JSON_PATH: Path to COCO JSON annotation file
73
- OUTPUT_DIR: Directory where labels/ and class.names will be created
78
+ OUTPUT_DIR: Directory where YOLO label files will be created (class.names will be auto-generated)
74
79
  """
75
80
  try:
76
81
  # Segmentation parameter is passed directly to converter
77
82
 
78
83
  click.echo(f"Converting COCO JSON: {coco_json_path}")
79
84
  click.echo(f"Output directory: {output_dir}")
85
+ # Classes file will be auto-generated as {os.path.join(output_dir, Config.YOLO_CLASSES_FILENAME)}
80
86
 
81
87
  # Create converter and perform conversion
82
88
  converter = CocoToYoloConverter(verbose=ctx.obj['verbose'])
83
- result = converter.convert(coco_json_path, output_dir, segmentation=segmentation)
89
+ result = converter.convert(coco_json_path, output_dir, classes_path=None, segmentation=segmentation)
84
90
 
85
91
  # Print summary
86
92
  click.echo("\n" + "="*50)
87
93
  click.echo("CONVERSION SUMMARY")
88
94
  click.echo("="*50)
89
95
  click.echo(f"COCO JSON: {coco_json_path}")
90
- click.echo(f"Output directory: {result.get('output_dir')}")
91
- click.echo(f"Labels directory: {result.get('labels_dir')}")
92
96
  click.echo(f"Classes file: {result.get('classes_file')}")
97
+ click.echo(f"Output directory: {result.get('output_dir')}")
93
98
  click.echo(f"Images processed: {result.get('images_processed', 0)}")
94
99
  click.echo(f"Annotations processed: {result.get('annotations_processed', 0)}")
95
- click.echo(f"Categories found: {result.get('categories_found', 0)}")
100
+ click.echo(f"Categories in classes file: {result.get('categories_found', 0)}")
101
+ click.echo(f"Categories in data: {result.get('categories_in_data', 0)}")
96
102
  click.echo(f"Segmentation mode: {'ON' if segmentation else 'OFF'}")
97
103
 
98
104
  click.echo("\n✅ Conversion completed successfully!")
@@ -240,38 +246,41 @@ def labelme2coco(ctx, label_dir, classes_path, output_json_path, segmentation):
240
246
 
241
247
  @convert.command(name='labelme2yolo')
242
248
  @click.argument('label_dir', type=click.Path(exists=True, file_okay=False))
249
+ @click.argument('classes_path', type=click.Path(exists=True, dir_okay=False))
243
250
  @click.argument('output_dir', type=click.Path(file_okay=False))
244
251
  @click.option('--segmentation', '-s', is_flag=True, help='Handle segmentation annotations')
245
252
  @click.pass_context
246
- def labelme2yolo(ctx, label_dir, output_dir, segmentation):
253
+ def labelme2yolo(ctx, label_dir, classes_path, output_dir, segmentation):
247
254
  """
248
255
  Convert LabelMe format to YOLO format.
249
256
 
250
257
  \b
251
258
  LABEL_DIR: Directory containing LabelMe JSON files
252
- OUTPUT_DIR: Directory where labels/ and class.names will be created
259
+ CLASSES_PATH: Path to class names file (e.g., class.names)
260
+ OUTPUT_DIR: Directory where YOLO label files will be created
253
261
  """
254
262
  try:
255
263
  click.echo(f"Label directory: {label_dir}")
264
+ click.echo(f"Classes file: {classes_path}")
256
265
  click.echo(f"Output directory: {output_dir}")
257
266
  if segmentation:
258
267
  click.echo("Segmentation mode: ON (strict)")
259
268
 
260
269
  # Create converter and perform conversion
261
270
  converter = LabelMeToYoloConverter(verbose=ctx.obj['verbose'])
262
- result = converter.convert(label_dir, output_dir, segmentation=segmentation)
271
+ result = converter.convert(label_dir, classes_path, output_dir, segmentation=segmentation)
263
272
 
264
273
  # Print summary
265
274
  click.echo("\n" + "="*50)
266
275
  click.echo("CONVERSION SUMMARY")
267
276
  click.echo("="*50)
268
277
  click.echo(f"Label directory: {result.get('label_dir')}")
278
+ click.echo(f"Classes file: {result.get('classes_file')}")
269
279
  click.echo(f"Output directory: {result.get('output_dir')}")
270
- click.echo(f"Labels directory: {result.get('labels_dir')}")
271
- click.echo(f"Classes file: {result.get('classes_file', 'Not created')}")
272
280
  click.echo(f"Images processed: {result.get('images_processed', 0)}")
273
281
  click.echo(f"Annotations processed: {result.get('annotations_processed', 0)}")
274
- click.echo(f"Categories found: {result.get('categories_found', 0)}")
282
+ click.echo(f"Categories in classes file: {result.get('categories_found', 0)}")
283
+ click.echo(f"Categories in data: {result.get('categories_in_data', 0)}")
275
284
  click.echo(f"Segmentation mode: {'ON' if segmentation else 'OFF'}")
276
285
 
277
286
  click.echo("\n✅ Conversion completed successfully!")
@@ -29,7 +29,9 @@ class CocoToLabelMeConverter(LabelBasedConverter):
29
29
  coco_json_path: Path to COCO JSON file
30
30
  output_dir: Output directory where LabelMe JSON files will be created
31
31
  segmentation: Whether to enforce segmentation annotations.
32
- If True, only annotations with segmentation data will be processed.
32
+ If True, only annotations with polygon segmentation data will be processed,
33
+ bounding box annotations will be skipped. If False, both bounding box and
34
+ segmentation annotations are processed.
33
35
 
34
36
  Returns:
35
37
  Dictionary with conversion statistics
@@ -119,7 +121,9 @@ class LabelMeToCocoConverter(LabelBasedConverter):
119
121
  classes_path: Path to class names file (e.g., class.names)
120
122
  output_json_path: Path to save COCO JSON file
121
123
  segmentation: Whether to enforce segmentation annotations.
122
- If True, only annotations with segmentation data will be processed.
124
+ If True, only polygon shapes (shape_type="polygon") will be processed,
125
+ rectangle shapes will be skipped. If False, both rectangle and polygon
126
+ shapes are processed.
123
127
 
124
128
  Returns:
125
129
  Dictionary with conversion statistics
@@ -10,7 +10,7 @@ reusing the label module handlers for consistent parsing and serialization.
10
10
  """
11
11
 
12
12
  import os
13
- from typing import Dict, List, Any
13
+ from typing import Dict, List, Any, Optional
14
14
 
15
15
  from .base import LabelBasedConverter
16
16
  from ..config import Config
@@ -21,13 +21,15 @@ from ..label.yolo import YoloHandler
21
21
  class CocoToYoloConverter(LabelBasedConverter):
22
22
  """Convert COCO JSON format to YOLO label format."""
23
23
 
24
- def convert(self, coco_json_path: str, output_dir: str, segmentation: bool = False) -> Dict[str, Any]:
24
+ def convert(self, coco_json_path: str, output_dir: str, classes_path: Optional[str] = None, segmentation: bool = False) -> Dict[str, Any]:
25
25
  """
26
26
  Convert COCO JSON file to YOLO format.
27
27
 
28
28
  Args:
29
29
  coco_json_path: Path to COCO JSON file
30
- output_dir: Output directory where labels/ and class.names will be created
30
+ output_dir: Output directory where YOLO label files will be created
31
+ classes_path: Optional path to class names file (e.g., class.names).
32
+ If not provided, will be automatically generated as `output_dir/class.names`.
31
33
  segmentation: Whether to enforce segmentation annotations.
32
34
  If True, only annotations with segmentation data will be processed.
33
35
 
@@ -39,6 +41,9 @@ class CocoToYoloConverter(LabelBasedConverter):
39
41
  """
40
42
  self.segmentation = segmentation
41
43
 
44
+ # Track if classes_path was auto-generated
45
+ original_classes_path = classes_path
46
+
42
47
  # 1. Validate input and output paths
43
48
  if not self.validate_input_path(coco_json_path, is_dir=False):
44
49
  raise ValueError(f"Invalid COCO JSON file: {coco_json_path}")
@@ -46,6 +51,17 @@ class CocoToYoloConverter(LabelBasedConverter):
46
51
  if not self.validate_output_path(output_dir, is_dir=True, create=True):
47
52
  raise ValueError(f"Invalid output directory: {output_dir}")
48
53
 
54
+ # 1.1 Handle classes_path: if None, auto-generate; otherwise validate
55
+ if classes_path is None:
56
+ classes_path = os.path.join(output_dir, Config.YOLO_CLASSES_FILENAME)
57
+ self.logger.info(f"Classes file not provided, will auto-generate: {classes_path}")
58
+ else:
59
+ if not self.validate_input_path(classes_path, is_dir=False):
60
+ raise ValueError(f"Invalid classes file: {classes_path}")
61
+
62
+ # 1.2 Create labels directory for YOLO output
63
+ labels_dir = self._create_labels_directory(output_dir)
64
+
49
65
  self.logger.info(f"Converting COCO to YOLO: {coco_json_path} -> {output_dir}")
50
66
 
51
67
  # 2. Use CocoHandler to read COCO data and convert to unified format
@@ -75,30 +91,47 @@ class CocoToYoloConverter(LabelBasedConverter):
75
91
  for ann in img_data.get("annotations", []):
76
92
  ann.pop("segmentation", None)
77
93
 
78
- # 4. Extract unique categories and write class.names file
79
- categories = self._extract_unique_categories(unified_data)
80
- classes_path = os.path.join(output_dir, Config.YOLO_CLASSES_FILENAME)
81
- if categories:
82
- if not self.write_classes_file(categories, classes_path):
83
- raise ValueError(f"Failed to write classes file: {classes_path}")
84
- self.logger.info(f"Written {len(categories)} categories to {classes_path}")
85
-
86
- # 5. Create labels directory
87
- labels_dir = os.path.join(output_dir, Config.YOLO_LABELS_DIRNAME)
88
- self.ensure_directory(labels_dir)
89
-
90
- # 6. Use YoloHandler to write YOLO format
94
+ # 4. Extract unique categories from COCO data
95
+ data_categories = self._extract_unique_categories(unified_data)
96
+ if not data_categories:
97
+ self.logger.warning("No categories found in COCO data")
98
+ data_categories = [] # Ensure it's an empty list
99
+
100
+ # 5. Handle classes based on whether it was auto-generated or provided
101
+ if original_classes_path is None:
102
+ # Auto-generated classes path: write categories to file if we have any
103
+ categories = data_categories
104
+ if data_categories:
105
+ self.write_classes_file(data_categories, classes_path)
106
+ self.logger.info(f"Auto-generated classes file: {classes_path}")
107
+ else:
108
+ self.logger.warning(f"No categories to write to classes file: {classes_path}")
109
+ # Still create empty file or leave it? For now, create empty file
110
+ self.write_classes_file([], classes_path)
111
+ else:
112
+ # User-provided classes path: read and validate
113
+ categories = self.read_classes_file(classes_path)
114
+ if not categories:
115
+ raise ValueError(f"No categories found in classes file: {classes_path}")
116
+
117
+ # Validate all categories in data are present in provided classes file
118
+ if data_categories:
119
+ for category in data_categories:
120
+ if category not in categories:
121
+ raise ValueError(f"Category '{category}' found in COCO data but not in classes file")
122
+
123
+ # 6. Use YoloHandler to write YOLO format to labels directory
91
124
  yolo_handler = YoloHandler(verbose=self.verbose)
92
125
  success = False
93
- if unified_data:
126
+ if unified_data and categories:
94
127
  success = yolo_handler.write_batch(unified_data, labels_dir, classes_path)
95
128
  else:
96
129
  # Create empty directory structure
97
130
  success = True
98
- self.logger.info("No annotations to write, created empty directory structure")
131
+ self.logger.info("No annotations or categories to write, created empty directory structure")
99
132
 
100
133
  if not success:
101
- raise ValueError(f"Failed to write YOLO label files to {labels_dir}")
134
+ raise ValueError(f"Failed to write YOLO label files to {output_dir}")
102
135
 
103
136
  # 7. Return statistics
104
137
  total_annotations = sum(len(img["annotations"]) for img in unified_data)
@@ -106,9 +139,9 @@ class CocoToYoloConverter(LabelBasedConverter):
106
139
  "images_processed": len(unified_data),
107
140
  "annotations_processed": total_annotations,
108
141
  "categories_found": len(categories),
142
+ "categories_in_data": len(data_categories) if unified_data else 0,
109
143
  "output_dir": output_dir,
110
144
  "classes_file": classes_path,
111
- "labels_dir": labels_dir,
112
145
  "segmentation_mode": segmentation,
113
146
  }
114
147
 
@@ -32,7 +32,9 @@ class YoloToLabelMeConverter(LabelBasedConverter):
32
32
  classes_path: Path to YOLO class names file (e.g., class.names)
33
33
  output_dir: Output directory where LabelMe JSON files will be created
34
34
  segmentation: Whether to enforce segmentation annotations.
35
- If True, only annotations with segmentation data will be processed.
35
+ If True, detection annotations (4 coordinates) will be converted to polygons
36
+ from bounding boxes, and segmentation annotations (6+ coordinates) will be
37
+ processed normally. If False, automatic format detection is used.
36
38
 
37
39
  Returns:
38
40
  Dictionary with conversion statistics
@@ -107,15 +109,18 @@ class YoloToLabelMeConverter(LabelBasedConverter):
107
109
  class LabelMeToYoloConverter(LabelBasedConverter):
108
110
  """Convert LabelMe format to YOLO format."""
109
111
 
110
- def convert(self, label_dir: str, output_dir: str, segmentation: bool = False) -> Dict[str, Any]:
112
+ def convert(self, label_dir: str, classes_path: str, output_dir: str, segmentation: bool = False) -> Dict[str, Any]:
111
113
  """
112
114
  Convert LabelMe format to YOLO format.
113
115
 
114
116
  Args:
115
117
  label_dir: Directory containing LabelMe JSON files
116
- output_dir: Output directory where labels/ and class.names will be created
118
+ classes_path: Path to class names file (e.g., class.names)
119
+ output_dir: Output directory where YOLO label files will be created
117
120
  segmentation: Whether to enforce segmentation annotations.
118
- If True, only annotations with segmentation data will be processed.
121
+ If True, only polygon shapes (shape_type="polygon") will be processed,
122
+ rectangle shapes will be skipped. If False, both rectangle and polygon
123
+ shapes are processed.
119
124
 
120
125
  Returns:
121
126
  Dictionary with conversion statistics
@@ -129,10 +134,16 @@ class LabelMeToYoloConverter(LabelBasedConverter):
129
134
  if not self.validate_input_path(label_dir, is_dir=True):
130
135
  raise ValueError(f"Invalid label directory: {label_dir}")
131
136
 
137
+ if not self.validate_input_path(classes_path, is_dir=False):
138
+ raise ValueError(f"Invalid classes file: {classes_path}")
139
+
132
140
  if not self.validate_output_path(output_dir, is_dir=True, create=True):
133
141
  raise ValueError(f"Invalid output directory: {output_dir}")
134
142
 
135
- self.logger.info(f"Converting LabelMe to YOLO: {label_dir} -> {output_dir}")
143
+ # Create labels directory for YOLO output
144
+ labels_dir = self._create_labels_directory(output_dir)
145
+
146
+ self.logger.info(f"Converting LabelMe to YOLO: {label_dir} -> {output_dir} (labels in {labels_dir})")
136
147
 
137
148
  # 2. Use LabelMeHandler to read LabelMe data in batch
138
149
  labelme_handler = LabelMeHandler(verbose=self.verbose)
@@ -152,23 +163,22 @@ class LabelMeToYoloConverter(LabelBasedConverter):
152
163
  if not self._validate_segmentation_annotations(img_data["annotations"]):
153
164
  raise ValueError(f"Image {img_data['image_id']} missing segmentation annotations")
154
165
 
155
- # 4. Extract unique categories from LabelMe data
156
- categories = self._extract_unique_categories(unified_data)
166
+ # 4. Read provided classes file and validate
167
+ categories = self.read_classes_file(classes_path)
157
168
  if not categories:
158
- self.logger.warning("No categories found in LabelMe data")
169
+ raise ValueError(f"No categories found in classes file: {classes_path}")
159
170
 
160
- # 5. Write class.names file
161
- classes_path = os.path.join(output_dir, Config.YOLO_CLASSES_FILENAME)
162
- if categories:
163
- if not self.write_classes_file(categories, classes_path):
164
- raise ValueError(f"Failed to write classes file: {classes_path}")
165
- self.logger.info(f"Written {len(categories)} categories to {classes_path}")
166
-
167
- # 6. Create labels directory
168
- labels_dir = os.path.join(output_dir, Config.YOLO_LABELS_DIRNAME)
169
- self.ensure_directory(labels_dir)
171
+ # 5. Extract unique categories from LabelMe data and validate against provided classes
172
+ data_categories = self._extract_unique_categories(unified_data)
173
+ if not data_categories:
174
+ self.logger.warning("No categories found in LabelMe data")
175
+ else:
176
+ # Validate all categories in data are present in provided classes file
177
+ for category in data_categories:
178
+ if category not in categories:
179
+ raise ValueError(f"Category '{category}' found in LabelMe data but not in classes file")
170
180
 
171
- # 7. Use YoloHandler to write YOLO format
181
+ # 6. Use YoloHandler to write YOLO format to labels directory
172
182
  yolo_handler = YoloHandler(verbose=self.verbose)
173
183
  success = False
174
184
  if unified_data and categories:
@@ -179,18 +189,18 @@ class LabelMeToYoloConverter(LabelBasedConverter):
179
189
  self.logger.info("No annotations or categories to write, created empty directory structure")
180
190
 
181
191
  if not success:
182
- raise ValueError(f"Failed to write YOLO label files to {labels_dir}")
192
+ raise ValueError(f"Failed to write YOLO label files to {output_dir}")
183
193
 
184
194
  # 8. Return statistics
185
195
  total_annotations = sum(len(img["annotations"]) for img in unified_data)
186
196
  stats = {
187
197
  "label_dir": label_dir,
198
+ "classes_file": classes_path,
188
199
  "output_dir": output_dir,
189
- "classes_file": classes_path if categories else None,
190
- "labels_dir": labels_dir,
191
200
  "images_processed": len(unified_data),
192
201
  "annotations_processed": total_annotations,
193
202
  "categories_found": len(categories),
203
+ "categories_in_data": len(data_categories) if unified_data else 0,
194
204
  "segmentation_mode": segmentation,
195
205
  }
196
206
 
@@ -358,24 +358,44 @@ class LabelMeHandler:
358
358
 
359
359
  width, height = image_size
360
360
 
361
- # 优先使用分割数据
361
+ # 检查分割数据
362
362
  if annotation.get("segmentation") and annotation["segmentation"][0]:
363
- # 分割标注
364
363
  points_flat = annotation["segmentation"][0]
365
- # 将展平的坐标转换为点列表
366
- points = []
367
- for i in range(0, len(points_flat), 2):
368
- if i + 1 < len(points_flat):
369
- x = max(0, min(points_flat[i], width - 1))
370
- y = max(0, min(points_flat[i + 1], height - 1))
371
- points.append([x, y])
372
-
373
- if len(points) >= 3:
374
- shape["points"] = points
375
- shape["shape_type"] = "polygon"
376
- return shape
377
-
378
- # 使用边界框数据
364
+
365
+ # 检查是否为从边界框生成的4点多边形(8个坐标)
366
+ is_bbox_polygon = (len(points_flat) == 8 and
367
+ annotation.get("force_polygon", False))
368
+
369
+ if is_bbox_polygon:
370
+ # 强制分割模式:从边界框生成的多边形→多边形
371
+ # 将展平的坐标转换为点列表
372
+ points = []
373
+ for i in range(0, len(points_flat), 2):
374
+ if i + 1 < len(points_flat):
375
+ x = max(0, min(points_flat[i], width - 1))
376
+ y = max(0, min(points_flat[i + 1], height - 1))
377
+ points.append([x, y])
378
+
379
+ if len(points) >= 3:
380
+ shape["points"] = points
381
+ shape["shape_type"] = "polygon"
382
+ return shape
383
+ else:
384
+ # 真实分割数据→多边形
385
+ # 将展平的坐标转换为点列表
386
+ points = []
387
+ for i in range(0, len(points_flat), 2):
388
+ if i + 1 < len(points_flat):
389
+ x = max(0, min(points_flat[i], width - 1))
390
+ y = max(0, min(points_flat[i + 1], height - 1))
391
+ points.append([x, y])
392
+
393
+ if len(points) >= 3:
394
+ shape["points"] = points
395
+ shape["shape_type"] = "polygon"
396
+ return shape
397
+
398
+ # 使用边界框数据→矩形
379
399
  if annotation.get("bbox"):
380
400
  bbox = annotation["bbox"]
381
401
  x_min, y_min, bbox_width, bbox_height = bbox
@@ -48,7 +48,7 @@ class YoloHandler:
48
48
  image_path: 对应图像文件路径
49
49
  classes: 类别名称列表
50
50
  image_size: 可选图像尺寸 (width, height)。如果未提供,将尝试从图像文件读取
51
- require_segmentation: 是否要求分割格式。如果True,只接受分割标注(至少6个坐标),检测格式将跳过
51
+ require_segmentation: 是否强制分割模式。如果True,检测标注(4个坐标)将生成为从边界框创建的多边形,分割标注(6+个坐标)正常处理。如果False,自动检测格式类型。
52
52
 
53
53
  Returns:
54
54
  统一格式的图像标注数据字典,结构如下:
@@ -113,14 +113,14 @@ class YoloHandler:
113
113
  # 分割格式: 2n个坐标 (多边形顶点)
114
114
  if len(coords) == 4:
115
115
  # 检测格式
116
- if require_segmentation:
117
- if self.verbose:
118
- print(f"警告: 行 {line_num}: 要求分割格式但检测到检测格式,跳过")
119
- continue
120
- annotation = self._parse_detection(coords, class_id, classes, (width, height))
116
+ # require_segmentation=True 表示强制分割模式,检测标注应生成为多边形
117
+ annotation = self._parse_detection(coords, class_id, classes, (width, height),
118
+ force_polygon=require_segmentation)
121
119
  elif len(coords) >= 6 and len(coords) % 2 == 0:
122
120
  # 分割格式(至少3个点)
123
- annotation = self._parse_segmentation(coords, class_id, classes, (width, height))
121
+ # require_segmentation=True 表示强制分割模式,但真实分割标注不需要force_polygon标记
122
+ annotation = self._parse_segmentation(coords, class_id, classes, (width, height),
123
+ force_polygon=require_segmentation)
124
124
  else:
125
125
  if self.verbose:
126
126
  print(f"警告: 行 {line_num}: 坐标数量无效: {len(coords)}")
@@ -183,7 +183,7 @@ class YoloHandler:
183
183
  classes_path: 类别文件路径
184
184
  label_ext: 标签文件扩展名
185
185
  image_exts: 图像文件扩展名元组
186
- require_segmentation: 是否要求分割格式。如果True,只接受分割标注(至少6个坐标),检测格式将跳过
186
+ require_segmentation: 是否强制分割模式。如果True,检测标注(4个坐标)将生成为从边界框创建的多边形,分割标注(6+个坐标)正常处理。如果False,自动检测格式类型。
187
187
 
188
188
  Returns:
189
189
  图像标注数据列表,每个元素为read()返回的格式
@@ -445,7 +445,8 @@ class YoloHandler:
445
445
  return False
446
446
 
447
447
  def _parse_detection(self, coords: List[float], class_id: int,
448
- classes: List[str], image_size: Tuple[int, int]) -> Optional[Dict]:
448
+ classes: List[str], image_size: Tuple[int, int],
449
+ force_polygon: bool = False) -> Optional[Dict]:
449
450
  """解析检测格式标注
450
451
 
451
452
  Args:
@@ -453,6 +454,7 @@ class YoloHandler:
453
454
  class_id: 类别ID
454
455
  classes: 类别名称列表
455
456
  image_size: 图像尺寸 (width, height)
457
+ force_polygon: 是否强制生成多边形分割数据
456
458
 
457
459
  Returns:
458
460
  统一格式的标注字典
@@ -486,20 +488,23 @@ class YoloHandler:
486
488
  "segmentation": None
487
489
  }
488
490
 
489
- # 从边界框创建简单多边形
490
- x_max = x_min + bbox_width
491
- y_max = y_min + bbox_height
492
- annotation["segmentation"] = [[
493
- x_min, y_min,
494
- x_max, y_min,
495
- x_max, y_max,
496
- x_min, y_max
497
- ]]
491
+ # 只有在强制分割模式时才从边界框创建简单多边形
492
+ if force_polygon:
493
+ x_max = x_min + bbox_width
494
+ y_max = y_min + bbox_height
495
+ annotation["segmentation"] = [[
496
+ x_min, y_min,
497
+ x_max, y_min,
498
+ x_max, y_max,
499
+ x_min, y_max
500
+ ]]
501
+ annotation["force_polygon"] = True # 标记为强制转换的多边形
498
502
 
499
503
  return annotation
500
504
 
501
505
  def _parse_segmentation(self, coords: List[float], class_id: int,
502
- classes: List[str], image_size: Tuple[int, int]) -> Optional[Dict]:
506
+ classes: List[str], image_size: Tuple[int, int],
507
+ force_polygon: bool = False) -> Optional[Dict]:
503
508
  """解析分割格式标注
504
509
 
505
510
  Args:
@@ -507,6 +512,7 @@ class YoloHandler:
507
512
  class_id: 类别ID
508
513
  classes: 类别名称列表
509
514
  image_size: 图像尺寸 (width, height)
515
+ force_polygon: 是否强制生成多边形分割数据(对于真实分割标注应为False)
510
516
 
511
517
  Returns:
512
518
  统一格式的标注字典
@@ -548,6 +554,10 @@ class YoloHandler:
548
554
  "segmentation": [denormalized_coords]
549
555
  }
550
556
 
557
+ # 真实分割标注不标记force_polygon,或标记为False
558
+ if force_polygon:
559
+ annotation["force_polygon"] = False
560
+
551
561
  return annotation
552
562
 
553
563
  def _normalize_coords(self, coords: List[float], image_size: Tuple[int, int]) -> Optional[List[float]]:
@@ -135,9 +135,23 @@ class BaseVisualizer:
135
135
  color_idx = class_id % len(self.DEFAULT_COLORS)
136
136
  return self.DEFAULT_COLORS[color_idx]
137
137
  else:
138
- # Generate distinct colors using HSV color space
139
- hue = int(179 * class_id / max(num_classes, 1))
140
- hsv_color = np.uint8([[[hue, 255, 255]]])
138
+ # Generate distinct colors using HSV color space with golden ratio distribution
139
+ # This provides better color separation than linear spacing
140
+ # Golden angle in degrees: 137.508 (gives optimal spacing on color wheel)
141
+ golden_angle = 137.508
142
+ # Use modulo 360 to wrap around the hue circle
143
+ hue_angle = (class_id * golden_angle) % 360.0
144
+ # Convert to OpenCV hue range (0-179, corresponding to 0-360 degrees)
145
+ hue = int(hue_angle * 179.0 / 360.0)
146
+
147
+ # Vary saturation and value slightly to increase color distinction
148
+ # while keeping colors bright and vibrant
149
+ # Use class_id to create patterns in saturation and value
150
+ # This adds extra dimension of variation beyond just hue
151
+ saturation = 220 + (class_id % 4) * 12 # 220-256 range
152
+ value = 220 + ((class_id // 4) % 4) * 12 # 220-256 range
153
+
154
+ hsv_color = np.uint8([[[hue, saturation, value]]])
141
155
  bgr_color = cv2.cvtColor(hsv_color, cv2.COLOR_HSV2BGR)
142
156
  return tuple(map(int, bgr_color[0][0]))
143
157
 
@@ -10,6 +10,7 @@
10
10
  import os
11
11
  import cv2
12
12
  import numpy as np
13
+ import logging
13
14
  from typing import List, Dict, Any, Optional, Tuple
14
15
 
15
16
  from .base import BaseVisualizer
@@ -74,6 +75,7 @@ class GenericVisualizer(BaseVisualizer):
74
75
  Image with annotations drawn
75
76
  """
76
77
  result_image = image.copy()
78
+ self.logger.debug(f"_draw_annotations called with {len(annotations)} annotations, {len(classes)} classes: {classes}")
77
79
 
78
80
  # Validate segmentation format if required (strict mode)
79
81
  if self.segmentation:
@@ -82,7 +84,61 @@ class GenericVisualizer(BaseVisualizer):
82
84
  for ann in annotations:
83
85
  category_id = ann.get("category_id", 0)
84
86
  category_name = ann.get("category_name", f"class_{category_id}")
85
- color = self.get_color_for_class(category_id, len(classes))
87
+
88
+ # Get color based on class name index in classes list
89
+ class_idx = None
90
+ original_category_name = category_name
91
+
92
+ # 1. Try exact match
93
+ try:
94
+ class_idx = classes.index(category_name)
95
+ color = self.get_color_for_class(class_idx, len(classes))
96
+ # Debug logging for successful color assignment
97
+ self.logger.debug(f"Color assigned: category_name='{category_name}', class_idx={class_idx}, color={color}")
98
+ except ValueError:
99
+ # 2. Try normalized match (strip whitespace, case-insensitive)
100
+ normalized_name = category_name.strip()
101
+ # Try case-insensitive match
102
+ try:
103
+ # Find case-insensitive match
104
+ for idx, cls in enumerate(classes):
105
+ if cls.strip().lower() == normalized_name.lower():
106
+ class_idx = idx
107
+ break
108
+ except Exception:
109
+ pass
110
+
111
+ if class_idx is not None:
112
+ color = self.get_color_for_class(class_idx, len(classes))
113
+ self.logger.warning(f"Class '{original_category_name}' matched case-insensitively to '{classes[class_idx]}', using index {class_idx}")
114
+ self.logger.debug(f"Case-insensitive match: category_name='{original_category_name}', matched='{classes[class_idx]}', class_idx={class_idx}, color={color}")
115
+ else:
116
+ # 3. Try to parse "class_X" format
117
+ if category_name.startswith("class_") and category_name[6:].isdigit():
118
+ try:
119
+ parsed_id = int(category_name[6:])
120
+ # Use parsed_id as fallback, but ensure it's within reasonable bounds
121
+ if parsed_id < len(classes) * 2: # Allow some flexibility
122
+ class_idx = parsed_id % len(classes) if len(classes) > 0 else 0
123
+ self.logger.warning(f"Class '{category_name}' parsed as class_{parsed_id}, using index {class_idx}")
124
+ else:
125
+ self.logger.warning(f"Parsed class ID {parsed_id} from '{category_name}' is too large, using category_id")
126
+ except ValueError:
127
+ pass
128
+
129
+ # 4. Final fallback to category_id
130
+ if class_idx is None:
131
+ self.logger.warning(f"Class '{category_name}' not found in classes list, using category_id {category_id} for color")
132
+ # Ensure category_id is within bounds
133
+ if category_id < len(classes):
134
+ class_idx = category_id
135
+ else:
136
+ # If category_id is out of bounds, use modulo
137
+ class_idx = category_id % len(classes) if len(classes) > 0 else 0
138
+
139
+ color = self.get_color_for_class(class_idx, len(classes))
140
+ # Debug logging for fallback case
141
+ self.logger.debug(f"Fallback color: category_name='{original_category_name}', category_id={category_id}, class_idx={class_idx}, color={color}")
86
142
 
87
143
  # Determine what to draw based on mode and available data
88
144
  if self.segmentation:
@@ -171,6 +171,10 @@ class LabelMeVisualizer(GenericVisualizer):
171
171
  class_name = annotation.get("category_name")
172
172
  if class_name:
173
173
  class_names.add(class_name)
174
+ else:
175
+ # Fallback to category_id if category_name is missing
176
+ category_id = annotation.get("category_id", 0)
177
+ class_names.add(f"class_{category_id}")
174
178
  return sorted(class_names)
175
179
 
176
180
  def _resolve_image_paths(self, annotations_list: List[Dict], image_dir: str) -> List[Dict]:
@@ -8,6 +8,8 @@
8
8
  """
9
9
 
10
10
  import os
11
+ import sys
12
+ import logging
11
13
  from typing import List, Dict, Any, Optional
12
14
 
13
15
  from .generic import GenericVisualizer
@@ -55,6 +57,10 @@ class YoloVisualizer(GenericVisualizer):
55
57
  - save_dir: Path where images were saved (if save_dir provided)
56
58
  """
57
59
  # Validate inputs
60
+ # Temporarily enable debug logging for troubleshooting
61
+ self.logger.setLevel(logging.DEBUG)
62
+ self.logger.debug("Debug logging enabled for YOLO visualization")
63
+
58
64
  if not self.validate_input_path(image_dir, is_dir=True):
59
65
  raise ValueError(f"Invalid image directory: {image_dir}")
60
66
  if not self.validate_input_path(label_dir, is_dir=True):
@@ -64,9 +70,11 @@ class YoloVisualizer(GenericVisualizer):
64
70
  if save_dir and not self.validate_output_path(save_dir, is_dir=True, create=True):
65
71
  raise ValueError(f"Invalid save directory: {save_dir}")
66
72
 
73
+
67
74
  # Read classes
68
75
  classes = self._read_classes_file(class_path)
69
76
  self.logger.info(f"Loaded {len(classes)} classes from {class_path}")
77
+ self.logger.debug(f"Classes list: {classes}")
70
78
 
71
79
  # Read annotations using YoloHandler
72
80
  try:
@@ -76,6 +84,14 @@ class YoloVisualizer(GenericVisualizer):
76
84
  classes_path=class_path,
77
85
  require_segmentation=self.segmentation
78
86
  )
87
+ # Debug logging for annotation data
88
+ if annotations_list:
89
+ print(f"[DEBUG] Read {len(annotations_list)} image annotations", flush=True)
90
+ for i, img_data in enumerate(annotations_list[:3]): # Check first 3 images
91
+ anns = img_data.get("annotations", [])
92
+ print(f"[DEBUG] Image {i}: {img_data.get('image_id', 'unknown')}, {len(anns)} annotations", flush=True)
93
+ for j, ann in enumerate(anns[:3]): # Check first 3 annotations per image
94
+ print(f"[DEBUG] Annotation {j}: category_id={ann.get('category_id')}, category_name={ann.get('category_name')}", flush=True)
79
95
  except Exception as e:
80
96
  raise ValueError(f"Failed to read YOLO annotations: {e}")
81
97
 
@@ -110,6 +126,10 @@ class YoloVisualizer(GenericVisualizer):
110
126
  f"Found {len(label_files)} label file(s) with only detection format or no annotations."
111
127
  )
112
128
 
129
+ # Merge classes from file with classes found in annotations
130
+ classes = self._extract_classes(annotations_list, classes)
131
+ self.logger.info(f"Using {len(classes)} classes for visualization")
132
+
113
133
  # Create results template
114
134
  results = self._create_results_template(
115
135
  image_dir=image_dir,
@@ -178,6 +198,87 @@ class YoloVisualizer(GenericVisualizer):
178
198
  self.logger.error(f"Error reading class file {class_path}: {e}")
179
199
  raise ValueError(f"Could not read class file: {class_path}") from e
180
200
 
201
+ def _extract_classes(self, annotations_list: List[Dict], file_classes: List[str]) -> List[str]:
202
+ """Extract and merge class names from annotations and file.
203
+
204
+ Args:
205
+ annotations_list: List of image annotation data
206
+ file_classes: List of class names from file
207
+
208
+ Returns:
209
+ Merged list of class names, ensuring all annotations have matching names
210
+ """
211
+ # Normalize file classes: strip whitespace, create mapping from normalized to original
212
+ file_class_map = {} # normalized -> original
213
+ normalized_file_classes = []
214
+ for class_name in file_classes:
215
+ normalized = class_name.strip()
216
+ file_class_map[normalized] = class_name
217
+ normalized_file_classes.append(normalized)
218
+
219
+ # Extract unique class names from annotations with normalization
220
+ annotation_classes = set() # Store normalized names
221
+ original_annotation_names = {} # normalized -> first original encountered
222
+ for image_data in annotations_list:
223
+ for annotation in image_data.get("annotations", []):
224
+ class_name = annotation.get("category_name")
225
+ if class_name:
226
+ normalized = class_name.strip()
227
+ annotation_classes.add(normalized)
228
+ if normalized not in original_annotation_names:
229
+ original_annotation_names[normalized] = class_name # Keep original for reference
230
+ else:
231
+ # Fallback to category_id
232
+ category_id = annotation.get("category_id", 0)
233
+ class_name = f"class_{category_id}"
234
+ normalized = class_name.strip()
235
+ annotation_classes.add(normalized)
236
+ if normalized not in original_annotation_names:
237
+ original_annotation_names[normalized] = class_name
238
+
239
+ # Merge with file classes, preserving file order for existing classes
240
+ merged_classes = []
241
+ matched_normalized = set()
242
+
243
+ # First add file classes that appear in annotations (case-insensitive and whitespace-insensitive)
244
+ for normalized, original in file_class_map.items():
245
+ if normalized in annotation_classes:
246
+ merged_classes.append(original) # Use original file class name
247
+ matched_normalized.add(normalized)
248
+ annotation_classes.remove(normalized)
249
+ else:
250
+ # Also check case-insensitive match
251
+ matched = False
252
+ for ann_normalized in list(annotation_classes):
253
+ if ann_normalized.lower() == normalized.lower():
254
+ merged_classes.append(original) # Use original file class name
255
+ matched_normalized.add(ann_normalized)
256
+ annotation_classes.remove(ann_normalized)
257
+ matched = True
258
+ self.logger.warning(f"Class name '{ann_normalized}' matched case-insensitively to file class '{original}'")
259
+ break
260
+ if not matched:
261
+ # File class not found in annotations, still include it
262
+ merged_classes.append(original)
263
+
264
+ # Add remaining annotation classes (not matched to file classes)
265
+ remaining = sorted(annotation_classes)
266
+ for normalized in remaining:
267
+ # Use original annotation name if available, otherwise normalized
268
+ original = original_annotation_names.get(normalized, normalized)
269
+ merged_classes.append(original)
270
+
271
+ self.logger.info(f"Merged classes: {len(file_classes)} from file, {len(merged_classes)} total after merge")
272
+ if len(merged_classes) > len(file_classes):
273
+ self.logger.warning(f"Found {len(merged_classes) - len(file_classes)} classes in annotations not in file")
274
+
275
+ # Debug logging for color assignment consistency
276
+ self.logger.debug(f"Merged classes list: {merged_classes}")
277
+ self.logger.debug(f"File classes: {file_classes}")
278
+ self.logger.debug(f"Annotation classes (normalized): {original_annotation_names}")
279
+
280
+ return merged_classes
281
+
181
282
  def batch_visualize(self,
182
283
  image_dirs: List[str],
183
284
  label_dirs: List[str],
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: dataflow-cv
3
- Version: 0.3.0
3
+ Version: 0.4.0
4
4
  Summary: A data processing library for computer vision datasets
5
5
  Home-page: https://github.com/zjykzj/DataFlow-CV
6
6
  Author: DataFlow Team
@@ -34,10 +34,8 @@ Dynamic: requires-python
34
34
  > **Where Vibe Coding meets CV data.** 🌊
35
35
  > Convert & visualize datasets. Built with the flow of Claude Code.
36
36
 
37
- ![Python Version](https://img.shields.io/badge/python-3.8%20|%203.9%20|%203.10%20|%203.11%20|%203.12-blue)
38
- ![License](https://img.shields.io/badge/license-MIT-green)
39
- ![Version](https://img.shields.io/badge/version-0.3.0-orange)
40
- ![Development Status](https://img.shields.io/badge/status-alpha-yellow)
37
+ ![Python Version](https://img.shields.io/badge/python-3.8%20|%203.9%20|%203.10%20|%203.11%20|%203.12-blue) ![License](https://img.shields.io/badge/license-MIT-green) [![PyPI](https://img.shields.io/pypi/v/dataflow-cv.svg)](https://pypi.org/project/dataflow-cv/) ![Development Status](https://img.shields.io/badge/status-alpha-yellow) [![GitHub Actions](https://github.com/zjykzj/DataFlow-CV/actions/workflows/python-publish.yml/badge.svg)](https://github.com/zjykzj/DataFlow-CV/actions/workflows/python-publish.yml)
38
+
41
39
 
42
40
  A data processing library for computer vision datasets, focusing on format conversion and visualization between LabelMe, COCO, and YOLO formats. Provides both a CLI and Python API.
43
41
 
@@ -50,6 +48,8 @@ A data processing library for computer vision datasets, focusing on format conve
50
48
  - [Core Dependencies](#core-dependencies)
51
49
  - [Quick Start](#quick-start)
52
50
  - [Installation](#installation)
51
+ - [Editable Installation (Development Mode)](#editable-installation-development-mode)
52
+ - [Build System](#build-system)
53
53
  - [Command Line Usage](#command-line-usage)
54
54
  - [Python API Usage](#python-api-usage)
55
55
  - [CLI Reference](#cli-reference)
@@ -61,6 +61,7 @@ A data processing library for computer vision datasets, focusing on format conve
61
61
  - [Segmentation Support](#segmentation-support)
62
62
  - [Running Tests](#running-tests)
63
63
  - [Examples](#examples)
64
+ - [Documentation](#documentation)
64
65
  - [License](#license)
65
66
 
66
67
  ## Project Structure
@@ -79,6 +80,7 @@ dataflow/
79
80
  ├── visualize/ # Annotation visualization module
80
81
  │ ├── __init__.py
81
82
  │ ├── base.py # Visualizer base class
83
+ │ ├── generic.py # Generic visualizer base class using label handlers
82
84
  │ ├── yolo.py # YOLO annotation visualizer
83
85
  │ ├── coco.py # COCO annotation visualizer
84
86
  │ └── labelme.py # LabelMe annotation visualizer
@@ -92,7 +94,11 @@ tests/
92
94
  ├── convert/ # Conversion tests
93
95
  │ ├── __init__.py
94
96
  │ ├── test_coco_to_yolo.py
95
- └── test_yolo_to_coco.py
97
+ ├── test_yolo_to_coco.py
98
+ │ ├── test_coco_to_labelme.py
99
+ │ ├── test_labelme_to_coco.py
100
+ │ ├── test_labelme_to_yolo.py
101
+ │ └── test_yolo_to_labelme.py
96
102
  ├── visualize/ # Visualization tests
97
103
  │ ├── __init__.py
98
104
  │ ├── test_yolo.py
@@ -130,6 +136,11 @@ samples/
130
136
  ├── api_yolo.py
131
137
  ├── api_coco.py
132
138
  └── api_labelme.py
139
+ docs/ # Data format documentation
140
+ ├── README.md # Documentation index
141
+ ├── yolo.md # YOLO format specification
142
+ ├── labelme.md # LabelMe format specification
143
+ └── coco.md # COCO format specification
133
144
  ```
134
145
 
135
146
  ## Requirements
@@ -468,6 +479,20 @@ Check the `samples/` directory for detailed usage examples:
468
479
  - `samples/api/convert/` - Python API conversion examples
469
480
  - `samples/api/visualize/` - Python API visualization examples
470
481
 
482
+ ### Documentation
483
+
484
+ Detailed data format specifications are available in the `docs/` directory:
485
+
486
+ - [`docs/README.md`](docs/README.md) - Documentation index
487
+ - [`docs/yolo.md`](docs/yolo.md) - YOLO format specification
488
+ - [`docs/labelme.md`](docs/labelme.md) - LabelMe format specification
489
+ - [`docs/coco.md`](docs/coco.md) - COCO format specification
490
+
491
+ These documents describe the annotation formats supported by DataFlow-CV, without covering tool usage.
492
+ ## Development
493
+
494
+ For development guidelines, architecture details, and contribution instructions, see [CLAUDE.md](CLAUDE.md). This file provides guidance for working with the codebase, including common development commands, architectural patterns, and writing principles.
495
+
471
496
  ## License
472
497
 
473
498
  [MIT License](LICENSE) © 2026 zjykzj
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "dataflow-cv"
7
- version = "0.3.0"
7
+ version = "0.4.0"
8
8
  description = "A data processing library for computer vision datasets"
9
9
  readme = "README.md"
10
10
  authors = [
@@ -37,7 +37,7 @@ class DevelopCommand(_develop):
37
37
 
38
38
  setup(
39
39
  name="dataflow-cv",
40
- version="0.3.0",
40
+ version="0.3.1",
41
41
  author="DataFlow Team",
42
42
  description="A data processing library for computer vision datasets",
43
43
  long_description=long_description,
File without changes
File without changes