synapse-sdk 2025.10.3__py3-none-any.whl → 2025.10.4__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.
Potentially problematic release.
This version of synapse-sdk might be problematic. Click here for more details.
- synapse_sdk/devtools/docs/docs/plugins/categories/pre-annotation-plugins/pre-annotation-plugin-overview.md +198 -0
- synapse_sdk/devtools/docs/docs/plugins/categories/pre-annotation-plugins/to-task-action-development.md +1645 -0
- synapse_sdk/devtools/docs/docs/plugins/categories/pre-annotation-plugins/to-task-overview.md +717 -0
- synapse_sdk/devtools/docs/docs/plugins/categories/pre-annotation-plugins/to-task-template-development.md +1380 -0
- synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/plugins/categories/pre-annotation-plugins/pre-annotation-plugin-overview.md +198 -0
- synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/plugins/categories/pre-annotation-plugins/to-task-action-development.md +1645 -0
- synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/plugins/categories/pre-annotation-plugins/to-task-overview.md +717 -0
- synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/plugins/categories/pre-annotation-plugins/to-task-template-development.md +1380 -0
- synapse_sdk/devtools/docs/sidebars.ts +14 -0
- synapse_sdk/plugins/categories/export/actions/export/action.py +8 -3
- synapse_sdk/plugins/categories/export/actions/export/utils.py +108 -8
- {synapse_sdk-2025.10.3.dist-info → synapse_sdk-2025.10.4.dist-info}/METADATA +1 -1
- {synapse_sdk-2025.10.3.dist-info → synapse_sdk-2025.10.4.dist-info}/RECORD +17 -9
- {synapse_sdk-2025.10.3.dist-info → synapse_sdk-2025.10.4.dist-info}/WHEEL +0 -0
- {synapse_sdk-2025.10.3.dist-info → synapse_sdk-2025.10.4.dist-info}/entry_points.txt +0 -0
- {synapse_sdk-2025.10.3.dist-info → synapse_sdk-2025.10.4.dist-info}/licenses/LICENSE +0 -0
- {synapse_sdk-2025.10.3.dist-info → synapse_sdk-2025.10.4.dist-info}/top_level.txt +0 -0
|
@@ -0,0 +1,1380 @@
|
|
|
1
|
+
---
|
|
2
|
+
id: to-task-template-development
|
|
3
|
+
title: ToTask Template Development with AnnotationToTask
|
|
4
|
+
sidebar_position: 4
|
|
5
|
+
---
|
|
6
|
+
|
|
7
|
+
# ToTask Template Development with AnnotationToTask
|
|
8
|
+
|
|
9
|
+
This guide is for plugin developers who want to create custom pre-annotation plugins using the `AnnotationToTask` template. The AnnotationToTask template provides a simple interface for converting data to task annotations in Synapse projects.
|
|
10
|
+
|
|
11
|
+
## Overview
|
|
12
|
+
|
|
13
|
+
The `AnnotationToTask` template (`synapse_sdk.plugins.categories.pre_annotation.templates.plugin.to_task`) provides a structured approach to building pre-annotation plugins. It handles the workflow integration while you focus on implementing custom data conversion logic.
|
|
14
|
+
|
|
15
|
+
### What is AnnotationToTask?
|
|
16
|
+
|
|
17
|
+
`AnnotationToTask` is a template class that defines two key conversion methods:
|
|
18
|
+
- **`convert_data_from_file()`**: Convert JSON data from files into task annotations
|
|
19
|
+
- **`convert_data_from_inference()`**: Convert model inference results into task annotations
|
|
20
|
+
|
|
21
|
+
The ToTaskAction framework automatically calls these methods during the annotation workflow, allowing you to customize how data is transformed into task objects.
|
|
22
|
+
|
|
23
|
+
### When to Use This Template
|
|
24
|
+
|
|
25
|
+
Use the AnnotationToTask template when you need to:
|
|
26
|
+
- Transform external annotation data into Synapse task format
|
|
27
|
+
- Convert model predictions to task annotations
|
|
28
|
+
- Implement custom data validation and transformation logic
|
|
29
|
+
- Create reusable annotation conversion plugins
|
|
30
|
+
|
|
31
|
+
## Getting Started
|
|
32
|
+
|
|
33
|
+
### Template Structure
|
|
34
|
+
|
|
35
|
+
When you create a pre-annotation plugin using the ToTask template, you get this structure:
|
|
36
|
+
|
|
37
|
+
```
|
|
38
|
+
synapse-{plugin-code}-plugin/
|
|
39
|
+
├── config.yaml # Plugin metadata and configuration
|
|
40
|
+
├── plugin/ # Source code directory
|
|
41
|
+
│ ├── __init__.py
|
|
42
|
+
│ └── to_task.py # AnnotationToTask implementation
|
|
43
|
+
├── requirements.txt # Python dependencies
|
|
44
|
+
├── pyproject.toml # Package configuration
|
|
45
|
+
└── README.md # Plugin documentation
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
### Basic Plugin Implementation
|
|
49
|
+
|
|
50
|
+
```python
|
|
51
|
+
# plugin/to_task.py
|
|
52
|
+
class AnnotationToTask:
|
|
53
|
+
"""Template for custom annotation conversion logic."""
|
|
54
|
+
|
|
55
|
+
def __init__(self, run, *args, **kwargs):
|
|
56
|
+
"""Initialize the plugin task pre annotation action class.
|
|
57
|
+
|
|
58
|
+
Args:
|
|
59
|
+
run: Plugin run object providing logging and context.
|
|
60
|
+
"""
|
|
61
|
+
self.run = run
|
|
62
|
+
|
|
63
|
+
def convert_data_from_file(
|
|
64
|
+
self,
|
|
65
|
+
primary_file_url: str,
|
|
66
|
+
primary_file_original_name: str,
|
|
67
|
+
data_file_url: str,
|
|
68
|
+
data_file_original_name: str,
|
|
69
|
+
) -> dict:
|
|
70
|
+
"""Convert data from a file to a task object.
|
|
71
|
+
|
|
72
|
+
Args:
|
|
73
|
+
primary_file_url: URL of the primary file (e.g., image being annotated)
|
|
74
|
+
primary_file_original_name: Original name of primary file
|
|
75
|
+
data_file_url: URL of the annotation data file (JSON)
|
|
76
|
+
data_file_original_name: Original name of annotation file
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
dict: Task object with annotations in Synapse format
|
|
80
|
+
"""
|
|
81
|
+
# Your custom implementation here
|
|
82
|
+
converted_data = {}
|
|
83
|
+
return converted_data
|
|
84
|
+
|
|
85
|
+
def convert_data_from_inference(self, data: dict) -> dict:
|
|
86
|
+
"""Convert data from inference result to a task object.
|
|
87
|
+
|
|
88
|
+
Args:
|
|
89
|
+
data: Raw inference results from pre-processor
|
|
90
|
+
|
|
91
|
+
Returns:
|
|
92
|
+
dict: Task object with annotations in Synapse format
|
|
93
|
+
"""
|
|
94
|
+
# Your custom implementation here
|
|
95
|
+
return data
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
## AnnotationToTask Class Reference
|
|
99
|
+
|
|
100
|
+
### Constructor
|
|
101
|
+
|
|
102
|
+
```python
|
|
103
|
+
def __init__(self, run, *args, **kwargs):
|
|
104
|
+
```
|
|
105
|
+
|
|
106
|
+
**Parameters:**
|
|
107
|
+
- `run`: Plugin run object providing logging and context access
|
|
108
|
+
- Use `self.run.log_message(msg)` for logging
|
|
109
|
+
- Access configuration via `self.run.params`
|
|
110
|
+
|
|
111
|
+
**Usage:**
|
|
112
|
+
```python
|
|
113
|
+
def __init__(self, run, *args, **kwargs):
|
|
114
|
+
self.run = run
|
|
115
|
+
# Initialize any custom attributes
|
|
116
|
+
self.confidence_threshold = 0.8
|
|
117
|
+
self.custom_mapping = {}
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
### Method: convert_data_from_file()
|
|
121
|
+
|
|
122
|
+
Converts annotation data from a JSON file into Synapse task object format.
|
|
123
|
+
|
|
124
|
+
```python
|
|
125
|
+
def convert_data_from_file(
|
|
126
|
+
self,
|
|
127
|
+
primary_file_url: str,
|
|
128
|
+
primary_file_original_name: str,
|
|
129
|
+
data_file_url: str,
|
|
130
|
+
data_file_original_name: str,
|
|
131
|
+
) -> dict:
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
**Parameters:**
|
|
135
|
+
- `primary_file_url` (str): HTTP/HTTPS URL of the primary file (e.g., the image being annotated)
|
|
136
|
+
- `primary_file_original_name` (str): Original filename of the primary file
|
|
137
|
+
- `data_file_url` (str): HTTP/HTTPS URL of the annotation JSON file
|
|
138
|
+
- `data_file_original_name` (str): Original filename of the annotation file
|
|
139
|
+
|
|
140
|
+
**Returns:**
|
|
141
|
+
- `dict`: Task object containing annotations in Synapse format
|
|
142
|
+
|
|
143
|
+
**Called By:**
|
|
144
|
+
- `FileAnnotationStrategy` during file-based annotation workflow
|
|
145
|
+
|
|
146
|
+
**Workflow:**
|
|
147
|
+
1. Download JSON data from `data_file_url`
|
|
148
|
+
2. Parse and validate the JSON structure
|
|
149
|
+
3. Transform data to match Synapse task object schema
|
|
150
|
+
4. Return formatted task object
|
|
151
|
+
|
|
152
|
+
**Example Implementation:**
|
|
153
|
+
|
|
154
|
+
```python
|
|
155
|
+
import requests
|
|
156
|
+
import json
|
|
157
|
+
|
|
158
|
+
def convert_data_from_file(
|
|
159
|
+
self,
|
|
160
|
+
primary_file_url: str,
|
|
161
|
+
primary_file_original_name: str,
|
|
162
|
+
data_file_url: str,
|
|
163
|
+
data_file_original_name: str,
|
|
164
|
+
) -> dict:
|
|
165
|
+
"""Convert COCO format annotations to Synapse task format."""
|
|
166
|
+
|
|
167
|
+
# Download annotation file
|
|
168
|
+
response = requests.get(data_file_url, timeout=30)
|
|
169
|
+
response.raise_for_status()
|
|
170
|
+
coco_data = response.json()
|
|
171
|
+
|
|
172
|
+
# Extract annotations
|
|
173
|
+
annotations = coco_data.get('annotations', [])
|
|
174
|
+
|
|
175
|
+
# Convert to Synapse format
|
|
176
|
+
task_objects = []
|
|
177
|
+
for idx, ann in enumerate(annotations):
|
|
178
|
+
task_object = {
|
|
179
|
+
'id': f'obj_{idx}',
|
|
180
|
+
'class_id': ann['category_id'],
|
|
181
|
+
'type': 'bbox',
|
|
182
|
+
'coordinates': {
|
|
183
|
+
'x': ann['bbox'][0],
|
|
184
|
+
'y': ann['bbox'][1],
|
|
185
|
+
'width': ann['bbox'][2],
|
|
186
|
+
'height': ann['bbox'][3]
|
|
187
|
+
},
|
|
188
|
+
'properties': {
|
|
189
|
+
'area': ann.get('area', 0),
|
|
190
|
+
'iscrowd': ann.get('iscrowd', 0)
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
task_objects.append(task_object)
|
|
194
|
+
|
|
195
|
+
# Log conversion info
|
|
196
|
+
self.run.log_message(
|
|
197
|
+
f'Converted {len(task_objects)} COCO annotations from {data_file_original_name}'
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
return {'objects': task_objects}
|
|
201
|
+
```
|
|
202
|
+
|
|
203
|
+
### Method: convert_data_from_inference()
|
|
204
|
+
|
|
205
|
+
Converts model inference results into Synapse task object format.
|
|
206
|
+
|
|
207
|
+
```python
|
|
208
|
+
def convert_data_from_inference(self, data: dict) -> dict:
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
**Parameters:**
|
|
212
|
+
- `data` (dict): Raw inference results from the pre-processor plugin
|
|
213
|
+
|
|
214
|
+
**Returns:**
|
|
215
|
+
- `dict`: Task object containing annotations in Synapse format
|
|
216
|
+
|
|
217
|
+
**Called By:**
|
|
218
|
+
- `InferenceAnnotationStrategy` during inference-based annotation workflow
|
|
219
|
+
|
|
220
|
+
**Workflow:**
|
|
221
|
+
1. Receive inference results from pre-processor
|
|
222
|
+
2. Extract predictions, bounding boxes, classes, etc.
|
|
223
|
+
3. Transform to Synapse task object schema
|
|
224
|
+
4. Apply any filtering or post-processing
|
|
225
|
+
5. Return formatted task object
|
|
226
|
+
|
|
227
|
+
**Example Implementation:**
|
|
228
|
+
|
|
229
|
+
```python
|
|
230
|
+
def convert_data_from_inference(self, data: dict) -> dict:
|
|
231
|
+
"""Convert YOLOv8 detection results to Synapse task format."""
|
|
232
|
+
|
|
233
|
+
# Extract detections from inference results
|
|
234
|
+
detections = data.get('detections', [])
|
|
235
|
+
|
|
236
|
+
# Filter by confidence threshold
|
|
237
|
+
confidence_threshold = 0.5
|
|
238
|
+
task_objects = []
|
|
239
|
+
|
|
240
|
+
for idx, det in enumerate(detections):
|
|
241
|
+
confidence = det.get('confidence', 0)
|
|
242
|
+
|
|
243
|
+
# Skip low-confidence detections
|
|
244
|
+
if confidence < confidence_threshold:
|
|
245
|
+
continue
|
|
246
|
+
|
|
247
|
+
# Convert to Synapse format
|
|
248
|
+
task_object = {
|
|
249
|
+
'id': f'det_{idx}',
|
|
250
|
+
'class_id': det['class_id'],
|
|
251
|
+
'type': 'bbox',
|
|
252
|
+
'coordinates': {
|
|
253
|
+
'x': det['bbox']['x'],
|
|
254
|
+
'y': det['bbox']['y'],
|
|
255
|
+
'width': det['bbox']['width'],
|
|
256
|
+
'height': det['bbox']['height']
|
|
257
|
+
},
|
|
258
|
+
'properties': {
|
|
259
|
+
'confidence': confidence,
|
|
260
|
+
'class_name': det.get('class_name', 'unknown')
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
task_objects.append(task_object)
|
|
264
|
+
|
|
265
|
+
# Log conversion info
|
|
266
|
+
self.run.log_message(
|
|
267
|
+
f'Converted {len(task_objects)} detections '
|
|
268
|
+
f'(filtered from {len(detections)} total)'
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
return {'objects': task_objects}
|
|
272
|
+
```
|
|
273
|
+
|
|
274
|
+
## Using SDK Data Converters
|
|
275
|
+
|
|
276
|
+
The Synapse SDK provides built-in data converters that handle common annotation formats (COCO, YOLO, Pascal VOC). Instead of writing custom parsing logic, you can leverage these converters in your templates for faster development and better reliability.
|
|
277
|
+
|
|
278
|
+
### Why Use SDK Converters?
|
|
279
|
+
|
|
280
|
+
- **Proven & Tested**: Converters are maintained and tested by the SDK team
|
|
281
|
+
- **Standard Formats**: Support for COCO, YOLO, Pascal VOC out of the box
|
|
282
|
+
- **Less Code**: Avoid reimplementing format parsers
|
|
283
|
+
- **Consistent**: Same conversion logic across all plugins
|
|
284
|
+
- **Error Handling**: Built-in validation and error messages
|
|
285
|
+
|
|
286
|
+
### Available Converters
|
|
287
|
+
|
|
288
|
+
| Converter | Format | Direction | Module Path | Use Case |
|
|
289
|
+
|-----------|--------|-----------|-------------|----------|
|
|
290
|
+
| `COCOToDMConverter` | COCO JSON | External → DM | `synapse_sdk.utils.converters.coco` | COCO format annotations |
|
|
291
|
+
| `YOLOToDMConverter` | YOLO .txt | External → DM | `synapse_sdk.utils.converters.yolo` | YOLO format labels |
|
|
292
|
+
| `PascalToDMConverter` | Pascal VOC XML | External → DM | `synapse_sdk.utils.converters.pascal` | Pascal VOC annotations |
|
|
293
|
+
| `DMV2ToV1Converter` | DM v2 | DM v2 → DM v1 | `synapse_sdk.utils.converters.dm` | Version conversion |
|
|
294
|
+
| `DMV1ToV2Converter` | DM v1 | DM v1 → DM v2 | `synapse_sdk.utils.converters.dm` | Version conversion |
|
|
295
|
+
|
|
296
|
+
**DM Format**: Synapse's internal Data Manager format (what task objects use)
|
|
297
|
+
|
|
298
|
+
### Using Converters in Templates
|
|
299
|
+
|
|
300
|
+
All To-DM converters provide a `convert_single_file()` method specifically designed for template usage.
|
|
301
|
+
|
|
302
|
+
#### In convert_data_from_file()
|
|
303
|
+
|
|
304
|
+
```python
|
|
305
|
+
import requests
|
|
306
|
+
from synapse_sdk.utils.converters.coco import COCOToDMConverter
|
|
307
|
+
|
|
308
|
+
class AnnotationToTask:
|
|
309
|
+
def convert_data_from_file(
|
|
310
|
+
self,
|
|
311
|
+
primary_file_url: str,
|
|
312
|
+
primary_file_original_name: str,
|
|
313
|
+
data_file_url: str,
|
|
314
|
+
data_file_original_name: str,
|
|
315
|
+
) -> dict:
|
|
316
|
+
"""Convert COCO annotations using SDK converter."""
|
|
317
|
+
|
|
318
|
+
# Download annotation file
|
|
319
|
+
response = requests.get(data_file_url, timeout=30)
|
|
320
|
+
response.raise_for_status()
|
|
321
|
+
coco_data = response.json()
|
|
322
|
+
|
|
323
|
+
# Create converter in single-file mode
|
|
324
|
+
converter = COCOToDMConverter(is_single_conversion=True)
|
|
325
|
+
|
|
326
|
+
# Create a mock file object with the image path
|
|
327
|
+
class FileObj:
|
|
328
|
+
def __init__(self, name):
|
|
329
|
+
self.name = name
|
|
330
|
+
|
|
331
|
+
# Convert using SDK converter
|
|
332
|
+
result = converter.convert_single_file(
|
|
333
|
+
data=coco_data,
|
|
334
|
+
original_file=FileObj(primary_file_url),
|
|
335
|
+
original_image_name=primary_file_original_name
|
|
336
|
+
)
|
|
337
|
+
|
|
338
|
+
# Return the DM format data
|
|
339
|
+
return result['dm_json']
|
|
340
|
+
```
|
|
341
|
+
|
|
342
|
+
#### In convert_data_from_inference()
|
|
343
|
+
|
|
344
|
+
```python
|
|
345
|
+
from synapse_sdk.utils.converters.dm import DMV2ToV1Converter
|
|
346
|
+
|
|
347
|
+
class AnnotationToTask:
|
|
348
|
+
def convert_data_from_inference(self, data: dict) -> dict:
|
|
349
|
+
"""Convert inference results with optional DM version conversion."""
|
|
350
|
+
|
|
351
|
+
# Your inference result processing
|
|
352
|
+
dm_v2_data = self._process_inference_results(data)
|
|
353
|
+
|
|
354
|
+
# Optionally convert DM v2 to v1 if needed
|
|
355
|
+
if self._needs_v1_format():
|
|
356
|
+
converter = DMV2ToV1Converter(new_dm_data=dm_v2_data)
|
|
357
|
+
dm_v1_data = converter.convert()
|
|
358
|
+
return dm_v1_data
|
|
359
|
+
|
|
360
|
+
return dm_v2_data
|
|
361
|
+
```
|
|
362
|
+
|
|
363
|
+
### Converter Examples
|
|
364
|
+
|
|
365
|
+
#### Example 1: COCO Converter
|
|
366
|
+
|
|
367
|
+
Complete implementation using `COCOToDMConverter`:
|
|
368
|
+
|
|
369
|
+
```python
|
|
370
|
+
# plugin/to_task.py
|
|
371
|
+
import requests
|
|
372
|
+
from synapse_sdk.utils.converters.coco import COCOToDMConverter
|
|
373
|
+
|
|
374
|
+
class AnnotationToTask:
|
|
375
|
+
"""Use SDK COCO converter for annotation conversion."""
|
|
376
|
+
|
|
377
|
+
def __init__(self, run, *args, **kwargs):
|
|
378
|
+
self.run = run
|
|
379
|
+
|
|
380
|
+
def convert_data_from_file(
|
|
381
|
+
self,
|
|
382
|
+
primary_file_url: str,
|
|
383
|
+
primary_file_original_name: str,
|
|
384
|
+
data_file_url: str,
|
|
385
|
+
data_file_original_name: str,
|
|
386
|
+
) -> dict:
|
|
387
|
+
"""Convert COCO JSON to Synapse task format using SDK converter."""
|
|
388
|
+
|
|
389
|
+
try:
|
|
390
|
+
# Download COCO annotation file
|
|
391
|
+
self.run.log_message(f'Downloading COCO annotations: {data_file_url}')
|
|
392
|
+
response = requests.get(data_file_url, timeout=30)
|
|
393
|
+
response.raise_for_status()
|
|
394
|
+
coco_data = response.json()
|
|
395
|
+
|
|
396
|
+
# Validate COCO structure
|
|
397
|
+
if 'annotations' not in coco_data or 'images' not in coco_data:
|
|
398
|
+
raise ValueError('Invalid COCO format: missing required fields')
|
|
399
|
+
|
|
400
|
+
# Create converter for single file conversion
|
|
401
|
+
converter = COCOToDMConverter(is_single_conversion=True)
|
|
402
|
+
|
|
403
|
+
# Create file object
|
|
404
|
+
class MockFile:
|
|
405
|
+
def __init__(self, path):
|
|
406
|
+
self.name = path
|
|
407
|
+
|
|
408
|
+
# Convert using SDK converter
|
|
409
|
+
result = converter.convert_single_file(
|
|
410
|
+
data=coco_data,
|
|
411
|
+
original_file=MockFile(primary_file_url),
|
|
412
|
+
original_image_name=primary_file_original_name
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
self.run.log_message(
|
|
416
|
+
f'Successfully converted COCO data using SDK converter'
|
|
417
|
+
)
|
|
418
|
+
|
|
419
|
+
# Return DM format
|
|
420
|
+
return result['dm_json']
|
|
421
|
+
|
|
422
|
+
except requests.RequestException as e:
|
|
423
|
+
self.run.log_message(f'Failed to download annotations: {str(e)}')
|
|
424
|
+
raise
|
|
425
|
+
except ValueError as e:
|
|
426
|
+
self.run.log_message(f'Invalid COCO data: {str(e)}')
|
|
427
|
+
raise
|
|
428
|
+
except Exception as e:
|
|
429
|
+
self.run.log_message(f'Conversion failed: {str(e)}')
|
|
430
|
+
raise
|
|
431
|
+
|
|
432
|
+
def convert_data_from_inference(self, data: dict) -> dict:
|
|
433
|
+
"""Not used for this plugin."""
|
|
434
|
+
return data
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
**Supported COCO Features:**
|
|
438
|
+
- Bounding boxes
|
|
439
|
+
- Keypoints
|
|
440
|
+
- Groups (bbox + keypoints)
|
|
441
|
+
- Category mapping
|
|
442
|
+
- Attributes
|
|
443
|
+
|
|
444
|
+
#### Example 2: YOLO Converter
|
|
445
|
+
|
|
446
|
+
Complete implementation using `YOLOToDMConverter`:
|
|
447
|
+
|
|
448
|
+
```python
|
|
449
|
+
# plugin/to_task.py
|
|
450
|
+
import requests
|
|
451
|
+
from synapse_sdk.utils.converters.yolo import YOLOToDMConverter
|
|
452
|
+
|
|
453
|
+
class AnnotationToTask:
|
|
454
|
+
"""Use SDK YOLO converter for label conversion."""
|
|
455
|
+
|
|
456
|
+
def __init__(self, run, *args, **kwargs):
|
|
457
|
+
self.run = run
|
|
458
|
+
# YOLO class names (must match your model)
|
|
459
|
+
self.class_names = ['person', 'car', 'truck', 'bicycle']
|
|
460
|
+
|
|
461
|
+
def convert_data_from_file(
|
|
462
|
+
self,
|
|
463
|
+
primary_file_url: str,
|
|
464
|
+
primary_file_original_name: str,
|
|
465
|
+
data_file_url: str,
|
|
466
|
+
data_file_original_name: str,
|
|
467
|
+
) -> dict:
|
|
468
|
+
"""Convert YOLO labels to Synapse task format using SDK converter."""
|
|
469
|
+
|
|
470
|
+
try:
|
|
471
|
+
# Download YOLO label file
|
|
472
|
+
self.run.log_message(f'Downloading YOLO labels: {data_file_url}')
|
|
473
|
+
response = requests.get(data_file_url, timeout=30)
|
|
474
|
+
response.raise_for_status()
|
|
475
|
+
label_text = response.text
|
|
476
|
+
|
|
477
|
+
# Parse label lines
|
|
478
|
+
label_lines = [line.strip() for line in label_text.splitlines() if line.strip()]
|
|
479
|
+
|
|
480
|
+
# Create converter with class names
|
|
481
|
+
converter = YOLOToDMConverter(
|
|
482
|
+
is_single_conversion=True,
|
|
483
|
+
class_names=self.class_names
|
|
484
|
+
)
|
|
485
|
+
|
|
486
|
+
# Create file object
|
|
487
|
+
class MockFile:
|
|
488
|
+
def __init__(self, path):
|
|
489
|
+
self.name = path
|
|
490
|
+
|
|
491
|
+
# Convert using SDK converter
|
|
492
|
+
result = converter.convert_single_file(
|
|
493
|
+
data=label_lines, # List of label strings
|
|
494
|
+
original_file=MockFile(primary_file_url)
|
|
495
|
+
)
|
|
496
|
+
|
|
497
|
+
self.run.log_message(
|
|
498
|
+
f'Successfully converted {len(label_lines)} YOLO labels'
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
return result['dm_json']
|
|
502
|
+
|
|
503
|
+
except Exception as e:
|
|
504
|
+
self.run.log_message(f'YOLO conversion failed: {str(e)}')
|
|
505
|
+
raise
|
|
506
|
+
|
|
507
|
+
def convert_data_from_inference(self, data: dict) -> dict:
|
|
508
|
+
"""Not used for this plugin."""
|
|
509
|
+
return data
|
|
510
|
+
```
|
|
511
|
+
|
|
512
|
+
**Supported YOLO Features:**
|
|
513
|
+
- Bounding boxes (standard YOLO format)
|
|
514
|
+
- Polygons (segmentation format)
|
|
515
|
+
- Keypoints (pose estimation format)
|
|
516
|
+
- Automatic coordinate denormalization
|
|
517
|
+
- Class name mapping
|
|
518
|
+
|
|
519
|
+
#### Example 3: Pascal VOC Converter
|
|
520
|
+
|
|
521
|
+
Complete implementation using `PascalToDMConverter`:
|
|
522
|
+
|
|
523
|
+
```python
|
|
524
|
+
# plugin/to_task.py
|
|
525
|
+
import requests
|
|
526
|
+
from synapse_sdk.utils.converters.pascal import PascalToDMConverter
|
|
527
|
+
|
|
528
|
+
class AnnotationToTask:
|
|
529
|
+
"""Use SDK Pascal VOC converter for XML annotation conversion."""
|
|
530
|
+
|
|
531
|
+
def __init__(self, run, *args, **kwargs):
|
|
532
|
+
self.run = run
|
|
533
|
+
|
|
534
|
+
def convert_data_from_file(
|
|
535
|
+
self,
|
|
536
|
+
primary_file_url: str,
|
|
537
|
+
primary_file_original_name: str,
|
|
538
|
+
data_file_url: str,
|
|
539
|
+
data_file_original_name: str,
|
|
540
|
+
) -> dict:
|
|
541
|
+
"""Convert Pascal VOC XML to Synapse task format using SDK converter."""
|
|
542
|
+
|
|
543
|
+
try:
|
|
544
|
+
# Download Pascal VOC XML file
|
|
545
|
+
self.run.log_message(f'Downloading Pascal VOC XML: {data_file_url}')
|
|
546
|
+
response = requests.get(data_file_url, timeout=30)
|
|
547
|
+
response.raise_for_status()
|
|
548
|
+
xml_content = response.text
|
|
549
|
+
|
|
550
|
+
# Create converter
|
|
551
|
+
converter = PascalToDMConverter(is_single_conversion=True)
|
|
552
|
+
|
|
553
|
+
# Create file object
|
|
554
|
+
class MockFile:
|
|
555
|
+
def __init__(self, path):
|
|
556
|
+
self.name = path
|
|
557
|
+
|
|
558
|
+
# Convert using SDK converter
|
|
559
|
+
result = converter.convert_single_file(
|
|
560
|
+
data=xml_content, # XML string
|
|
561
|
+
original_file=MockFile(primary_file_url)
|
|
562
|
+
)
|
|
563
|
+
|
|
564
|
+
self.run.log_message('Successfully converted Pascal VOC annotations')
|
|
565
|
+
|
|
566
|
+
return result['dm_json']
|
|
567
|
+
|
|
568
|
+
except Exception as e:
|
|
569
|
+
self.run.log_message(f'Pascal VOC conversion failed: {str(e)}')
|
|
570
|
+
raise
|
|
571
|
+
|
|
572
|
+
def convert_data_from_inference(self, data: dict) -> dict:
|
|
573
|
+
"""Not used for this plugin."""
|
|
574
|
+
return data
|
|
575
|
+
```
|
|
576
|
+
|
|
577
|
+
**Supported Pascal VOC Features:**
|
|
578
|
+
- Bounding boxes (xmin, ymin, xmax, ymax)
|
|
579
|
+
- Object names/classes
|
|
580
|
+
- Automatic width/height calculation
|
|
581
|
+
- XML parsing and validation
|
|
582
|
+
|
|
583
|
+
### Best Practices with Converters
|
|
584
|
+
|
|
585
|
+
#### 1. When to Use Converters
|
|
586
|
+
|
|
587
|
+
**Use SDK Converters When:**
|
|
588
|
+
- Working with standard formats (COCO, YOLO, Pascal VOC)
|
|
589
|
+
- Need reliable, tested conversion logic
|
|
590
|
+
- Want to minimize maintenance burden
|
|
591
|
+
- Working with complex formats (COCO with keypoints, YOLO segmentation)
|
|
592
|
+
|
|
593
|
+
**Write Custom Code When:**
|
|
594
|
+
- Format is non-standard or proprietary
|
|
595
|
+
- Need special preprocessing before conversion
|
|
596
|
+
- Converter doesn't support your specific variant
|
|
597
|
+
- Performance optimization is critical
|
|
598
|
+
|
|
599
|
+
#### 2. Error Handling with Converters
|
|
600
|
+
|
|
601
|
+
Always wrap converter calls in try-except blocks:
|
|
602
|
+
|
|
603
|
+
```python
|
|
604
|
+
def convert_data_from_file(self, *args) -> dict:
|
|
605
|
+
try:
|
|
606
|
+
converter = COCOToDMConverter(is_single_conversion=True)
|
|
607
|
+
result = converter.convert_single_file(...)
|
|
608
|
+
return result['dm_json']
|
|
609
|
+
|
|
610
|
+
except ValueError as e:
|
|
611
|
+
# Validation errors from converter
|
|
612
|
+
self.run.log_message(f'Invalid data format: {str(e)}')
|
|
613
|
+
raise
|
|
614
|
+
|
|
615
|
+
except KeyError as e:
|
|
616
|
+
# Missing required fields
|
|
617
|
+
self.run.log_message(f'Missing field in result: {str(e)}')
|
|
618
|
+
raise
|
|
619
|
+
|
|
620
|
+
except Exception as e:
|
|
621
|
+
# Unexpected errors
|
|
622
|
+
self.run.log_message(f'Converter error: {str(e)}')
|
|
623
|
+
raise
|
|
624
|
+
```
|
|
625
|
+
|
|
626
|
+
#### 3. Combining Converters with Custom Logic
|
|
627
|
+
|
|
628
|
+
You can post-process converter output:
|
|
629
|
+
|
|
630
|
+
```python
|
|
631
|
+
def convert_data_from_file(self, *args) -> dict:
|
|
632
|
+
# Use converter for basic conversion
|
|
633
|
+
converter = YOLOToDMConverter(
|
|
634
|
+
is_single_conversion=True,
|
|
635
|
+
class_names=self.class_names
|
|
636
|
+
)
|
|
637
|
+
result = converter.convert_single_file(...)
|
|
638
|
+
dm_data = result['dm_json']
|
|
639
|
+
|
|
640
|
+
# Add custom post-processing
|
|
641
|
+
for img in dm_data.get('images', []):
|
|
642
|
+
for bbox in img.get('bounding_box', []):
|
|
643
|
+
# Add custom attributes
|
|
644
|
+
bbox['attrs'].append({
|
|
645
|
+
'name': 'source',
|
|
646
|
+
'value': 'yolo_model_v2'
|
|
647
|
+
})
|
|
648
|
+
|
|
649
|
+
# Filter by size
|
|
650
|
+
if bbox['data'][2] < 10 or bbox['data'][3] < 10:
|
|
651
|
+
# Mark small boxes
|
|
652
|
+
bbox['attrs'].append({
|
|
653
|
+
'name': 'too_small',
|
|
654
|
+
'value': True
|
|
655
|
+
})
|
|
656
|
+
|
|
657
|
+
return dm_data
|
|
658
|
+
```
|
|
659
|
+
|
|
660
|
+
#### 4. Performance Considerations
|
|
661
|
+
|
|
662
|
+
**Converters are optimized but:**
|
|
663
|
+
- Download files efficiently (use timeouts, streaming if large)
|
|
664
|
+
- Cache converter instances if processing multiple files
|
|
665
|
+
- Log conversion progress for monitoring
|
|
666
|
+
|
|
667
|
+
```python
|
|
668
|
+
def __init__(self, run, *args, **kwargs):
|
|
669
|
+
self.run = run
|
|
670
|
+
# Cache converter instance
|
|
671
|
+
self.coco_converter = COCOToDMConverter(is_single_conversion=True)
|
|
672
|
+
|
|
673
|
+
def convert_data_from_file(self, *args) -> dict:
|
|
674
|
+
# Reuse cached converter
|
|
675
|
+
result = self.coco_converter.convert_single_file(...)
|
|
676
|
+
return result['dm_json']
|
|
677
|
+
```
|
|
678
|
+
|
|
679
|
+
#### 5. Testing with Converters
|
|
680
|
+
|
|
681
|
+
Test both converter integration and edge cases:
|
|
682
|
+
|
|
683
|
+
```python
|
|
684
|
+
# test_to_task.py
|
|
685
|
+
import pytest
|
|
686
|
+
from plugin.to_task import AnnotationToTask
|
|
687
|
+
|
|
688
|
+
class MockRun:
|
|
689
|
+
def log_message(self, msg):
|
|
690
|
+
print(msg)
|
|
691
|
+
|
|
692
|
+
def test_coco_converter_integration():
|
|
693
|
+
"""Test COCO converter integration."""
|
|
694
|
+
converter = AnnotationToTask(MockRun())
|
|
695
|
+
|
|
696
|
+
# Test with valid COCO data
|
|
697
|
+
coco_data = {
|
|
698
|
+
'images': [{'id': 1, 'file_name': 'test.jpg'}],
|
|
699
|
+
'annotations': [{
|
|
700
|
+
'id': 1,
|
|
701
|
+
'image_id': 1,
|
|
702
|
+
'category_id': 1,
|
|
703
|
+
'bbox': [10, 20, 100, 200]
|
|
704
|
+
}],
|
|
705
|
+
'categories': [{'id': 1, 'name': 'person'}]
|
|
706
|
+
}
|
|
707
|
+
|
|
708
|
+
result = converter._convert_with_coco_converter(coco_data, 'test.jpg')
|
|
709
|
+
|
|
710
|
+
# Verify DM structure
|
|
711
|
+
assert 'images' in result
|
|
712
|
+
assert len(result['images']) == 1
|
|
713
|
+
assert 'bounding_box' in result['images'][0]
|
|
714
|
+
|
|
715
|
+
def test_invalid_format_handling():
|
|
716
|
+
"""Test error handling for invalid data."""
|
|
717
|
+
converter = AnnotationToTask(MockRun())
|
|
718
|
+
|
|
719
|
+
# Test with invalid COCO data
|
|
720
|
+
invalid_data = {'invalid': 'data'}
|
|
721
|
+
|
|
722
|
+
with pytest.raises(ValueError):
|
|
723
|
+
converter._convert_with_coco_converter(invalid_data, 'test.jpg')
|
|
724
|
+
```
|
|
725
|
+
|
|
726
|
+
### Converter API Reference
|
|
727
|
+
|
|
728
|
+
#### COCOToDMConverter.convert_single_file()
|
|
729
|
+
|
|
730
|
+
```python
|
|
731
|
+
def convert_single_file(
|
|
732
|
+
data: Dict[str, Any],
|
|
733
|
+
original_file: IO,
|
|
734
|
+
original_image_name: str
|
|
735
|
+
) -> Dict[str, Any]:
|
|
736
|
+
```
|
|
737
|
+
|
|
738
|
+
**Parameters:**
|
|
739
|
+
- `data`: COCO format dictionary (JSON content)
|
|
740
|
+
- `original_file`: File object with `.name` attribute
|
|
741
|
+
- `original_image_name`: Name of the image file
|
|
742
|
+
|
|
743
|
+
**Returns:**
|
|
744
|
+
```python
|
|
745
|
+
{
|
|
746
|
+
'dm_json': {...}, # DM format data
|
|
747
|
+
'image_path': str, # Path from file object
|
|
748
|
+
'image_name': str # Basename of image
|
|
749
|
+
}
|
|
750
|
+
```
|
|
751
|
+
|
|
752
|
+
#### YOLOToDMConverter.convert_single_file()
|
|
753
|
+
|
|
754
|
+
```python
|
|
755
|
+
def convert_single_file(
|
|
756
|
+
data: List[str],
|
|
757
|
+
original_file: IO
|
|
758
|
+
) -> Dict[str, Any]:
|
|
759
|
+
```
|
|
760
|
+
|
|
761
|
+
**Parameters:**
|
|
762
|
+
- `data`: List of YOLO label lines (strings from .txt file)
|
|
763
|
+
- `original_file`: File object with `.name` attribute
|
|
764
|
+
|
|
765
|
+
**Returns:**
|
|
766
|
+
```python
|
|
767
|
+
{
|
|
768
|
+
'dm_json': {...}, # DM format data
|
|
769
|
+
'image_path': str, # Path from file object
|
|
770
|
+
'image_name': str # Basename of image
|
|
771
|
+
}
|
|
772
|
+
```
|
|
773
|
+
|
|
774
|
+
#### PascalToDMConverter.convert_single_file()
|
|
775
|
+
|
|
776
|
+
```python
|
|
777
|
+
def convert_single_file(
|
|
778
|
+
data: str,
|
|
779
|
+
original_file: IO
|
|
780
|
+
) -> Dict[str, Any]:
|
|
781
|
+
```
|
|
782
|
+
|
|
783
|
+
**Parameters:**
|
|
784
|
+
- `data`: Pascal VOC XML content as string
|
|
785
|
+
- `original_file`: File object with `.name` attribute
|
|
786
|
+
|
|
787
|
+
**Returns:**
|
|
788
|
+
```python
|
|
789
|
+
{
|
|
790
|
+
'dm_json': {...}, # DM format data
|
|
791
|
+
'image_path': str, # Path from file object
|
|
792
|
+
'image_name': str # Basename of image
|
|
793
|
+
}
|
|
794
|
+
```
|
|
795
|
+
|
|
796
|
+
## Complete Examples
|
|
797
|
+
|
|
798
|
+
### Example 1: COCO Format Annotation Plugin
|
|
799
|
+
|
|
800
|
+
A complete plugin for converting COCO format annotations to Synapse tasks.
|
|
801
|
+
|
|
802
|
+
```python
|
|
803
|
+
# plugin/to_task.py
|
|
804
|
+
import requests
|
|
805
|
+
import json
|
|
806
|
+
from typing import Dict, List
|
|
807
|
+
|
|
808
|
+
class AnnotationToTask:
|
|
809
|
+
"""Convert COCO format annotations to Synapse task objects."""
|
|
810
|
+
|
|
811
|
+
def __init__(self, run, *args, **kwargs):
|
|
812
|
+
self.run = run
|
|
813
|
+
# COCO category ID to Synapse class ID mapping
|
|
814
|
+
self.category_mapping = {
|
|
815
|
+
1: 1, # person
|
|
816
|
+
2: 2, # bicycle
|
|
817
|
+
3: 3, # car
|
|
818
|
+
# Add more mappings as needed
|
|
819
|
+
}
|
|
820
|
+
|
|
821
|
+
def convert_data_from_file(
|
|
822
|
+
self,
|
|
823
|
+
primary_file_url: str,
|
|
824
|
+
primary_file_original_name: str,
|
|
825
|
+
data_file_url: str,
|
|
826
|
+
data_file_original_name: str,
|
|
827
|
+
) -> Dict:
|
|
828
|
+
"""Convert COCO JSON file to Synapse task format."""
|
|
829
|
+
|
|
830
|
+
try:
|
|
831
|
+
# Download COCO annotation file
|
|
832
|
+
self.run.log_message(f'Downloading: {data_file_url}')
|
|
833
|
+
response = requests.get(data_file_url, timeout=30)
|
|
834
|
+
response.raise_for_status()
|
|
835
|
+
coco_data = response.json()
|
|
836
|
+
|
|
837
|
+
# Validate COCO structure
|
|
838
|
+
if 'annotations' not in coco_data:
|
|
839
|
+
raise ValueError('Invalid COCO format: missing annotations')
|
|
840
|
+
|
|
841
|
+
# Convert annotations
|
|
842
|
+
task_objects = self._convert_coco_annotations(
|
|
843
|
+
coco_data['annotations']
|
|
844
|
+
)
|
|
845
|
+
|
|
846
|
+
self.run.log_message(
|
|
847
|
+
f'Successfully converted {len(task_objects)} annotations'
|
|
848
|
+
)
|
|
849
|
+
|
|
850
|
+
return {
|
|
851
|
+
'objects': task_objects,
|
|
852
|
+
'metadata': {
|
|
853
|
+
'source': 'coco',
|
|
854
|
+
'file': data_file_original_name
|
|
855
|
+
}
|
|
856
|
+
}
|
|
857
|
+
|
|
858
|
+
except requests.RequestException as e:
|
|
859
|
+
self.run.log_message(f'Failed to download file: {str(e)}')
|
|
860
|
+
raise
|
|
861
|
+
except json.JSONDecodeError as e:
|
|
862
|
+
self.run.log_message(f'Invalid JSON format: {str(e)}')
|
|
863
|
+
raise
|
|
864
|
+
except Exception as e:
|
|
865
|
+
self.run.log_message(f'Conversion failed: {str(e)}')
|
|
866
|
+
raise
|
|
867
|
+
|
|
868
|
+
def _convert_coco_annotations(self, annotations: List[Dict]) -> List[Dict]:
|
|
869
|
+
"""Convert COCO annotations to Synapse task objects."""
|
|
870
|
+
task_objects = []
|
|
871
|
+
|
|
872
|
+
for idx, ann in enumerate(annotations):
|
|
873
|
+
# Map COCO category to Synapse class
|
|
874
|
+
coco_category = ann.get('category_id')
|
|
875
|
+
synapse_class = self.category_mapping.get(coco_category)
|
|
876
|
+
|
|
877
|
+
if not synapse_class:
|
|
878
|
+
self.run.log_message(
|
|
879
|
+
f'Warning: Unmapped category {coco_category}, skipping'
|
|
880
|
+
)
|
|
881
|
+
continue
|
|
882
|
+
|
|
883
|
+
# Convert bbox format: [x, y, width, height]
|
|
884
|
+
bbox = ann.get('bbox', [])
|
|
885
|
+
if len(bbox) != 4:
|
|
886
|
+
continue
|
|
887
|
+
|
|
888
|
+
task_object = {
|
|
889
|
+
'id': f'coco_{ann.get("id", idx)}',
|
|
890
|
+
'class_id': synapse_class,
|
|
891
|
+
'type': 'bbox',
|
|
892
|
+
'coordinates': {
|
|
893
|
+
'x': float(bbox[0]),
|
|
894
|
+
'y': float(bbox[1]),
|
|
895
|
+
'width': float(bbox[2]),
|
|
896
|
+
'height': float(bbox[3])
|
|
897
|
+
},
|
|
898
|
+
'properties': {
|
|
899
|
+
'area': ann.get('area', 0),
|
|
900
|
+
'iscrowd': ann.get('iscrowd', 0),
|
|
901
|
+
'original_category': coco_category
|
|
902
|
+
}
|
|
903
|
+
}
|
|
904
|
+
task_objects.append(task_object)
|
|
905
|
+
|
|
906
|
+
return task_objects
|
|
907
|
+
|
|
908
|
+
def convert_data_from_inference(self, data: Dict) -> Dict:
|
|
909
|
+
"""Not used for this plugin - file-based only."""
|
|
910
|
+
return data
|
|
911
|
+
```
|
|
912
|
+
|
|
913
|
+
### Example 2: Object Detection Inference Plugin
|
|
914
|
+
|
|
915
|
+
A complete plugin for converting object detection model outputs.
|
|
916
|
+
|
|
917
|
+
```python
|
|
918
|
+
# plugin/to_task.py
|
|
919
|
+
from typing import Dict, List
|
|
920
|
+
|
|
921
|
+
class AnnotationToTask:
|
|
922
|
+
"""Convert object detection inference results to Synapse tasks."""
|
|
923
|
+
|
|
924
|
+
def __init__(self, run, *args, **kwargs):
|
|
925
|
+
self.run = run
|
|
926
|
+
# Configuration
|
|
927
|
+
self.confidence_threshold = 0.7
|
|
928
|
+
self.nms_threshold = 0.5
|
|
929
|
+
self.max_detections = 100
|
|
930
|
+
|
|
931
|
+
def convert_data_from_file(
|
|
932
|
+
self,
|
|
933
|
+
primary_file_url: str,
|
|
934
|
+
primary_file_original_name: str,
|
|
935
|
+
data_file_url: str,
|
|
936
|
+
data_file_original_name: str,
|
|
937
|
+
) -> Dict:
|
|
938
|
+
"""Not used for this plugin - inference-based only."""
|
|
939
|
+
return {}
|
|
940
|
+
|
|
941
|
+
def convert_data_from_inference(self, data: Dict) -> Dict:
|
|
942
|
+
"""Convert YOLOv8 detection results to Synapse format."""
|
|
943
|
+
|
|
944
|
+
try:
|
|
945
|
+
# Extract predictions
|
|
946
|
+
predictions = data.get('predictions', [])
|
|
947
|
+
|
|
948
|
+
if not predictions:
|
|
949
|
+
self.run.log_message('No predictions found in inference results')
|
|
950
|
+
return {'objects': []}
|
|
951
|
+
|
|
952
|
+
# Filter and convert detections
|
|
953
|
+
task_objects = self._process_detections(predictions)
|
|
954
|
+
|
|
955
|
+
# Apply NMS if needed
|
|
956
|
+
if len(task_objects) > self.max_detections:
|
|
957
|
+
task_objects = self._apply_nms(task_objects)
|
|
958
|
+
|
|
959
|
+
self.run.log_message(
|
|
960
|
+
f'Converted {len(task_objects)} detections '
|
|
961
|
+
f'(threshold: {self.confidence_threshold})'
|
|
962
|
+
)
|
|
963
|
+
|
|
964
|
+
return {
|
|
965
|
+
'objects': task_objects,
|
|
966
|
+
'metadata': {
|
|
967
|
+
'model': data.get('model_name', 'unknown'),
|
|
968
|
+
'inference_time': data.get('inference_time_ms', 0),
|
|
969
|
+
'confidence_threshold': self.confidence_threshold
|
|
970
|
+
}
|
|
971
|
+
}
|
|
972
|
+
|
|
973
|
+
except Exception as e:
|
|
974
|
+
self.run.log_message(f'Inference conversion failed: {str(e)}')
|
|
975
|
+
raise
|
|
976
|
+
|
|
977
|
+
def _process_detections(self, predictions: List[Dict]) -> List[Dict]:
|
|
978
|
+
"""Process and filter detections."""
|
|
979
|
+
task_objects = []
|
|
980
|
+
|
|
981
|
+
for idx, pred in enumerate(predictions):
|
|
982
|
+
confidence = pred.get('confidence', 0.0)
|
|
983
|
+
|
|
984
|
+
# Filter by confidence
|
|
985
|
+
if confidence < self.confidence_threshold:
|
|
986
|
+
continue
|
|
987
|
+
|
|
988
|
+
# Extract bbox coordinates
|
|
989
|
+
bbox = pred.get('bbox', {})
|
|
990
|
+
|
|
991
|
+
task_object = {
|
|
992
|
+
'id': f'det_{idx}',
|
|
993
|
+
'class_id': pred.get('class_id', 0),
|
|
994
|
+
'type': 'bbox',
|
|
995
|
+
'coordinates': {
|
|
996
|
+
'x': float(bbox.get('x', 0)),
|
|
997
|
+
'y': float(bbox.get('y', 0)),
|
|
998
|
+
'width': float(bbox.get('width', 0)),
|
|
999
|
+
'height': float(bbox.get('height', 0))
|
|
1000
|
+
},
|
|
1001
|
+
'properties': {
|
|
1002
|
+
'confidence': float(confidence),
|
|
1003
|
+
'class_name': pred.get('class_name', 'unknown'),
|
|
1004
|
+
'model_version': pred.get('model_version', '1.0')
|
|
1005
|
+
}
|
|
1006
|
+
}
|
|
1007
|
+
task_objects.append(task_object)
|
|
1008
|
+
|
|
1009
|
+
return task_objects
|
|
1010
|
+
|
|
1011
|
+
def _apply_nms(self, detections: List[Dict]) -> List[Dict]:
|
|
1012
|
+
"""Apply Non-Maximum Suppression to reduce overlapping boxes."""
|
|
1013
|
+
# Sort by confidence
|
|
1014
|
+
sorted_dets = sorted(
|
|
1015
|
+
detections,
|
|
1016
|
+
key=lambda x: x['properties']['confidence'],
|
|
1017
|
+
reverse=True
|
|
1018
|
+
)
|
|
1019
|
+
|
|
1020
|
+
# Return top N detections
|
|
1021
|
+
return sorted_dets[:self.max_detections]
|
|
1022
|
+
```
|
|
1023
|
+
|
|
1024
|
+
### Example 3: Hybrid Plugin (File + Inference)
|
|
1025
|
+
|
|
1026
|
+
A plugin supporting both annotation methods.
|
|
1027
|
+
|
|
1028
|
+
```python
|
|
1029
|
+
# plugin/to_task.py
|
|
1030
|
+
import requests
|
|
1031
|
+
import json
|
|
1032
|
+
from typing import Dict
|
|
1033
|
+
|
|
1034
|
+
class AnnotationToTask:
|
|
1035
|
+
"""Hybrid plugin supporting both file and inference annotation."""
|
|
1036
|
+
|
|
1037
|
+
def __init__(self, run, *args, **kwargs):
|
|
1038
|
+
self.run = run
|
|
1039
|
+
self.default_confidence = 0.8
|
|
1040
|
+
|
|
1041
|
+
def convert_data_from_file(
|
|
1042
|
+
self,
|
|
1043
|
+
primary_file_url: str,
|
|
1044
|
+
primary_file_original_name: str,
|
|
1045
|
+
data_file_url: str,
|
|
1046
|
+
data_file_original_name: str,
|
|
1047
|
+
) -> Dict:
|
|
1048
|
+
"""Handle custom JSON annotation format."""
|
|
1049
|
+
|
|
1050
|
+
# Download annotation file
|
|
1051
|
+
response = requests.get(data_file_url, timeout=30)
|
|
1052
|
+
response.raise_for_status()
|
|
1053
|
+
annotation_data = response.json()
|
|
1054
|
+
|
|
1055
|
+
# Convert from custom format
|
|
1056
|
+
task_objects = []
|
|
1057
|
+
for obj in annotation_data.get('objects', []):
|
|
1058
|
+
task_object = {
|
|
1059
|
+
'id': obj['id'],
|
|
1060
|
+
'class_id': obj['class'],
|
|
1061
|
+
'type': obj.get('type', 'bbox'),
|
|
1062
|
+
'coordinates': obj['coords'],
|
|
1063
|
+
'properties': obj.get('props', {})
|
|
1064
|
+
}
|
|
1065
|
+
task_objects.append(task_object)
|
|
1066
|
+
|
|
1067
|
+
return {'objects': task_objects}
|
|
1068
|
+
|
|
1069
|
+
def convert_data_from_inference(self, data: Dict) -> Dict:
|
|
1070
|
+
"""Handle inference results with validation."""
|
|
1071
|
+
|
|
1072
|
+
# Extract and validate predictions
|
|
1073
|
+
predictions = data.get('predictions', [])
|
|
1074
|
+
|
|
1075
|
+
task_objects = []
|
|
1076
|
+
for idx, pred in enumerate(predictions):
|
|
1077
|
+
# Validate required fields
|
|
1078
|
+
if not self._validate_prediction(pred):
|
|
1079
|
+
continue
|
|
1080
|
+
|
|
1081
|
+
task_object = {
|
|
1082
|
+
'id': f'pred_{idx}',
|
|
1083
|
+
'class_id': pred['class_id'],
|
|
1084
|
+
'type': 'bbox',
|
|
1085
|
+
'coordinates': pred['bbox'],
|
|
1086
|
+
'properties': {
|
|
1087
|
+
'confidence': pred.get('confidence', self.default_confidence),
|
|
1088
|
+
'source': 'inference'
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
task_objects.append(task_object)
|
|
1092
|
+
|
|
1093
|
+
return {'objects': task_objects}
|
|
1094
|
+
|
|
1095
|
+
def _validate_prediction(self, pred: Dict) -> bool:
|
|
1096
|
+
"""Validate prediction has required fields."""
|
|
1097
|
+
required_fields = ['class_id', 'bbox']
|
|
1098
|
+
return all(field in pred for field in required_fields)
|
|
1099
|
+
```
|
|
1100
|
+
|
|
1101
|
+
## Best Practices
|
|
1102
|
+
|
|
1103
|
+
### 1. Data Validation
|
|
1104
|
+
|
|
1105
|
+
Always validate input data before conversion:
|
|
1106
|
+
|
|
1107
|
+
```python
|
|
1108
|
+
def convert_data_from_file(self, *args) -> dict:
|
|
1109
|
+
# Validate JSON structure
|
|
1110
|
+
if 'required_field' not in data:
|
|
1111
|
+
raise ValueError('Missing required field in annotation data')
|
|
1112
|
+
|
|
1113
|
+
# Validate data types
|
|
1114
|
+
if not isinstance(data['objects'], list):
|
|
1115
|
+
raise TypeError('Objects must be a list')
|
|
1116
|
+
|
|
1117
|
+
# Validate values
|
|
1118
|
+
for obj in data['objects']:
|
|
1119
|
+
if obj.get('confidence', 0) < 0 or obj.get('confidence', 1) > 1:
|
|
1120
|
+
raise ValueError('Confidence must be between 0 and 1')
|
|
1121
|
+
```
|
|
1122
|
+
|
|
1123
|
+
### 2. Error Handling
|
|
1124
|
+
|
|
1125
|
+
Implement comprehensive error handling:
|
|
1126
|
+
|
|
1127
|
+
```python
|
|
1128
|
+
def convert_data_from_file(self, *args) -> dict:
|
|
1129
|
+
try:
|
|
1130
|
+
# Conversion logic
|
|
1131
|
+
return converted_data
|
|
1132
|
+
|
|
1133
|
+
except requests.RequestException as e:
|
|
1134
|
+
self.run.log_message(f'Network error: {str(e)}')
|
|
1135
|
+
raise
|
|
1136
|
+
|
|
1137
|
+
except json.JSONDecodeError as e:
|
|
1138
|
+
self.run.log_message(f'Invalid JSON: {str(e)}')
|
|
1139
|
+
raise
|
|
1140
|
+
|
|
1141
|
+
except KeyError as e:
|
|
1142
|
+
self.run.log_message(f'Missing field: {str(e)}')
|
|
1143
|
+
raise
|
|
1144
|
+
|
|
1145
|
+
except Exception as e:
|
|
1146
|
+
self.run.log_message(f'Unexpected error: {str(e)}')
|
|
1147
|
+
raise
|
|
1148
|
+
```
|
|
1149
|
+
|
|
1150
|
+
### 3. Logging
|
|
1151
|
+
|
|
1152
|
+
Use logging to track conversion progress:
|
|
1153
|
+
|
|
1154
|
+
```python
|
|
1155
|
+
def convert_data_from_inference(self, data: dict) -> dict:
|
|
1156
|
+
# Log start
|
|
1157
|
+
self.run.log_message('Starting inference data conversion')
|
|
1158
|
+
|
|
1159
|
+
predictions = data.get('predictions', [])
|
|
1160
|
+
self.run.log_message(f'Processing {len(predictions)} predictions')
|
|
1161
|
+
|
|
1162
|
+
# Process data
|
|
1163
|
+
filtered = [p for p in predictions if p['confidence'] > 0.5]
|
|
1164
|
+
self.run.log_message(f'Filtered to {len(filtered)} high-confidence predictions')
|
|
1165
|
+
|
|
1166
|
+
# Log completion
|
|
1167
|
+
self.run.log_message('Conversion completed successfully')
|
|
1168
|
+
|
|
1169
|
+
return converted_data
|
|
1170
|
+
```
|
|
1171
|
+
|
|
1172
|
+
### 4. Configuration
|
|
1173
|
+
|
|
1174
|
+
Make your plugin configurable:
|
|
1175
|
+
|
|
1176
|
+
```python
|
|
1177
|
+
class AnnotationToTask:
|
|
1178
|
+
def __init__(self, run, *args, **kwargs):
|
|
1179
|
+
self.run = run
|
|
1180
|
+
|
|
1181
|
+
# Get configuration from plugin params
|
|
1182
|
+
params = getattr(run, 'params', {})
|
|
1183
|
+
pre_processor_params = params.get('pre_processor_params', {})
|
|
1184
|
+
|
|
1185
|
+
# Set configuration
|
|
1186
|
+
self.confidence_threshold = pre_processor_params.get('confidence_threshold', 0.7)
|
|
1187
|
+
self.nms_threshold = pre_processor_params.get('nms_threshold', 0.5)
|
|
1188
|
+
self.max_detections = pre_processor_params.get('max_detections', 100)
|
|
1189
|
+
```
|
|
1190
|
+
|
|
1191
|
+
### 5. Testing
|
|
1192
|
+
|
|
1193
|
+
Test your conversions thoroughly:
|
|
1194
|
+
|
|
1195
|
+
```python
|
|
1196
|
+
# test_to_task.py
|
|
1197
|
+
import pytest
|
|
1198
|
+
from plugin.to_task import AnnotationToTask
|
|
1199
|
+
|
|
1200
|
+
class MockRun:
|
|
1201
|
+
def log_message(self, msg):
|
|
1202
|
+
print(msg)
|
|
1203
|
+
|
|
1204
|
+
def test_convert_coco_format():
|
|
1205
|
+
"""Test COCO format conversion."""
|
|
1206
|
+
converter = AnnotationToTask(MockRun())
|
|
1207
|
+
|
|
1208
|
+
# Mock COCO data
|
|
1209
|
+
coco_data = {
|
|
1210
|
+
'annotations': [
|
|
1211
|
+
{
|
|
1212
|
+
'id': 1,
|
|
1213
|
+
'category_id': 1,
|
|
1214
|
+
'bbox': [10, 20, 100, 200],
|
|
1215
|
+
'area': 20000
|
|
1216
|
+
}
|
|
1217
|
+
]
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
result = converter._convert_coco_annotations(coco_data['annotations'])
|
|
1221
|
+
|
|
1222
|
+
assert len(result) == 1
|
|
1223
|
+
assert result[0]['class_id'] == 1
|
|
1224
|
+
assert result[0]['coordinates']['x'] == 10
|
|
1225
|
+
|
|
1226
|
+
def test_confidence_filtering():
|
|
1227
|
+
"""Test confidence threshold filtering."""
|
|
1228
|
+
converter = AnnotationToTask(MockRun())
|
|
1229
|
+
converter.confidence_threshold = 0.7
|
|
1230
|
+
|
|
1231
|
+
predictions = [
|
|
1232
|
+
{'confidence': 0.9, 'class_id': 1, 'bbox': {}},
|
|
1233
|
+
{'confidence': 0.5, 'class_id': 2, 'bbox': {}}, # Below threshold
|
|
1234
|
+
{'confidence': 0.8, 'class_id': 3, 'bbox': {}},
|
|
1235
|
+
]
|
|
1236
|
+
|
|
1237
|
+
result = converter._process_detections(predictions)
|
|
1238
|
+
|
|
1239
|
+
# Only 2 should pass threshold
|
|
1240
|
+
assert len(result) == 2
|
|
1241
|
+
```
|
|
1242
|
+
|
|
1243
|
+
## Integration with ToTaskAction
|
|
1244
|
+
|
|
1245
|
+
### How Template Methods Are Called
|
|
1246
|
+
|
|
1247
|
+
Your template methods are called by the ToTaskAction framework during workflow execution:
|
|
1248
|
+
|
|
1249
|
+
**File-based Annotation Flow:**
|
|
1250
|
+
```
|
|
1251
|
+
1. ToTaskAction.start()
|
|
1252
|
+
↓
|
|
1253
|
+
2. ToTaskOrchestrator.execute_workflow()
|
|
1254
|
+
↓
|
|
1255
|
+
3. FileAnnotationStrategy.process_task()
|
|
1256
|
+
↓
|
|
1257
|
+
4. annotation_to_task = context.entrypoint(logger)
|
|
1258
|
+
↓
|
|
1259
|
+
5. converted_data = annotation_to_task.convert_data_from_file(...)
|
|
1260
|
+
↓
|
|
1261
|
+
6. client.annotate_task_data(task_id, data=converted_data)
|
|
1262
|
+
```
|
|
1263
|
+
|
|
1264
|
+
**Inference-based Annotation Flow:**
|
|
1265
|
+
```
|
|
1266
|
+
1. ToTaskAction.start()
|
|
1267
|
+
↓
|
|
1268
|
+
2. ToTaskOrchestrator.execute_workflow()
|
|
1269
|
+
↓
|
|
1270
|
+
3. InferenceAnnotationStrategy.process_task()
|
|
1271
|
+
↓
|
|
1272
|
+
4. inference_result = preprocessor_api.predict(...)
|
|
1273
|
+
↓
|
|
1274
|
+
5. annotation_to_task = context.entrypoint(logger)
|
|
1275
|
+
↓
|
|
1276
|
+
6. converted_data = annotation_to_task.convert_data_from_inference(inference_result)
|
|
1277
|
+
↓
|
|
1278
|
+
7. client.annotate_task_data(task_id, data=converted_data)
|
|
1279
|
+
```
|
|
1280
|
+
|
|
1281
|
+
### Debugging Templates
|
|
1282
|
+
|
|
1283
|
+
When debugging your template:
|
|
1284
|
+
|
|
1285
|
+
1. **Check Logs**: Review plugin run logs for your log messages
|
|
1286
|
+
2. **Validate Returns**: Ensure return format matches Synapse task object schema
|
|
1287
|
+
3. **Test Locally**: Test conversion methods independently before deploying
|
|
1288
|
+
4. **Inspect Inputs**: Log input parameters to verify data being received
|
|
1289
|
+
5. **Handle Errors**: Catch and log exceptions with descriptive messages
|
|
1290
|
+
|
|
1291
|
+
```python
|
|
1292
|
+
def convert_data_from_file(self, *args) -> dict:
|
|
1293
|
+
# Debug logging
|
|
1294
|
+
self.run.log_message(f'Received URLs: primary={args[0]}, data={args[2]}')
|
|
1295
|
+
|
|
1296
|
+
try:
|
|
1297
|
+
# Your logic
|
|
1298
|
+
result = process_data()
|
|
1299
|
+
|
|
1300
|
+
# Validate result
|
|
1301
|
+
self.run.log_message(f'Converted {len(result["objects"])} objects')
|
|
1302
|
+
|
|
1303
|
+
return result
|
|
1304
|
+
|
|
1305
|
+
except Exception as e:
|
|
1306
|
+
# Detailed error logging
|
|
1307
|
+
self.run.log_message(f'Conversion failed: {type(e).__name__}: {str(e)}')
|
|
1308
|
+
import traceback
|
|
1309
|
+
self.run.log_message(traceback.format_exc())
|
|
1310
|
+
raise
|
|
1311
|
+
```
|
|
1312
|
+
|
|
1313
|
+
## Common Pitfalls
|
|
1314
|
+
|
|
1315
|
+
### 1. Incorrect Return Format
|
|
1316
|
+
|
|
1317
|
+
**Wrong:**
|
|
1318
|
+
```python
|
|
1319
|
+
def convert_data_from_file(self, *args) -> dict:
|
|
1320
|
+
return [obj1, obj2, obj3] # Returns list, not dict
|
|
1321
|
+
```
|
|
1322
|
+
|
|
1323
|
+
**Correct:**
|
|
1324
|
+
```python
|
|
1325
|
+
def convert_data_from_file(self, *args) -> dict:
|
|
1326
|
+
return {'objects': [obj1, obj2, obj3]} # Returns dict with 'objects' key
|
|
1327
|
+
```
|
|
1328
|
+
|
|
1329
|
+
### 2. Missing Error Handling
|
|
1330
|
+
|
|
1331
|
+
**Wrong:**
|
|
1332
|
+
```python
|
|
1333
|
+
def convert_data_from_file(self, *args) -> dict:
|
|
1334
|
+
response = requests.get(url) # No timeout, no error handling
|
|
1335
|
+
data = response.json()
|
|
1336
|
+
return data
|
|
1337
|
+
```
|
|
1338
|
+
|
|
1339
|
+
**Correct:**
|
|
1340
|
+
```python
|
|
1341
|
+
def convert_data_from_file(self, *args) -> dict:
|
|
1342
|
+
try:
|
|
1343
|
+
response = requests.get(url, timeout=30)
|
|
1344
|
+
response.raise_for_status()
|
|
1345
|
+
data = response.json()
|
|
1346
|
+
return self._transform_data(data)
|
|
1347
|
+
except Exception as e:
|
|
1348
|
+
self.run.log_message(f'Error: {str(e)}')
|
|
1349
|
+
raise
|
|
1350
|
+
```
|
|
1351
|
+
|
|
1352
|
+
### 3. Not Using Logging
|
|
1353
|
+
|
|
1354
|
+
**Wrong:**
|
|
1355
|
+
```python
|
|
1356
|
+
def convert_data_from_inference(self, data: dict) -> dict:
|
|
1357
|
+
# Silent conversion - no visibility
|
|
1358
|
+
return process(data)
|
|
1359
|
+
```
|
|
1360
|
+
|
|
1361
|
+
**Correct:**
|
|
1362
|
+
```python
|
|
1363
|
+
def convert_data_from_inference(self, data: dict) -> dict:
|
|
1364
|
+
self.run.log_message(f'Converting {len(data["predictions"])} predictions')
|
|
1365
|
+
result = process(data)
|
|
1366
|
+
self.run.log_message(f'Conversion complete: {len(result["objects"])} objects')
|
|
1367
|
+
return result
|
|
1368
|
+
```
|
|
1369
|
+
|
|
1370
|
+
## Related Documentation
|
|
1371
|
+
|
|
1372
|
+
- [ToTask Overview](./to-task-overview.md) - User guide for ToTask action
|
|
1373
|
+
- [ToTask Action Development](./to-task-action-development.md) - SDK developer guide
|
|
1374
|
+
- [Pre-annotation Plugin Overview](./pre-annotation-plugin-overview.md) - Category overview
|
|
1375
|
+
- Plugin Development Guide - General plugin development
|
|
1376
|
+
|
|
1377
|
+
## Template Source Code
|
|
1378
|
+
|
|
1379
|
+
- Template: `synapse_sdk/plugins/categories/pre_annotation/templates/plugin/to_task.py`
|
|
1380
|
+
- Called by: `synapse_sdk/plugins/categories/pre_annotation/actions/to_task/strategies/annotation.py`
|