synapse-sdk 1.0.0a23__py3-none-any.whl → 2025.12.3__py3-none-any.whl
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.
- synapse_sdk/__init__.py +24 -0
- synapse_sdk/cli/__init__.py +310 -5
- synapse_sdk/cli/alias/__init__.py +22 -0
- synapse_sdk/cli/alias/create.py +36 -0
- synapse_sdk/cli/alias/dataclass.py +31 -0
- synapse_sdk/cli/alias/default.py +16 -0
- synapse_sdk/cli/alias/delete.py +15 -0
- synapse_sdk/cli/alias/list.py +19 -0
- synapse_sdk/cli/alias/read.py +15 -0
- synapse_sdk/cli/alias/update.py +17 -0
- synapse_sdk/cli/alias/utils.py +61 -0
- synapse_sdk/cli/code_server.py +687 -0
- synapse_sdk/cli/config.py +440 -0
- synapse_sdk/cli/devtools.py +90 -0
- synapse_sdk/cli/plugin/__init__.py +33 -0
- synapse_sdk/cli/{create_plugin.py → plugin/create.py} +2 -2
- synapse_sdk/{plugins/cli → cli/plugin}/publish.py +23 -15
- synapse_sdk/clients/agent/__init__.py +9 -3
- synapse_sdk/clients/agent/container.py +143 -0
- synapse_sdk/clients/agent/core.py +19 -0
- synapse_sdk/clients/agent/ray.py +298 -9
- synapse_sdk/clients/backend/__init__.py +30 -12
- synapse_sdk/clients/backend/annotation.py +13 -5
- synapse_sdk/clients/backend/core.py +31 -4
- synapse_sdk/clients/backend/data_collection.py +186 -0
- synapse_sdk/clients/backend/hitl.py +17 -0
- synapse_sdk/clients/backend/integration.py +16 -1
- synapse_sdk/clients/backend/ml.py +5 -1
- synapse_sdk/clients/backend/models.py +78 -0
- synapse_sdk/clients/base.py +384 -41
- synapse_sdk/clients/ray/serve.py +2 -0
- synapse_sdk/clients/validators/collections.py +31 -0
- synapse_sdk/devtools/config.py +94 -0
- synapse_sdk/devtools/server.py +41 -0
- synapse_sdk/devtools/streamlit_app/__init__.py +5 -0
- synapse_sdk/devtools/streamlit_app/app.py +128 -0
- synapse_sdk/devtools/streamlit_app/services/__init__.py +11 -0
- synapse_sdk/devtools/streamlit_app/services/job_service.py +233 -0
- synapse_sdk/devtools/streamlit_app/services/plugin_service.py +236 -0
- synapse_sdk/devtools/streamlit_app/services/serve_service.py +95 -0
- synapse_sdk/devtools/streamlit_app/ui/__init__.py +15 -0
- synapse_sdk/devtools/streamlit_app/ui/config_tab.py +76 -0
- synapse_sdk/devtools/streamlit_app/ui/deployment_tab.py +66 -0
- synapse_sdk/devtools/streamlit_app/ui/http_tab.py +125 -0
- synapse_sdk/devtools/streamlit_app/ui/jobs_tab.py +573 -0
- synapse_sdk/devtools/streamlit_app/ui/serve_tab.py +346 -0
- synapse_sdk/devtools/streamlit_app/ui/status_bar.py +118 -0
- synapse_sdk/devtools/streamlit_app/utils/__init__.py +40 -0
- synapse_sdk/devtools/streamlit_app/utils/json_viewer.py +197 -0
- synapse_sdk/devtools/streamlit_app/utils/log_formatter.py +38 -0
- synapse_sdk/devtools/streamlit_app/utils/styles.py +241 -0
- synapse_sdk/devtools/streamlit_app/utils/ui_components.py +289 -0
- synapse_sdk/devtools/streamlit_app.py +10 -0
- synapse_sdk/loggers.py +120 -9
- synapse_sdk/plugins/README.md +1340 -0
- synapse_sdk/plugins/__init__.py +0 -13
- synapse_sdk/plugins/categories/base.py +117 -11
- synapse_sdk/plugins/categories/data_validation/actions/validation.py +72 -0
- synapse_sdk/plugins/categories/data_validation/templates/plugin/validation.py +33 -5
- synapse_sdk/plugins/categories/export/actions/__init__.py +3 -0
- synapse_sdk/plugins/categories/export/actions/export/__init__.py +28 -0
- synapse_sdk/plugins/categories/export/actions/export/action.py +165 -0
- synapse_sdk/plugins/categories/export/actions/export/enums.py +113 -0
- synapse_sdk/plugins/categories/export/actions/export/exceptions.py +53 -0
- synapse_sdk/plugins/categories/export/actions/export/models.py +74 -0
- synapse_sdk/plugins/categories/export/actions/export/run.py +195 -0
- synapse_sdk/plugins/categories/export/actions/export/utils.py +187 -0
- synapse_sdk/plugins/categories/export/templates/config.yaml +21 -0
- synapse_sdk/plugins/categories/export/templates/plugin/__init__.py +390 -0
- synapse_sdk/plugins/categories/export/templates/plugin/export.py +160 -0
- synapse_sdk/plugins/categories/neural_net/actions/deployment.py +13 -12
- synapse_sdk/plugins/categories/neural_net/actions/train.py +1134 -31
- synapse_sdk/plugins/categories/neural_net/actions/tune.py +534 -0
- synapse_sdk/plugins/categories/neural_net/base/inference.py +1 -1
- synapse_sdk/plugins/categories/neural_net/templates/config.yaml +32 -4
- synapse_sdk/plugins/categories/neural_net/templates/plugin/inference.py +26 -10
- synapse_sdk/plugins/categories/pre_annotation/actions/__init__.py +4 -0
- synapse_sdk/plugins/categories/pre_annotation/actions/pre_annotation/__init__.py +3 -0
- synapse_sdk/plugins/categories/{export/actions/export.py → pre_annotation/actions/pre_annotation/action.py} +4 -4
- synapse_sdk/plugins/categories/pre_annotation/actions/to_task/__init__.py +28 -0
- synapse_sdk/plugins/categories/pre_annotation/actions/to_task/action.py +148 -0
- synapse_sdk/plugins/categories/pre_annotation/actions/to_task/enums.py +269 -0
- synapse_sdk/plugins/categories/pre_annotation/actions/to_task/exceptions.py +14 -0
- synapse_sdk/plugins/categories/pre_annotation/actions/to_task/factory.py +76 -0
- synapse_sdk/plugins/categories/pre_annotation/actions/to_task/models.py +100 -0
- synapse_sdk/plugins/categories/pre_annotation/actions/to_task/orchestrator.py +248 -0
- synapse_sdk/plugins/categories/pre_annotation/actions/to_task/run.py +64 -0
- synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/__init__.py +17 -0
- synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/annotation.py +265 -0
- synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/base.py +170 -0
- synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/extraction.py +83 -0
- synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/metrics.py +92 -0
- synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/preprocessor.py +243 -0
- synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/validation.py +143 -0
- synapse_sdk/plugins/categories/pre_annotation/templates/config.yaml +19 -0
- synapse_sdk/plugins/categories/pre_annotation/templates/plugin/to_task.py +40 -0
- synapse_sdk/plugins/categories/smart_tool/templates/config.yaml +2 -0
- synapse_sdk/plugins/categories/upload/__init__.py +0 -0
- synapse_sdk/plugins/categories/upload/actions/__init__.py +0 -0
- synapse_sdk/plugins/categories/upload/actions/upload/__init__.py +19 -0
- synapse_sdk/plugins/categories/upload/actions/upload/action.py +236 -0
- synapse_sdk/plugins/categories/upload/actions/upload/context.py +185 -0
- synapse_sdk/plugins/categories/upload/actions/upload/enums.py +493 -0
- synapse_sdk/plugins/categories/upload/actions/upload/exceptions.py +36 -0
- synapse_sdk/plugins/categories/upload/actions/upload/factory.py +138 -0
- synapse_sdk/plugins/categories/upload/actions/upload/models.py +214 -0
- synapse_sdk/plugins/categories/upload/actions/upload/orchestrator.py +183 -0
- synapse_sdk/plugins/categories/upload/actions/upload/registry.py +113 -0
- synapse_sdk/plugins/categories/upload/actions/upload/run.py +179 -0
- synapse_sdk/plugins/categories/upload/actions/upload/steps/__init__.py +1 -0
- synapse_sdk/plugins/categories/upload/actions/upload/steps/base.py +107 -0
- synapse_sdk/plugins/categories/upload/actions/upload/steps/cleanup.py +62 -0
- synapse_sdk/plugins/categories/upload/actions/upload/steps/collection.py +63 -0
- synapse_sdk/plugins/categories/upload/actions/upload/steps/generate.py +91 -0
- synapse_sdk/plugins/categories/upload/actions/upload/steps/initialize.py +82 -0
- synapse_sdk/plugins/categories/upload/actions/upload/steps/metadata.py +235 -0
- synapse_sdk/plugins/categories/upload/actions/upload/steps/organize.py +201 -0
- synapse_sdk/plugins/categories/upload/actions/upload/steps/upload.py +104 -0
- synapse_sdk/plugins/categories/upload/actions/upload/steps/validate.py +71 -0
- synapse_sdk/plugins/categories/upload/actions/upload/strategies/__init__.py +1 -0
- synapse_sdk/plugins/categories/upload/actions/upload/strategies/base.py +82 -0
- synapse_sdk/plugins/categories/upload/actions/upload/strategies/data_unit/__init__.py +1 -0
- synapse_sdk/plugins/categories/upload/actions/upload/strategies/data_unit/batch.py +39 -0
- synapse_sdk/plugins/categories/upload/actions/upload/strategies/data_unit/single.py +29 -0
- synapse_sdk/plugins/categories/upload/actions/upload/strategies/file_discovery/__init__.py +1 -0
- synapse_sdk/plugins/categories/upload/actions/upload/strategies/file_discovery/flat.py +300 -0
- synapse_sdk/plugins/categories/upload/actions/upload/strategies/file_discovery/recursive.py +287 -0
- synapse_sdk/plugins/categories/upload/actions/upload/strategies/metadata/__init__.py +1 -0
- synapse_sdk/plugins/categories/upload/actions/upload/strategies/metadata/excel.py +174 -0
- synapse_sdk/plugins/categories/upload/actions/upload/strategies/metadata/none.py +16 -0
- synapse_sdk/plugins/categories/upload/actions/upload/strategies/upload/__init__.py +1 -0
- synapse_sdk/plugins/categories/upload/actions/upload/strategies/upload/sync.py +84 -0
- synapse_sdk/plugins/categories/upload/actions/upload/strategies/validation/__init__.py +1 -0
- synapse_sdk/plugins/categories/upload/actions/upload/strategies/validation/default.py +60 -0
- synapse_sdk/plugins/categories/upload/actions/upload/utils.py +250 -0
- synapse_sdk/plugins/categories/upload/templates/README.md +470 -0
- synapse_sdk/plugins/categories/upload/templates/config.yaml +33 -0
- synapse_sdk/plugins/categories/upload/templates/plugin/__init__.py +310 -0
- synapse_sdk/plugins/categories/upload/templates/plugin/upload.py +102 -0
- synapse_sdk/plugins/enums.py +3 -1
- synapse_sdk/plugins/models.py +148 -11
- synapse_sdk/plugins/templates/plugin-config-schema.json +406 -0
- synapse_sdk/plugins/templates/schema.json +491 -0
- synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/config.yaml +1 -0
- synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/requirements.txt +1 -1
- synapse_sdk/plugins/utils/__init__.py +46 -0
- synapse_sdk/plugins/utils/actions.py +119 -0
- synapse_sdk/plugins/utils/config.py +203 -0
- synapse_sdk/plugins/{utils.py → utils/legacy.py} +26 -46
- synapse_sdk/plugins/utils/ray_gcs.py +66 -0
- synapse_sdk/plugins/utils/registry.py +58 -0
- synapse_sdk/shared/__init__.py +25 -0
- synapse_sdk/shared/enums.py +93 -0
- synapse_sdk/types.py +19 -0
- synapse_sdk/utils/converters/__init__.py +240 -0
- synapse_sdk/utils/converters/coco/__init__.py +0 -0
- synapse_sdk/utils/converters/coco/from_dm.py +322 -0
- synapse_sdk/utils/converters/coco/to_dm.py +215 -0
- synapse_sdk/utils/converters/dm/__init__.py +57 -0
- synapse_sdk/utils/converters/dm/base.py +137 -0
- synapse_sdk/utils/converters/dm/from_v1.py +273 -0
- synapse_sdk/utils/converters/dm/to_v1.py +321 -0
- synapse_sdk/utils/converters/dm/tools/__init__.py +214 -0
- synapse_sdk/utils/converters/dm/tools/answer.py +95 -0
- synapse_sdk/utils/converters/dm/tools/bounding_box.py +132 -0
- synapse_sdk/utils/converters/dm/tools/bounding_box_3d.py +121 -0
- synapse_sdk/utils/converters/dm/tools/classification.py +75 -0
- synapse_sdk/utils/converters/dm/tools/keypoint.py +117 -0
- synapse_sdk/utils/converters/dm/tools/named_entity.py +111 -0
- synapse_sdk/utils/converters/dm/tools/polygon.py +122 -0
- synapse_sdk/utils/converters/dm/tools/polyline.py +124 -0
- synapse_sdk/utils/converters/dm/tools/prompt.py +94 -0
- synapse_sdk/utils/converters/dm/tools/relation.py +86 -0
- synapse_sdk/utils/converters/dm/tools/segmentation.py +141 -0
- synapse_sdk/utils/converters/dm/tools/segmentation_3d.py +83 -0
- synapse_sdk/utils/converters/dm/types.py +168 -0
- synapse_sdk/utils/converters/dm/utils.py +162 -0
- synapse_sdk/utils/converters/dm_legacy/__init__.py +56 -0
- synapse_sdk/utils/converters/dm_legacy/from_v1.py +627 -0
- synapse_sdk/utils/converters/dm_legacy/to_v1.py +367 -0
- synapse_sdk/utils/converters/pascal/__init__.py +0 -0
- synapse_sdk/utils/converters/pascal/from_dm.py +244 -0
- synapse_sdk/utils/converters/pascal/to_dm.py +214 -0
- synapse_sdk/utils/converters/yolo/__init__.py +0 -0
- synapse_sdk/utils/converters/yolo/from_dm.py +384 -0
- synapse_sdk/utils/converters/yolo/to_dm.py +267 -0
- synapse_sdk/utils/dataset.py +46 -0
- synapse_sdk/utils/encryption.py +158 -0
- synapse_sdk/utils/file/__init__.py +58 -0
- synapse_sdk/utils/file/archive.py +32 -0
- synapse_sdk/utils/file/checksum.py +56 -0
- synapse_sdk/utils/file/chunking.py +31 -0
- synapse_sdk/utils/file/download.py +385 -0
- synapse_sdk/utils/file/encoding.py +40 -0
- synapse_sdk/utils/file/io.py +22 -0
- synapse_sdk/utils/file/upload.py +165 -0
- synapse_sdk/utils/file/video/__init__.py +29 -0
- synapse_sdk/utils/file/video/transcode.py +307 -0
- synapse_sdk/utils/file.py.backup +301 -0
- synapse_sdk/utils/http.py +138 -0
- synapse_sdk/utils/network.py +309 -0
- synapse_sdk/utils/storage/__init__.py +72 -0
- synapse_sdk/utils/storage/providers/__init__.py +183 -0
- synapse_sdk/utils/storage/providers/file_system.py +134 -0
- synapse_sdk/utils/storage/providers/gcp.py +13 -0
- synapse_sdk/utils/storage/providers/http.py +190 -0
- synapse_sdk/utils/storage/providers/s3.py +91 -0
- synapse_sdk/utils/storage/providers/sftp.py +47 -0
- synapse_sdk/utils/storage/registry.py +17 -0
- synapse_sdk-2025.12.3.dist-info/METADATA +123 -0
- synapse_sdk-2025.12.3.dist-info/RECORD +279 -0
- {synapse_sdk-1.0.0a23.dist-info → synapse_sdk-2025.12.3.dist-info}/WHEEL +1 -1
- synapse_sdk/clients/backend/dataset.py +0 -51
- synapse_sdk/plugins/categories/import/actions/import.py +0 -10
- synapse_sdk/plugins/cli/__init__.py +0 -21
- synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/.env +0 -24
- synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/.env.dist +0 -24
- synapse_sdk/plugins/templates/synapse-{{cookiecutter.plugin_code}}-plugin/main.py +0 -4
- synapse_sdk/utils/file.py +0 -168
- synapse_sdk/utils/storage.py +0 -91
- synapse_sdk-1.0.0a23.dist-info/METADATA +0 -44
- synapse_sdk-1.0.0a23.dist-info/RECORD +0 -114
- /synapse_sdk/{plugins/cli → cli/plugin}/run.py +0 -0
- /synapse_sdk/{plugins/categories/import → clients/validators}/__init__.py +0 -0
- /synapse_sdk/{plugins/categories/import/actions → devtools}/__init__.py +0 -0
- {synapse_sdk-1.0.0a23.dist-info → synapse_sdk-2025.12.3.dist-info}/entry_points.txt +0 -0
- {synapse_sdk-1.0.0a23.dist-info → synapse_sdk-2025.12.3.dist-info/licenses}/LICENSE +0 -0
- {synapse_sdk-1.0.0a23.dist-info → synapse_sdk-2025.12.3.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,215 @@
|
|
|
1
|
+
import json
|
|
2
|
+
import os
|
|
3
|
+
from typing import IO, Any, Dict
|
|
4
|
+
|
|
5
|
+
from synapse_sdk.utils.converters import ToDMConverter
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class COCOToDMConverter(ToDMConverter):
|
|
9
|
+
"""Convert COCO format annotations to DM (Data Manager) format."""
|
|
10
|
+
|
|
11
|
+
def __init__(self, root_dir: str = None, is_categorized_dataset: bool = False, is_single_conversion: bool = False):
|
|
12
|
+
super().__init__(root_dir, is_categorized_dataset, is_single_conversion)
|
|
13
|
+
|
|
14
|
+
def convert(self):
|
|
15
|
+
if self.is_categorized_dataset:
|
|
16
|
+
splits = self._validate_splits(['train', 'valid'], ['test'])
|
|
17
|
+
all_split_data = {}
|
|
18
|
+
for split, split_dir in splits.items():
|
|
19
|
+
annotation_path = os.path.join(split_dir, 'annotations.json')
|
|
20
|
+
if not os.path.exists(annotation_path):
|
|
21
|
+
raise FileNotFoundError(f'annotations.json not found in {split_dir}')
|
|
22
|
+
with open(annotation_path, 'r', encoding='utf-8') as f:
|
|
23
|
+
coco_data = json.load(f)
|
|
24
|
+
split_data = self._convert_coco_ann_to_dm(coco_data, split_dir)
|
|
25
|
+
all_split_data[split] = split_data
|
|
26
|
+
self.converted_data = all_split_data
|
|
27
|
+
return all_split_data
|
|
28
|
+
else:
|
|
29
|
+
annotation_path = os.path.join(self.root_dir, 'annotations.json')
|
|
30
|
+
if not os.path.exists(annotation_path):
|
|
31
|
+
raise FileNotFoundError(f'annotations.json not found in {self.root_dir}')
|
|
32
|
+
with open(annotation_path, 'r', encoding='utf-8') as f:
|
|
33
|
+
coco_data = json.load(f)
|
|
34
|
+
converted_data = self._convert_coco_ann_to_dm(coco_data, self.root_dir)
|
|
35
|
+
self.converted_data = converted_data
|
|
36
|
+
return converted_data
|
|
37
|
+
|
|
38
|
+
def _convert_coco_ann_to_dm(self, coco_data, base_dir):
|
|
39
|
+
"""Convert COCO annotations to DM format."""
|
|
40
|
+
dataset_type = coco_data.get('type', 'image') # Default to 'image' if type is not specified
|
|
41
|
+
if dataset_type == 'image':
|
|
42
|
+
return self._process_image_data(coco_data, base_dir)
|
|
43
|
+
else:
|
|
44
|
+
raise ValueError(f'Unsupported dataset type: {dataset_type}')
|
|
45
|
+
|
|
46
|
+
def _process_image_data(self, coco_data, img_base_dir):
|
|
47
|
+
"""Process COCO image data and convert to DM format."""
|
|
48
|
+
images = coco_data.get('images', [])
|
|
49
|
+
annotations = coco_data.get('annotations', [])
|
|
50
|
+
categories = coco_data.get('categories', [])
|
|
51
|
+
cat_map = {cat['id']: cat for cat in categories}
|
|
52
|
+
|
|
53
|
+
# Build image_id -> annotation list
|
|
54
|
+
ann_by_img_id = {}
|
|
55
|
+
for ann in annotations:
|
|
56
|
+
img_id = ann['image_id']
|
|
57
|
+
ann_by_img_id.setdefault(img_id, []).append(ann)
|
|
58
|
+
|
|
59
|
+
result = {}
|
|
60
|
+
for img in images:
|
|
61
|
+
img_id = img['id']
|
|
62
|
+
img_filename = img['file_name']
|
|
63
|
+
img_path = os.path.join(img_base_dir, img_filename)
|
|
64
|
+
anns = ann_by_img_id.get(img_id, [])
|
|
65
|
+
|
|
66
|
+
# DM image structure
|
|
67
|
+
dm_img = {
|
|
68
|
+
'bounding_box': [],
|
|
69
|
+
'keypoint': [],
|
|
70
|
+
'relation': [],
|
|
71
|
+
'group': [],
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
# Handle bounding_box
|
|
75
|
+
bbox_ids = []
|
|
76
|
+
for ann in anns:
|
|
77
|
+
cat = cat_map.get(ann['category_id'], {})
|
|
78
|
+
if 'bbox' in ann and ann['bbox']:
|
|
79
|
+
bbox_id = self._generate_unique_id()
|
|
80
|
+
bbox_ids.append(bbox_id)
|
|
81
|
+
dm_img['bounding_box'].append({
|
|
82
|
+
'id': bbox_id,
|
|
83
|
+
'classification': cat.get('name', str(ann['category_id'])),
|
|
84
|
+
'attrs': ann.get('attrs', []),
|
|
85
|
+
'data': list(ann['bbox']),
|
|
86
|
+
})
|
|
87
|
+
|
|
88
|
+
# Handle keypoints
|
|
89
|
+
for ann in anns:
|
|
90
|
+
cat = cat_map.get(ann['category_id'], {})
|
|
91
|
+
attrs = ann.get('attrs', [])
|
|
92
|
+
if 'keypoints' in ann and ann['keypoints']:
|
|
93
|
+
kp_names = cat.get('keypoints', [])
|
|
94
|
+
kps = ann['keypoints']
|
|
95
|
+
keypoint_ids = []
|
|
96
|
+
for idx in range(min(len(kps) // 3, len(kp_names))):
|
|
97
|
+
x, y, v = kps[idx * 3 : idx * 3 + 3]
|
|
98
|
+
kp_id = self._generate_unique_id()
|
|
99
|
+
keypoint_ids.append(kp_id)
|
|
100
|
+
dm_img['keypoint'].append({
|
|
101
|
+
'id': kp_id,
|
|
102
|
+
'classification': kp_names[idx] if idx < len(kp_names) else f'keypoint_{idx}',
|
|
103
|
+
'attrs': attrs,
|
|
104
|
+
'data': [x, y],
|
|
105
|
+
})
|
|
106
|
+
group_ids = bbox_ids + keypoint_ids
|
|
107
|
+
if group_ids:
|
|
108
|
+
dm_img['group'].append({
|
|
109
|
+
'id': self._generate_unique_id(),
|
|
110
|
+
'classification': cat.get('name', str(ann['category_id'])),
|
|
111
|
+
'attrs': attrs,
|
|
112
|
+
'data': group_ids,
|
|
113
|
+
})
|
|
114
|
+
|
|
115
|
+
dm_json = {'images': [dm_img]}
|
|
116
|
+
result[img_filename] = (dm_json, img_path)
|
|
117
|
+
return result
|
|
118
|
+
|
|
119
|
+
def convert_single_file(self, data: Dict[str, Any], original_file: IO, original_image_name: str) -> Dict[str, Any]:
|
|
120
|
+
"""Convert a single COCO annotation data and corresponding image to DM format.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
data: COCO format data dictionary (JSON content)
|
|
124
|
+
original_file: File object for the corresponding original image
|
|
125
|
+
original_image_name: Original image name
|
|
126
|
+
|
|
127
|
+
Returns:
|
|
128
|
+
Dictionary containing DM format data for the single file
|
|
129
|
+
"""
|
|
130
|
+
if not self.is_single_conversion:
|
|
131
|
+
raise RuntimeError('convert_single_file is only available when is_single_conversion=True')
|
|
132
|
+
|
|
133
|
+
images = data.get('images', [])
|
|
134
|
+
annotations = data.get('annotations', [])
|
|
135
|
+
categories = data.get('categories', [])
|
|
136
|
+
|
|
137
|
+
if not images:
|
|
138
|
+
raise ValueError('No images found in COCO data')
|
|
139
|
+
|
|
140
|
+
# Get file name from original_file
|
|
141
|
+
img_path = getattr(original_file, 'name', None)
|
|
142
|
+
if not img_path:
|
|
143
|
+
raise ValueError('original_file must have a "name" attribute representing its path or filename.')
|
|
144
|
+
img_basename = os.path.basename(img_path)
|
|
145
|
+
|
|
146
|
+
# Find the matching image info in COCO 'images' section by comparing file name
|
|
147
|
+
# COCO image dicts might use 'file_name', 'filename', or similar
|
|
148
|
+
matched_img = None
|
|
149
|
+
for img in images:
|
|
150
|
+
for key in ['file_name', 'filename', 'name']:
|
|
151
|
+
if key in img and os.path.basename(img[key]) == original_image_name:
|
|
152
|
+
matched_img = img
|
|
153
|
+
break
|
|
154
|
+
if matched_img:
|
|
155
|
+
break
|
|
156
|
+
|
|
157
|
+
if not matched_img:
|
|
158
|
+
raise ValueError(f'No matching image found in COCO data for file: {img_basename}')
|
|
159
|
+
|
|
160
|
+
img_id = matched_img['id']
|
|
161
|
+
cat_map = {cat['id']: cat for cat in categories}
|
|
162
|
+
anns = [ann for ann in annotations if ann['image_id'] == img_id]
|
|
163
|
+
|
|
164
|
+
dm_img = {
|
|
165
|
+
'bounding_box': [],
|
|
166
|
+
'keypoint': [],
|
|
167
|
+
'relation': [],
|
|
168
|
+
'group': [],
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
bbox_ids = []
|
|
172
|
+
for ann in anns:
|
|
173
|
+
cat = cat_map.get(ann['category_id'], {})
|
|
174
|
+
if 'bbox' in ann and ann['bbox']:
|
|
175
|
+
bbox_id = self._generate_unique_id()
|
|
176
|
+
bbox_ids.append(bbox_id)
|
|
177
|
+
dm_img['bounding_box'].append({
|
|
178
|
+
'id': bbox_id,
|
|
179
|
+
'classification': cat.get('name', str(ann['category_id'])),
|
|
180
|
+
'attrs': ann.get('attrs', []),
|
|
181
|
+
'data': list(ann['bbox']),
|
|
182
|
+
})
|
|
183
|
+
|
|
184
|
+
for ann in anns:
|
|
185
|
+
cat = cat_map.get(ann['category_id'], {})
|
|
186
|
+
attrs = ann.get('attrs', [])
|
|
187
|
+
if 'keypoints' in ann and ann['keypoints']:
|
|
188
|
+
kp_names = cat.get('keypoints', [])
|
|
189
|
+
kps = ann['keypoints']
|
|
190
|
+
keypoint_ids = []
|
|
191
|
+
for idx in range(min(len(kps) // 3, len(kp_names))):
|
|
192
|
+
x, y, _ = kps[idx * 3 : idx * 3 + 3]
|
|
193
|
+
kp_id = self._generate_unique_id()
|
|
194
|
+
keypoint_ids.append(kp_id)
|
|
195
|
+
dm_img['keypoint'].append({
|
|
196
|
+
'id': kp_id,
|
|
197
|
+
'classification': kp_names[idx] if idx < len(kp_names) else f'keypoint_{idx}',
|
|
198
|
+
'attrs': attrs,
|
|
199
|
+
'data': [x, y],
|
|
200
|
+
})
|
|
201
|
+
group_ids = bbox_ids + keypoint_ids
|
|
202
|
+
if group_ids:
|
|
203
|
+
dm_img['group'].append({
|
|
204
|
+
'id': self._generate_unique_id(),
|
|
205
|
+
'classification': cat.get('name', str(ann['category_id'])),
|
|
206
|
+
'attrs': attrs,
|
|
207
|
+
'data': group_ids,
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
dm_json = {'images': [dm_img]}
|
|
211
|
+
return {
|
|
212
|
+
'dm_json': dm_json,
|
|
213
|
+
'image_path': img_path,
|
|
214
|
+
'image_name': img_basename,
|
|
215
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
"""
|
|
2
|
+
DM Schema V1/V2 Bidirectional Converter
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from .types import (
|
|
9
|
+
AnnotationMeta,
|
|
10
|
+
V2AnnotationData,
|
|
11
|
+
V2ConversionResult,
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def convert_v1_to_v2(v1_data: dict[str, Any]) -> V2ConversionResult:
|
|
16
|
+
"""Convert DM Schema V1 data to V2 (separated result)
|
|
17
|
+
|
|
18
|
+
Args:
|
|
19
|
+
v1_data: DM Schema V1 format data
|
|
20
|
+
|
|
21
|
+
Returns:
|
|
22
|
+
V2ConversionResult: Separated conversion result
|
|
23
|
+
- annotation_data: V2 common annotation structure
|
|
24
|
+
- annotation_meta: Preserved V1 top-level structure
|
|
25
|
+
"""
|
|
26
|
+
from .from_v1 import DMV1ToV2Converter
|
|
27
|
+
|
|
28
|
+
converter = DMV1ToV2Converter()
|
|
29
|
+
return converter.convert(v1_data)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def convert_v2_to_v1(
|
|
33
|
+
v2_data: V2ConversionResult | dict[str, Any],
|
|
34
|
+
annotation_meta: AnnotationMeta | None = None,
|
|
35
|
+
) -> dict[str, Any]:
|
|
36
|
+
"""Convert DM Schema V2 data to V1
|
|
37
|
+
|
|
38
|
+
Args:
|
|
39
|
+
v2_data: DM Schema V2 format data
|
|
40
|
+
annotation_meta: Optional V1 top-level structure passed separately
|
|
41
|
+
|
|
42
|
+
Returns:
|
|
43
|
+
DM Schema V1 format data
|
|
44
|
+
"""
|
|
45
|
+
from .to_v1 import DMV2ToV1Converter
|
|
46
|
+
|
|
47
|
+
converter = DMV2ToV1Converter()
|
|
48
|
+
return converter.convert(v2_data, annotation_meta)
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
__all__ = [
|
|
52
|
+
'convert_v1_to_v2',
|
|
53
|
+
'convert_v2_to_v1',
|
|
54
|
+
'V2ConversionResult',
|
|
55
|
+
'V2AnnotationData',
|
|
56
|
+
'AnnotationMeta',
|
|
57
|
+
]
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
"""
|
|
2
|
+
DM Schema V1/V2 Converter Base Class
|
|
3
|
+
|
|
4
|
+
Created: 2025-12-11
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from abc import ABC, abstractmethod
|
|
8
|
+
from typing import TYPE_CHECKING, Any
|
|
9
|
+
|
|
10
|
+
from .types import MEDIA_TYPE_MAP, SUPPORTED_FILE_TYPES
|
|
11
|
+
from .utils import detect_file_type, extract_media_type_info
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from .tools import ToolProcessor
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class BaseDMConverter(ABC):
|
|
18
|
+
"""DM Schema Converter Base Class
|
|
19
|
+
|
|
20
|
+
Abstract base class for all DM converters.
|
|
21
|
+
|
|
22
|
+
Attributes:
|
|
23
|
+
file_type: File type to process (None for auto-detection)
|
|
24
|
+
SUPPORTED_FILE_TYPES: Tuple of supported file types
|
|
25
|
+
MEDIA_TYPE_MAP: Media type mapping dictionary
|
|
26
|
+
|
|
27
|
+
Example:
|
|
28
|
+
>>> class MyConverter(BaseDMConverter):
|
|
29
|
+
... def convert(self, data):
|
|
30
|
+
... # implementation
|
|
31
|
+
... pass
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
SUPPORTED_FILE_TYPES = SUPPORTED_FILE_TYPES
|
|
35
|
+
MEDIA_TYPE_MAP = MEDIA_TYPE_MAP
|
|
36
|
+
|
|
37
|
+
def __init__(self, file_type: str | None = None) -> None:
|
|
38
|
+
"""
|
|
39
|
+
Args:
|
|
40
|
+
file_type: File type to process (None for auto-detection)
|
|
41
|
+
|
|
42
|
+
Raises:
|
|
43
|
+
ValueError: Unsupported file type
|
|
44
|
+
"""
|
|
45
|
+
if file_type is not None and file_type not in self.SUPPORTED_FILE_TYPES:
|
|
46
|
+
raise ValueError(
|
|
47
|
+
f'Unsupported file type: {file_type}. Supported types: {", ".join(self.SUPPORTED_FILE_TYPES)}'
|
|
48
|
+
)
|
|
49
|
+
self.file_type = file_type
|
|
50
|
+
self._tool_processors: dict[str, 'ToolProcessor'] = {}
|
|
51
|
+
self._setup_tool_processors()
|
|
52
|
+
|
|
53
|
+
@abstractmethod
|
|
54
|
+
def _setup_tool_processors(self) -> None:
|
|
55
|
+
"""Register tool processors
|
|
56
|
+
|
|
57
|
+
Subclasses implement this to register supported tool processors.
|
|
58
|
+
|
|
59
|
+
Example:
|
|
60
|
+
>>> def _setup_tool_processors(self):
|
|
61
|
+
... from .tools import BoundingBoxProcessor, PolygonProcessor
|
|
62
|
+
... self.register_processor(BoundingBoxProcessor())
|
|
63
|
+
... self.register_processor(PolygonProcessor())
|
|
64
|
+
"""
|
|
65
|
+
...
|
|
66
|
+
|
|
67
|
+
def register_processor(self, processor: 'ToolProcessor') -> None:
|
|
68
|
+
"""Register a tool processor
|
|
69
|
+
|
|
70
|
+
Use this method to register processors when adding new tool support.
|
|
71
|
+
Allows extension without modifying existing code (AR-001).
|
|
72
|
+
|
|
73
|
+
Args:
|
|
74
|
+
processor: ToolProcessor implementation
|
|
75
|
+
|
|
76
|
+
Example:
|
|
77
|
+
>>> class KeypointProcessor:
|
|
78
|
+
... tool_name = "keypoint"
|
|
79
|
+
... def to_v2(self, v1_annotation, v1_data): ...
|
|
80
|
+
... def to_v1(self, v2_annotation): ...
|
|
81
|
+
>>> converter.register_processor(KeypointProcessor())
|
|
82
|
+
"""
|
|
83
|
+
self._tool_processors[processor.tool_name] = processor
|
|
84
|
+
|
|
85
|
+
def get_processor(self, tool_name: str) -> 'ToolProcessor | None':
|
|
86
|
+
"""Get a registered tool processor
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
tool_name: Tool name (e.g., 'bounding_box', 'polygon')
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
Registered processor or None
|
|
93
|
+
"""
|
|
94
|
+
return self._tool_processors.get(tool_name)
|
|
95
|
+
|
|
96
|
+
@abstractmethod
|
|
97
|
+
def convert(self, data: dict[str, Any]) -> dict[str, Any]:
|
|
98
|
+
"""Perform data conversion
|
|
99
|
+
|
|
100
|
+
Args:
|
|
101
|
+
data: Input data (V1 or V2)
|
|
102
|
+
|
|
103
|
+
Returns:
|
|
104
|
+
Converted data (V2 or V1)
|
|
105
|
+
|
|
106
|
+
Raises:
|
|
107
|
+
ValueError: Data cannot be converted
|
|
108
|
+
"""
|
|
109
|
+
...
|
|
110
|
+
|
|
111
|
+
def _detect_file_type(self, data: dict[str, Any], is_v2: bool = False) -> str:
|
|
112
|
+
"""Auto-detect file type from data
|
|
113
|
+
|
|
114
|
+
Args:
|
|
115
|
+
data: Input data
|
|
116
|
+
is_v2: Whether the format is V2
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
Detected file type ('image', 'video', etc.)
|
|
120
|
+
|
|
121
|
+
Raises:
|
|
122
|
+
ValueError: Unable to detect file type
|
|
123
|
+
"""
|
|
124
|
+
if self.file_type:
|
|
125
|
+
return self.file_type
|
|
126
|
+
return detect_file_type(data, is_v2)
|
|
127
|
+
|
|
128
|
+
def _extract_media_type_info(self, media_id: str) -> tuple[str, str]:
|
|
129
|
+
"""Extract type information from media ID
|
|
130
|
+
|
|
131
|
+
Args:
|
|
132
|
+
media_id: Media ID (e.g., 'image_1', 'video_2')
|
|
133
|
+
|
|
134
|
+
Returns:
|
|
135
|
+
(singular, plural) tuple (e.g., ('image', 'images'))
|
|
136
|
+
"""
|
|
137
|
+
return extract_media_type_info(media_id)
|
|
@@ -0,0 +1,273 @@
|
|
|
1
|
+
"""
|
|
2
|
+
DM Schema V1 → V2 Converter
|
|
3
|
+
|
|
4
|
+
Created: 2025-12-11
|
|
5
|
+
|
|
6
|
+
V1→V2 conversion separates the result into annotation_data and annotation_meta.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import Any
|
|
10
|
+
|
|
11
|
+
from .base import BaseDMConverter
|
|
12
|
+
from .types import (
|
|
13
|
+
AnnotationMeta,
|
|
14
|
+
V2AnnotationData,
|
|
15
|
+
V2ConversionResult,
|
|
16
|
+
)
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
class DMV1ToV2Converter(BaseDMConverter):
|
|
20
|
+
"""Converter from DM Schema V1 to V2
|
|
21
|
+
|
|
22
|
+
V1→V2 conversion separates the result into annotation_data and annotation_meta.
|
|
23
|
+
|
|
24
|
+
Example:
|
|
25
|
+
>>> converter = DMV1ToV2Converter()
|
|
26
|
+
>>> result = converter.convert(v1_data)
|
|
27
|
+
>>> annotation_data = result["annotation_data"]
|
|
28
|
+
>>> annotation_meta = result["annotation_meta"]
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def _setup_tool_processors(self) -> None:
|
|
32
|
+
"""Register tool processors"""
|
|
33
|
+
from .tools.bounding_box import BoundingBoxProcessor
|
|
34
|
+
|
|
35
|
+
self.register_processor(BoundingBoxProcessor())
|
|
36
|
+
|
|
37
|
+
# polygon to be added later
|
|
38
|
+
try:
|
|
39
|
+
from .tools.polygon import PolygonProcessor
|
|
40
|
+
|
|
41
|
+
self.register_processor(PolygonProcessor())
|
|
42
|
+
except ImportError:
|
|
43
|
+
pass
|
|
44
|
+
|
|
45
|
+
try:
|
|
46
|
+
from .tools.polyline import PolylineProcessor
|
|
47
|
+
|
|
48
|
+
self.register_processor(PolylineProcessor())
|
|
49
|
+
except ImportError:
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
from .tools.keypoint import KeypointProcessor
|
|
54
|
+
|
|
55
|
+
self.register_processor(KeypointProcessor())
|
|
56
|
+
except ImportError:
|
|
57
|
+
pass
|
|
58
|
+
|
|
59
|
+
try:
|
|
60
|
+
from .tools.bounding_box_3d import BoundingBox3DProcessor
|
|
61
|
+
|
|
62
|
+
self.register_processor(BoundingBox3DProcessor())
|
|
63
|
+
except ImportError:
|
|
64
|
+
pass
|
|
65
|
+
|
|
66
|
+
try:
|
|
67
|
+
from .tools.segmentation import SegmentationProcessor
|
|
68
|
+
|
|
69
|
+
self.register_processor(SegmentationProcessor())
|
|
70
|
+
except ImportError:
|
|
71
|
+
pass
|
|
72
|
+
|
|
73
|
+
try:
|
|
74
|
+
from .tools.named_entity import NamedEntityProcessor
|
|
75
|
+
|
|
76
|
+
self.register_processor(NamedEntityProcessor())
|
|
77
|
+
except ImportError:
|
|
78
|
+
pass
|
|
79
|
+
|
|
80
|
+
try:
|
|
81
|
+
from .tools.segmentation_3d import Segmentation3DProcessor
|
|
82
|
+
|
|
83
|
+
self.register_processor(Segmentation3DProcessor())
|
|
84
|
+
except ImportError:
|
|
85
|
+
pass
|
|
86
|
+
|
|
87
|
+
try:
|
|
88
|
+
from .tools.classification import ClassificationProcessor
|
|
89
|
+
|
|
90
|
+
self.register_processor(ClassificationProcessor())
|
|
91
|
+
except ImportError:
|
|
92
|
+
pass
|
|
93
|
+
|
|
94
|
+
try:
|
|
95
|
+
from .tools.relation import RelationProcessor
|
|
96
|
+
|
|
97
|
+
self.register_processor(RelationProcessor())
|
|
98
|
+
except ImportError:
|
|
99
|
+
pass
|
|
100
|
+
|
|
101
|
+
try:
|
|
102
|
+
from .tools.prompt import PromptProcessor
|
|
103
|
+
|
|
104
|
+
self.register_processor(PromptProcessor())
|
|
105
|
+
except ImportError:
|
|
106
|
+
pass
|
|
107
|
+
|
|
108
|
+
try:
|
|
109
|
+
from .tools.answer import AnswerProcessor
|
|
110
|
+
|
|
111
|
+
self.register_processor(AnswerProcessor())
|
|
112
|
+
except ImportError:
|
|
113
|
+
pass
|
|
114
|
+
|
|
115
|
+
def convert(self, v1_data: dict[str, Any]) -> V2ConversionResult:
|
|
116
|
+
"""Convert V1 data to V2 format (separated result)
|
|
117
|
+
|
|
118
|
+
Args:
|
|
119
|
+
v1_data: DM Schema V1 format data
|
|
120
|
+
|
|
121
|
+
Returns:
|
|
122
|
+
V2ConversionResult: Separated conversion result
|
|
123
|
+
- annotation_data: V2 common annotation structure
|
|
124
|
+
- annotation_meta: Preserved V1 top-level structure
|
|
125
|
+
|
|
126
|
+
Raises:
|
|
127
|
+
ValueError: Missing required fields or invalid format
|
|
128
|
+
"""
|
|
129
|
+
# Input validation
|
|
130
|
+
if 'annotations' not in v1_data:
|
|
131
|
+
raise ValueError("V1 data requires 'annotations' field")
|
|
132
|
+
if 'annotationsData' not in v1_data:
|
|
133
|
+
raise ValueError("V1 data requires 'annotationsData' field")
|
|
134
|
+
|
|
135
|
+
# Create annotation_data
|
|
136
|
+
annotation_data = self._build_annotation_data(v1_data)
|
|
137
|
+
|
|
138
|
+
# Create annotation_meta (preserve V1 top-level structure)
|
|
139
|
+
annotation_meta = self._build_annotation_meta(v1_data)
|
|
140
|
+
|
|
141
|
+
return {
|
|
142
|
+
'annotation_data': annotation_data,
|
|
143
|
+
'annotation_meta': annotation_meta,
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
def _build_annotation_data(self, v1_data: dict[str, Any]) -> V2AnnotationData:
|
|
147
|
+
"""Create annotation_data (V2 common structure) from V1 data
|
|
148
|
+
|
|
149
|
+
Args:
|
|
150
|
+
v1_data: V1 data
|
|
151
|
+
|
|
152
|
+
Returns:
|
|
153
|
+
V2 common annotation structure
|
|
154
|
+
"""
|
|
155
|
+
annotations = v1_data.get('annotations', {})
|
|
156
|
+
annotations_data = v1_data.get('annotationsData', {})
|
|
157
|
+
|
|
158
|
+
# Build classification map
|
|
159
|
+
classification_map = self._build_classification_map(annotations)
|
|
160
|
+
|
|
161
|
+
# Convert annotations by media type
|
|
162
|
+
result: V2AnnotationData = {
|
|
163
|
+
'classification': classification_map,
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
# Process by media ID
|
|
167
|
+
for media_id, ann_list in annotations.items():
|
|
168
|
+
# Detect media type
|
|
169
|
+
singular_type, plural_type = self._extract_media_type_info(media_id)
|
|
170
|
+
|
|
171
|
+
# Initialize media type array
|
|
172
|
+
if plural_type not in result:
|
|
173
|
+
result[plural_type] = []
|
|
174
|
+
|
|
175
|
+
# Convert media item
|
|
176
|
+
media_item = self._convert_media_item(media_id, ann_list, annotations_data.get(media_id, []))
|
|
177
|
+
|
|
178
|
+
result[plural_type].append(media_item)
|
|
179
|
+
|
|
180
|
+
return result
|
|
181
|
+
|
|
182
|
+
def _build_annotation_meta(self, v1_data: dict[str, Any]) -> AnnotationMeta:
|
|
183
|
+
"""Create annotation_meta (V1 top-level structure) from V1 data
|
|
184
|
+
|
|
185
|
+
Args:
|
|
186
|
+
v1_data: Complete V1 data
|
|
187
|
+
|
|
188
|
+
Returns:
|
|
189
|
+
V1 top-level structure (preserved as-is)
|
|
190
|
+
"""
|
|
191
|
+
return {
|
|
192
|
+
'extra': v1_data.get('extra', {}),
|
|
193
|
+
'annotations': v1_data.get('annotations', {}),
|
|
194
|
+
'annotationsData': v1_data.get('annotationsData', {}),
|
|
195
|
+
'relations': v1_data.get('relations', {}),
|
|
196
|
+
'annotationGroups': v1_data.get('annotationGroups', {}),
|
|
197
|
+
'assignmentId': v1_data.get('assignmentId'),
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
def _build_classification_map(self, annotations: dict[str, list[dict[str, Any]]]) -> dict[str, list[str]]:
|
|
201
|
+
"""Build classification map from annotations
|
|
202
|
+
|
|
203
|
+
Args:
|
|
204
|
+
annotations: V1 annotations data
|
|
205
|
+
|
|
206
|
+
Returns:
|
|
207
|
+
Class label map by tool
|
|
208
|
+
e.g., {"bounding_box": ["person", "car"], "polygon": ["road"]}
|
|
209
|
+
"""
|
|
210
|
+
classification_map: dict[str, set[str]] = {}
|
|
211
|
+
|
|
212
|
+
for media_id, ann_list in annotations.items():
|
|
213
|
+
for ann in ann_list:
|
|
214
|
+
tool = ann.get('tool', '')
|
|
215
|
+
classification_obj = ann.get('classification') or {}
|
|
216
|
+
class_label = classification_obj.get('class', '')
|
|
217
|
+
|
|
218
|
+
if tool and class_label:
|
|
219
|
+
if tool not in classification_map:
|
|
220
|
+
classification_map[tool] = set()
|
|
221
|
+
classification_map[tool].add(class_label)
|
|
222
|
+
|
|
223
|
+
# Convert set to list
|
|
224
|
+
return {tool: sorted(list(labels)) for tool, labels in classification_map.items()}
|
|
225
|
+
|
|
226
|
+
def _convert_media_item(
|
|
227
|
+
self,
|
|
228
|
+
media_id: str,
|
|
229
|
+
annotations: list[dict[str, Any]],
|
|
230
|
+
annotations_data: list[dict[str, Any]],
|
|
231
|
+
) -> dict[str, list[dict[str, Any]]]:
|
|
232
|
+
"""Convert annotations for a single media item
|
|
233
|
+
|
|
234
|
+
Args:
|
|
235
|
+
media_id: Media ID
|
|
236
|
+
annotations: V1 annotations for this media
|
|
237
|
+
annotations_data: V1 annotationsData for this media
|
|
238
|
+
|
|
239
|
+
Returns:
|
|
240
|
+
V2 annotations grouped by tool
|
|
241
|
+
"""
|
|
242
|
+
# Create ID → annotationData mapping
|
|
243
|
+
data_by_id = {item['id']: item for item in annotations_data if 'id' in item}
|
|
244
|
+
|
|
245
|
+
# Group by tool
|
|
246
|
+
result: dict[str, list[dict[str, Any]]] = {}
|
|
247
|
+
|
|
248
|
+
for ann in annotations:
|
|
249
|
+
ann_id = ann.get('id', '')
|
|
250
|
+
tool = ann.get('tool', '')
|
|
251
|
+
|
|
252
|
+
if not tool:
|
|
253
|
+
continue
|
|
254
|
+
|
|
255
|
+
# Get processor
|
|
256
|
+
processor = self.get_processor(tool)
|
|
257
|
+
if not processor:
|
|
258
|
+
# Raise error for unsupported tool
|
|
259
|
+
supported_tools = list(self._tool_processors.keys())
|
|
260
|
+
raise ValueError(f"Unsupported tool: '{tool}'. Supported tools: {', '.join(sorted(supported_tools))}")
|
|
261
|
+
|
|
262
|
+
# Find annotationData for this ID
|
|
263
|
+
ann_data = data_by_id.get(ann_id, {})
|
|
264
|
+
|
|
265
|
+
# Convert to V2
|
|
266
|
+
v2_annotation = processor.to_v2(ann, ann_data)
|
|
267
|
+
|
|
268
|
+
# Group by tool
|
|
269
|
+
if tool not in result:
|
|
270
|
+
result[tool] = []
|
|
271
|
+
result[tool].append(v2_annotation)
|
|
272
|
+
|
|
273
|
+
return result
|