|
| 1 | +# pylint: disable=too-many-lines |
1 | 2 | import copy
|
2 | 3 | import logging
|
3 | 4 | import mimetypes
|
4 | 5 | import time
|
5 | 6 | import urllib
|
6 | 7 | import urllib.parse
|
| 8 | +import uuid |
7 | 9 | import warnings
|
| 10 | +from dataclasses import dataclass |
8 | 11 | from typing import Any, Callable, Dict, List, Optional
|
9 | 12 |
|
10 | 13 | import annofabapi.utils
|
11 | 14 | from annofabapi import AnnofabApi
|
12 | 15 | from annofabapi.exceptions import AnnofabApiException
|
13 |
| -from annofabapi.models import (AnnotationSpecsV1, InputData, Inspection, InspectionStatus, Instruction, JobInfo, |
14 |
| - JobStatus, JobType, MyOrganization, Organization, OrganizationMember, Project, |
15 |
| - ProjectMember, SupplementaryData, Task) |
16 |
| -from annofabapi.utils import allow_404_error |
| 16 | +from annofabapi.models import (AnnotationDataHoldingType, AnnotationSpecsV1, InputData, Inspection, InspectionStatus, |
| 17 | + Instruction, JobInfo, JobStatus, JobType, MyOrganization, Organization, |
| 18 | + OrganizationMember, Project, ProjectMember, SupplementaryData, Task) |
| 19 | +from annofabapi.utils import allow_404_error, first_true |
17 | 20 |
|
18 | 21 | logger = logging.getLogger(__name__)
|
19 | 22 |
|
20 | 23 |
|
| 24 | +@dataclass(frozen=True) |
| 25 | +class TaskFrameKey: |
| 26 | + project_id: str |
| 27 | + task_id: str |
| 28 | + input_data_id: str |
| 29 | + |
| 30 | + |
| 31 | +@dataclass(frozen=True) |
| 32 | +class ChoiceKey: |
| 33 | + additional_data_definition_id: str |
| 34 | + choice_id: str |
| 35 | + |
| 36 | + |
| 37 | +@dataclass(frozen=True) |
| 38 | +class AnnotationSpecsRelation: |
| 39 | + label_id: Dict[str, str] |
| 40 | + additional_data_definition_id: Dict[str, str] |
| 41 | + choice_id: Dict[ChoiceKey, ChoiceKey] |
| 42 | + |
| 43 | + |
21 | 44 | class Wrapper:
|
22 | 45 | """
|
23 | 46 | AnnofabApiのラッパー.
|
@@ -68,7 +91,7 @@ def _get_all_objects(func_get_list: Callable, limit: int, **kwargs_for_func_get_
|
68 | 91 | """
|
69 | 92 | get_all_XXX関数の共通処理
|
70 | 93 |
|
71 |
| - Args: |
| 94 | + Args:c |
72 | 95 | func_get_list: AnnofabApiのget_XXX関数
|
73 | 96 | limit: 1ページあたりの取得するデータ件数
|
74 | 97 | **kwargs_for_func_get_list: `func_get_list`に渡す引数。
|
@@ -158,12 +181,143 @@ def get_all_annotation_list(self, project_id: str,
|
158 | 181 | project_id: プロジェクトID
|
159 | 182 | query_params: `api.get_annotation_list` メソッドのQuery Parameter
|
160 | 183 |
|
161 |
| - Returns: |
| 184 | + Returns:l |
162 | 185 | すべてのアノテーション一覧
|
163 | 186 | """
|
164 | 187 | return self._get_all_objects(self.api.get_annotation_list, limit=200, project_id=project_id,
|
165 | 188 | query_params=query_params)
|
166 | 189 |
|
| 190 | + @staticmethod |
| 191 | + def __create_annotation_id(detail: Dict[str, Any]) -> str: |
| 192 | + if detail["data_holding_type"] == AnnotationDataHoldingType.INNER.value and detail["data"] is None: |
| 193 | + # annotation_typeがclassificationのときは、label_idとannotation_idを一致させる必要がある。 |
| 194 | + return detail["label_id"] |
| 195 | + else: |
| 196 | + return str(uuid.uuid4()) |
| 197 | + |
| 198 | + @staticmethod |
| 199 | + def __replace_annotation_specs_id(detail: Dict[str, Any], |
| 200 | + annotation_specs_relation: AnnotationSpecsRelation) -> Optional[Dict[str, Any]]: |
| 201 | + """ |
| 202 | + アノテーション仕様関係のIDを、新しいIDに置換する。 |
| 203 | +
|
| 204 | + Args: |
| 205 | + detail: (IN/OUT) 1個のアノテーション詳細情報 |
| 206 | +
|
| 207 | + Returns: |
| 208 | + IDを置換した後のアノテーション詳細情報. |
| 209 | + """ |
| 210 | + label_id = detail["label_id"] |
| 211 | + |
| 212 | + new_label_id = annotation_specs_relation.label_id.get(label_id) |
| 213 | + if new_label_id is None: |
| 214 | + return None |
| 215 | + else: |
| 216 | + detail["label_id"] = new_label_id |
| 217 | + |
| 218 | + additional_data_list = detail["additional_data_list"] |
| 219 | + new_additional_data_list = [] |
| 220 | + for additional_data in additional_data_list: |
| 221 | + additional_data_definition_id = additional_data["additional_data_definition_id"] |
| 222 | + new_additional_data_definition_id = annotation_specs_relation.additional_data_definition_id.get( |
| 223 | + additional_data_definition_id) |
| 224 | + if new_additional_data_definition_id is None: |
| 225 | + continue |
| 226 | + additional_data["additional_data_definition_id"] = new_additional_data_definition_id |
| 227 | + |
| 228 | + if additional_data["choice"] is not None: |
| 229 | + new_choice = annotation_specs_relation.choice_id.get( |
| 230 | + ChoiceKey(additional_data_definition_id, additional_data["choice"])) |
| 231 | + additional_data["choice"] = new_choice.choice_id if new_choice is not None else None |
| 232 | + |
| 233 | + new_additional_data_list.append(additional_data) |
| 234 | + |
| 235 | + detail["additional_data_list"] = new_additional_data_list |
| 236 | + return detail |
| 237 | + |
| 238 | + def __to_dest_annotation_detail( |
| 239 | + self, |
| 240 | + dest_project_id: str, |
| 241 | + detail: Dict[str, Any], |
| 242 | + account_id: str, |
| 243 | + ) -> Dict[str, Any]: |
| 244 | + """ |
| 245 | + コピー元の1個のアノテーションを、コピー先用に変換する。 |
| 246 | + 塗りつぶし画像の場合、S3にアップロードする。 |
| 247 | +
|
| 248 | + Notes: |
| 249 | + annotation_id をUUIDv4で生成すると、アノテーションリンク属性をコピーしたときに対応できないので、暫定的にannotation_idは維持するようにする。 |
| 250 | + """ |
| 251 | + dest_detail = detail |
| 252 | + dest_detail["account_id"] = account_id |
| 253 | + |
| 254 | + if detail["data_holding_type"] == AnnotationDataHoldingType.OUTER.value: |
| 255 | + outer_file_url = detail["url"] |
| 256 | + src_response = self.api.session.get(outer_file_url) |
| 257 | + s3_path = self.upload_data_to_s3(dest_project_id, data=src_response.content, |
| 258 | + content_type=src_response.headers["Content-Type"]) |
| 259 | + logger.debug("%s に塗りつぶし画像をアップロードしました。", s3_path) |
| 260 | + dest_detail["path"] = s3_path |
| 261 | + dest_detail["url"] = None |
| 262 | + dest_detail["etag"] = None |
| 263 | + |
| 264 | + return dest_detail |
| 265 | + |
| 266 | + def __create_request_body_for_copy_annotation( |
| 267 | + self, project_id: str, task_id: str, input_data_id: str, src_details: List[Dict[str, Any]], account_id: str, |
| 268 | + annotation_specs_relation: Optional[AnnotationSpecsRelation] = None) -> Dict[str, Any]: |
| 269 | + dest_details: List[Dict[str, Any]] = [] |
| 270 | + |
| 271 | + for src_detail in src_details: |
| 272 | + if annotation_specs_relation is not None: |
| 273 | + tmp_detail = self.__replace_annotation_specs_id(src_detail, annotation_specs_relation) |
| 274 | + if tmp_detail is None: |
| 275 | + continue |
| 276 | + src_detail = tmp_detail |
| 277 | + |
| 278 | + dest_detail = self.__to_dest_annotation_detail(project_id, src_detail, account_id=account_id) |
| 279 | + dest_details.append(dest_detail) |
| 280 | + |
| 281 | + request_body = { |
| 282 | + "project_id": project_id, |
| 283 | + "task_id": task_id, |
| 284 | + "input_data_id": input_data_id, |
| 285 | + "details": dest_details, |
| 286 | + } |
| 287 | + return request_body |
| 288 | + |
| 289 | + def copy_annotation(self, src: TaskFrameKey, dest: TaskFrameKey, account_id: str, |
| 290 | + annotation_specs_relation: Optional[AnnotationSpecsRelation] = None) -> bool: |
| 291 | + """ |
| 292 | + アノテーションをコピーする。 |
| 293 | +
|
| 294 | + Args: |
| 295 | + src: コピー元のTaskFrame情報 |
| 296 | + dest: コピー先のTaskFrame情報 |
| 297 | + account_id: アノテーションを登録するユーザのアカウントID |
| 298 | + annotation_specs_relation: アノテーション仕様間の紐付け情報 |
| 299 | +
|
| 300 | + Returns: |
| 301 | + アノテーションのコピー実施したかどうか |
| 302 | +
|
| 303 | + """ |
| 304 | + src_annotation, _ = self.api.get_editor_annotation(src.project_id, src.task_id, src.input_data_id) |
| 305 | + src_annotation_details: List[Dict[str, Any]] = src_annotation["details"] |
| 306 | + |
| 307 | + if len(src_annotation_details) == 0: |
| 308 | + logger.debug("コピー元にアノテーションが1つもないため、アノテーションのコピーをスキップします。") |
| 309 | + return False |
| 310 | + |
| 311 | + old_dest_annotation, _ = self.api.get_editor_annotation(dest.project_id, dest.task_id, dest.input_data_id) |
| 312 | + updated_datetime = old_dest_annotation["updated_datetime"] |
| 313 | + |
| 314 | + request_body = self.__create_request_body_for_copy_annotation( |
| 315 | + dest.project_id, dest.task_id, dest.input_data_id, src_details=src_annotation_details, |
| 316 | + account_id=account_id, annotation_specs_relation=annotation_specs_relation) |
| 317 | + request_body["updated_datetime"] = updated_datetime |
| 318 | + self.api.put_annotation(dest.project_id, dest.task_id, dest.input_data_id, request_body=request_body) |
| 319 | + return True |
| 320 | + |
167 | 321 | #########################################
|
168 | 322 | # Public Method : AnnotationSpecs
|
169 | 323 | #########################################
|
@@ -195,6 +349,78 @@ def copy_annotation_specs(self, src_project_id: str, dest_project_id: str,
|
195 | 349 | }
|
196 | 350 | return self.api.put_annotation_specs(dest_project_id, request_body=request_body)[0]
|
197 | 351 |
|
| 352 | + @staticmethod |
| 353 | + def __get_label_name_en(label: Dict[str, Any]) -> str: |
| 354 | + """label情報から英語名を取得する""" |
| 355 | + label_name_messages = label["label_name"]["messages"] |
| 356 | + return [e["message"] for e in label_name_messages if e["lang"] == "en-US"][0] |
| 357 | + |
| 358 | + @staticmethod |
| 359 | + def __get_additional_data_definition_name_en(additional_data_definition: Dict[str, Any]) -> str: |
| 360 | + """additional_data_definitionから英語名を取得する""" |
| 361 | + messages = additional_data_definition["name"]["messages"] |
| 362 | + return [e["message"] for e in messages if e["lang"] == "en-US"][0] |
| 363 | + |
| 364 | + @staticmethod |
| 365 | + def __get_choice_name_en(choice: Dict[str, Any]) -> str: |
| 366 | + """choiceから英語名を取得する""" |
| 367 | + messages = choice["name"]["messages"] |
| 368 | + return [e["message"] for e in messages if e["lang"] == "en-US"][0] |
| 369 | + |
| 370 | + def get_annotation_specs_relation(self, src_project_id: str, dest_project_id: str) -> AnnotationSpecsRelation: |
| 371 | + """ |
| 372 | + プロジェクト間のアノテーション仕様の紐付け情報を取得する。ラベル、属性、選択肢の英語名で紐付ける。 |
| 373 | + 紐付け先がない場合は無視する。 |
| 374 | + ``copy_annotation`` メソッドで利用する。 |
| 375 | +
|
| 376 | + Args: |
| 377 | + src_project_id: 紐付け元のプロジェクトID |
| 378 | + dest_project_id: 紐付け先のプロジェクトID |
| 379 | +
|
| 380 | + Returns: |
| 381 | + アノテーション仕様の紐付け情報 |
| 382 | +
|
| 383 | + """ |
| 384 | + src_annotation_specs, _ = self.api.get_annotation_specs(src_project_id, query_params={"v": "2"}) |
| 385 | + dest_annotation_specs, _ = self.api.get_annotation_specs(dest_project_id, query_params={"v": "2"}) |
| 386 | + dest_labels = dest_annotation_specs["labels"] |
| 387 | + dest_additionals = dest_annotation_specs["additionals"] |
| 388 | + |
| 389 | + dict_label_id: Dict[str, str] = {} |
| 390 | + for src_label in src_annotation_specs["labels"]: |
| 391 | + src_label_name_en = self.__get_label_name_en(src_label) |
| 392 | + dest_label = first_true(dest_labels, pred=lambda e, f=src_label_name_en: self.__get_label_name_en(e) == f) |
| 393 | + if dest_label is not None: |
| 394 | + dict_label_id[src_label["label_id"]] = dest_label["label_id"] |
| 395 | + |
| 396 | + dict_additional_data_definition_id: Dict[str, str] = {} |
| 397 | + dict_choice_id: Dict[ChoiceKey, ChoiceKey] = {} |
| 398 | + for src_additional in src_annotation_specs["additionals"]: |
| 399 | + src_additional_name_en = self.__get_additional_data_definition_name_en(src_additional) |
| 400 | + dest_additional = first_true( |
| 401 | + dest_additionals, |
| 402 | + pred=lambda e, f=src_additional_name_en: self.__get_additional_data_definition_name_en(e) == f) |
| 403 | + if dest_additional is None: |
| 404 | + continue |
| 405 | + |
| 406 | + dict_additional_data_definition_id[ |
| 407 | + src_additional["additional_data_definition_id"]] = dest_additional["additional_data_definition_id"] |
| 408 | + |
| 409 | + dest_choices = dest_additional["choices"] |
| 410 | + for src_choice in src_additional["choices"]: |
| 411 | + src_choice_name_en = self.__get_choice_name_en(src_choice) |
| 412 | + dest_choice = first_true(dest_choices, |
| 413 | + pred=lambda e, f=src_choice_name_en: self.__get_choice_name_en(e) == f) |
| 414 | + if dest_choice is not None: |
| 415 | + dict_choice_id[ChoiceKey(src_additional["additional_data_definition_id"], |
| 416 | + src_choice["choice_id"])] = ChoiceKey( |
| 417 | + dest_additional["additional_data_definition_id"], |
| 418 | + dest_choice["choice_id"]) |
| 419 | + |
| 420 | + return AnnotationSpecsRelation(label_id=dict_label_id, |
| 421 | + additional_data_definition_id=dict_additional_data_definition_id, |
| 422 | + choice_id=dict_choice_id) |
| 423 | + |
198 | 424 | #########################################
|
199 | 425 | # Public Method : Input
|
200 | 426 | #########################################
|
|
0 commit comments