annofabcli 1.100.5__py3-none-any.whl → 1.102.0__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.
- annofabcli/annotation/change_annotation_attributes.py +3 -2
- annofabcli/annotation/change_annotation_properties.py +8 -7
- annofabcli/annotation/copy_annotation.py +4 -4
- annofabcli/annotation/delete_annotation.py +254 -29
- annofabcli/annotation/dump_annotation.py +14 -7
- annofabcli/annotation/import_annotation.py +304 -227
- annofabcli/annotation/restore_annotation.py +7 -7
- annofabcli/annotation_specs/list_annotation_specs_attribute.py +1 -1
- annofabcli/annotation_specs/list_annotation_specs_choice.py +1 -1
- annofabcli/annotation_specs/list_annotation_specs_label.py +1 -1
- annofabcli/annotation_specs/list_annotation_specs_label_attribute.py +1 -1
- annofabcli/comment/delete_comment.py +7 -5
- annofabcli/comment/put_comment.py +1 -1
- annofabcli/comment/put_comment_simply.py +7 -5
- annofabcli/common/cli.py +10 -10
- annofabcli/common/download.py +28 -29
- annofabcli/common/image.py +4 -2
- annofabcli/input_data/delete_input_data.py +4 -4
- annofabcli/input_data/update_metadata_of_input_data.py +1 -1
- annofabcli/instruction/upload_instruction.py +2 -2
- annofabcli/statistics/list_annotation_area.py +320 -0
- annofabcli/statistics/subcommand_statistics.py +2 -0
- annofabcli/statistics/visualize_statistics.py +1 -7
- annofabcli/supplementary/delete_supplementary_data.py +8 -4
- {annofabcli-1.100.5.dist-info → annofabcli-1.102.0.dist-info}/METADATA +3 -2
- {annofabcli-1.100.5.dist-info → annofabcli-1.102.0.dist-info}/RECORD +29 -29
- annofabcli/__version__.py +0 -1
- {annofabcli-1.100.5.dist-info → annofabcli-1.102.0.dist-info}/WHEEL +0 -0
- {annofabcli-1.100.5.dist-info → annofabcli-1.102.0.dist-info}/entry_points.txt +0 -0
- {annofabcli-1.100.5.dist-info → annofabcli-1.102.0.dist-info}/licenses/LICENSE +0 -0
|
@@ -13,13 +13,10 @@ from pathlib import Path
|
|
|
13
13
|
from typing import Any, Optional, Union
|
|
14
14
|
|
|
15
15
|
import annofabapi
|
|
16
|
-
|
|
16
|
+
import ulid
|
|
17
17
|
from annofabapi.models import (
|
|
18
18
|
AdditionalDataDefinitionType,
|
|
19
|
-
AdditionalDataDefinitionV1,
|
|
20
|
-
AnnotationDataHoldingType,
|
|
21
19
|
DefaultAnnotationType,
|
|
22
|
-
LabelV1,
|
|
23
20
|
ProjectMemberRole,
|
|
24
21
|
TaskStatus,
|
|
25
22
|
)
|
|
@@ -29,10 +26,11 @@ from annofabapi.parser import (
|
|
|
29
26
|
lazy_parse_simple_annotation_dir_by_task,
|
|
30
27
|
lazy_parse_simple_annotation_zip_by_task,
|
|
31
28
|
)
|
|
32
|
-
from annofabapi.plugin import ThreeDimensionAnnotationType
|
|
33
|
-
from annofabapi.
|
|
29
|
+
from annofabapi.plugin import EditorPluginId, ThreeDimensionAnnotationType
|
|
30
|
+
from annofabapi.pydantic_models.input_data_type import InputDataType
|
|
31
|
+
from annofabapi.util.annotation_specs import AnnotationSpecsAccessor, get_choice
|
|
32
|
+
from annofabapi.utils import can_put_annotation
|
|
34
33
|
from dataclasses_json import DataClassJsonMixin
|
|
35
|
-
from more_itertools import first_true
|
|
36
34
|
|
|
37
35
|
import annofabcli
|
|
38
36
|
from annofabcli.common.cli import (
|
|
@@ -44,7 +42,8 @@ from annofabcli.common.cli import (
|
|
|
44
42
|
build_annofabapi_resource_and_login,
|
|
45
43
|
)
|
|
46
44
|
from annofabcli.common.facade import AnnofabApiFacade
|
|
47
|
-
from annofabcli.common.
|
|
45
|
+
from annofabcli.common.type_util import assert_noreturn
|
|
46
|
+
from annofabcli.common.visualize import AddProps
|
|
48
47
|
|
|
49
48
|
logger = logging.getLogger(__name__)
|
|
50
49
|
|
|
@@ -78,272 +77,310 @@ class ImportedSimpleAnnotation(DataClassJsonMixin):
|
|
|
78
77
|
"""矩形、ポリゴン、全体アノテーションなど個々のアノテーションの配列。"""
|
|
79
78
|
|
|
80
79
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
all_yes: bool,
|
|
88
|
-
is_force: bool,
|
|
89
|
-
is_merge: bool,
|
|
90
|
-
is_overwrite: bool,
|
|
91
|
-
) -> None:
|
|
92
|
-
self.service = service
|
|
93
|
-
self.facade = AnnofabApiFacade(service)
|
|
94
|
-
CommandLineWithConfirm.__init__(self, all_yes)
|
|
80
|
+
def is_image_segmentation_label(label_info: dict[str, Any]) -> bool:
|
|
81
|
+
"""
|
|
82
|
+
ラベルの種類が、画像プロジェクトのセグメンテーションかどうか
|
|
83
|
+
"""
|
|
84
|
+
annotation_type = label_info["annotation_type"]
|
|
85
|
+
return annotation_type in {DefaultAnnotationType.SEGMENTATION.value, DefaultAnnotationType.SEGMENTATION_V2.value}
|
|
95
86
|
|
|
96
|
-
self.project_id = project_id
|
|
97
|
-
self.is_force = is_force
|
|
98
|
-
self.is_merge = is_merge
|
|
99
|
-
self.is_overwrite = is_overwrite
|
|
100
|
-
self.visualize = AddProps(service, project_id)
|
|
101
87
|
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
88
|
+
def is_3dpc_segment_label(label_info: dict[str, Any]) -> bool:
|
|
89
|
+
"""
|
|
90
|
+
3次元のセグメントかどうか
|
|
91
|
+
"""
|
|
92
|
+
# 理想はプラグイン情報を見て、3次元アノテーションの種類かどうか判定した方がよい
|
|
93
|
+
# が、当分は文字列判定だけでも十分なので、文字列で判定する
|
|
94
|
+
return label_info["annotation_type"] in {
|
|
95
|
+
ThreeDimensionAnnotationType.INSTANCE_SEGMENT.value,
|
|
96
|
+
ThreeDimensionAnnotationType.SEMANTIC_SEGMENT.value,
|
|
97
|
+
}
|
|
107
98
|
|
|
108
|
-
logger.warning(f"アノテーション仕様に label_name='{label_name}' のラベルが存在しません。")
|
|
109
|
-
return None
|
|
110
99
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
if additional_data_name_en is not None and additional_data_name_en == attribute_name:
|
|
117
|
-
return additional_data
|
|
100
|
+
def is_3dpc_project(project: dict[str, Any]) -> bool:
|
|
101
|
+
"""3次元プロジェクトか否か"""
|
|
102
|
+
editor_plugin_id = project["configuration"]["plugin_id"]
|
|
103
|
+
return project["input_data_type"] == InputDataType.CUSTOM.value and editor_plugin_id == EditorPluginId.THREE_DIMENSION.value
|
|
104
|
+
|
|
118
105
|
|
|
119
|
-
|
|
106
|
+
def create_annotation_id(label_info: dict[str, Any], project: dict[str, Any]) -> str:
|
|
107
|
+
"""
|
|
108
|
+
デフォルトのアノテーションIDを生成します。
|
|
109
|
+
"""
|
|
110
|
+
if is_3dpc_project(project):
|
|
111
|
+
# 3次元エディタ画面ではULIDが発行されるので、それに合わせる
|
|
112
|
+
return str(ulid.new())
|
|
113
|
+
elif label_info["annotation_type"] == DefaultAnnotationType.CLASSIFICATION.value:
|
|
114
|
+
# 全体アノテーションの場合、annotation_idはlabel_idである必要がある
|
|
115
|
+
return label_info["label_id"]
|
|
116
|
+
else:
|
|
117
|
+
return str(uuid.uuid4())
|
|
118
|
+
|
|
119
|
+
|
|
120
|
+
def get_3dpc_segment_data_uri(annotation_data: dict[str, Any]) -> str:
|
|
121
|
+
"""
|
|
122
|
+
3DセグメントのURIを取得する
|
|
123
|
+
|
|
124
|
+
Notes:
|
|
125
|
+
3dpc editorに依存したコード。annofab側でSimple Annotationのフォーマットが改善されたら、このコードを削除する
|
|
126
|
+
"""
|
|
127
|
+
data_uri = annotation_data["data"]
|
|
128
|
+
# この時点で data_uriは f"./{input_data_id}/{annotation_id}"
|
|
129
|
+
# parser.open_data_uriメソッドに渡す値は、先頭のinput_data_idは不要なので、これを取り除く
|
|
130
|
+
path = Path(data_uri)
|
|
131
|
+
return str(path.relative_to(path.parts[0]))
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
class AnnotationConverter:
|
|
135
|
+
"""
|
|
136
|
+
Simpleアノテーションを、`put_annotation` API(v2)のリクエストボディに変換するクラスです。
|
|
137
|
+
|
|
138
|
+
Args:
|
|
139
|
+
project: プロジェクト情報
|
|
140
|
+
annotation_specs: アノテーション仕様情報(v3)
|
|
141
|
+
is_strict: Trueの場合、存在しない属性名や属性値の型不一致があった場合に例外を発生させる
|
|
142
|
+
service: Annofab APIにアクセスするためのサービスオブジェクト。外部アノテーションをアップロードするのに利用する。
|
|
143
|
+
"""
|
|
144
|
+
|
|
145
|
+
def __init__(self, project: dict[str, Any], annotation_specs: dict[str, Any], *, service: annofabapi.Resource, is_strict: bool = False) -> None:
|
|
146
|
+
self.project = project
|
|
147
|
+
self.project_id = project["project_id"]
|
|
148
|
+
self.annotation_specs = annotation_specs
|
|
149
|
+
self.annotation_specs_accessor = AnnotationSpecsAccessor(annotation_specs)
|
|
150
|
+
self.is_strict = is_strict
|
|
151
|
+
self.service = service
|
|
152
|
+
|
|
153
|
+
def _convert_attribute_value( # noqa: PLR0911, PLR0912
|
|
154
|
+
self,
|
|
155
|
+
attribute_value: Union[str, int, bool], # noqa: FBT001
|
|
156
|
+
additional_data_type: AdditionalDataDefinitionType,
|
|
157
|
+
attribute_name: str,
|
|
158
|
+
choices: list[dict[str, Any]],
|
|
159
|
+
*,
|
|
160
|
+
log_message_suffix: str,
|
|
161
|
+
) -> Optional[dict[str, Any]]:
|
|
162
|
+
if additional_data_type == AdditionalDataDefinitionType.FLAG:
|
|
163
|
+
if not isinstance(attribute_value, bool):
|
|
164
|
+
message = f"属性'{attribute_name}'に対応する属性値の型は bool である必要があります。 :: attribute_value='{attribute_value}', additional_data_type='{additional_data_type}' :: {log_message_suffix}" # noqa: E501
|
|
165
|
+
logger.warning(message)
|
|
166
|
+
if self.is_strict:
|
|
167
|
+
raise ValueError(message)
|
|
168
|
+
return None
|
|
169
|
+
return {"_type": "Flag", "value": attribute_value}
|
|
170
|
+
|
|
171
|
+
elif additional_data_type == AdditionalDataDefinitionType.INTEGER:
|
|
172
|
+
if not isinstance(attribute_value, int):
|
|
173
|
+
message = f"属性'{attribute_name}'に対応する属性値の型は int である必要があります。 :: attribute_value='{attribute_value}', additional_data_type='{additional_data_type}' :: {log_message_suffix}" # noqa: E501
|
|
174
|
+
logger.warning(message)
|
|
175
|
+
if self.is_strict:
|
|
176
|
+
raise ValueError(message)
|
|
177
|
+
return None
|
|
178
|
+
return {"_type": "Integer", "value": attribute_value}
|
|
179
|
+
|
|
180
|
+
# 以降の属性は、属性値の型はstr型であるが、型をチェックしない。
|
|
181
|
+
# str型に変換しても、特に期待していない動作にならないと思われるため
|
|
182
|
+
elif additional_data_type == AdditionalDataDefinitionType.COMMENT:
|
|
183
|
+
return {"_type": "Comment", "value": str(attribute_value)}
|
|
184
|
+
|
|
185
|
+
elif additional_data_type == AdditionalDataDefinitionType.TEXT:
|
|
186
|
+
return {"_type": "Text", "value": str(attribute_value)}
|
|
187
|
+
|
|
188
|
+
elif additional_data_type == AdditionalDataDefinitionType.TRACKING:
|
|
189
|
+
return {"_type": "Tracking", "value": str(attribute_value)}
|
|
190
|
+
|
|
191
|
+
elif additional_data_type == AdditionalDataDefinitionType.LINK:
|
|
192
|
+
if attribute_value == "":
|
|
193
|
+
# `annotation_id`に空文字を設定すると、エディタ画面ではエラーになるため、Noneを返す
|
|
194
|
+
return None
|
|
195
|
+
return {"_type": "Link", "annotation_id": str(attribute_value)}
|
|
196
|
+
|
|
197
|
+
elif additional_data_type == AdditionalDataDefinitionType.CHOICE:
|
|
198
|
+
if attribute_value == "":
|
|
199
|
+
return None
|
|
200
|
+
try:
|
|
201
|
+
choice = get_choice(choices, choice_name=str(attribute_value))
|
|
202
|
+
except ValueError:
|
|
203
|
+
logger.warning(
|
|
204
|
+
f"アノテーション仕様の属性'{attribute_name}'に選択肢名(英語)が'{attribute_value}'である選択肢情報は存在しないか、複数存在します。 :: {log_message_suffix}" # noqa: E501
|
|
205
|
+
)
|
|
206
|
+
if self.is_strict:
|
|
207
|
+
raise
|
|
208
|
+
return None
|
|
209
|
+
|
|
210
|
+
return {"_type": "Choice", "choice_id": choice["choice_id"]}
|
|
211
|
+
elif additional_data_type == AdditionalDataDefinitionType.SELECT:
|
|
212
|
+
if attribute_value == "":
|
|
213
|
+
return None
|
|
214
|
+
try:
|
|
215
|
+
choice = get_choice(choices, choice_name=str(attribute_value))
|
|
216
|
+
except ValueError:
|
|
217
|
+
logger.warning(
|
|
218
|
+
f"アノテーション仕様の属性'{attribute_name}'に選択肢名(英語)が'{attribute_value}'である選択肢情報は存在しないか、複数存在します。 :: {log_message_suffix}" # noqa: E501
|
|
219
|
+
)
|
|
220
|
+
if self.is_strict:
|
|
221
|
+
raise
|
|
222
|
+
return None
|
|
223
|
+
|
|
224
|
+
return {"_type": "Select", "choice_id": choice["choice_id"]}
|
|
120
225
|
|
|
121
|
-
def _get_choice_id_from_name(self, name: str, choices: list[dict[str, Any]]) -> Optional[str]:
|
|
122
|
-
choice_info = first_true(choices, pred=lambda e: self.facade.get_choice_name_en(e) == name)
|
|
123
|
-
if choice_info is not None:
|
|
124
|
-
return choice_info["choice_id"]
|
|
125
226
|
else:
|
|
126
|
-
|
|
227
|
+
assert_noreturn(additional_data_type)
|
|
127
228
|
|
|
128
|
-
|
|
129
|
-
|
|
229
|
+
def convert_attributes(
|
|
230
|
+
self, attributes: dict[str, Any], *, label_name: Optional[str] = None, log_message_suffix: str = ""
|
|
231
|
+
) -> list[dict[str, Any]]:
|
|
130
232
|
"""
|
|
131
|
-
|
|
233
|
+
インポート対象のアノテーションJSONに格納されている`attributes`を`AdditionalDataListV2`のlistに変換します。
|
|
132
234
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
return str(path.relative_to(path.parts[0]))
|
|
141
|
-
|
|
142
|
-
@classmethod
|
|
143
|
-
def _is_3dpc_segment_label(cls, label_info: dict[str, Any]) -> bool:
|
|
144
|
-
"""
|
|
145
|
-
3次元のセグメントかどうか
|
|
235
|
+
Args:
|
|
236
|
+
attributes: インポート対象のアノテーションJSONに格納されている`attributes`
|
|
237
|
+
label_name: `attributes`に紐づくラベルの名前。指定した場合は、指定したラベルに紐づく属性を探します。
|
|
238
|
+
log_message_suffix: ログメッセージのサフィックス
|
|
239
|
+
|
|
240
|
+
Raises:
|
|
241
|
+
ValueError: `self.is_strict`がTrueの場合:存在しない属性名が指定されたり、属性値の型が適切でない
|
|
146
242
|
"""
|
|
147
|
-
|
|
148
|
-
# が、当分は文字列判定だけでも十分なので、文字列で判定する
|
|
149
|
-
if label_info["annotation_type"] in {
|
|
150
|
-
ThreeDimensionAnnotationType.INSTANCE_SEGMENT.value,
|
|
151
|
-
ThreeDimensionAnnotationType.SEMANTIC_SEGMENT.value,
|
|
152
|
-
}:
|
|
153
|
-
return True
|
|
154
|
-
|
|
155
|
-
# 標準の3次元アノテーション仕様用のプラグインを利用していない場合
|
|
156
|
-
# TODO すべての3次元プロジェクトが、標準の3次元アノテーション仕様用のプラグインを利用するようになったら、この処理を削除する
|
|
157
|
-
if label_info["annotation_type"] == DefaultAnnotationType.CUSTOM.value:
|
|
158
|
-
metadata = label_info["metadata"]
|
|
159
|
-
if metadata.get("type") == "SEGMENT":
|
|
160
|
-
return True
|
|
161
|
-
|
|
162
|
-
return False
|
|
163
|
-
|
|
164
|
-
@classmethod
|
|
165
|
-
def _get_data_holding_type_from_data(cls, label_info: dict[str, Any]) -> AnnotationDataHoldingType:
|
|
166
|
-
annotation_type = label_info["annotation_type"]
|
|
167
|
-
if annotation_type in [DefaultAnnotationType.SEGMENTATION.value, DefaultAnnotationType.SEGMENTATION_V2.value]:
|
|
168
|
-
return AnnotationDataHoldingType.OUTER
|
|
169
|
-
|
|
170
|
-
# TODO: 3dpc editorに依存したコード。annofab側でSimple Annotationのフォーマットが改善されたら、このコードを削除する
|
|
171
|
-
if cls._is_3dpc_segment_label(label_info):
|
|
172
|
-
return AnnotationDataHoldingType.OUTER
|
|
173
|
-
|
|
174
|
-
return AnnotationDataHoldingType.INNER
|
|
175
|
-
|
|
176
|
-
def _to_additional_data_list(self, attributes: dict[str, Any], label_info: LabelV1) -> list[AdditionalDataV1]:
|
|
177
|
-
additional_data_list: list[AdditionalDataV1] = []
|
|
178
|
-
for key, value in attributes.items():
|
|
179
|
-
specs_additional_data = self._get_additional_data_from_attribute_name(key, label_info)
|
|
180
|
-
if specs_additional_data is None:
|
|
181
|
-
logger.warning(f"アノテーション仕様に attribute_name='{key}' が存在しません。")
|
|
182
|
-
continue
|
|
243
|
+
additional_data_list: list[dict[str, Any]] = []
|
|
183
244
|
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
additional_data.integer = value
|
|
196
|
-
elif additional_data_type in [
|
|
197
|
-
AdditionalDataDefinitionType.TEXT,
|
|
198
|
-
AdditionalDataDefinitionType.COMMENT,
|
|
199
|
-
AdditionalDataDefinitionType.TRACKING,
|
|
200
|
-
AdditionalDataDefinitionType.LINK,
|
|
201
|
-
]:
|
|
202
|
-
additional_data.comment = value
|
|
203
|
-
elif additional_data_type in [AdditionalDataDefinitionType.CHOICE, AdditionalDataDefinitionType.SELECT]:
|
|
204
|
-
additional_data.choice = self._get_choice_id_from_name(value, specs_additional_data["choices"])
|
|
205
|
-
else:
|
|
206
|
-
logger.warning(f"additional_data_type='{additional_data_type}'が不正です。")
|
|
245
|
+
label = self.annotation_specs_accessor.get_label(label_name=label_name) if label_name is not None else None
|
|
246
|
+
|
|
247
|
+
for attribute_name, attribute_value in attributes.items():
|
|
248
|
+
try:
|
|
249
|
+
specs_additional_data = self.annotation_specs_accessor.get_attribute(attribute_name=attribute_name, label=label)
|
|
250
|
+
except ValueError:
|
|
251
|
+
logger.warning(
|
|
252
|
+
f"アノテーション仕様に属性名(英語)が'{attribute_name}'である属性情報が存在しないか、複数存在します。 :: {log_message_suffix}"
|
|
253
|
+
)
|
|
254
|
+
if self.is_strict:
|
|
255
|
+
raise
|
|
207
256
|
continue
|
|
208
257
|
|
|
258
|
+
additional_data = {
|
|
259
|
+
"definition_id": specs_additional_data["additional_data_definition_id"],
|
|
260
|
+
"value": self._convert_attribute_value(
|
|
261
|
+
attribute_value,
|
|
262
|
+
AdditionalDataDefinitionType(specs_additional_data["type"]),
|
|
263
|
+
attribute_name,
|
|
264
|
+
specs_additional_data["choices"],
|
|
265
|
+
log_message_suffix=log_message_suffix,
|
|
266
|
+
),
|
|
267
|
+
}
|
|
268
|
+
|
|
209
269
|
additional_data_list.append(additional_data)
|
|
210
270
|
|
|
211
271
|
return additional_data_list
|
|
212
272
|
|
|
213
|
-
def
|
|
214
|
-
self,
|
|
215
|
-
|
|
273
|
+
def convert_annotation_detail(
|
|
274
|
+
self,
|
|
275
|
+
parser: SimpleAnnotationParser,
|
|
276
|
+
detail: ImportedSimpleAnnotationDetail,
|
|
277
|
+
*,
|
|
278
|
+
log_message_suffix: str = "",
|
|
279
|
+
) -> dict[str, Any]:
|
|
216
280
|
"""
|
|
217
|
-
|
|
281
|
+
アノテーションJSONに記載された1個のアノテーション情報を、`put_annotation` APIに渡す形式に変換する。
|
|
282
|
+
Outerアノテーションならば、AWS S3に外部ファイルをアップロードします。
|
|
283
|
+
|
|
218
284
|
|
|
219
285
|
Args:
|
|
220
286
|
parser:
|
|
221
|
-
detail:
|
|
287
|
+
detail: インポート対象の1個のアノテーション情報
|
|
222
288
|
|
|
223
289
|
Returns:
|
|
224
|
-
|
|
290
|
+
`put_annotation` API(v2)のリクエストボディに格納するアノテーション情報(`AnnotationDetailV2Input`)
|
|
291
|
+
|
|
292
|
+
Raises:
|
|
293
|
+
ValueError: 存在しないラベル名が指定された場合(`self.is_strict`がFalseでもraiseされる9
|
|
225
294
|
|
|
226
295
|
"""
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
if detail.attributes is not None: # noqa: SIM108
|
|
242
|
-
additional_data_list = self._to_additional_data_list(detail.attributes, label_info)
|
|
296
|
+
log_message_suffix = (
|
|
297
|
+
f"task_id='{parser.task_id}', input_data_id='{parser.input_data_id}', label_name='{detail.label}', annotation_id='{detail.annotation_id}'"
|
|
298
|
+
)
|
|
299
|
+
|
|
300
|
+
try:
|
|
301
|
+
label_info = self.annotation_specs_accessor.get_label(label_name=detail.label)
|
|
302
|
+
except ValueError:
|
|
303
|
+
logger.warning(
|
|
304
|
+
f"アノテーション仕様にラベル名(英語)が'{detail.label}'であるラベル情報が存在しないか、または複数存在します。 :: {log_message_suffix}"
|
|
305
|
+
)
|
|
306
|
+
raise
|
|
307
|
+
|
|
308
|
+
if detail.attributes is not None:
|
|
309
|
+
additional_data_list = self.convert_attributes(detail.attributes, label_name=detail.label, log_message_suffix=log_message_suffix)
|
|
243
310
|
else:
|
|
244
311
|
additional_data_list = []
|
|
245
312
|
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
data=detail.data,
|
|
254
|
-
additional_data_list=additional_data_list,
|
|
255
|
-
is_protected=False,
|
|
256
|
-
etag=None,
|
|
257
|
-
url=None,
|
|
258
|
-
path=None,
|
|
259
|
-
created_datetime=now_datetime,
|
|
260
|
-
updated_datetime=now_datetime,
|
|
261
|
-
)
|
|
313
|
+
result = {
|
|
314
|
+
"_type": "Create",
|
|
315
|
+
"label_id": label_info["label_id"],
|
|
316
|
+
"annotation_id": detail.annotation_id if detail.annotation_id is not None else create_annotation_id(label_info, self.project),
|
|
317
|
+
"additional_data_list": additional_data_list,
|
|
318
|
+
"editor_props": {},
|
|
319
|
+
}
|
|
262
320
|
|
|
263
|
-
if
|
|
321
|
+
if is_3dpc_segment_label(label_info):
|
|
264
322
|
# TODO: 3dpc editorに依存したコード。annofab側でSimple Annotationのフォーマットが改善されたら、このコードを削除する
|
|
265
|
-
|
|
266
|
-
|
|
323
|
+
with parser.open_outer_file(get_3dpc_segment_data_uri(detail.data)) as f:
|
|
324
|
+
s3_path = self.service.wrapper.upload_data_to_s3(self.project_id, f, content_type="application/json")
|
|
325
|
+
result["body"] = {"_type": "Outer", "path": s3_path}
|
|
326
|
+
|
|
327
|
+
elif is_image_segmentation_label(label_info):
|
|
328
|
+
with parser.open_outer_file(detail.data["data_uri"]) as f:
|
|
267
329
|
s3_path = self.service.wrapper.upload_data_to_s3(self.project_id, f, content_type="image/png")
|
|
268
|
-
|
|
330
|
+
result["body"] = {"_type": "Outer", "path": s3_path}
|
|
269
331
|
|
|
270
|
-
|
|
332
|
+
else:
|
|
333
|
+
result["body"] = {"_type": "Inner", "data": detail.data}
|
|
334
|
+
return result
|
|
271
335
|
|
|
272
|
-
def
|
|
336
|
+
def convert_annotation_details(
|
|
273
337
|
self,
|
|
274
338
|
parser: SimpleAnnotationParser,
|
|
275
339
|
details: list[ImportedSimpleAnnotationDetail],
|
|
276
|
-
|
|
340
|
+
old_details: list[dict[str, Any]],
|
|
341
|
+
*,
|
|
342
|
+
updated_datetime: Optional[str] = None,
|
|
277
343
|
) -> dict[str, Any]:
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
for detail in details:
|
|
281
|
-
try:
|
|
282
|
-
request_detail = self._to_annotation_detail_for_request(parser, detail, now_datetime=now_datetime)
|
|
283
|
-
except Exception:
|
|
284
|
-
logger.warning(
|
|
285
|
-
f"{parser.task_id}/{parser.input_data_id} :: アノテーションをrequest_bodyに変換するのに失敗しました。 :: "
|
|
286
|
-
f"annotation_id='{detail.annotation_id}', label='{detail.label}'",
|
|
287
|
-
exc_info=True,
|
|
288
|
-
)
|
|
289
|
-
continue
|
|
290
|
-
|
|
291
|
-
if request_detail is not None:
|
|
292
|
-
request_details.append(request_detail.to_dict(encode_json=True))
|
|
293
|
-
|
|
294
|
-
updated_datetime = old_annotation["updated_datetime"] if old_annotation is not None else None
|
|
295
|
-
|
|
296
|
-
request_body = {
|
|
297
|
-
"project_id": self.project_id,
|
|
298
|
-
"task_id": parser.task_id,
|
|
299
|
-
"input_data_id": parser.input_data_id,
|
|
300
|
-
"details": request_details,
|
|
301
|
-
"updated_datetime": updated_datetime,
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
return request_body
|
|
344
|
+
"""
|
|
345
|
+
アノテーションJSONに記載されたアノテーション情報を、`put_annotation` APIに渡す形式に変換します。
|
|
305
346
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
old_details = old_annotation["details"]
|
|
347
|
+
Args:
|
|
348
|
+
parser: アノテーションJSONをパースするためのクラス
|
|
349
|
+
details: インポート対象のアノテーション情報
|
|
350
|
+
old_details: 既存のアノテーション情報。既存のアノテーション情報に加えて、インポート対象のアノテーションを登録するためのリクエストボディを作成します。
|
|
351
|
+
updated_datetime: 更新日時
|
|
352
|
+
""" # noqa: E501
|
|
313
353
|
old_dict_detail = {}
|
|
314
354
|
INDEX_KEY = "_index" # noqa: N806
|
|
315
355
|
for index, old_detail in enumerate(old_details):
|
|
316
|
-
|
|
317
|
-
# 外部アノテーションを利用する際はurlが不要でpathが必要なので、対応する
|
|
318
|
-
old_detail.pop("url", None)
|
|
356
|
+
old_detail.update({"_type": "Update", "body": None})
|
|
319
357
|
|
|
320
358
|
# 一時的にインデックスを格納
|
|
321
359
|
old_detail.update({INDEX_KEY: index})
|
|
322
360
|
old_dict_detail[old_detail["annotation_id"]] = old_detail
|
|
323
361
|
|
|
324
362
|
new_request_details: list[dict[str, Any]] = []
|
|
325
|
-
now_datetime = str_now()
|
|
326
363
|
for detail in details:
|
|
327
364
|
try:
|
|
328
|
-
|
|
329
|
-
|
|
365
|
+
log_message_suffix = f"task_id='{parser.task_id}', input_data_id='{parser.input_data_id}', label_name='{detail.label}', annotation_id='{detail.annotation_id}'" # noqa: E501
|
|
366
|
+
|
|
367
|
+
request_detail = self.convert_annotation_detail(parser, detail, log_message_suffix=log_message_suffix)
|
|
368
|
+
except Exception as e:
|
|
330
369
|
logger.warning(
|
|
331
|
-
f"
|
|
332
|
-
f"annotation_id='{detail.annotation_id}', label='{detail.label}'",
|
|
333
|
-
exc_info=True,
|
|
370
|
+
f"アノテーション情報を`putAnnotation`APIのリクエストボディへ変換するのに失敗しました。 :: {e!r} :: {log_message_suffix}",
|
|
334
371
|
)
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
if request_detail is None:
|
|
372
|
+
if self.is_strict:
|
|
373
|
+
raise
|
|
338
374
|
continue
|
|
339
375
|
|
|
340
376
|
if detail.annotation_id in old_dict_detail:
|
|
341
377
|
# アノテーションを上書き
|
|
342
378
|
old_detail = old_dict_detail[detail.annotation_id]
|
|
343
|
-
|
|
379
|
+
request_detail["_type"] = "Update"
|
|
380
|
+
old_details[old_detail[INDEX_KEY]] = request_detail
|
|
344
381
|
else:
|
|
345
382
|
# アノテーションの追加
|
|
346
|
-
new_request_details.append(request_detail
|
|
383
|
+
new_request_details.append(request_detail)
|
|
347
384
|
|
|
348
385
|
new_details = old_details + new_request_details
|
|
349
386
|
|
|
@@ -352,11 +389,35 @@ class ImportAnnotationMain(CommandLineWithConfirm):
|
|
|
352
389
|
"task_id": parser.task_id,
|
|
353
390
|
"input_data_id": parser.input_data_id,
|
|
354
391
|
"details": new_details,
|
|
355
|
-
"updated_datetime":
|
|
392
|
+
"updated_datetime": updated_datetime,
|
|
393
|
+
"format_version": "2.0.0",
|
|
356
394
|
}
|
|
357
395
|
|
|
358
396
|
return request_body
|
|
359
397
|
|
|
398
|
+
|
|
399
|
+
class ImportAnnotationMain(CommandLineWithConfirm):
|
|
400
|
+
def __init__(
|
|
401
|
+
self,
|
|
402
|
+
service: annofabapi.Resource,
|
|
403
|
+
*,
|
|
404
|
+
project_id: str,
|
|
405
|
+
all_yes: bool,
|
|
406
|
+
is_force: bool,
|
|
407
|
+
is_merge: bool,
|
|
408
|
+
is_overwrite: bool,
|
|
409
|
+
converter: AnnotationConverter,
|
|
410
|
+
) -> None:
|
|
411
|
+
self.service = service
|
|
412
|
+
self.facade = AnnofabApiFacade(service)
|
|
413
|
+
CommandLineWithConfirm.__init__(self, all_yes)
|
|
414
|
+
|
|
415
|
+
self.project_id = project_id
|
|
416
|
+
self.is_force = is_force
|
|
417
|
+
self.is_merge = is_merge
|
|
418
|
+
self.is_overwrite = is_overwrite
|
|
419
|
+
self.converter = converter
|
|
420
|
+
|
|
360
421
|
def put_annotation_for_input_data(self, parser: SimpleAnnotationParser) -> bool:
|
|
361
422
|
task_id = parser.task_id
|
|
362
423
|
input_data_id = parser.input_data_id
|
|
@@ -364,32 +425,36 @@ class ImportAnnotationMain(CommandLineWithConfirm):
|
|
|
364
425
|
simple_annotation: ImportedSimpleAnnotation = ImportedSimpleAnnotation.from_dict(parser.load_json())
|
|
365
426
|
if len(simple_annotation.details) == 0:
|
|
366
427
|
logger.debug(
|
|
367
|
-
f"task_id={task_id}, input_data_id={input_data_id}
|
|
428
|
+
f"task_id='{task_id}', input_data_id='{input_data_id}' :: インポート元にアノテーションデータがないため、アノテーションの登録をスキップします。" # noqa: E501
|
|
368
429
|
)
|
|
369
430
|
return False
|
|
370
431
|
|
|
371
432
|
input_data = self.service.wrapper.get_input_data_or_none(self.project_id, input_data_id)
|
|
372
433
|
if input_data is None:
|
|
373
|
-
logger.warning(f"
|
|
434
|
+
logger.warning(f"input_data_id='{input_data_id}'という入力データは存在しません。 :: task_id='{task_id}'")
|
|
374
435
|
return False
|
|
375
436
|
|
|
376
|
-
old_annotation, _ = self.service.api.get_editor_annotation(self.project_id, task_id, input_data_id)
|
|
437
|
+
old_annotation, _ = self.service.api.get_editor_annotation(self.project_id, task_id, input_data_id, query_params={"v": "2"})
|
|
377
438
|
if len(old_annotation["details"]) > 0: # noqa: SIM102
|
|
378
439
|
if not self.is_overwrite and not self.is_merge:
|
|
379
440
|
logger.debug(
|
|
380
|
-
f"task_id={task_id}, input_data_id={input_data_id}
|
|
381
|
-
f"
|
|
441
|
+
f"task_id='{task_id}', input_data_id='{input_data_id}' :: "
|
|
442
|
+
f"インポート先のタスク内の入力データに既にアノテーションが存在するため、アノテーションの登録をスキップします。"
|
|
382
443
|
f"アノテーションをインポートする場合は、`--overwrite` または '--merge' を指定してください。"
|
|
383
444
|
)
|
|
384
445
|
return False
|
|
385
446
|
|
|
386
447
|
logger.info(f"task_id='{task_id}', input_data_id='{input_data_id}' :: {len(simple_annotation.details)} 件のアノテーションを登録します。")
|
|
387
448
|
if self.is_merge:
|
|
388
|
-
request_body = self.
|
|
449
|
+
request_body = self.converter.convert_annotation_details(
|
|
450
|
+
parser, simple_annotation.details, old_details=old_annotation["details"], updated_datetime=old_annotation["updated_datetime"]
|
|
451
|
+
)
|
|
389
452
|
else:
|
|
390
|
-
request_body = self.
|
|
453
|
+
request_body = self.converter.convert_annotation_details(
|
|
454
|
+
parser, simple_annotation.details, old_details=[], updated_datetime=old_annotation["updated_datetime"]
|
|
455
|
+
)
|
|
391
456
|
|
|
392
|
-
self.service.api.put_annotation(self.project_id, task_id, input_data_id, request_body=request_body)
|
|
457
|
+
self.service.api.put_annotation(self.project_id, task_id, input_data_id, request_body=request_body, query_params={"v": "2"})
|
|
393
458
|
return True
|
|
394
459
|
|
|
395
460
|
def put_annotation_for_task(self, task_parser: SimpleAnnotationParserByTask) -> int:
|
|
@@ -418,15 +483,15 @@ class ImportAnnotationMain(CommandLineWithConfirm):
|
|
|
418
483
|
|
|
419
484
|
"""
|
|
420
485
|
task_id = task_parser.task_id
|
|
421
|
-
if not self.confirm_processing(f"task_id={task_id} のアノテーションをインポートしますか?"):
|
|
486
|
+
if not self.confirm_processing(f"task_id='{task_id}' のアノテーションをインポートしますか?"):
|
|
422
487
|
return False
|
|
423
488
|
|
|
424
489
|
logger_prefix = f"{task_index + 1!s} 件目: " if task_index is not None else ""
|
|
425
|
-
logger.info(f"{logger_prefix}task_id={task_id} に対して処理します。")
|
|
490
|
+
logger.info(f"{logger_prefix}task_id='{task_id}' に対して処理します。")
|
|
426
491
|
|
|
427
492
|
task = self.service.wrapper.get_task_or_none(self.project_id, task_id)
|
|
428
493
|
if task is None:
|
|
429
|
-
logger.warning(f"task_id
|
|
494
|
+
logger.warning(f"task_id='{task_id}'であるタスクは存在しません。")
|
|
430
495
|
return False
|
|
431
496
|
|
|
432
497
|
if task["status"] in [TaskStatus.WORKING.value, TaskStatus.COMPLETE.value]:
|
|
@@ -582,6 +647,10 @@ class ImportAnnotation(CommandLine):
|
|
|
582
647
|
logger.warning(f"annotation_path: '{annotation_path}' は、zipファイルまたはディレクトリではありませんでした。")
|
|
583
648
|
return
|
|
584
649
|
|
|
650
|
+
annotation_specs_v3, _ = self.service.api.get_annotation_specs(project_id, query_params={"v": "3"})
|
|
651
|
+
project, _ = self.service.api.get_project(project_id)
|
|
652
|
+
converter = AnnotationConverter(project=project, annotation_specs=annotation_specs_v3, is_strict=args.strict, service=self.service)
|
|
653
|
+
|
|
585
654
|
main_obj = ImportAnnotationMain(
|
|
586
655
|
self.service,
|
|
587
656
|
project_id=project_id,
|
|
@@ -589,6 +658,7 @@ class ImportAnnotation(CommandLine):
|
|
|
589
658
|
is_merge=args.merge,
|
|
590
659
|
is_overwrite=args.overwrite,
|
|
591
660
|
is_force=args.force,
|
|
661
|
+
converter=converter,
|
|
592
662
|
)
|
|
593
663
|
|
|
594
664
|
main_obj.main(iter_task_parser, target_task_ids=target_task_ids, parallelism=args.parallelism)
|
|
@@ -638,6 +708,13 @@ def parse_args(parser: argparse.ArgumentParser) -> None:
|
|
|
638
708
|
help="過去に割り当てられていて現在の担当者が自分自身でない場合、タスクの担当者を自分自身に変更してからアノテーションをインポートします。",
|
|
639
709
|
)
|
|
640
710
|
|
|
711
|
+
parser.add_argument(
|
|
712
|
+
"--strict",
|
|
713
|
+
action="store_true",
|
|
714
|
+
help="アノテーションJSONに、存在しないラベル名や属性名など適切でない記載が場合、そのアノテーションJSONの登録をスキップします。"
|
|
715
|
+
"デフォルトでは、適切でない箇所のみスキップして、できるだけアノテーションを登録するようにします。",
|
|
716
|
+
)
|
|
717
|
+
|
|
641
718
|
parser.add_argument(
|
|
642
719
|
"--parallelism",
|
|
643
720
|
type=int,
|