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 템플릿 개발 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`
|