annofabcli 1.97.0__py3-none-any.whl → 1.98.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/__version__.py CHANGED
@@ -1 +1 @@
1
- __version__ = "1.97.0" # `poetry-dynamic-versioning`を使ってGitHubのバージョンタグを取得している。変更不要
1
+ __version__ = "1.98.0" # `poetry-dynamic-versioning`を使ってGitHubのバージョンタグを取得している。変更不要
@@ -0,0 +1,390 @@
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.default_annotation_type import DefaultAnnotationType
16
+ from annofabapi.pydantic_models.task_status import TaskStatus
17
+ from annofabapi.segmentation import read_binary_image, write_binary_image
18
+ from annofabapi.util.annotation_specs import AnnotationSpecsAccessor
19
+ from annofabapi.utils import can_put_annotation
20
+
21
+ import annofabcli
22
+ from annofabcli.common.cli import (
23
+ COMMAND_LINE_ERROR_STATUS_CODE,
24
+ PARALLELISM_CHOICES,
25
+ ArgumentParser,
26
+ CommandLine,
27
+ CommandLineWithConfirm,
28
+ build_annofabapi_resource_and_login,
29
+ )
30
+ from annofabcli.common.facade import AnnofabApiFacade
31
+
32
+ logger = logging.getLogger(__name__)
33
+
34
+
35
+ def merge_binary_image_array(binary_image_array_list: list[numpy.ndarray]) -> numpy.ndarray:
36
+ """
37
+ 塗りつぶし画像を読み込んだboolのndarrayのlistから、1個のndarrayを作成します。
38
+
39
+ Args:
40
+ binary_image_array_list: 塗りつぶし画像を読み込んだboolのndarrayのlist
41
+ """
42
+ if len(binary_image_array_list) == 0:
43
+ raise ValueError("'binary_image_array_list' must not be empty.")
44
+
45
+ merged_array = numpy.zeros_like(binary_image_array_list[0], dtype=bool)
46
+ for binary_image_array in binary_image_array_list:
47
+ merged_array = numpy.logical_or(merged_array, binary_image_array)
48
+ return merged_array
49
+
50
+
51
+ class MergeSegmentationMain(CommandLineWithConfirm):
52
+ def __init__(
53
+ self,
54
+ annofab_service: annofabapi.Resource,
55
+ *,
56
+ project_id: str,
57
+ label_ids: Collection[str],
58
+ label_names: Collection[str],
59
+ all_yes: bool,
60
+ is_force: bool,
61
+ ) -> None:
62
+ self.annofab_service = annofab_service
63
+ self.project_id = project_id
64
+ self.is_force = is_force
65
+ self.label_ids = label_ids
66
+ self.label_names = label_names
67
+
68
+ super().__init__(all_yes)
69
+
70
+ def write_merged_segmentation_file(self, details: list[dict[str, Any]], output_dir: Path) -> tuple[list[str], list[str]]:
71
+ """
72
+ `getEditorAnnotation` APIで取得した`details`から、指定したラベルに対応する塗りつぶしアノテーションを1個にまとめて、
73
+ `output_dir`に出力します。
74
+ 塗りつぶし画像のファイル名は`${annotation_id}.png`です。
75
+
76
+ Args:
77
+ details: `getEditorAnnotation` APIで取得した`details`
78
+ label_ids: 更新対象のアノテーションに対応するラベルIDのcollection
79
+ output_dir: 塗りつぶし画像の出力先のディレクトリ。
80
+
81
+ Returns:
82
+ tuple[0]: 更新対象の塗りつぶしアノテーションのannotation_idのlist(最前面のアノテーション)
83
+ tuple[1]: 削除対象の塗りつぶしアノテーションのannotation_idのlist
84
+ """
85
+
86
+ def func(label_id: str) -> tuple[Optional[str], list[str]]:
87
+ updated_annotation_id = None
88
+ deleted_annotation_id_list = []
89
+ binary_image_array_list = []
90
+ segmentation_details = [e for e in details if e["label_id"] == label_id]
91
+ if len(segmentation_details) <= 1:
92
+ return None, []
93
+
94
+ updated_annotation_id = segmentation_details[0]["annotation_id"]
95
+ deleted_annotation_id_list = [e["annotation_id"] for e in segmentation_details[1:]]
96
+ for detail in segmentation_details:
97
+ segmentation_response = self.annofab_service.wrapper.execute_http_get(detail["body"]["url"], stream=True)
98
+ segmentation_response.raw.decode_content = True
99
+ binary_image_array_list.append(read_binary_image(segmentation_response.raw))
100
+
101
+ merged_binary_image_array = merge_binary_image_array(binary_image_array_list)
102
+ output_file_path = output_dir / f"{updated_annotation_id}.png"
103
+ write_binary_image(merged_binary_image_array, output_file_path)
104
+ return updated_annotation_id, deleted_annotation_id_list
105
+
106
+ updated_annotation_id_list = []
107
+ deleted_annotation_id_list = []
108
+ for label_id in self.label_ids:
109
+ updated_annotation_id, sub_deleted_annotation_id_list = func(label_id)
110
+ if updated_annotation_id is not None:
111
+ updated_annotation_id_list.append(updated_annotation_id)
112
+
113
+ deleted_annotation_id_list.extend(sub_deleted_annotation_id_list)
114
+
115
+ return updated_annotation_id_list, deleted_annotation_id_list
116
+
117
+ def merge_segmentation_annotation(self, task_id: str, input_data_id: str, log_message_prefix: str = "") -> bool:
118
+ """
119
+ label_idに対応する複数の塗りつぶしアノテーションを1つにまとめます。
120
+
121
+ Args:
122
+ project_id: プロジェクトID
123
+ task_id: タスクID
124
+ annotation_id_list: 更新する塗りつぶし画像のannotation_idのlist
125
+ """
126
+ old_annotation, _ = self.annofab_service.api.get_editor_annotation(self.project_id, task_id, input_data_id, query_params={"v": "2"})
127
+ old_details = old_annotation["details"]
128
+ with tempfile.TemporaryDirectory() as temp_dir:
129
+ temp_dir_path = Path(temp_dir)
130
+ updated_annotation_id_list, deleted_annotation_id_list = self.write_merged_segmentation_file(old_details, temp_dir_path)
131
+ if len(updated_annotation_id_list) == 0:
132
+ assert len(deleted_annotation_id_list) == 0
133
+ logger.debug(
134
+ f"{log_message_prefix}更新対象の塗りつぶしアノテーションはなかった"
135
+ "(1個のラベルに塗りつぶしアノテーションは複数なかった)ので、スキップします。 :: "
136
+ f"task_id='{task_id}', input_data_id='{input_data_id}'"
137
+ )
138
+ return False
139
+
140
+ logger.debug(
141
+ f"{log_message_prefix}{len(updated_annotation_id_list)} 件の塗りつぶしアノテーションを更新して、"
142
+ f"{len(deleted_annotation_id_list)} 件の塗りつぶしアノテーションを削除します。 :: "
143
+ f"task_id='{task_id}', input_data_id='{input_data_id}', "
144
+ f"更新対象のannotation_id_list={updated_annotation_id_list}, "
145
+ f"削除対象のannotation_id_list={deleted_annotation_id_list}"
146
+ )
147
+ new_details = []
148
+ for detail in old_details:
149
+ annotation_id = detail["annotation_id"]
150
+ if annotation_id in deleted_annotation_id_list:
151
+ continue
152
+
153
+ new_detail = copy.deepcopy(detail)
154
+ new_detail["_type"] = "Update"
155
+ if annotation_id in updated_annotation_id_list:
156
+ with (temp_dir_path / f"{annotation_id}.png").open("rb") as f:
157
+ s3_path = self.annofab_service.wrapper.upload_data_to_s3(self.project_id, data=f, content_type="image/png")
158
+
159
+ new_detail["body"]["path"] = s3_path
160
+
161
+ else:
162
+ # 更新しない場合は、`body`をNoneにする
163
+ new_detail["body"] = None
164
+
165
+ new_details.append(new_detail)
166
+
167
+ request_body = {
168
+ "project_id": self.project_id,
169
+ "task_id": task_id,
170
+ "input_data_id": input_data_id,
171
+ "details": new_details,
172
+ "format_version": "2.0.0",
173
+ "updated_datetime": old_annotation["updated_datetime"],
174
+ }
175
+ self.annofab_service.api.put_annotation(self.project_id, task_id, input_data_id, query_params={"v": "2"}, request_body=request_body)
176
+ logger.debug(
177
+ f"{log_message_prefix}{len(updated_annotation_id_list)} 件の塗りつぶしアノテーションを更新して、"
178
+ f"{len(deleted_annotation_id_list)} 件の塗りつぶしアノテーションを削除しました。 :: "
179
+ f"task_id='{task_id}', input_data_id='{input_data_id}'"
180
+ )
181
+ return True
182
+
183
+ def merge_segmentation_annotation_for_task(self, task_id: str, *, task_index: Optional[int] = None) -> int:
184
+ """
185
+ 1個のタスクに対して、label_idに対応する複数の塗りつぶしアノテーションを1つにまとめます。
186
+
187
+ Returns:
188
+ アノテーションを更新した入力データ数(フレーム数)
189
+ """
190
+ log_message_prefix = f"{task_index + 1} 件目 :: " if task_index is not None else ""
191
+
192
+ task = self.annofab_service.wrapper.get_task_or_none(project_id=self.project_id, task_id=task_id)
193
+ if task is None:
194
+ logger.warning(f"{log_message_prefix}task_id='{task_id}'であるタスクは存在しません。")
195
+ return 0
196
+
197
+ if task["status"] in {TaskStatus.WORKING.value, TaskStatus.COMPLETE.value}:
198
+ logger.debug(
199
+ f"{log_message_prefix}task_id='{task_id}'のタスクの状態は「作業中」または「完了」であるため、"
200
+ f"アノテーションの更新をスキップします。 :: status='{task['status']}'"
201
+ )
202
+ return 0
203
+
204
+ if not self.confirm_processing(
205
+ f"task_id='{task_id}'の次のラベル名に対応する複数の塗りつぶしアノテーションを1つにまとめますか? :: {self.label_names}"
206
+ ):
207
+ return 0
208
+
209
+ # 担当者割り当て変更チェック
210
+ changed_operator = False
211
+ original_operator_account_id = task["account_id"]
212
+ if not can_put_annotation(task, self.annofab_service.api.account_id):
213
+ if self.is_force:
214
+ logger.debug(f"{log_message_prefix}task_id='{task_id}' のタスクの担当者を自分自身に変更します。")
215
+ changed_operator = True
216
+ task = self.annofab_service.wrapper.change_task_operator(
217
+ self.project_id,
218
+ task_id,
219
+ self.annofab_service.api.account_id,
220
+ last_updated_datetime=task["updated_datetime"],
221
+ )
222
+ else:
223
+ logger.debug(
224
+ f"{log_message_prefix}task_id='{task_id}' のタスクは、過去に誰かに割り当てられたタスクで、"
225
+ f"現在の担当者が自分自身でないため、アノテーションの更新をスキップします。"
226
+ f"担当者を自分自身に変更してアノテーションを更新する場合は、コマンドライン引数 '--force' を指定してください。"
227
+ )
228
+ return 0
229
+
230
+ success_input_data_count = 0
231
+ for input_data_id in task["input_data_id_list"]:
232
+ try:
233
+ result = self.merge_segmentation_annotation(task_id, input_data_id, log_message_prefix=log_message_prefix)
234
+ if result:
235
+ success_input_data_count += 1
236
+ except Exception:
237
+ logger.warning(
238
+ f"{log_message_prefix}task_id='{task_id}', input_data_id='{input_data_id}'のアノテーションの更新に失敗しました。", exc_info=True
239
+ )
240
+ continue
241
+
242
+ # 担当者を元に戻す
243
+ if changed_operator:
244
+ logger.debug(
245
+ f"{log_message_prefix}task_id='{task_id}' のタスクの担当者を、元の担当者(account_id='{original_operator_account_id}')に変更します。"
246
+ )
247
+ self.annofab_service.wrapper.change_task_operator(
248
+ self.project_id,
249
+ task_id,
250
+ original_operator_account_id,
251
+ last_updated_datetime=task["updated_datetime"],
252
+ )
253
+
254
+ return success_input_data_count
255
+
256
+ def update_segmentation_annotation_for_task_wrapper(self, tpl: tuple[int, str]) -> int:
257
+ try:
258
+ task_index, task_id = tpl
259
+ return self.merge_segmentation_annotation_for_task(task_id, task_index=task_index)
260
+ except Exception:
261
+ logger.warning(f"task_id='{task_id}' のアノテーションの更新に失敗しました。", exc_info=True)
262
+ return 0
263
+
264
+ def main(
265
+ self,
266
+ task_ids: Collection[str],
267
+ parallelism: Optional[int] = None,
268
+ ) -> None:
269
+ logger.info(f"{len(task_ids)} 件のタスクに対して、複数の塗りつぶしアノテーションを1個にまとめます。")
270
+ success_input_data_count = 0
271
+ if parallelism is not None:
272
+ with multiprocessing.Pool(parallelism) as pool:
273
+ result_count_list = pool.map(self.update_segmentation_annotation_for_task_wrapper, enumerate(task_ids))
274
+ success_input_data_count = sum(result_count_list)
275
+
276
+ else:
277
+ for task_index, task_id in enumerate(task_ids):
278
+ try:
279
+ result = self.merge_segmentation_annotation_for_task(task_id, task_index=task_index)
280
+ success_input_data_count += result
281
+ except Exception:
282
+ logger.warning(f"task_id='{task_id}' のアノテーションの更新に失敗しました。", exc_info=True)
283
+ continue
284
+
285
+ logger.info(f"{len(task_ids)} 件のタスクに含まれる入力データ {success_input_data_count} 件の塗りつぶしアノテーションを更新しました。")
286
+
287
+
288
+ class MergeSegmentation(CommandLine):
289
+ COMMON_MESSAGE = "annofabcli annotation merge_segmentation: error:"
290
+
291
+ def validate(self, args: argparse.Namespace) -> bool:
292
+ if args.parallelism is not None and not args.yes:
293
+ print( # noqa: T201
294
+ f"{self.COMMON_MESSAGE} argument --parallelism: '--parallelism'を指定するときは、'--yes' を指定してください。",
295
+ file=sys.stderr,
296
+ )
297
+ return False
298
+
299
+ return True
300
+
301
+ def main(self) -> None:
302
+ args = self.args
303
+ if not self.validate(args):
304
+ sys.exit(COMMAND_LINE_ERROR_STATUS_CODE)
305
+
306
+ project_id = args.project_id
307
+ task_id_list = annofabcli.common.cli.get_list_from_args(args.task_id)
308
+ label_name_list = annofabcli.common.cli.get_list_from_args(args.label_name)
309
+
310
+ annotation_specs, _ = self.service.api.get_annotation_specs(project_id, query_params={"v": "3"})
311
+ accessor = AnnotationSpecsAccessor(annotation_specs)
312
+ label_id_list = []
313
+ invalid_label_name_list = []
314
+ for label_name in label_name_list:
315
+ try:
316
+ label = accessor.get_label(label_name=label_name)
317
+ if label["annotation_type"] not in {DefaultAnnotationType.SEGMENTATION.value, DefaultAnnotationType.SEGMENTATION_V2.value}:
318
+ invalid_label_name_list.append(label_name)
319
+
320
+ except ValueError:
321
+ invalid_label_name_list.append(label_name)
322
+ continue
323
+
324
+ label_id_list.append(label["label_id"])
325
+
326
+ if len(invalid_label_name_list) > 0:
327
+ print( # noqa: T201
328
+ f"{self.COMMON_MESSAGE} --label_name: 次のラベル名(英語)はアノテーション仕様に存在しないか、"
329
+ f"アノテーションの種類が「塗りつぶし」ではありません。 :: {invalid_label_name_list}",
330
+ file=sys.stderr,
331
+ )
332
+ sys.exit(COMMAND_LINE_ERROR_STATUS_CODE)
333
+
334
+ main_obj = MergeSegmentationMain(
335
+ self.service,
336
+ project_id=project_id,
337
+ label_ids=label_id_list,
338
+ label_names=label_name_list,
339
+ all_yes=self.all_yes,
340
+ is_force=args.force,
341
+ )
342
+
343
+ main_obj.main(task_id_list, parallelism=args.parallelism)
344
+
345
+
346
+ def main(args: argparse.Namespace) -> None:
347
+ service = build_annofabapi_resource_and_login(args)
348
+ facade = AnnofabApiFacade(service)
349
+ MergeSegmentation(service, facade, args).main()
350
+
351
+
352
+ def parse_args(parser: argparse.ArgumentParser) -> None:
353
+ argument_parser = ArgumentParser(parser)
354
+ argument_parser.add_project_id()
355
+ argument_parser.add_task_id()
356
+
357
+ parser.add_argument(
358
+ "--label_name",
359
+ type=str,
360
+ nargs="+",
361
+ required=True,
362
+ help="変更対象のアノテーションのラベル名(英語)を指定します。",
363
+ )
364
+
365
+ parser.add_argument(
366
+ "--force",
367
+ action="store_true",
368
+ help="過去に担当者を割り当てられていて、かつ現在の担当者が自分自身でない場合、タスクの担当者を一時的に自分自身に変更してからアノテーションをコピーします。",
369
+ )
370
+
371
+ parser.add_argument(
372
+ "--parallelism",
373
+ type=int,
374
+ choices=PARALLELISM_CHOICES,
375
+ help="並列度。指定しない場合は、逐次的に処理します。指定した場合は、``--yes`` も指定してください。",
376
+ )
377
+
378
+ parser.set_defaults(subcommand_func=main)
379
+
380
+
381
+ def add_parser(subparsers: Optional[argparse._SubParsersAction] = None) -> argparse.ArgumentParser:
382
+ subcommand_name = "merge_segmentation"
383
+ subcommand_help = "複数の塗りつぶしアノテーションを1つにまとめます。"
384
+ description = (
385
+ "複数の塗りつぶしアノテーションを1つにまとめます。"
386
+ "ラベルの種類を「塗りつぶし(インスタンスセグメンテーション)」から「塗りつぶしv2(セマンティックセグメンテーション)」に変更する場合などに有用です。"
387
+ )
388
+ parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, subcommand_help, description)
389
+ parse_args(parser)
390
+ return parser
@@ -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
 
@@ -55,14 +55,14 @@ class Task:
55
55
  Returns:
56
56
  必須の列が存在するかどうか
57
57
  """
58
- return len(set(self.columns) - set(df.columns)) == 0
58
+ return len(set(self.required_columns) - set(df.columns)) == 0
59
59
 
60
- def missing_columns(self, df: pandas.DataFrame) -> list[str]:
60
+ def missing_required_columns(self, df: pandas.DataFrame) -> list[str]:
61
61
  """
62
- 欠損している列名を取得します。
62
+ 欠損している必須の列名を取得します。
63
63
 
64
64
  """
65
- return list(set(self.columns) - set(df.columns))
65
+ return list(set(self.required_columns) - set(df.columns))
66
66
 
67
67
  def __init__(self, df: pandas.DataFrame, *, custom_production_volume_list: Optional[list[ProductionVolumeColumn]] = None) -> None:
68
68
  self.custom_production_volume_list = custom_production_volume_list if custom_production_volume_list is not None else []
@@ -72,7 +72,8 @@ class Task:
72
72
 
73
73
  if not self.required_columns_exist(df):
74
74
  raise ValueError(
75
- f"引数'df'の'columns'に次の列が存在していません。 {self.missing_columns(df)} :: 次の列が必須です。{self.columns}の列が必要です。"
75
+ f"引数'df'の'columns'に次の列が存在していません。 {self.missing_required_columns(df)} :: "
76
+ f"次の列が必須です。{self.required_columns} の列が必要です。"
76
77
  )
77
78
 
78
79
  self.df = df
@@ -97,7 +98,19 @@ class Task:
97
98
  return Task(df, custom_production_volume_list=self.custom_production_volume_list)
98
99
 
99
100
  @property
100
- def columns(self) -> list[str]:
101
+ def optional_columns(self) -> list[str]:
102
+ return [
103
+ # 抜取検査または抜取受入によりスキップされたか
104
+ "inspection_is_skipped",
105
+ "acceptance_is_skipped",
106
+ # 差し戻し後の作業時間
107
+ "post_rejection_annotation_worktime_hour",
108
+ "post_rejection_inspection_worktime_hour",
109
+ "post_rejection_acceptance_worktime_hour",
110
+ ]
111
+
112
+ @property
113
+ def required_columns(self) -> list[str]:
101
114
  return [
102
115
  # 基本的な情報
103
116
  "project_id",
@@ -144,6 +157,10 @@ class Task:
144
157
  "acceptance_is_skipped",
145
158
  ]
146
159
 
160
+ @property
161
+ def columns(self) -> list[str]:
162
+ return self.required_columns + self.optional_columns
163
+
147
164
  @classmethod
148
165
  def from_api_content(
149
166
  cls,
@@ -502,7 +519,9 @@ class Task:
502
519
  if not self._validate_df_for_output(output_file):
503
520
  return
504
521
 
505
- print_csv(self.df[self.columns], str(output_file))
522
+ existing_optional_columns = [col for col in self.optional_columns if col in set(self.df.columns)]
523
+ columns = self.required_columns + existing_optional_columns
524
+ print_csv(self.df[columns], str(output_file))
506
525
 
507
526
  def mask_user_info(
508
527
  self,
@@ -9,19 +9,90 @@ import annofabapi
9
9
  import more_itertools
10
10
  import pandas
11
11
  from annofabapi.models import Task, TaskHistory, TaskPhase, TaskStatus
12
+ from annofabapi.util.task_history import find_rejected_task_history_indices
12
13
  from annofabapi.utils import get_task_history_index_skipped_acceptance, get_task_history_index_skipped_inspection
13
14
 
14
15
  import annofabcli
15
16
  from annofabcli.common.cli import ArgumentParser, CommandLine, build_annofabapi_resource_and_login
16
17
  from annofabcli.common.enums import FormatArgument
17
18
  from annofabcli.common.facade import AnnofabApiFacade
18
- from annofabcli.common.utils import print_csv, print_json
19
+ from annofabcli.common.utils import isoduration_to_hour, print_csv, print_json
19
20
  from annofabcli.common.visualize import AddProps
20
21
  from annofabcli.task.list_tasks import ListTasksMain
21
22
 
22
23
  logger = logging.getLogger(__name__)
23
24
 
24
25
 
26
+ def get_post_rejection_annotation_worktime_hour(task_histories: list[TaskHistory]) -> float:
27
+ """
28
+ 検査/受入フェーズでの差し戻し後の教師付作業時間を算出します。
29
+ 指摘による修正にかかった時間を把握するのに利用できます。
30
+
31
+ Args:
32
+ task_histories: タスク履歴
33
+
34
+ """
35
+ rejected_task_history_indices = find_rejected_task_history_indices(task_histories)
36
+ if len(rejected_task_history_indices) == 0:
37
+ return 0.0
38
+
39
+ # 差し戻された履歴の直後で、教師付フェーズの作業時間を算出する
40
+ min_rejected_task_history_index = min(rejected_task_history_indices)
41
+ return sum(
42
+ isoduration_to_hour(history["accumulated_labor_time_milliseconds"])
43
+ for history in task_histories[min_rejected_task_history_index + 1 :]
44
+ if history["phase"] == TaskPhase.ANNOTATION.value
45
+ )
46
+
47
+
48
+ def get_post_rejection_inspection_worktime_hour(task_histories: list[TaskHistory]) -> float:
49
+ """
50
+ 検査/受入フェーズでの差し戻し後の検査作業時間を算出します。
51
+
52
+ Args:
53
+ task_histories: タスク履歴
54
+
55
+ """
56
+ rejected_task_history_indices = find_rejected_task_history_indices(task_histories)
57
+ if len(rejected_task_history_indices) == 0:
58
+ return 0.0
59
+
60
+ # 差し戻された履歴の直後で、検査フェーズの作業時間を算出する
61
+ min_rejected_task_history_index = min(rejected_task_history_indices)
62
+ return sum(
63
+ isoduration_to_hour(history["accumulated_labor_time_milliseconds"])
64
+ for history in task_histories[min_rejected_task_history_index + 1 :]
65
+ if history["phase"] == TaskPhase.INSPECTION.value
66
+ )
67
+
68
+
69
+ def get_post_rejection_acceptance_worktime_hour(task_histories: list[TaskHistory]) -> float:
70
+ """
71
+ 受入フェーズでの差し戻し後の受入作業時間を算出します。
72
+
73
+
74
+ Args:
75
+ task_histories: タスク履歴
76
+
77
+ """
78
+ rejected_task_history_indices = find_rejected_task_history_indices(task_histories)
79
+
80
+ # 検査フェーズでの差し戻しは除外する
81
+ # 検査フェーズでの差し戻しは、受入作業の回数に影響しないため
82
+ acceptance_rejected_indices = [index for index in rejected_task_history_indices if task_histories[index]["phase"] == TaskPhase.ACCEPTANCE.value]
83
+ if len(acceptance_rejected_indices) == 0:
84
+ return 0.0
85
+
86
+ min_rejected_acceptance_task_history_index = min(acceptance_rejected_indices)
87
+
88
+ # 差し戻された履歴の直後以降で、受入フェーズの作業時間を算出する
89
+ return sum(
90
+ isoduration_to_hour(history["accumulated_labor_time_milliseconds"])
91
+ for history in task_histories[min_rejected_acceptance_task_history_index + 1 :]
92
+ if history["phase"] == TaskPhase.ACCEPTANCE.value
93
+ )
94
+
95
+
25
96
  class AddingAdditionalInfoToTask:
26
97
  """タスクに付加的な情報を追加するためのクラス
27
98
 
@@ -210,12 +281,12 @@ class AddingAdditionalInfoToTask:
210
281
  }
211
282
  )
212
283
 
213
- organization_member = self.visualize.get_project_member_from_account_id(account_id)
214
- if organization_member is not None:
284
+ member = self.visualize.get_project_member_from_account_id(account_id)
285
+ if member is not None:
215
286
  task.update(
216
287
  {
217
- f"{column_prefix}_user_id": organization_member["user_id"],
218
- f"{column_prefix}_username": organization_member["username"],
288
+ f"{column_prefix}_user_id": member["user_id"],
289
+ f"{column_prefix}_username": member["username"],
219
290
  }
220
291
  )
221
292
  else:
@@ -243,7 +314,7 @@ class AddingAdditionalInfoToTask:
243
314
 
244
315
  return task
245
316
 
246
- def add_additional_info_to_task(self, task: dict[str, Any]): # noqa: ANN201
317
+ def add_additional_info_to_task(self, task: dict[str, Any]) -> None:
247
318
  """タスクの付加的情報を、タスクに追加する。
248
319
  以下の列を追加する。
249
320
  * user_id
@@ -298,6 +369,10 @@ class AddingAdditionalInfoToTask:
298
369
  task["inspection_is_skipped"] = self.is_inspection_phase_skipped(task_histories)
299
370
  task["acceptance_is_skipped"] = self.is_acceptance_phase_skipped(task_histories)
300
371
 
372
+ task["post_rejection_annotation_worktime_hour"] = get_post_rejection_annotation_worktime_hour(task_histories)
373
+ task["post_rejection_inspection_worktime_hour"] = get_post_rejection_inspection_worktime_hour(task_histories)
374
+ task["post_rejection_acceptance_worktime_hour"] = get_post_rejection_acceptance_worktime_hour(task_histories)
375
+
301
376
 
302
377
  class ListTasksAddedTaskHistoryMain:
303
378
  def __init__(self, service: annofabapi.Resource, project_id: str) -> None:
@@ -373,9 +448,17 @@ class TasksAddedTaskHistoryOutput:
373
448
  for info in ["user_id", "username", "started_datetime", "worktime_hour"]
374
449
  ]
375
450
 
376
- return base_columns + task_history_columns
451
+ return (
452
+ base_columns
453
+ + task_history_columns
454
+ + [
455
+ "post_rejection_annotation_worktime_hour",
456
+ "post_rejection_inspection_worktime_hour",
457
+ "post_rejection_acceptance_worktime_hour",
458
+ ]
459
+ )
377
460
 
378
- def output(self, output_path: Path, output_format: FormatArgument): # noqa: ANN201
461
+ def output(self, output_path: Path, output_format: FormatArgument) -> None:
379
462
  task_list = self.task_list
380
463
  if len(task_list) == 0:
381
464
  logger.info("タスク一覧の件数が0件のため、出力しません。")
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.1
2
2
  Name: annofabcli
3
- Version: 1.97.0
3
+ Version: 1.98.0
4
4
  Summary: Utility Command Line Interface for AnnoFab
5
5
  Home-page: https://github.com/kurusugawa-computer/annofab-cli
6
6
  License: MIT
@@ -19,7 +19,7 @@ Classifier: Programming Language :: Python :: 3.11
19
19
  Classifier: Programming Language :: Python :: 3.12
20
20
  Classifier: Topic :: Utilities
21
21
  Requires-Dist: Pillow
22
- Requires-Dist: annofabapi (>=1.1,<2.0)
22
+ Requires-Dist: annofabapi (>=1.4.1,<2.0.0)
23
23
  Requires-Dist: bokeh (>=3.3,<3.7)
24
24
  Requires-Dist: dictdiffer
25
25
  Requires-Dist: isodate
@@ -1,6 +1,6 @@
1
1
  annofabcli/__init__.py,sha256=NMA7kFxmLlCiILQPHJa9mEuqXxtLALw_dwyXYsvz4VM,71
2
2
  annofabcli/__main__.py,sha256=JzfycqVG9ENhWOCxTouZwpHwWTSrI-grLsaMudxjyBM,5283
3
- annofabcli/__version__.py,sha256=dqx_NARSo9UcLPvxc6zJBGKIFxXd0yc57SlNZJUxSbg,132
3
+ annofabcli/__version__.py,sha256=dHLiPS6l0hIriGlUklNuZkVcdn7vcXe4J9YqcWD68UA,132
4
4
  annofabcli/annotation/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
5
5
  annofabcli/annotation/annotation_query.py,sha256=ke3W3RT1-WfFzwt-TXcQwGmghG34vcKJkM_jxgbNKjU,15922
6
6
  annofabcli/annotation/change_annotation_attributes.py,sha256=zHXyENZfbMGL_15xiK7Cy4cQ2sV0GjSVmKuPm3sOX7Y,17173
@@ -12,8 +12,10 @@ annofabcli/annotation/dump_annotation.py,sha256=YIl9eP6cfvMcE13CLooZO7siHzotc67b
12
12
  annofabcli/annotation/import_annotation.py,sha256=BlGAcXF9T19Ea1wtplUMPrOfiWYLBajK2XizOctdUtA,29304
13
13
  annofabcli/annotation/list_annotation.py,sha256=B8ZFI7SRQPy-TgbpaX-tzXhveViY17YuCPE9BuK9mrs,10790
14
14
  annofabcli/annotation/list_annotation_count.py,sha256=T9fbaoxWeDJIVgW_YgHRldbwrVZWiE-57lfJrDQrj80,6474
15
+ annofabcli/annotation/merge_segmentation.py,sha256=ldEQlcyCrog0KFYn92sTkScZQ7gmTjlujuCsKBemjhU,17950
16
+ annofabcli/annotation/remove_segmentation_overlap.py,sha256=gONYqaI0PMEGYJduB4JsmGBn3LCY9JtLSFgosWYyc3M,15957
15
17
  annofabcli/annotation/restore_annotation.py,sha256=naUEbt48ION9JSijCBR2aQdaoCrRu005tYq0vgUtyp0,14683
16
- annofabcli/annotation/subcommand_annotation.py,sha256=Xu4SOHLpo_kPZbvE8hQHr9NRduPTESqu7pYC4LGDlXg,1872
18
+ annofabcli/annotation/subcommand_annotation.py,sha256=ku9mzb7zZilHcjf1MFV1E7EJ8OvfSUDHpcunM38teto,2122
17
19
  annofabcli/annotation_specs/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
20
  annofabcli/annotation_specs/export_annotation_specs.py,sha256=eZF5fj2P5U22_5UKgbpsUhZEUvVPMPerMpN4miIcnSA,4821
19
21
  annofabcli/annotation_specs/get_annotation_specs_with_attribute_id_replaced.py,sha256=L0H0M31Amnm3ZEg9JdQe2A1vprsCLPTpj0LUFnP6GDY,8330
@@ -150,7 +152,7 @@ annofabcli/statistics/visualization/dataframe/input_data_count.py,sha256=wDRFtoI
150
152
  annofabcli/statistics/visualization/dataframe/inspection_comment_count.py,sha256=RxpQzRy4U2hKEpgbksUXotcxH2sKz__NO20mxpMqK1w,4382
151
153
  annofabcli/statistics/visualization/dataframe/productivity_per_date.py,sha256=tMap7E3z7hibon1zJnZRJnbMmtzqh04ocoV0oxBpssU,27249
152
154
  annofabcli/statistics/visualization/dataframe/project_performance.py,sha256=hdTMPvLfGDMZFjpIl58GtTEOopsOvitbdaj5hQAEp8o,8496
153
- annofabcli/statistics/visualization/dataframe/task.py,sha256=KanuLy67ZGORdLry21eN7uSNzkoJvIre1JN7Bq-fRlg,23452
155
+ annofabcli/statistics/visualization/dataframe/task.py,sha256=Heb0sx7T6KI422aFVKQUCP2wqD_czpM4KMsgqb4wjVw,24269
154
156
  annofabcli/statistics/visualization/dataframe/task_history.py,sha256=3b9e4ok6yKE5x647KzRqvp01P33XMAHLEEbLJ5GCmRo,2760
155
157
  annofabcli/statistics/visualization/dataframe/task_worktime_by_phase_user.py,sha256=AtlbeNIkttjLtuxtZYCyZin4eVKRvcYEMnLzEZtZUlY,13134
156
158
  annofabcli/statistics/visualization/dataframe/user.py,sha256=EHn7nlf6D6UX-gsVXy8m_3QaCsHsUhr0iy2rbNozOgc,1707
@@ -184,7 +186,7 @@ annofabcli/task/download_task_json.py,sha256=Ocjecmdf2WV_Sq3u1InfMLIsT3XSw0ojyJm
184
186
  annofabcli/task/list_all_tasks.py,sha256=F9GpzzgWffF3lUeGrFIvjweq-iEwJ1c-g8usskO_2dE,6506
185
187
  annofabcli/task/list_all_tasks_added_task_history.py,sha256=fkdiuo64iS7xxvIfGKzSiUPPEMiCVnJjjcAtMxe2Ngs,9551
186
188
  annofabcli/task/list_tasks.py,sha256=O4jjp_zdmurcGNWXFp9JXHJsH4nhlR5e3ok96YnD1SI,10237
187
- annofabcli/task/list_tasks_added_task_history.py,sha256=7avkLYFErcdSNxGc8CQXr4FFIF1z9FtQJBSloD-tzBI,19675
189
+ annofabcli/task/list_tasks_added_task_history.py,sha256=IferAd2Q-fHvXMkuzGvEOzYlpokbykm-TkvUcAlMpGY,23151
188
190
  annofabcli/task/put_tasks.py,sha256=hT2xPowJmcNJhjxoAm-MFiKTw_RFcJUYlpeanegVrAU,13400
189
191
  annofabcli/task/put_tasks_by_count.py,sha256=MUHfWhqtSAXnB3O36p3bMSSgQ_3Zek9GT5qRvHGx8Lo,6041
190
192
  annofabcli/task/reject_tasks.py,sha256=5ByAN6VnKwvU5BT_cfsHwA1jLDl74bonqk3bwtnrkPU,23139
@@ -200,8 +202,8 @@ annofabcli/task_history_event/download_task_history_event_json.py,sha256=hQLVbQ0
200
202
  annofabcli/task_history_event/list_all_task_history_event.py,sha256=JQEgwOIXbbTIfeX23AVaoySHViOR9UGm9uoXuhVEBqo,6446
201
203
  annofabcli/task_history_event/list_worktime.py,sha256=9jsRYa2C9bva8E1Aqxv9CCKDuCP0MvbiaIyQFTDpjqY,13150
202
204
  annofabcli/task_history_event/subcommand_task_history_event.py,sha256=mJVJoT4RXk4HWnY7-Nrsl4If-gtaIIEXd2z7eFZwM2I,1260
203
- annofabcli-1.97.0.dist-info/LICENSE,sha256=pcqWYfxFtxBzhvKp3x9MXNM4xciGb2eFewaRhXUNHlo,1081
204
- annofabcli-1.97.0.dist-info/METADATA,sha256=71xtnSNlS97JSBeUVYWYcoas9gqXjL9v735krW5zioc,5626
205
- annofabcli-1.97.0.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
206
- annofabcli-1.97.0.dist-info/entry_points.txt,sha256=A8vlN9fiMhbYRcdBfSpl7piYzAwvkMhRXIPQUAvQFUo,55
207
- annofabcli-1.97.0.dist-info/RECORD,,
205
+ annofabcli-1.98.0.dist-info/LICENSE,sha256=pcqWYfxFtxBzhvKp3x9MXNM4xciGb2eFewaRhXUNHlo,1081
206
+ annofabcli-1.98.0.dist-info/METADATA,sha256=QOFLl_jHIBCsV06Dj8l_UQ-ZnJ5RiVxSCuaOjHNHQFk,5630
207
+ annofabcli-1.98.0.dist-info/WHEEL,sha256=FMvqSimYX_P7y0a7UY-_Mc83r5zkBZsCYPm7Lr0Bsq4,88
208
+ annofabcli-1.98.0.dist-info/entry_points.txt,sha256=A8vlN9fiMhbYRcdBfSpl7piYzAwvkMhRXIPQUAvQFUo,55
209
+ annofabcli-1.98.0.dist-info/RECORD,,