annofabcli 1.97.0__py3-none-any.whl → 1.99.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.
@@ -0,0 +1,343 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import copy
5
+ import logging
6
+ import multiprocessing
7
+ import sys
8
+ import tempfile
9
+ from collections.abc import Collection
10
+ from pathlib import Path
11
+ from typing import Any, Optional
12
+
13
+ import annofabapi
14
+ import numpy
15
+ from annofabapi.pydantic_models.task_status import TaskStatus
16
+ from annofabapi.segmentation import read_binary_image, write_binary_image
17
+ from annofabapi.utils import can_put_annotation
18
+
19
+ import annofabcli
20
+ from annofabcli.common.cli import (
21
+ COMMAND_LINE_ERROR_STATUS_CODE,
22
+ PARALLELISM_CHOICES,
23
+ ArgumentParser,
24
+ CommandLine,
25
+ CommandLineWithConfirm,
26
+ build_annofabapi_resource_and_login,
27
+ )
28
+ from annofabcli.common.facade import AnnofabApiFacade
29
+
30
+ logger = logging.getLogger(__name__)
31
+
32
+
33
+ def remove_overlap_of_binary_image_array(
34
+ binary_image_array_by_annotation: dict[str, numpy.ndarray], annotation_id_list: list[str]
35
+ ) -> dict[str, numpy.ndarray]:
36
+ """
37
+ 塗りつぶし画像の重なりを除去したbool配列をdictで返します。
38
+
39
+ Args:
40
+ binary_image_array_by_annotation: annotation_idをkeyとし、塗りつぶし画像のbool配列をvalueとするdict
41
+ annotation_id_list: 塗りつぶし画像のannotation_idのlist。背面から前面の順に格納されている
42
+
43
+ Returns:
44
+ 重なりを除去した塗りつぶし画像のbool配列が格納されているdict。keyはannotation_id
45
+
46
+ """
47
+ assert set(binary_image_array_by_annotation.keys()) == set(annotation_id_list)
48
+
49
+ whole_2d_array = None # 複数の塗りつぶしアノテーションを1枚に重ね合わせた状態。各要素はannotation_id
50
+
51
+ # 背面から塗りつぶしアノテーションのbool配列を重ねていく
52
+ for annotation_id in annotation_id_list:
53
+ input_binary_image_array = binary_image_array_by_annotation[annotation_id]
54
+ if whole_2d_array is None:
55
+ whole_2d_array = numpy.full(input_binary_image_array.shape, "", dtype=str)
56
+
57
+ whole_2d_array = numpy.where(input_binary_image_array, annotation_id, whole_2d_array)
58
+
59
+ output_binary_image_array_by_annotation = {}
60
+ for annotation_id in annotation_id_list:
61
+ output_binary_image_array: numpy.ndarray = whole_2d_array == annotation_id # type: ignore[assignment]
62
+ output_binary_image_array_by_annotation[annotation_id] = output_binary_image_array
63
+
64
+ return output_binary_image_array_by_annotation
65
+
66
+
67
+ class RemoveSegmentationOverlapMain(CommandLineWithConfirm):
68
+ def __init__(self, annofab_service: annofabapi.Resource, *, project_id: str, all_yes: bool, is_force: bool) -> None:
69
+ self.annofab_service = annofab_service
70
+ self.project_id = project_id
71
+ self.is_force = is_force
72
+ super().__init__(all_yes)
73
+
74
+ def remove_segmentation_overlap_and_save(self, details: list[dict[str, Any]], output_dir: Path) -> list[str]:
75
+ """
76
+ `getEditorAnnotation` APIで取得した`details`から、塗りつぶし画像の重なりの除去が必要な場合に、
77
+ 重なりを除去した塗りつぶし画像を`output_dir`に出力します。
78
+ 塗りつぶし画像のファイル名は`${annotation_id}.png`です。
79
+
80
+ Args:
81
+ details: `getEditorAnnotation` APIで取得した`details`
82
+ output_dir: 塗りつぶし画像の出力先のディレクトリ。
83
+
84
+ Returns:
85
+ 重なりの除去が必要な塗りつぶし画像のannotation_idのlist
86
+ """
87
+ input_binary_image_array_by_annotation = {}
88
+ segmentation_annotation_id_list = []
89
+
90
+ for detail in details:
91
+ if detail["body"]["_type"] != "Outer":
92
+ continue
93
+
94
+ segmentation_response = self.annofab_service.wrapper.execute_http_get(detail["body"]["url"], stream=True)
95
+ segmentation_response.raw.decode_content = True
96
+ input_binary_image_array_by_annotation[detail["annotation_id"]] = read_binary_image(segmentation_response.raw)
97
+ segmentation_annotation_id_list.append(detail["annotation_id"])
98
+
99
+ # reversedを使っている理由:
100
+ # `details`には、前面から背面の順にアノテーションが格納されているため、
101
+ output_binary_image_array_by_annotation = remove_overlap_of_binary_image_array(
102
+ input_binary_image_array_by_annotation, list(reversed(segmentation_annotation_id_list))
103
+ )
104
+
105
+ updated_annotation_id_list = []
106
+ for annotation_id, output_binary_image_array in output_binary_image_array_by_annotation.items():
107
+ input_binary_image_array = input_binary_image_array_by_annotation[annotation_id]
108
+ if not numpy.array_equal(input_binary_image_array, output_binary_image_array):
109
+ output_file_path = output_dir / f"{annotation_id}.png"
110
+ write_binary_image(output_binary_image_array, output_file_path)
111
+ updated_annotation_id_list.append(annotation_id)
112
+
113
+ return updated_annotation_id_list
114
+
115
+ def update_segmentation_annotation(self, task_id: str, input_data_id: str, log_message_prefix: str = "") -> bool:
116
+ """
117
+ 塗りつぶしアノテーションの重なりがあれば、`putAnnotation` APIを使用して重なりを除去します。
118
+
119
+ Args:
120
+ project_id: プロジェクトID
121
+ task_id: タスクID
122
+ annotation_id_list: 更新する塗りつぶし画像のannotation_idのlist
123
+ """
124
+ old_annotation, _ = self.annofab_service.api.get_editor_annotation(self.project_id, task_id, input_data_id, query_params={"v": "2"})
125
+ old_details = old_annotation["details"]
126
+ with tempfile.TemporaryDirectory() as temp_dir:
127
+ temp_dir_path = Path(temp_dir)
128
+ updated_annotation_id_list = self.remove_segmentation_overlap_and_save(old_details, temp_dir_path)
129
+ if len(updated_annotation_id_list) == 0:
130
+ logger.debug(
131
+ f"{log_message_prefix}塗りつぶしアノテーションの重なりはなかったので、スキップします。 :: "
132
+ f"task_id='{task_id}', input_data_id='{input_data_id}'"
133
+ )
134
+ return False
135
+
136
+ logger.debug(
137
+ f"{log_message_prefix}{len(updated_annotation_id_list)} 件の塗りつぶしアノテーションを更新します。 :: "
138
+ f"task_id='{task_id}', input_data_id='{input_data_id}', annotation_id_list={updated_annotation_id_list}"
139
+ )
140
+ new_details = []
141
+ for detail in old_details:
142
+ annotation_id = detail["annotation_id"]
143
+ new_detail = copy.deepcopy(detail)
144
+ new_detail["_type"] = "Update"
145
+ if annotation_id in updated_annotation_id_list:
146
+ with (temp_dir_path / f"{annotation_id}.png").open("rb") as f:
147
+ s3_path = self.annofab_service.wrapper.upload_data_to_s3(self.project_id, data=f, content_type="image/png")
148
+
149
+ new_detail["body"]["path"] = s3_path
150
+
151
+ else:
152
+ # 更新しない場合は、`body`をNoneにする
153
+ new_detail["body"] = None
154
+
155
+ new_details.append(new_detail)
156
+
157
+ request_body = {
158
+ "project_id": self.project_id,
159
+ "task_id": task_id,
160
+ "input_data_id": input_data_id,
161
+ "details": new_details,
162
+ "format_version": "2.0.0",
163
+ "updated_datetime": old_annotation["updated_datetime"],
164
+ }
165
+ self.annofab_service.api.put_annotation(self.project_id, task_id, input_data_id, query_params={"v": "2"}, request_body=request_body)
166
+ logger.debug(
167
+ f"{log_message_prefix}{len(updated_annotation_id_list)} 件の塗りつぶしアノテーションを更新しました。 :: "
168
+ f"task_id='{task_id}', input_data_id='{input_data_id}'"
169
+ )
170
+ return True
171
+
172
+ def update_segmentation_annotation_for_task(self, task_id: str, *, task_index: Optional[int] = None) -> int:
173
+ """
174
+ 1個のタスクに対して、塗りつぶしアノテーションの重なりを除去します。
175
+
176
+ Returns:
177
+ アノテーションを更新した入力データ数(フレーム数)
178
+ """
179
+ log_message_prefix = f"{task_index + 1} 件目 :: " if task_index is not None else ""
180
+
181
+ task = self.annofab_service.wrapper.get_task_or_none(project_id=self.project_id, task_id=task_id)
182
+ if task is None:
183
+ logger.warning(f"{log_message_prefix}task_id='{task_id}'であるタスクは存在しません。")
184
+ return 0
185
+
186
+ if task["status"] in {TaskStatus.WORKING.value, TaskStatus.COMPLETE.value}:
187
+ logger.debug(
188
+ f"{log_message_prefix}task_id='{task_id}'のタスクの状態は「作業中」または「完了」であるため、"
189
+ f"アノテーションの更新をスキップします。 :: status='{task['status']}'"
190
+ )
191
+ return 0
192
+
193
+ if not self.confirm_processing(f"task_id='{task_id}'の塗りつぶしアノテーションの重なりを除去しますか?"):
194
+ return 0
195
+
196
+ # 担当者割り当て変更チェック
197
+ changed_operator = False
198
+ original_operator_account_id = task["account_id"]
199
+ if not can_put_annotation(task, self.annofab_service.api.account_id):
200
+ if self.is_force:
201
+ logger.debug(f"{log_message_prefix}task_id='{task_id}' のタスクの担当者を自分自身に変更します。")
202
+ changed_operator = True
203
+ task = self.annofab_service.wrapper.change_task_operator(
204
+ self.project_id,
205
+ task_id,
206
+ self.annofab_service.api.account_id,
207
+ last_updated_datetime=task["updated_datetime"],
208
+ )
209
+ else:
210
+ logger.debug(
211
+ f"{log_message_prefix}task_id='{task_id}' のタスクは、過去に誰かに割り当てられたタスクで、"
212
+ f"現在の担当者が自分自身でないため、アノテーションの更新をスキップします。"
213
+ f"担当者を自分自身に変更してアノテーションを更新する場合は、コマンドライン引数 '--force' を指定してください。"
214
+ )
215
+ return 0
216
+
217
+ success_input_data_count = 0
218
+ for input_data_id in task["input_data_id_list"]:
219
+ try:
220
+ result = self.update_segmentation_annotation(task_id, input_data_id, log_message_prefix=log_message_prefix)
221
+ if result:
222
+ success_input_data_count += 1
223
+ except Exception:
224
+ logger.warning(
225
+ f"{log_message_prefix}task_id='{task_id}', input_data_id='{input_data_id}'のアノテーションの更新に失敗しました。", exc_info=True
226
+ )
227
+ continue
228
+
229
+ # 担当者を元に戻す
230
+ if changed_operator:
231
+ logger.debug(
232
+ f"{log_message_prefix}task_id='{task_id}' のタスクの担当者を、元の担当者(account_id='{original_operator_account_id}')に変更します。"
233
+ )
234
+ self.annofab_service.wrapper.change_task_operator(
235
+ self.project_id,
236
+ task_id,
237
+ original_operator_account_id,
238
+ last_updated_datetime=task["updated_datetime"],
239
+ )
240
+
241
+ return success_input_data_count
242
+
243
+ def update_segmentation_annotation_for_task_wrapper(self, tpl: tuple[int, str]) -> int:
244
+ try:
245
+ task_index, task_id = tpl
246
+ return self.update_segmentation_annotation_for_task(task_id, task_index=task_index)
247
+ except Exception:
248
+ logger.warning(f"task_id='{task_id}' のアノテーションの更新に失敗しました。", exc_info=True)
249
+ return 0
250
+
251
+ def main(
252
+ self,
253
+ task_ids: Collection[str],
254
+ parallelism: Optional[int] = None,
255
+ ) -> None:
256
+ logger.info(f"{len(task_ids)} 件のタスクの塗りつぶしアノテーションの重なりを除去します。")
257
+ success_input_data_count = 0
258
+ if parallelism is not None:
259
+ with multiprocessing.Pool(parallelism) as pool:
260
+ result_count_list = pool.map(self.update_segmentation_annotation_for_task_wrapper, enumerate(task_ids))
261
+ success_input_data_count = sum(result_count_list)
262
+
263
+ else:
264
+ for task_index, task_id in enumerate(task_ids):
265
+ try:
266
+ result = self.update_segmentation_annotation_for_task(task_id, task_index=task_index)
267
+ success_input_data_count += result
268
+ except Exception:
269
+ logger.warning(f"task_id='{task_id}' のアノテーションの更新に失敗しました。", exc_info=True)
270
+ continue
271
+
272
+ logger.info(f"{len(task_ids)} 件のタスクに含まれる入力データ {success_input_data_count} 件の塗りつぶしアノテーションを更新しました。")
273
+
274
+
275
+ class RemoveSegmentationOverlap(CommandLine):
276
+ COMMON_MESSAGE = "annofabcli annotation remove_segmentation_overlap: error:"
277
+
278
+ def validate(self, args: argparse.Namespace) -> bool:
279
+ if args.parallelism is not None and not args.yes:
280
+ print( # noqa: T201
281
+ f"{self.COMMON_MESSAGE} argument --parallelism: '--parallelism'を指定するときは、'--yes' を指定してください。",
282
+ file=sys.stderr,
283
+ )
284
+ return False
285
+
286
+ return True
287
+
288
+ def main(self) -> None:
289
+ args = self.args
290
+ if not self.validate(args):
291
+ sys.exit(COMMAND_LINE_ERROR_STATUS_CODE)
292
+
293
+ project_id = args.project_id
294
+ task_id_list = annofabcli.common.cli.get_list_from_args(args.task_id)
295
+
296
+ main_obj = RemoveSegmentationOverlapMain(
297
+ self.service,
298
+ project_id=project_id,
299
+ all_yes=self.all_yes,
300
+ is_force=args.force,
301
+ )
302
+
303
+ main_obj.main(task_id_list, parallelism=args.parallelism)
304
+
305
+
306
+ def main(args: argparse.Namespace) -> None:
307
+ service = build_annofabapi_resource_and_login(args)
308
+ facade = AnnofabApiFacade(service)
309
+ RemoveSegmentationOverlap(service, facade, args).main()
310
+
311
+
312
+ def parse_args(parser: argparse.ArgumentParser) -> None:
313
+ argument_parser = ArgumentParser(parser)
314
+ argument_parser.add_project_id()
315
+ argument_parser.add_task_id()
316
+
317
+ parser.add_argument(
318
+ "--force",
319
+ action="store_true",
320
+ help="過去に担当者を割り当てられていて、かつ現在の担当者が自分自身でない場合、タスクの担当者を一時的に自分自身に変更してからアノテーションをコピーします。",
321
+ )
322
+
323
+ parser.add_argument(
324
+ "--parallelism",
325
+ type=int,
326
+ choices=PARALLELISM_CHOICES,
327
+ help="並列度。指定しない場合は、逐次的に処理します。指定した場合は、``--yes`` も指定してください。",
328
+ )
329
+
330
+ parser.set_defaults(subcommand_func=main)
331
+
332
+
333
+ def add_parser(subparsers: Optional[argparse._SubParsersAction] = None) -> argparse.ArgumentParser:
334
+ subcommand_name = "remove_segmentation_overlap"
335
+ subcommand_help = "塗りつぶしアノテーションの重なりを除去します。"
336
+ description = (
337
+ "塗りつぶしアノテーションの重なりを除去します。"
338
+ "Annofabでインスタンスセグメンテーションは重ねることができてしまいます。"
339
+ "この重なりをなくしたいときに有用です。"
340
+ )
341
+ parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, subcommand_help, description)
342
+ parse_args(parser)
343
+ return parser
@@ -10,6 +10,8 @@ import annofabcli.annotation.dump_annotation
10
10
  import annofabcli.annotation.import_annotation
11
11
  import annofabcli.annotation.list_annotation
12
12
  import annofabcli.annotation.list_annotation_count
13
+ import annofabcli.annotation.merge_segmentation
14
+ import annofabcli.annotation.remove_segmentation_overlap
13
15
  import annofabcli.annotation.restore_annotation
14
16
  import annofabcli.common.cli
15
17
 
@@ -27,6 +29,8 @@ def parse_args(parser: argparse.ArgumentParser) -> None:
27
29
  annofabcli.annotation.import_annotation.add_parser(subparsers)
28
30
  annofabcli.annotation.list_annotation.add_parser(subparsers)
29
31
  annofabcli.annotation.list_annotation_count.add_parser(subparsers)
32
+ annofabcli.annotation.merge_segmentation.add_parser(subparsers)
33
+ annofabcli.annotation.remove_segmentation_overlap.add_parser(subparsers)
30
34
  annofabcli.annotation.restore_annotation.add_parser(subparsers)
31
35
 
32
36
 
@@ -2,6 +2,7 @@ from __future__ import annotations
2
2
 
3
3
  import argparse
4
4
  import logging
5
+ import sys
5
6
  from dataclasses import dataclass
6
7
  from typing import Any, Optional
7
8
 
@@ -10,6 +11,7 @@ import annofabapi
10
11
  import annofabcli
11
12
  import annofabcli.common.cli
12
13
  from annofabcli.common.cli import (
14
+ COMMAND_LINE_ERROR_STATUS_CODE,
13
15
  ArgumentParser,
14
16
  CommandLine,
15
17
  CommandLineWithConfirm,
@@ -100,6 +102,9 @@ class PutLabelColor(CommandLine):
100
102
  args = self.args
101
103
  label_color = get_json_from_args(args.json)
102
104
 
105
+ if not isinstance(label_color, dict):
106
+ print("annofabcli annotation_specs put_label_color: error: JSON形式が不正です。オブジェクトを指定してください。", file=sys.stderr) # noqa: T201
107
+ sys.exit(COMMAND_LINE_ERROR_STATUS_CODE)
103
108
  main_obj = PuttingLabelColorMain(service=self.service, project_id=args.project_id, all_yes=args.yes)
104
109
  main_obj.main(label_color, comment=args.comment)
105
110
 
@@ -225,6 +225,9 @@ class DeleteComment(CommandLine):
225
225
  super().validate_project(args.project_id, [ProjectMemberRole.ACCEPTER, ProjectMemberRole.OWNER])
226
226
 
227
227
  dict_comments = annofabcli.common.cli.get_json_from_args(args.json)
228
+ if not isinstance(dict_comments, dict):
229
+ print(f"{self.COMMON_MESSAGE} argument --json: JSON形式が不正です。オブジェクトを指定してください。", file=sys.stderr) # noqa: T201
230
+ sys.exit(COMMAND_LINE_ERROR_STATUS_CODE)
228
231
  main_obj = DeleteCommentMain(self.service, project_id=args.project_id, all_yes=self.all_yes)
229
232
  main_obj.delete_comments_for_task_list(
230
233
  comment_ids_for_task_list=dict_comments,
@@ -44,6 +44,9 @@ class PutInspectionComment(CommandLine):
44
44
  super().validate_project(args.project_id, [ProjectMemberRole.ACCEPTER, ProjectMemberRole.OWNER])
45
45
 
46
46
  dict_comments = annofabcli.common.cli.get_json_from_args(args.json)
47
+ if not isinstance(dict_comments, dict):
48
+ print(f"{self.COMMON_MESSAGE} argument --json: JSON形式が不正です。オブジェクトを指定してください。", file=sys.stderr) # noqa: T201
49
+ sys.exit(COMMAND_LINE_ERROR_STATUS_CODE)
47
50
  comments_for_task_list = convert_cli_comments(dict_comments, comment_type=CommentType.INSPECTION)
48
51
  main_obj = PutCommentMain(self.service, project_id=args.project_id, comment_type=CommentType.INSPECTION, all_yes=self.all_yes)
49
52
  main_obj.add_comments_for_task_list(
@@ -42,6 +42,9 @@ class PutInspectionComment(CommandLine):
42
42
  super().validate_project(args.project_id)
43
43
 
44
44
  dict_comments = annofabcli.common.cli.get_json_from_args(args.json)
45
+ if not isinstance(dict_comments, dict):
46
+ print(f"{self.COMMON_MESSAGE} argument --json: JSON形式が不正です。オブジェクトを指定してください。", file=sys.stderr) # noqa: T201
47
+ sys.exit(COMMAND_LINE_ERROR_STATUS_CODE)
45
48
  comments_for_task_list = convert_cli_comments(
46
49
  dict_comments,
47
50
  comment_type=CommentType.ONHOLD,
@@ -177,7 +177,10 @@ class ChangeInputDataName(CommandLine):
177
177
  changed_input_data_list = create_changed_input_data_list_from_csv(args.csv)
178
178
 
179
179
  elif args.json is not None:
180
- input_data_dict_list: list[dict[str, str]] = get_json_from_args(args.json)
180
+ input_data_dict_list = get_json_from_args(args.json)
181
+ if not isinstance(input_data_dict_list, list):
182
+ print("annofabcli input_data change_name: error: JSON形式が不正です。オブジェクトの配列を指定してください。", file=sys.stderr) # noqa: T201
183
+ sys.exit(COMMAND_LINE_ERROR_STATUS_CODE)
181
184
  changed_input_data_list = create_changed_input_data_list_from_dict(input_data_dict_list)
182
185
  else:
183
186
  raise RuntimeError("'--csv'または'--json'のいずれかを指定してください。")
@@ -356,6 +356,10 @@ class PutInputData(CommandLine):
356
356
 
357
357
  elif args.json is not None:
358
358
  input_data_dict_list = get_json_from_args(args.json)
359
+ if not isinstance(input_data_dict_list, list):
360
+ print(f"{self.COMMON_MESSAGE} argument --json: JSON形式が不正です。オブジェクトの配列を指定してください。", file=sys.stderr) # noqa: T201
361
+ sys.exit(COMMAND_LINE_ERROR_STATUS_CODE)
362
+
359
363
  input_data_list = self.get_input_data_list_from_dict(input_data_dict_list, allow_duplicated_input_data=args.allow_duplicated_input_data)
360
364
  self.put_input_data_list(project_id, input_data_list=input_data_list, overwrite=args.overwrite, parallelism=args.parallelism)
361
365