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 +1 -1
- annofabcli/annotation/merge_segmentation.py +390 -0
- annofabcli/annotation/remove_segmentation_overlap.py +343 -0
- annofabcli/annotation/subcommand_annotation.py +4 -0
- annofabcli/statistics/visualization/dataframe/task.py +26 -7
- annofabcli/task/list_tasks_added_task_history.py +91 -8
- {annofabcli-1.97.0.dist-info → annofabcli-1.98.0.dist-info}/METADATA +2 -2
- {annofabcli-1.97.0.dist-info → annofabcli-1.98.0.dist-info}/RECORD +11 -9
- {annofabcli-1.97.0.dist-info → annofabcli-1.98.0.dist-info}/LICENSE +0 -0
- {annofabcli-1.97.0.dist-info → annofabcli-1.98.0.dist-info}/WHEEL +0 -0
- {annofabcli-1.97.0.dist-info → annofabcli-1.98.0.dist-info}/entry_points.txt +0 -0
annofabcli/__version__.py
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
__version__ = "1.
|
|
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.
|
|
58
|
+
return len(set(self.required_columns) - set(df.columns)) == 0
|
|
59
59
|
|
|
60
|
-
def
|
|
60
|
+
def missing_required_columns(self, df: pandas.DataFrame) -> list[str]:
|
|
61
61
|
"""
|
|
62
|
-
|
|
62
|
+
欠損している必須の列名を取得します。
|
|
63
63
|
|
|
64
64
|
"""
|
|
65
|
-
return list(set(self.
|
|
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
214
|
-
if
|
|
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":
|
|
218
|
-
f"{column_prefix}_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])
|
|
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
|
|
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)
|
|
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.
|
|
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=
|
|
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=
|
|
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=
|
|
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=
|
|
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.
|
|
204
|
-
annofabcli-1.
|
|
205
|
-
annofabcli-1.
|
|
206
|
-
annofabcli-1.
|
|
207
|
-
annofabcli-1.
|
|
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,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|