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.
- {dataflow_cv-0.3.0/dataflow_cv.egg-info → dataflow_cv-0.4.0}/PKG-INFO +31 -6
- {dataflow_cv-0.3.0 → dataflow_cv-0.4.0}/README.md +30 -5
- {dataflow_cv-0.3.0 → dataflow_cv-0.4.0}/dataflow/__init__.py +8 -6
- {dataflow_cv-0.3.0 → dataflow_cv-0.4.0}/dataflow/cli.py +20 -11
- {dataflow_cv-0.3.0 → dataflow_cv-0.4.0}/dataflow/convert/coco_and_labelme.py +6 -2
- {dataflow_cv-0.3.0 → dataflow_cv-0.4.0}/dataflow/convert/coco_and_yolo.py +53 -20
- {dataflow_cv-0.3.0 → dataflow_cv-0.4.0}/dataflow/convert/yolo_and_labelme.py +32 -22
- {dataflow_cv-0.3.0 → dataflow_cv-0.4.0}/dataflow/label/labelme.py +36 -16
- {dataflow_cv-0.3.0 → dataflow_cv-0.4.0}/dataflow/label/yolo.py +29 -19
- {dataflow_cv-0.3.0 → dataflow_cv-0.4.0}/dataflow/visualize/base.py +17 -3
- {dataflow_cv-0.3.0 → dataflow_cv-0.4.0}/dataflow/visualize/generic.py +57 -1
- {dataflow_cv-0.3.0 → dataflow_cv-0.4.0}/dataflow/visualize/labelme.py +4 -0
- {dataflow_cv-0.3.0 → dataflow_cv-0.4.0}/dataflow/visualize/yolo.py +101 -0
- {dataflow_cv-0.3.0 → dataflow_cv-0.4.0/dataflow_cv.egg-info}/PKG-INFO +31 -6
- {dataflow_cv-0.3.0 → dataflow_cv-0.4.0}/pyproject.toml +1 -1
- {dataflow_cv-0.3.0 → dataflow_cv-0.4.0}/setup.py +1 -1
- {dataflow_cv-0.3.0 → dataflow_cv-0.4.0}/LICENSE +0 -0
- {dataflow_cv-0.3.0 → dataflow_cv-0.4.0}/dataflow/config.py +0 -0
- {dataflow_cv-0.3.0 → dataflow_cv-0.4.0}/dataflow/convert/__init__.py +0 -0
- {dataflow_cv-0.3.0 → dataflow_cv-0.4.0}/dataflow/convert/base.py +0 -0
- {dataflow_cv-0.3.0 → dataflow_cv-0.4.0}/dataflow/label/__init__.py +0 -0
- {dataflow_cv-0.3.0 → dataflow_cv-0.4.0}/dataflow/label/coco.py +0 -0
- {dataflow_cv-0.3.0 → dataflow_cv-0.4.0}/dataflow/visualize/__init__.py +0 -0
- {dataflow_cv-0.3.0 → dataflow_cv-0.4.0}/dataflow/visualize/coco.py +0 -0
- {dataflow_cv-0.3.0 → dataflow_cv-0.4.0}/dataflow_cv.egg-info/SOURCES.txt +0 -0
- {dataflow_cv-0.3.0 → dataflow_cv-0.4.0}/dataflow_cv.egg-info/dependency_links.txt +0 -0
- {dataflow_cv-0.3.0 → dataflow_cv-0.4.0}/dataflow_cv.egg-info/entry_points.txt +0 -0
- {dataflow_cv-0.3.0 → dataflow_cv-0.4.0}/dataflow_cv.egg-info/requires.txt +0 -0
- {dataflow_cv-0.3.0 → dataflow_cv-0.4.0}/dataflow_cv.egg-info/top_level.txt +0 -0
- {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
|
+
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
|
-

|
|
38
|
-
|
|
39
|
-

|
|
40
|
-

|
|
37
|
+
  [](https://pypi.org/project/dataflow-cv/)  [](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
|
-
│
|
|
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
|
-

|
|
7
|
-
|
|
8
|
-

|
|
9
|
-

|
|
6
|
+
  [](https://pypi.org/project/dataflow-cv/)  [](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
|
-
│
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
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
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
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 {
|
|
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,
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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.
|
|
156
|
-
categories = self.
|
|
166
|
+
# 4. Read provided classes file and validate
|
|
167
|
+
categories = self.read_classes_file(classes_path)
|
|
157
168
|
if not categories:
|
|
158
|
-
|
|
169
|
+
raise ValueError(f"No categories found in classes file: {classes_path}")
|
|
159
170
|
|
|
160
|
-
# 5.
|
|
161
|
-
|
|
162
|
-
if
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
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
|
-
#
|
|
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 {
|
|
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
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
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:
|
|
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
|
-
|
|
117
|
-
|
|
118
|
-
|
|
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
|
-
|
|
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:
|
|
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]
|
|
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
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
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]
|
|
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
|
-
|
|
140
|
-
|
|
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
|
-
|
|
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
|
+
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
|
-

|
|
38
|
-
|
|
39
|
-

|
|
40
|
-

|
|
37
|
+
  [](https://pypi.org/project/dataflow-cv/)  [](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
|
-
│
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|