annofabcli 1.109.0__py3-none-any.whl → 1.111.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.
@@ -16,12 +16,7 @@ from dataclasses_json import DataClassJsonMixin
16
16
  import annofabcli
17
17
  import annofabcli.common.cli
18
18
  from annofabcli.common.annofab.annotation_zip import lazy_parse_simple_annotation_by_input_data
19
- from annofabcli.common.cli import (
20
- COMMAND_LINE_ERROR_STATUS_CODE,
21
- ArgumentParser,
22
- CommandLine,
23
- build_annofabapi_resource_and_login,
24
- )
19
+ from annofabcli.common.cli import COMMAND_LINE_ERROR_STATUS_CODE, ArgumentParser, CommandLine, build_annofabapi_resource_and_login, get_list_from_args
25
20
  from annofabcli.common.download import DownloadingFile
26
21
  from annofabcli.common.enums import FormatArgument
27
22
  from annofabcli.common.facade import (
@@ -56,10 +51,16 @@ class AnnotationBoundingBoxInfo(DataClassJsonMixin):
56
51
  height: int
57
52
 
58
53
 
59
- def get_annotation_bounding_box_info_list(simple_annotation: dict[str, Any]) -> list[AnnotationBoundingBoxInfo]:
54
+ def get_annotation_bounding_box_info_list(simple_annotation: dict[str, Any], *, target_label_names: Optional[Collection[str]] = None) -> list[AnnotationBoundingBoxInfo]:
60
55
  result = []
56
+ target_label_names_set = set(target_label_names) if target_label_names is not None else None
61
57
  for detail in simple_annotation["details"]:
62
58
  if detail["data"]["_type"] == "BoundingBox":
59
+ label = detail["label"]
60
+ # ラベル名によるフィルタリング
61
+ if target_label_names_set is not None and label not in target_label_names_set:
62
+ continue
63
+
63
64
  left_top = detail["data"]["left_top"]
64
65
  right_bottom = detail["data"]["right_bottom"]
65
66
  width = abs(right_bottom["x"] - left_top["x"])
@@ -74,7 +75,7 @@ def get_annotation_bounding_box_info_list(simple_annotation: dict[str, Any]) ->
74
75
  task_status=simple_annotation["task_status"],
75
76
  input_data_id=simple_annotation["input_data_id"],
76
77
  input_data_name=simple_annotation["input_data_name"],
77
- label=detail["label"],
78
+ label=label,
78
79
  annotation_id=detail["annotation_id"],
79
80
  left_top=left_top,
80
81
  right_bottom=right_bottom,
@@ -92,6 +93,7 @@ def get_annotation_bounding_box_info_list_from_annotation_path(
92
93
  *,
93
94
  target_task_ids: Optional[Collection[str]] = None,
94
95
  task_query: Optional[TaskQuery] = None,
96
+ target_label_names: Optional[Collection[str]] = None,
95
97
  ) -> list[AnnotationBoundingBoxInfo]:
96
98
  annotation_bbox_list = []
97
99
  target_task_ids = set(target_task_ids) if target_task_ids is not None else None
@@ -105,7 +107,7 @@ def get_annotation_bounding_box_info_list_from_annotation_path(
105
107
  dict_simple_annotation = parser.load_json()
106
108
  if task_query is not None and not match_annotation_with_task_query(dict_simple_annotation, task_query):
107
109
  continue
108
- sub_annotation_bbox_list = get_annotation_bounding_box_info_list(dict_simple_annotation)
110
+ sub_annotation_bbox_list = get_annotation_bounding_box_info_list(dict_simple_annotation, target_label_names=target_label_names)
109
111
  annotation_bbox_list.extend(sub_annotation_bbox_list)
110
112
  return annotation_bbox_list
111
113
 
@@ -131,32 +133,30 @@ def create_df(
131
133
  "width",
132
134
  "height",
133
135
  ]
134
- if len(annotation_bbox_list) > 0:
135
- df = pandas.DataFrame(
136
- [
137
- {
138
- "project_id": e.project_id,
139
- "task_id": e.task_id,
140
- "task_status": e.task_status,
141
- "task_phase": e.task_phase,
142
- "task_phase_stage": e.task_phase_stage,
143
- "input_data_id": e.input_data_id,
144
- "input_data_name": e.input_data_name,
145
- "updated_datetime": e.updated_datetime,
146
- "label": e.label,
147
- "annotation_id": e.annotation_id,
148
- "left_top.x": e.left_top["x"],
149
- "left_top.y": e.left_top["y"],
150
- "right_bottom.x": e.right_bottom["x"],
151
- "right_bottom.y": e.right_bottom["y"],
152
- "width": e.width,
153
- "height": e.height,
154
- }
155
- for e in annotation_bbox_list
156
- ]
157
- )
158
- else:
159
- df = pandas.DataFrame(columns=columns)
136
+ df = pandas.DataFrame(
137
+ [
138
+ {
139
+ "project_id": e.project_id,
140
+ "task_id": e.task_id,
141
+ "task_status": e.task_status,
142
+ "task_phase": e.task_phase,
143
+ "task_phase_stage": e.task_phase_stage,
144
+ "input_data_id": e.input_data_id,
145
+ "input_data_name": e.input_data_name,
146
+ "updated_datetime": e.updated_datetime,
147
+ "label": e.label,
148
+ "annotation_id": e.annotation_id,
149
+ "left_top.x": e.left_top["x"],
150
+ "left_top.y": e.left_top["y"],
151
+ "right_bottom.x": e.right_bottom["x"],
152
+ "right_bottom.y": e.right_bottom["y"],
153
+ "width": e.width,
154
+ "height": e.height,
155
+ }
156
+ for e in annotation_bbox_list
157
+ ],
158
+ columns=columns,
159
+ )
160
160
 
161
161
  return df[columns]
162
162
 
@@ -168,11 +168,13 @@ def print_annotation_bounding_box(
168
168
  *,
169
169
  target_task_ids: Optional[Collection[str]] = None,
170
170
  task_query: Optional[TaskQuery] = None,
171
+ target_label_names: Optional[Collection[str]] = None,
171
172
  ) -> None:
172
173
  annotation_bbox_list = get_annotation_bounding_box_info_list_from_annotation_path(
173
174
  annotation_path,
174
175
  target_task_ids=target_task_ids,
175
176
  task_query=task_query,
177
+ target_label_names=target_label_names,
176
178
  )
177
179
 
178
180
  logger.info(f"{len(annotation_bbox_list)} 件のバウンディングボックスアノテーションの情報を出力します。 :: output='{output_file}'")
@@ -195,7 +197,7 @@ def print_annotation_bounding_box(
195
197
 
196
198
 
197
199
  class ListAnnotationBoundingBox2d(CommandLine):
198
- COMMON_MESSAGE = "annofabcli annotation_zip list_annotation_bounding_box_2d: error:"
200
+ COMMON_MESSAGE = "annofabcli annotation_zip list_bounding_box_annotation: error:"
199
201
 
200
202
  def validate(self, args: argparse.Namespace) -> bool:
201
203
  if args.project_id is None and args.annotation is None:
@@ -224,6 +226,7 @@ class ListAnnotationBoundingBox2d(CommandLine):
224
226
 
225
227
  task_id_list = annofabcli.common.cli.get_list_from_args(args.task_id) if args.task_id is not None else None
226
228
  task_query = TaskQuery.from_dict(annofabcli.common.cli.get_json_from_args(args.task_query)) if args.task_query is not None else None
229
+ label_name_list = get_list_from_args(args.label_name) if args.label_name is not None else None
227
230
 
228
231
  output_file: Path = args.output
229
232
  output_format = FormatArgument(args.format)
@@ -243,6 +246,7 @@ class ListAnnotationBoundingBox2d(CommandLine):
243
246
  output_format,
244
247
  target_task_ids=task_id_list,
245
248
  task_query=task_query,
249
+ target_label_names=label_name_list,
246
250
  )
247
251
 
248
252
  if project_id is not None:
@@ -263,6 +267,7 @@ class ListAnnotationBoundingBox2d(CommandLine):
263
267
  output_format,
264
268
  target_task_ids=task_id_list,
265
269
  task_query=task_query,
270
+ target_label_names=label_name_list,
266
271
  )
267
272
 
268
273
 
@@ -294,6 +299,13 @@ def parse_args(parser: argparse.ArgumentParser) -> None:
294
299
  )
295
300
  argument_parser.add_task_id(required=False)
296
301
 
302
+ parser.add_argument(
303
+ "--label_name",
304
+ type=str,
305
+ nargs="*",
306
+ help="指定したラベル名のバウンディングボックスアノテーションのみを対象にします。複数指定できます。",
307
+ )
308
+
297
309
  parser.add_argument(
298
310
  "--latest",
299
311
  action="store_true",
@@ -316,7 +328,7 @@ def main(args: argparse.Namespace) -> None:
316
328
 
317
329
 
318
330
  def add_parser(subparsers: Optional[argparse._SubParsersAction] = None) -> argparse.ArgumentParser:
319
- subcommand_name = "list_annotation_bounding_box_2d"
331
+ subcommand_name = "list_bounding_box_annotation"
320
332
  subcommand_help = "アノテーションZIPからバウンディングボックス(矩形)アノテーションの座標情報を出力します。"
321
333
  epilog = "アノテーションZIPをダウンロードする場合は、オーナロールまたはアノテーションユーザロールを持つユーザで実行してください。"
322
334
  parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, subcommand_help, description=subcommand_help, epilog=epilog)
@@ -0,0 +1,308 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import logging
5
+ import sys
6
+ import tempfile
7
+ from collections.abc import Collection
8
+ from dataclasses import dataclass
9
+ from pathlib import Path
10
+ from typing import Any, Optional
11
+
12
+ import pandas
13
+ from annofabapi.models import InputDataType, ProjectMemberRole
14
+ from dataclasses_json import DataClassJsonMixin
15
+
16
+ import annofabcli
17
+ import annofabcli.common.cli
18
+ from annofabcli.common.annofab.annotation_zip import lazy_parse_simple_annotation_by_input_data
19
+ from annofabcli.common.cli import COMMAND_LINE_ERROR_STATUS_CODE, ArgumentParser, CommandLine, build_annofabapi_resource_and_login, get_list_from_args
20
+ from annofabcli.common.download import DownloadingFile
21
+ from annofabcli.common.enums import FormatArgument
22
+ from annofabcli.common.facade import (
23
+ AnnofabApiFacade,
24
+ TaskQuery,
25
+ match_annotation_with_task_query,
26
+ )
27
+ from annofabcli.common.utils import print_csv, print_json
28
+
29
+ logger = logging.getLogger(__name__)
30
+
31
+
32
+ @dataclass(frozen=True)
33
+ class RangeAnnotationInfo(DataClassJsonMixin):
34
+ project_id: str
35
+ task_id: str
36
+ task_status: str
37
+ task_phase: str
38
+ task_phase_stage: int
39
+
40
+ input_data_id: str
41
+ input_data_name: str
42
+
43
+ updated_datetime: Optional[str]
44
+ """アノテーションJSONに格納されているアノテーションの更新日時"""
45
+
46
+ label: str
47
+ annotation_id: str
48
+ begin_second: float
49
+ end_second: float
50
+ duration_second: float
51
+
52
+
53
+ def get_range_annotation_info_list(simple_annotation: dict[str, Any], *, target_label_names: Optional[Collection[str]] = None) -> list[RangeAnnotationInfo]:
54
+ result = []
55
+ target_label_names_set = set(target_label_names) if target_label_names is not None else None
56
+ for detail in simple_annotation["details"]:
57
+ if detail["data"]["_type"] == "Range":
58
+ label = detail["label"]
59
+ # ラベル名によるフィルタリング
60
+ if target_label_names_set is not None and label not in target_label_names_set:
61
+ continue
62
+
63
+ begin_millisecond = detail["data"]["begin"]
64
+ end_millisecond = detail["data"]["end"]
65
+ begin_second = begin_millisecond / 1000
66
+ end_second = end_millisecond / 1000
67
+ duration_second = end_second - begin_second
68
+
69
+ result.append(
70
+ RangeAnnotationInfo(
71
+ project_id=simple_annotation["project_id"],
72
+ task_id=simple_annotation["task_id"],
73
+ task_phase=simple_annotation["task_phase"],
74
+ task_phase_stage=simple_annotation["task_phase_stage"],
75
+ task_status=simple_annotation["task_status"],
76
+ input_data_id=simple_annotation["input_data_id"],
77
+ input_data_name=simple_annotation["input_data_name"],
78
+ label=label,
79
+ annotation_id=detail["annotation_id"],
80
+ begin_second=begin_second,
81
+ end_second=end_second,
82
+ duration_second=duration_second,
83
+ updated_datetime=simple_annotation["updated_datetime"],
84
+ )
85
+ )
86
+
87
+ return result
88
+
89
+
90
+ def get_range_annotation_info_list_from_annotation_path(
91
+ annotation_path: Path,
92
+ *,
93
+ target_task_ids: Optional[Collection[str]] = None,
94
+ task_query: Optional[TaskQuery] = None,
95
+ target_label_names: Optional[Collection[str]] = None,
96
+ ) -> list[RangeAnnotationInfo]:
97
+ range_annotation_list = []
98
+ target_task_ids = set(target_task_ids) if target_task_ids is not None else None
99
+ iter_parser = lazy_parse_simple_annotation_by_input_data(annotation_path)
100
+ logger.info(f"アノテーションZIPまたはディレクトリ'{annotation_path}'を読み込みます。")
101
+ for index, parser in enumerate(iter_parser):
102
+ if (index + 1) % 10000 == 0:
103
+ logger.info(f"{index + 1} 件目のJSONを読み込み中")
104
+ if target_task_ids is not None and parser.task_id not in target_task_ids:
105
+ continue
106
+ dict_simple_annotation = parser.load_json()
107
+ if task_query is not None and not match_annotation_with_task_query(dict_simple_annotation, task_query):
108
+ continue
109
+ sub_range_annotation_list = get_range_annotation_info_list(dict_simple_annotation, target_label_names=target_label_names)
110
+ range_annotation_list.extend(sub_range_annotation_list)
111
+ return range_annotation_list
112
+
113
+
114
+ def create_df(
115
+ range_annotation_list: list[RangeAnnotationInfo],
116
+ ) -> pandas.DataFrame:
117
+ columns = [
118
+ "project_id",
119
+ "task_id",
120
+ "task_status",
121
+ "task_phase",
122
+ "task_phase_stage",
123
+ "input_data_id",
124
+ "input_data_name",
125
+ "updated_datetime",
126
+ "label",
127
+ "annotation_id",
128
+ "begin_second",
129
+ "end_second",
130
+ "duration_second",
131
+ ]
132
+ df = pandas.DataFrame([e.to_dict(encode_json=True) for e in range_annotation_list], columns=columns)
133
+
134
+ return df[columns]
135
+
136
+
137
+ def print_range_annotation(
138
+ annotation_path: Path,
139
+ output_file: Path,
140
+ output_format: FormatArgument,
141
+ *,
142
+ target_task_ids: Optional[Collection[str]] = None,
143
+ task_query: Optional[TaskQuery] = None,
144
+ target_label_names: Optional[Collection[str]] = None,
145
+ ) -> None:
146
+ range_annotation_list = get_range_annotation_info_list_from_annotation_path(
147
+ annotation_path,
148
+ target_task_ids=target_task_ids,
149
+ task_query=task_query,
150
+ target_label_names=target_label_names,
151
+ )
152
+
153
+ logger.info(f"{len(range_annotation_list)} 件の区間アノテーションの情報を出力します。 :: output='{output_file}'")
154
+
155
+ if output_format == FormatArgument.CSV:
156
+ df = create_df(range_annotation_list)
157
+ print_csv(df, output_file)
158
+
159
+ elif output_format in [FormatArgument.PRETTY_JSON, FormatArgument.JSON]:
160
+ json_is_pretty = output_format == FormatArgument.PRETTY_JSON
161
+ # DataClassJsonMixinを使用したtoJSON処理
162
+ print_json(
163
+ [e.to_dict(encode_json=True) for e in range_annotation_list],
164
+ is_pretty=json_is_pretty,
165
+ output=output_file,
166
+ )
167
+
168
+ else:
169
+ raise ValueError(f"出力形式 '{output_format}' はサポートされていません。")
170
+
171
+
172
+ class ListRangeAnnotation(CommandLine):
173
+ COMMON_MESSAGE = "annofabcli annotation_zip list_range_annotation: error:"
174
+
175
+ def validate(self, args: argparse.Namespace) -> bool:
176
+ if args.project_id is None and args.annotation is None:
177
+ print( # noqa: T201
178
+ f"{self.COMMON_MESSAGE} argument --project_id: '--annotation'が未指定のときは、'--project_id' を指定してください。",
179
+ file=sys.stderr,
180
+ )
181
+ return False
182
+ return True
183
+
184
+ def main(self) -> None:
185
+ args = self.args
186
+
187
+ if not self.validate(args):
188
+ sys.exit(COMMAND_LINE_ERROR_STATUS_CODE)
189
+
190
+ project_id: Optional[str] = args.project_id
191
+ if project_id is not None:
192
+ super().validate_project(project_id, project_member_roles=[ProjectMemberRole.OWNER, ProjectMemberRole.TRAINING_DATA_USER])
193
+ project, _ = self.service.api.get_project(project_id)
194
+ if project["input_data_type"] != InputDataType.MOVIE.value:
195
+ print(f"project_id='{project_id}'であるプロジェクトは動画プロジェクトでないので、終了します", file=sys.stderr) # noqa: T201
196
+ sys.exit(COMMAND_LINE_ERROR_STATUS_CODE)
197
+
198
+ annotation_path = Path(args.annotation) if args.annotation is not None else None
199
+
200
+ task_id_list = annofabcli.common.cli.get_list_from_args(args.task_id) if args.task_id is not None else None
201
+ task_query = TaskQuery.from_dict(annofabcli.common.cli.get_json_from_args(args.task_query)) if args.task_query is not None else None
202
+ label_name_list = get_list_from_args(args.label_name) if args.label_name is not None else None
203
+
204
+ output_file: Path = args.output
205
+ output_format = FormatArgument(args.format)
206
+
207
+ downloading_obj = DownloadingFile(self.service)
208
+
209
+ def download_and_print_range_annotation(project_id: str, temp_dir: Path, *, is_latest: bool) -> None:
210
+ annotation_path = downloading_obj.download_annotation_zip_to_dir(
211
+ project_id,
212
+ temp_dir,
213
+ is_latest=is_latest,
214
+ )
215
+ print_range_annotation(
216
+ annotation_path,
217
+ output_file,
218
+ output_format,
219
+ target_task_ids=task_id_list,
220
+ task_query=task_query,
221
+ target_label_names=label_name_list,
222
+ )
223
+
224
+ if project_id is not None:
225
+ if args.temp_dir is not None:
226
+ download_and_print_range_annotation(project_id=project_id, temp_dir=args.temp_dir, is_latest=args.latest)
227
+ else:
228
+ with tempfile.TemporaryDirectory() as str_temp_dir:
229
+ download_and_print_range_annotation(
230
+ project_id=project_id,
231
+ temp_dir=Path(str_temp_dir),
232
+ is_latest=args.latest,
233
+ )
234
+ else:
235
+ assert annotation_path is not None
236
+ print_range_annotation(
237
+ annotation_path,
238
+ output_file,
239
+ output_format,
240
+ target_task_ids=task_id_list,
241
+ task_query=task_query,
242
+ target_label_names=label_name_list,
243
+ )
244
+
245
+
246
+ def parse_args(parser: argparse.ArgumentParser) -> None:
247
+ argument_parser = ArgumentParser(parser)
248
+
249
+ group = parser.add_mutually_exclusive_group(required=True)
250
+ group.add_argument(
251
+ "--annotation",
252
+ type=str,
253
+ help="アノテーションzip、またはzipを展開したディレクトリを指定します。",
254
+ )
255
+
256
+ group.add_argument("-p", "--project_id", type=str, help="project_id。アノテーションZIPをダウンロードします。")
257
+
258
+ argument_parser.add_format(
259
+ choices=[FormatArgument.CSV, FormatArgument.JSON, FormatArgument.PRETTY_JSON],
260
+ default=FormatArgument.CSV,
261
+ )
262
+
263
+ argument_parser.add_output()
264
+
265
+ parser.add_argument(
266
+ "-tq",
267
+ "--task_query",
268
+ type=str,
269
+ help="集計対象タスクを絞り込むためのクエリ条件をJSON形式で指定します。使用できるキーは task_id, status, phase, phase_stage です。"
270
+ " ``file://`` を先頭に付けると、JSON形式のファイルを指定できます。",
271
+ )
272
+ argument_parser.add_task_id(required=False)
273
+
274
+ parser.add_argument(
275
+ "--label_name",
276
+ type=str,
277
+ nargs="*",
278
+ help="指定したラベル名の区間アノテーションのみを対象にします。複数指定できます。",
279
+ )
280
+
281
+ parser.add_argument(
282
+ "--latest",
283
+ action="store_true",
284
+ help="``--annotation`` を指定しないとき、最新のアノテーションzipを参照します。このオプションを指定すると、アノテーションzipを更新するのに数分待ちます。",
285
+ )
286
+
287
+ parser.add_argument(
288
+ "--temp_dir",
289
+ type=Path,
290
+ help="指定したディレクトリに、アノテーションZIPなどの一時ファイルをダウンロードします。",
291
+ )
292
+
293
+ parser.set_defaults(subcommand_func=main)
294
+
295
+
296
+ def main(args: argparse.Namespace) -> None:
297
+ service = build_annofabapi_resource_and_login(args)
298
+ facade = AnnofabApiFacade(service)
299
+ ListRangeAnnotation(service, facade, args).main()
300
+
301
+
302
+ def add_parser(subparsers: Optional[argparse._SubParsersAction] = None) -> argparse.ArgumentParser:
303
+ subcommand_name = "list_range_annotation"
304
+ subcommand_help = "アノテーションZIPから動画プロジェクトの区間アノテーションの情報を出力します。"
305
+ epilog = "アノテーションZIPをダウンロードする場合は、オーナロールまたはアノテーションユーザロールを持つユーザで実行してください。"
306
+ parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, subcommand_help, description=subcommand_help, epilog=epilog)
307
+ parse_args(parser)
308
+ return parser