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.

Files changed (17) hide show
  1. synapse_sdk/devtools/docs/docs/plugins/categories/pre-annotation-plugins/pre-annotation-plugin-overview.md +198 -0
  2. synapse_sdk/devtools/docs/docs/plugins/categories/pre-annotation-plugins/to-task-action-development.md +1645 -0
  3. synapse_sdk/devtools/docs/docs/plugins/categories/pre-annotation-plugins/to-task-overview.md +717 -0
  4. synapse_sdk/devtools/docs/docs/plugins/categories/pre-annotation-plugins/to-task-template-development.md +1380 -0
  5. synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/plugins/categories/pre-annotation-plugins/pre-annotation-plugin-overview.md +198 -0
  6. synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/plugins/categories/pre-annotation-plugins/to-task-action-development.md +1645 -0
  7. synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/plugins/categories/pre-annotation-plugins/to-task-overview.md +717 -0
  8. synapse_sdk/devtools/docs/i18n/ko/docusaurus-plugin-content-docs/current/plugins/categories/pre-annotation-plugins/to-task-template-development.md +1380 -0
  9. synapse_sdk/devtools/docs/sidebars.ts +14 -0
  10. synapse_sdk/plugins/categories/export/actions/export/action.py +8 -3
  11. synapse_sdk/plugins/categories/export/actions/export/utils.py +108 -8
  12. {synapse_sdk-2025.10.3.dist-info → synapse_sdk-2025.10.4.dist-info}/METADATA +1 -1
  13. {synapse_sdk-2025.10.3.dist-info → synapse_sdk-2025.10.4.dist-info}/RECORD +17 -9
  14. {synapse_sdk-2025.10.3.dist-info → synapse_sdk-2025.10.4.dist-info}/WHEEL +0 -0
  15. {synapse_sdk-2025.10.3.dist-info → synapse_sdk-2025.10.4.dist-info}/entry_points.txt +0 -0
  16. {synapse_sdk-2025.10.3.dist-info → synapse_sdk-2025.10.4.dist-info}/licenses/LICENSE +0 -0
  17. {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 템플릿 개발 with AnnotationToTask
4
+ sidebar_position: 4
5
+ ---
6
+
7
+ # ToTask 템플릿 개발 with AnnotationToTask
8
+
9
+ 이 가이드는 `AnnotationToTask` 템플릿을 사용하여 커스텀 pre-annotation 플러그인을 만들고자 하는 플러그인 개발자를 위한 것입니다. AnnotationToTask 템플릿은 Synapse 프로젝트에서 데이터를 작업 주석으로 변환하는 간단한 인터페이스를 제공합니다.
10
+
11
+ ## 개요
12
+
13
+ `AnnotationToTask` 템플릿(`synapse_sdk.plugins.categories.pre_annotation.templates.plugin.to_task`)은 pre-annotation 플러그인을 구축하는 구조화된 접근 방식을 제공합니다. 워크플로우 통합을 처리하는 동안 커스텀 데이터 변환 로직 구현에 집중할 수 있습니다.
14
+
15
+ ### AnnotationToTask란?
16
+
17
+ `AnnotationToTask`는 두 가지 핵심 변환 메서드를 정의하는 템플릿 클래스입니다:
18
+ - **`convert_data_from_file()`**: 파일의 JSON 데이터를 작업 주석으로 변환
19
+ - **`convert_data_from_inference()`**: 모델 추론 결과를 작업 주석으로 변환
20
+
21
+ ToTaskAction 프레임워크는 주석 워크플로우 중에 이러한 메서드를 자동으로 호출하여 데이터가 작업 객체로 변환되는 방식을 사용자 정의할 수 있습니다.
22
+
23
+ ### 이 템플릿을 사용해야 하는 경우
24
+
25
+ 다음이 필요한 경우 AnnotationToTask 템플릿을 사용하세요:
26
+ - 외부 주석 데이터를 Synapse 작업 형식으로 변환
27
+ - 모델 예측을 작업 주석으로 변환
28
+ - 커스텀 데이터 검증 및 변환 로직 구현
29
+ - 재사용 가능한 주석 변환 플러그인 생성
30
+
31
+ ## 시작하기
32
+
33
+ ### 템플릿 구조
34
+
35
+ ToTask 템플릿을 사용하여 pre-annotation 플러그인을 생성하면 다음과 같은 구조를 얻습니다:
36
+
37
+ ```
38
+ synapse-{plugin-code}-plugin/
39
+ ├── config.yaml # 플러그인 메타데이터 및 구성
40
+ ├── plugin/ # 소스 코드 디렉토리
41
+ │ ├── __init__.py
42
+ │ └── to_task.py # AnnotationToTask 구현
43
+ ├── requirements.txt # Python 의존성
44
+ ├── pyproject.toml # 패키지 구성
45
+ └── README.md # 플러그인 문서
46
+ ```
47
+
48
+ ### 기본 플러그인 구현
49
+
50
+ ```python
51
+ # plugin/to_task.py
52
+ class AnnotationToTask:
53
+ """커스텀 주석 변환 로직을 위한 템플릿."""
54
+
55
+ def __init__(self, run, *args, **kwargs):
56
+ """플러그인 작업 pre annotation 액션 클래스 초기화.
57
+
58
+ Args:
59
+ run: 로깅 및 컨텍스트를 제공하는 플러그인 run 객체.
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
+ """파일의 데이터를 작업 객체로 변환.
71
+
72
+ Args:
73
+ primary_file_url: 주 파일(예: 주석이 추가되는 이미지)의 URL
74
+ primary_file_original_name: 주 파일의 원본 이름
75
+ data_file_url: 주석 데이터 파일(JSON)의 URL
76
+ data_file_original_name: 주석 파일의 원본 이름
77
+
78
+ Returns:
79
+ dict: Synapse 형식의 주석이 있는 작업 객체
80
+ """
81
+ # 여기에 커스텀 구현
82
+ converted_data = {}
83
+ return converted_data
84
+
85
+ def convert_data_from_inference(self, data: dict) -> dict:
86
+ """추론 결과의 데이터를 작업 객체로 변환.
87
+
88
+ Args:
89
+ data: 전처리기의 원시 추론 결과
90
+
91
+ Returns:
92
+ dict: Synapse 형식의 주석이 있는 작업 객체
93
+ """
94
+ # 여기에 커스텀 구현
95
+ return data
96
+ ```
97
+
98
+ ## AnnotationToTask 클래스 레퍼런스
99
+
100
+ ### 생성자
101
+
102
+ ```python
103
+ def __init__(self, run, *args, **kwargs):
104
+ ```
105
+
106
+ **파라미터:**
107
+ - `run`: 로깅 및 컨텍스트 접근을 제공하는 플러그인 run 객체
108
+ - 로깅에 `self.run.log_message(msg)` 사용
109
+ - `self.run.params`를 통한 구성 접근
110
+
111
+ **사용법:**
112
+ ```python
113
+ def __init__(self, run, *args, **kwargs):
114
+ self.run = run
115
+ # 커스텀 속성 초기화
116
+ self.confidence_threshold = 0.8
117
+ self.custom_mapping = {}
118
+ ```
119
+
120
+ ### 메서드: convert_data_from_file()
121
+
122
+ JSON 파일의 주석 데이터를 Synapse 작업 객체 형식으로 변환합니다.
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
+ **파라미터:**
135
+ - `primary_file_url` (str): 주 파일(예: 주석이 추가되는 이미지)의 HTTP/HTTPS URL
136
+ - `primary_file_original_name` (str): 주 파일의 원본 파일명
137
+ - `data_file_url` (str): 주석 JSON 파일의 HTTP/HTTPS URL
138
+ - `data_file_original_name` (str): 주석 파일의 원본 파일명
139
+
140
+ **반환값:**
141
+ - `dict`: Synapse 형식의 주석을 포함하는 작업 객체
142
+
143
+ **호출자:**
144
+ - 파일 기반 주석 워크플로우 중 `FileAnnotationStrategy`
145
+
146
+ **워크플로우:**
147
+ 1. `data_file_url`에서 JSON 데이터 다운로드
148
+ 2. JSON 구조 파싱 및 검증
149
+ 3. Synapse 작업 객체 스키마에 맞게 데이터 변환
150
+ 4. 형식화된 작업 객체 반환
151
+
152
+ **예제 구현:**
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
+ """COCO 형식 주석을 Synapse 작업 형식으로 변환."""
166
+
167
+ # 주석 파일 다운로드
168
+ response = requests.get(data_file_url, timeout=30)
169
+ response.raise_for_status()
170
+ coco_data = response.json()
171
+
172
+ # 주석 추출
173
+ annotations = coco_data.get('annotations', [])
174
+
175
+ # Synapse 형식으로 변환
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
+ # 변환 정보 로깅
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
+ ### 메서드: convert_data_from_inference()
204
+
205
+ 모델 추론 결과를 Synapse 작업 객체 형식으로 변환합니다.
206
+
207
+ ```python
208
+ def convert_data_from_inference(self, data: dict) -> dict:
209
+ ```
210
+
211
+ **파라미터:**
212
+ - `data` (dict): 전처리기 플러그인의 원시 추론 결과
213
+
214
+ **반환값:**
215
+ - `dict`: Synapse 형식의 주석을 포함하는 작업 객체
216
+
217
+ **호출자:**
218
+ - 추론 기반 주석 워크플로우 중 `InferenceAnnotationStrategy`
219
+
220
+ **워크플로우:**
221
+ 1. 전처리기로부터 추론 결과 수신
222
+ 2. 예측, 경계 상자, 클래스 등 추출
223
+ 3. Synapse 작업 객체 스키마로 변환
224
+ 4. 필터링 또는 후처리 적용
225
+ 5. 형식화된 작업 객체 반환
226
+
227
+ **예제 구현:**
228
+
229
+ ```python
230
+ def convert_data_from_inference(self, data: dict) -> dict:
231
+ """YOLOv8 탐지 결과를 Synapse 작업 형식으로 변환."""
232
+
233
+ # 추론 결과에서 탐지 추출
234
+ detections = data.get('detections', [])
235
+
236
+ # 신뢰도 임계값으로 필터링
237
+ confidence_threshold = 0.5
238
+ task_objects = []
239
+
240
+ for idx, det in enumerate(detections):
241
+ confidence = det.get('confidence', 0)
242
+
243
+ # 낮은 신뢰도 탐지 건너뛰기
244
+ if confidence < confidence_threshold:
245
+ continue
246
+
247
+ # Synapse 형식으로 변환
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
+ # 변환 정보 로깅
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
+ ## SDK 데이터 컨버터 사용하기
275
+
276
+ Synapse SDK는 일반적인 주석 형식(COCO, YOLO, Pascal VOC)을 처리하는 내장 데이터 컨버터를 제공합니다. 커스텀 파싱 로직을 작성하는 대신 템플릿에서 이러한 컨버터를 활용하여 더 빠른 개발과 더 나은 안정성을 얻을 수 있습니다.
277
+
278
+ ### SDK 컨버터를 사용하는 이유?
279
+
280
+ - **검증 및 테스트 완료**: 컨버터는 SDK 팀이 유지관리하고 테스트합니다
281
+ - **표준 형식**: COCO, YOLO, Pascal VOC를 기본적으로 지원
282
+ - **코드 감소**: 형식 파서 재구현 방지
283
+ - **일관성**: 모든 플러그인에서 동일한 변환 로직
284
+ - **오류 처리**: 내장된 검증 및 오류 메시지
285
+
286
+ ### 사용 가능한 컨버터
287
+
288
+ | 컨버터 | 형식 | 방향 | 모듈 경로 | 사용 사례 |
289
+ |--------|------|------|-----------|----------|
290
+ | `COCOToDMConverter` | COCO JSON | External → DM | `synapse_sdk.utils.converters.coco` | COCO 형식 주석 |
291
+ | `YOLOToDMConverter` | YOLO .txt | External → DM | `synapse_sdk.utils.converters.yolo` | YOLO 형식 레이블 |
292
+ | `PascalToDMConverter` | Pascal VOC XML | External → DM | `synapse_sdk.utils.converters.pascal` | Pascal VOC 주석 |
293
+ | `DMV2ToV1Converter` | DM v2 | DM v2 → DM v1 | `synapse_sdk.utils.converters.dm` | 버전 변환 |
294
+ | `DMV1ToV2Converter` | DM v1 | DM v1 → DM v2 | `synapse_sdk.utils.converters.dm` | 버전 변환 |
295
+
296
+ **DM 형식**: Synapse의 내부 Data Manager 형식(작업 객체가 사용하는 형식)
297
+
298
+ ### 템플릿에서 컨버터 사용하기
299
+
300
+ 모든 To-DM 컨버터는 템플릿 사용을 위해 특별히 설계된 `convert_single_file()` 메서드를 제공합니다.
301
+
302
+ #### 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
+ """SDK 컨버터를 사용하여 COCO 주석 변환."""
317
+
318
+ # 주석 파일 다운로드
319
+ response = requests.get(data_file_url, timeout=30)
320
+ response.raise_for_status()
321
+ coco_data = response.json()
322
+
323
+ # 단일 파일 모드로 컨버터 생성
324
+ converter = COCOToDMConverter(is_single_conversion=True)
325
+
326
+ # 이미지 경로로 모의 파일 객체 생성
327
+ class FileObj:
328
+ def __init__(self, name):
329
+ self.name = name
330
+
331
+ # SDK 컨버터를 사용하여 변환
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
+ # DM 형식 데이터 반환
339
+ return result['dm_json']
340
+ ```
341
+
342
+ #### 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
+ """선택적 DM 버전 변환을 사용한 추론 결과 변환."""
350
+
351
+ # 추론 결과 처리
352
+ dm_v2_data = self._process_inference_results(data)
353
+
354
+ # 필요시 DM v2를 v1로 변환
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
+ ### 컨버터 예제
364
+
365
+ #### 예제 1: COCO 컨버터
366
+
367
+ `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
+ """주석 변환을 위한 SDK COCO 컨버터 사용."""
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
+ """SDK 컨버터를 사용하여 COCO JSON을 Synapse 작업 형식으로 변환."""
388
+
389
+ try:
390
+ # COCO 주석 파일 다운로드
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
+ # COCO 구조 검증
397
+ if 'annotations' not in coco_data or 'images' not in coco_data:
398
+ raise ValueError('Invalid COCO format: missing required fields')
399
+
400
+ # 단일 파일 변환을 위한 컨버터 생성
401
+ converter = COCOToDMConverter(is_single_conversion=True)
402
+
403
+ # 파일 객체 생성
404
+ class MockFile:
405
+ def __init__(self, path):
406
+ self.name = path
407
+
408
+ # SDK 컨버터를 사용하여 변환
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
+ # DM 형식 반환
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
+ """이 플러그인에서는 사용하지 않음."""
434
+ return data
435
+ ```
436
+
437
+ **지원되는 COCO 기능:**
438
+ - 경계 상자 (Bounding boxes)
439
+ - 키포인트 (Keypoints)
440
+ - 그룹 (bbox + keypoints)
441
+ - 카테고리 매핑
442
+ - 속성 (Attributes)
443
+
444
+ #### 예제 2: YOLO 컨버터
445
+
446
+ `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
+ """레이블 변환을 위한 SDK YOLO 컨버터 사용."""
455
+
456
+ def __init__(self, run, *args, **kwargs):
457
+ self.run = run
458
+ # YOLO 클래스 이름 (모델과 일치해야 함)
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
+ """SDK 컨버터를 사용하여 YOLO 레이블을 Synapse 작업 형식으로 변환."""
469
+
470
+ try:
471
+ # YOLO 레이블 파일 다운로드
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
+ # 레이블 라인 파싱
478
+ label_lines = [line.strip() for line in label_text.splitlines() if line.strip()]
479
+
480
+ # 클래스 이름으로 컨버터 생성
481
+ converter = YOLOToDMConverter(
482
+ is_single_conversion=True,
483
+ class_names=self.class_names
484
+ )
485
+
486
+ # 파일 객체 생성
487
+ class MockFile:
488
+ def __init__(self, path):
489
+ self.name = path
490
+
491
+ # SDK 컨버터를 사용하여 변환
492
+ result = converter.convert_single_file(
493
+ data=label_lines, # 레이블 문자열 리스트
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
+ """이 플러그인에서는 사용하지 않음."""
509
+ return data
510
+ ```
511
+
512
+ **지원되는 YOLO 기능:**
513
+ - 경계 상자 (표준 YOLO 형식)
514
+ - 폴리곤 (세그멘테이션 형식)
515
+ - 키포인트 (포즈 추정 형식)
516
+ - 자동 좌표 비정규화
517
+ - 클래스 이름 매핑
518
+
519
+ #### 예제 3: Pascal VOC 컨버터
520
+
521
+ `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
+ """XML 주석 변환을 위한 SDK Pascal VOC 컨버터 사용."""
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
+ """SDK 컨버터를 사용하여 Pascal VOC XML을 Synapse 작업 형식으로 변환."""
542
+
543
+ try:
544
+ # Pascal VOC XML 파일 다운로드
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
+ # 컨버터 생성
551
+ converter = PascalToDMConverter(is_single_conversion=True)
552
+
553
+ # 파일 객체 생성
554
+ class MockFile:
555
+ def __init__(self, path):
556
+ self.name = path
557
+
558
+ # SDK 컨버터를 사용하여 변환
559
+ result = converter.convert_single_file(
560
+ data=xml_content, # XML 문자열
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
+ """이 플러그인에서는 사용하지 않음."""
574
+ return data
575
+ ```
576
+
577
+ **지원되는 Pascal VOC 기능:**
578
+ - 경계 상자 (xmin, ymin, xmax, ymax)
579
+ - 객체 이름/클래스
580
+ - 자동 너비/높이 계산
581
+ - XML 파싱 및 검증
582
+
583
+ ### 컨버터 모범 사례
584
+
585
+ #### 1. 컨버터 사용 시기
586
+
587
+ **SDK 컨버터 사용:**
588
+ - 표준 형식(COCO, YOLO, Pascal VOC) 작업 시
589
+ - 신뢰할 수 있고 테스트된 변환 로직 필요 시
590
+ - 유지보수 부담 최소화 원할 때
591
+ - 복잡한 형식(키포인트가 있는 COCO, YOLO 세그멘테이션) 작업 시
592
+
593
+ **커스텀 코드 작성:**
594
+ - 형식이 비표준이거나 독점적인 경우
595
+ - 변환 전 특별한 전처리 필요 시
596
+ - 컨버터가 특정 변형을 지원하지 않는 경우
597
+ - 성능 최적화가 중요한 경우
598
+
599
+ #### 2. 컨버터 오류 처리
600
+
601
+ 항상 try-except 블록으로 컨버터 호출을 감싸세요:
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
+ # 컨버터의 검증 오류
612
+ self.run.log_message(f'Invalid data format: {str(e)}')
613
+ raise
614
+
615
+ except KeyError as e:
616
+ # 필수 필드 누락
617
+ self.run.log_message(f'Missing field in result: {str(e)}')
618
+ raise
619
+
620
+ except Exception as e:
621
+ # 예상치 못한 오류
622
+ self.run.log_message(f'Converter error: {str(e)}')
623
+ raise
624
+ ```
625
+
626
+ #### 3. 컨버터와 커스텀 로직 결합
627
+
628
+ 컨버터 출력을 후처리할 수 있습니다:
629
+
630
+ ```python
631
+ def convert_data_from_file(self, *args) -> dict:
632
+ # 기본 변환에 컨버터 사용
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
+ # 커스텀 후처리 추가
641
+ for img in dm_data.get('images', []):
642
+ for bbox in img.get('bounding_box', []):
643
+ # 커스텀 속성 추가
644
+ bbox['attrs'].append({
645
+ 'name': 'source',
646
+ 'value': 'yolo_model_v2'
647
+ })
648
+
649
+ # 크기로 필터링
650
+ if bbox['data'][2] < 10 or bbox['data'][3] < 10:
651
+ # 작은 박스 표시
652
+ bbox['attrs'].append({
653
+ 'name': 'too_small',
654
+ 'value': True
655
+ })
656
+
657
+ return dm_data
658
+ ```
659
+
660
+ #### 4. 성능 고려사항
661
+
662
+ **컨버터는 최적화되어 있지만:**
663
+ - 파일을 효율적으로 다운로드 (타임아웃, 큰 파일인 경우 스트리밍 사용)
664
+ - 여러 파일 처리 시 컨버터 인스턴스 캐싱
665
+ - 모니터링을 위한 변환 진행 로깅
666
+
667
+ ```python
668
+ def __init__(self, run, *args, **kwargs):
669
+ self.run = run
670
+ # 컨버터 인스턴스 캐시
671
+ self.coco_converter = COCOToDMConverter(is_single_conversion=True)
672
+
673
+ def convert_data_from_file(self, *args) -> dict:
674
+ # 캐시된 컨버터 재사용
675
+ result = self.coco_converter.convert_single_file(...)
676
+ return result['dm_json']
677
+ ```
678
+
679
+ #### 5. 컨버터 테스트
680
+
681
+ 컨버터 통합과 엣지 케이스 모두 테스트:
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
+ """COCO 컨버터 통합 테스트."""
694
+ converter = AnnotationToTask(MockRun())
695
+
696
+ # 유효한 COCO 데이터로 테스트
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
+ # DM 구조 검증
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
+ """잘못된 데이터에 대한 오류 처리 테스트."""
717
+ converter = AnnotationToTask(MockRun())
718
+
719
+ # 잘못된 COCO 데이터로 테스트
720
+ invalid_data = {'invalid': 'data'}
721
+
722
+ with pytest.raises(ValueError):
723
+ converter._convert_with_coco_converter(invalid_data, 'test.jpg')
724
+ ```
725
+
726
+ ### 컨버터 API 레퍼런스
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
+ **파라미터:**
739
+ - `data`: COCO 형식 딕셔너리 (JSON 내용)
740
+ - `original_file`: `.name` 속성이 있는 파일 객체
741
+ - `original_image_name`: 이미지 파일 이름
742
+
743
+ **반환값:**
744
+ ```python
745
+ {
746
+ 'dm_json': {...}, # DM 형식 데이터
747
+ 'image_path': str, # 파일 객체의 경로
748
+ 'image_name': str # 이미지의 베이스네임
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
+ **파라미터:**
762
+ - `data`: YOLO 레이블 라인 리스트 (.txt 파일의 문자열)
763
+ - `original_file`: `.name` 속성이 있는 파일 객체
764
+
765
+ **반환값:**
766
+ ```python
767
+ {
768
+ 'dm_json': {...}, # DM 형식 데이터
769
+ 'image_path': str, # 파일 객체의 경로
770
+ 'image_name': str # 이미지의 베이스네임
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
+ **파라미터:**
784
+ - `data`: Pascal VOC XML 내용 (문자열)
785
+ - `original_file`: `.name` 속성이 있는 파일 객체
786
+
787
+ **반환값:**
788
+ ```python
789
+ {
790
+ 'dm_json': {...}, # DM 형식 데이터
791
+ 'image_path': str, # 파일 객체의 경로
792
+ 'image_name': str # 이미지의 베이스네임
793
+ }
794
+ ```
795
+
796
+ ## 완전한 예제
797
+
798
+ ### 예제 1: COCO 형식 주석 플러그인
799
+
800
+ COCO 형식 주석을 Synapse 작업으로 변환하는 완전한 플러그인입니다.
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
+ """COCO 형식 주석을 Synapse 작업 객체로 변환."""
810
+
811
+ def __init__(self, run, *args, **kwargs):
812
+ self.run = run
813
+ # COCO 카테고리 ID를 Synapse 클래스 ID로 매핑
814
+ self.category_mapping = {
815
+ 1: 1, # person
816
+ 2: 2, # bicycle
817
+ 3: 3, # car
818
+ # 필요에 따라 더 많은 매핑 추가
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
+ """COCO JSON 파일을 Synapse 작업 형식으로 변환."""
829
+
830
+ try:
831
+ # COCO 주석 파일 다운로드
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
+ # COCO 구조 검증
838
+ if 'annotations' not in coco_data:
839
+ raise ValueError('Invalid COCO format: missing annotations')
840
+
841
+ # 주석 변환
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
+ """COCO 주석을 Synapse 작업 객체로 변환."""
870
+ task_objects = []
871
+
872
+ for idx, ann in enumerate(annotations):
873
+ # COCO 카테고리를 Synapse 클래스로 매핑
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
+ # bbox 형식 변환: [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
+ """이 플러그인에서는 사용하지 않음 - 파일 기반만."""
910
+ return data
911
+ ```
912
+
913
+ ### 예제 2: 객체 탐지 추론 플러그인
914
+
915
+ 객체 탐지 모델 출력을 변환하는 완전한 플러그인입니다.
916
+
917
+ ```python
918
+ # plugin/to_task.py
919
+ from typing import Dict, List
920
+
921
+ class AnnotationToTask:
922
+ """객체 탐지 추론 결과를 Synapse 작업으로 변환."""
923
+
924
+ def __init__(self, run, *args, **kwargs):
925
+ self.run = run
926
+ # 구성
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
+ """이 플러그인에서는 사용하지 않음 - 추론 기반만."""
939
+ return {}
940
+
941
+ def convert_data_from_inference(self, data: Dict) -> Dict:
942
+ """YOLOv8 탐지 결과를 Synapse 형식으로 변환."""
943
+
944
+ try:
945
+ # 예측 추출
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
+ # 탐지 필터링 및 변환
953
+ task_objects = self._process_detections(predictions)
954
+
955
+ # 필요시 NMS 적용
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
+ """탐지 처리 및 필터링."""
979
+ task_objects = []
980
+
981
+ for idx, pred in enumerate(predictions):
982
+ confidence = pred.get('confidence', 0.0)
983
+
984
+ # 신뢰도로 필터링
985
+ if confidence < self.confidence_threshold:
986
+ continue
987
+
988
+ # bbox 좌표 추출
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
+ """겹치는 박스를 줄이기 위해 Non-Maximum Suppression 적용."""
1013
+ # 신뢰도로 정렬
1014
+ sorted_dets = sorted(
1015
+ detections,
1016
+ key=lambda x: x['properties']['confidence'],
1017
+ reverse=True
1018
+ )
1019
+
1020
+ # 상위 N개 탐지 반환
1021
+ return sorted_dets[:self.max_detections]
1022
+ ```
1023
+
1024
+ ### 예제 3: 하이브리드 플러그인 (파일 + 추론)
1025
+
1026
+ 두 가지 주석 방법을 모두 지원하는 플러그인입니다.
1027
+
1028
+ ```python
1029
+ # plugin/to_task.py
1030
+ import requests
1031
+ import json
1032
+ from typing import Dict
1033
+
1034
+ class AnnotationToTask:
1035
+ """파일 및 추론 주석을 모두 지원하는 하이브리드 플러그인."""
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
+ """커스텀 JSON 주석 형식 처리."""
1049
+
1050
+ # 주석 파일 다운로드
1051
+ response = requests.get(data_file_url, timeout=30)
1052
+ response.raise_for_status()
1053
+ annotation_data = response.json()
1054
+
1055
+ # 커스텀 형식에서 변환
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
+ """검증이 포함된 추론 결과 처리."""
1071
+
1072
+ # 예측 추출 및 검증
1073
+ predictions = data.get('predictions', [])
1074
+
1075
+ task_objects = []
1076
+ for idx, pred in enumerate(predictions):
1077
+ # 필수 필드 검증
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
+ """예측에 필수 필드가 있는지 검증."""
1097
+ required_fields = ['class_id', 'bbox']
1098
+ return all(field in pred for field in required_fields)
1099
+ ```
1100
+
1101
+ ## 모범 사례
1102
+
1103
+ ### 1. 데이터 검증
1104
+
1105
+ 변환 전에 항상 입력 데이터를 검증하세요:
1106
+
1107
+ ```python
1108
+ def convert_data_from_file(self, *args) -> dict:
1109
+ # JSON 구조 검증
1110
+ if 'required_field' not in data:
1111
+ raise ValueError('Missing required field in annotation data')
1112
+
1113
+ # 데이터 타입 검증
1114
+ if not isinstance(data['objects'], list):
1115
+ raise TypeError('Objects must be a list')
1116
+
1117
+ # 값 검증
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. 오류 처리
1124
+
1125
+ 포괄적인 오류 처리를 구현하세요:
1126
+
1127
+ ```python
1128
+ def convert_data_from_file(self, *args) -> dict:
1129
+ try:
1130
+ # 변환 로직
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. 로깅
1151
+
1152
+ 로깅을 사용하여 변환 진행 상황을 추적하세요:
1153
+
1154
+ ```python
1155
+ def convert_data_from_inference(self, data: dict) -> dict:
1156
+ # 시작 로그
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
+ # 데이터 처리
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
+ # 완료 로그
1167
+ self.run.log_message('Conversion completed successfully')
1168
+
1169
+ return converted_data
1170
+ ```
1171
+
1172
+ ### 4. 구성
1173
+
1174
+ 플러그인을 구성 가능하게 만드세요:
1175
+
1176
+ ```python
1177
+ class AnnotationToTask:
1178
+ def __init__(self, run, *args, **kwargs):
1179
+ self.run = run
1180
+
1181
+ # 플러그인 params에서 구성 가져오기
1182
+ params = getattr(run, 'params', {})
1183
+ pre_processor_params = params.get('pre_processor_params', {})
1184
+
1185
+ # 구성 설정
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. 테스트
1192
+
1193
+ 변환을 철저히 테스트하세요:
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
+ """COCO 형식 변환 테스트."""
1206
+ converter = AnnotationToTask(MockRun())
1207
+
1208
+ # 모의 COCO 데이터
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
+ """신뢰도 임계값 필터링 테스트."""
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': {}}, # 임계값 미만
1234
+ {'confidence': 0.8, 'class_id': 3, 'bbox': {}},
1235
+ ]
1236
+
1237
+ result = converter._process_detections(predictions)
1238
+
1239
+ # 2개만 임계값 통과해야 함
1240
+ assert len(result) == 2
1241
+ ```
1242
+
1243
+ ## ToTaskAction과의 통합
1244
+
1245
+ ### 템플릿 메서드가 호출되는 방법
1246
+
1247
+ 템플릿 메서드는 워크플로우 실행 중 ToTaskAction 프레임워크에 의해 호출됩니다:
1248
+
1249
+ **파일 기반 주석 흐름:**
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
+ **추론 기반 주석 흐름:**
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
+ ### 템플릿 디버깅
1282
+
1283
+ 템플릿을 디버깅할 때:
1284
+
1285
+ 1. **로그 확인**: 로그 메시지에 대한 플러그인 실행 로그 검토
1286
+ 2. **반환 검증**: 반환 형식이 Synapse 작업 객체 스키마와 일치하는지 확인
1287
+ 3. **로컬 테스트**: 배포 전 변환 메서드를 독립적으로 테스트
1288
+ 4. **입력 검사**: 수신 중인 데이터를 확인하기 위해 입력 파라미터 로깅
1289
+ 5. **오류 처리**: 설명적인 메시지와 함께 예외 catch 및 로깅
1290
+
1291
+ ```python
1292
+ def convert_data_from_file(self, *args) -> dict:
1293
+ # 디버그 로깅
1294
+ self.run.log_message(f'Received URLs: primary={args[0]}, data={args[2]}')
1295
+
1296
+ try:
1297
+ # 로직
1298
+ result = process_data()
1299
+
1300
+ # 결과 검증
1301
+ self.run.log_message(f'Converted {len(result["objects"])} objects')
1302
+
1303
+ return result
1304
+
1305
+ except Exception as e:
1306
+ # 상세한 오류 로깅
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
+ ## 일반적인 함정
1314
+
1315
+ ### 1. 잘못된 반환 형식
1316
+
1317
+ **잘못됨:**
1318
+ ```python
1319
+ def convert_data_from_file(self, *args) -> dict:
1320
+ return [obj1, obj2, obj3] # 리스트 반환, dict 아님
1321
+ ```
1322
+
1323
+ **올바름:**
1324
+ ```python
1325
+ def convert_data_from_file(self, *args) -> dict:
1326
+ return {'objects': [obj1, obj2, obj3]} # 'objects' 키가 있는 dict 반환
1327
+ ```
1328
+
1329
+ ### 2. 오류 처리 누락
1330
+
1331
+ **잘못됨:**
1332
+ ```python
1333
+ def convert_data_from_file(self, *args) -> dict:
1334
+ response = requests.get(url) # 타임아웃 없음, 오류 처리 없음
1335
+ data = response.json()
1336
+ return data
1337
+ ```
1338
+
1339
+ **올바름:**
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. 로깅 사용 안 함
1353
+
1354
+ **잘못됨:**
1355
+ ```python
1356
+ def convert_data_from_inference(self, data: dict) -> dict:
1357
+ # 조용한 변환 - 가시성 없음
1358
+ return process(data)
1359
+ ```
1360
+
1361
+ **올바름:**
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
+ ## 관련 문서
1371
+
1372
+ - [ToTask 개요](./to-task-overview.md) - ToTask 액션 사용자 가이드
1373
+ - [ToTask 액션 개발](./to-task-action-development.md) - SDK 개발자 가이드
1374
+ - [Pre-annotation 플러그인 개요](./pre-annotation-plugin-overview.md) - 카테고리 개요
1375
+ - 플러그인 개발 가이드 - 일반 플러그인 개발
1376
+
1377
+ ## 템플릿 소스 코드
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`