annofabcli 1.101.0__py3-none-any.whl → 1.102.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,320 @@
1
+ from __future__ import annotations
2
+
3
+ import argparse
4
+ import logging
5
+ import sys
6
+ import tempfile
7
+ import zipfile
8
+ from collections.abc import Collection, Iterator
9
+ from dataclasses import dataclass
10
+ from pathlib import Path
11
+ from typing import Any, Optional
12
+
13
+ import numpy
14
+ import pandas
15
+ from annofabapi.models import InputDataType, ProjectMemberRole
16
+ from annofabapi.parser import (
17
+ SimpleAnnotationParser,
18
+ lazy_parse_simple_annotation_dir,
19
+ lazy_parse_simple_annotation_zip,
20
+ )
21
+ from annofabapi.segmentation import read_binary_image
22
+ from dataclasses_json import DataClassJsonMixin
23
+
24
+ import annofabcli
25
+ import annofabcli.common.cli
26
+ from annofabcli.common.cli import (
27
+ COMMAND_LINE_ERROR_STATUS_CODE,
28
+ ArgumentParser,
29
+ CommandLine,
30
+ build_annofabapi_resource_and_login,
31
+ )
32
+ from annofabcli.common.download import DownloadingFile
33
+ from annofabcli.common.enums import FormatArgument
34
+ from annofabcli.common.facade import (
35
+ AnnofabApiFacade,
36
+ TaskQuery,
37
+ match_annotation_with_task_query,
38
+ )
39
+ from annofabcli.common.utils import print_csv, print_json
40
+
41
+ logger = logging.getLogger(__name__)
42
+
43
+
44
+ def lazy_parse_simple_annotation_by_input_data(annotation_path: Path) -> Iterator[SimpleAnnotationParser]:
45
+ if not annotation_path.exists():
46
+ raise RuntimeError(f"'{annotation_path}' は存在しません。")
47
+
48
+ if annotation_path.is_dir():
49
+ return lazy_parse_simple_annotation_dir(annotation_path)
50
+ elif zipfile.is_zipfile(str(annotation_path)):
51
+ return lazy_parse_simple_annotation_zip(annotation_path)
52
+ else:
53
+ raise RuntimeError(f"'{annotation_path}'は、zipファイルまたはディレクトリではありません。")
54
+
55
+
56
+ @dataclass(frozen=True)
57
+ class AnnotationAreaInfo(DataClassJsonMixin):
58
+ task_id: str
59
+ task_status: str
60
+ task_phase: str
61
+ task_phase_stage: int
62
+
63
+ input_data_id: str
64
+ input_data_name: str
65
+
66
+ label: str
67
+ annotation_id: str
68
+ annotation_area: int
69
+
70
+
71
+ def calculate_bounding_box_area(data: dict[str, Any]) -> int:
72
+ width = abs(data["right_bottom"]["x"] - data["left_top"]["x"])
73
+ height = abs(data["right_bottom"]["y"] - data["left_top"]["y"])
74
+ return width * height
75
+
76
+
77
+ def calculate_segmentation_area(outer_file: Path) -> int:
78
+ nd_array = read_binary_image(outer_file)
79
+ return numpy.count_nonzero(nd_array)
80
+
81
+
82
+ def get_annotation_area_info_list(parser: SimpleAnnotationParser, simple_annotation: dict[str, Any]) -> list[AnnotationAreaInfo]:
83
+ result = []
84
+ for detail in simple_annotation["details"]:
85
+ if detail["data"]["_type"] in {"Segmentation", "SegmentationV2"}:
86
+ annotation_area = calculate_segmentation_area(parser.open_outer_file(detail["data"]["data_uri"]))
87
+ elif detail["data"]["_type"] == "BoundingBox":
88
+ annotation_area = calculate_bounding_box_area(detail["data"])
89
+
90
+ else:
91
+ continue
92
+
93
+ result.append(
94
+ AnnotationAreaInfo(
95
+ task_id=simple_annotation["task_id"],
96
+ task_phase=simple_annotation["task_phase"],
97
+ task_phase_stage=simple_annotation["task_phase_stage"],
98
+ task_status=simple_annotation["task_status"],
99
+ input_data_id=simple_annotation["input_data_id"],
100
+ input_data_name=simple_annotation["input_data_name"],
101
+ label=detail["label"],
102
+ annotation_id=detail["annotation_id"],
103
+ annotation_area=annotation_area,
104
+ )
105
+ )
106
+
107
+ return result
108
+
109
+
110
+ def get_annotation_area_info_list_from_annotation_path(
111
+ annotation_path: Path,
112
+ *,
113
+ target_task_ids: Optional[Collection[str]] = None,
114
+ task_query: Optional[TaskQuery] = None,
115
+ ) -> list[AnnotationAreaInfo]:
116
+ annotation_area_list = []
117
+ target_task_ids = set(target_task_ids) if target_task_ids is not None else None
118
+ iter_parser = lazy_parse_simple_annotation_by_input_data(annotation_path)
119
+ logger.debug("アノテーションzip/ディレクトリを読み込み中")
120
+ for index, parser in enumerate(iter_parser):
121
+ if (index + 1) % 1000 == 0:
122
+ logger.debug(f"{index + 1} 件目のJSONを読み込み中")
123
+ if target_task_ids is not None and parser.task_id not in target_task_ids:
124
+ continue
125
+ simple_annotation_dict = parser.load_json()
126
+ if task_query is not None: # noqa: SIM102
127
+ if not match_annotation_with_task_query(simple_annotation_dict, task_query):
128
+ continue
129
+ sub_annotation_area_list = get_annotation_area_info_list(parser, simple_annotation_dict)
130
+ annotation_area_list.extend(sub_annotation_area_list)
131
+ return annotation_area_list
132
+
133
+
134
+ def create_df(
135
+ annotation_area_list: list[AnnotationAreaInfo],
136
+ ) -> pandas.DataFrame:
137
+ columns = [
138
+ "task_id",
139
+ "task_status",
140
+ "task_phase",
141
+ "task_phase_stage",
142
+ "input_data_id",
143
+ "input_data_name",
144
+ "label",
145
+ "annotation_id",
146
+ "annotation_area",
147
+ ]
148
+ df = pandas.DataFrame(e.to_dict() for e in annotation_area_list)
149
+ if len(df) == 0:
150
+ df = pandas.DataFrame(columns=columns)
151
+
152
+ df = df.fillna({"annotation_area": 0})
153
+ return df[columns]
154
+
155
+
156
+ def print_annotation_area(
157
+ annotation_path: Path,
158
+ output_file: Path,
159
+ output_format: FormatArgument,
160
+ *,
161
+ target_task_ids: Optional[Collection[str]] = None,
162
+ task_query: Optional[TaskQuery] = None,
163
+ ) -> None:
164
+ annotation_area_list = get_annotation_area_info_list_from_annotation_path(
165
+ annotation_path,
166
+ target_task_ids=target_task_ids,
167
+ task_query=task_query,
168
+ )
169
+
170
+ logger.info(f"{len(annotation_area_list)} 件のタスクに含まれる塗りつぶしアノテーションのピクセル数情報を出力します。")
171
+
172
+ if output_format == FormatArgument.CSV:
173
+ df = create_df(annotation_area_list)
174
+ print_csv(df, output_file)
175
+
176
+ elif output_format in [FormatArgument.PRETTY_JSON, FormatArgument.JSON]:
177
+ json_is_pretty = output_format == FormatArgument.PRETTY_JSON
178
+ print_json(
179
+ [e.to_dict(encode_json=True) for e in annotation_area_list],
180
+ is_pretty=json_is_pretty,
181
+ output=output_file,
182
+ )
183
+
184
+ else:
185
+ raise RuntimeError(f"出力形式 '{output_format}' はサポートされていません。")
186
+
187
+
188
+ class ListAnnotationArea(CommandLine):
189
+ COMMON_MESSAGE = "annofabcli statistics list_annotation_area: error:"
190
+
191
+ def validate(self, args: argparse.Namespace) -> bool:
192
+ if args.project_id is None and args.annotation is None:
193
+ print( # noqa: T201
194
+ f"{self.COMMON_MESSAGE} argument --project_id: '--annotation'が未指定のときは、'--project_id' を指定してください。",
195
+ file=sys.stderr,
196
+ )
197
+ return False
198
+ return True
199
+
200
+ def main(self) -> None:
201
+ args = self.args
202
+
203
+ if not self.validate(args):
204
+ sys.exit(COMMAND_LINE_ERROR_STATUS_CODE)
205
+
206
+ project_id: Optional[str] = args.project_id
207
+ if project_id is not None:
208
+ super().validate_project(project_id, project_member_roles=[ProjectMemberRole.OWNER, ProjectMemberRole.TRAINING_DATA_USER])
209
+ project, _ = self.service.api.get_project(project_id)
210
+ if project["input_data_type"] != InputDataType.IMAGE.value:
211
+ logger.warning(f"project_id='{project_id}'であるプロジェクトは、画像プロジェクトでないので、出力されるデータは0件になります。")
212
+
213
+ annotation_path = Path(args.annotation) if args.annotation is not None else None
214
+
215
+ task_id_list = annofabcli.common.cli.get_list_from_args(args.task_id) if args.task_id is not None else None
216
+ task_query = TaskQuery.from_dict(annofabcli.common.cli.get_json_from_args(args.task_query)) if args.task_query is not None else None
217
+
218
+ output_file: Path = args.output
219
+ output_format = FormatArgument(args.format)
220
+
221
+ downloading_obj = DownloadingFile(self.service)
222
+
223
+ def download_and_print_annotation_area(project_id: str, temp_dir: Path, *, is_latest: bool, annotation_path: Optional[Path]) -> None:
224
+ if annotation_path is None:
225
+ annotation_path = temp_dir / f"{project_id}__annotation.zip"
226
+ downloading_obj.download_annotation_zip(
227
+ project_id,
228
+ dest_path=annotation_path,
229
+ is_latest=is_latest,
230
+ )
231
+ print_annotation_area(
232
+ output_format=output_format,
233
+ output_file=output_file,
234
+ target_task_ids=task_id_list,
235
+ task_query=task_query,
236
+ annotation_path=annotation_path,
237
+ )
238
+
239
+ if project_id is not None:
240
+ if args.temp_dir is not None:
241
+ download_and_print_annotation_area(
242
+ project_id=project_id, temp_dir=args.temp_dir, is_latest=args.latest, annotation_path=annotation_path
243
+ )
244
+ else:
245
+ with tempfile.TemporaryDirectory() as str_temp_dir:
246
+ download_and_print_annotation_area(
247
+ project_id=project_id, temp_dir=Path(str_temp_dir), is_latest=args.latest, annotation_path=annotation_path
248
+ )
249
+ else:
250
+ assert annotation_path is not None
251
+ print_annotation_area(
252
+ output_format=output_format,
253
+ output_file=output_file,
254
+ target_task_ids=task_id_list,
255
+ task_query=task_query,
256
+ annotation_path=annotation_path,
257
+ )
258
+
259
+
260
+ def parse_args(parser: argparse.ArgumentParser) -> None:
261
+ argument_parser = ArgumentParser(parser)
262
+
263
+ parser.add_argument(
264
+ "--annotation",
265
+ type=str,
266
+ help="アノテーションzip、またはzipを展開したディレクトリを指定します。指定しない場合はAnnofabからダウンロードします。",
267
+ )
268
+
269
+ parser.add_argument(
270
+ "-p",
271
+ "--project_id",
272
+ type=str,
273
+ help="project_id。``--annotation`` が未指定のときは必須です。\n"
274
+ "``--annotation`` が指定されているときに ``--project_id`` を指定すると、アノテーション仕様を参照して、集計対象の属性やCSV列順が決まります。",
275
+ )
276
+
277
+ argument_parser.add_format(
278
+ choices=[FormatArgument.CSV, FormatArgument.JSON, FormatArgument.PRETTY_JSON],
279
+ default=FormatArgument.CSV,
280
+ )
281
+
282
+ argument_parser.add_output()
283
+
284
+ parser.add_argument(
285
+ "-tq",
286
+ "--task_query",
287
+ type=str,
288
+ help="集計対象タスクを絞り込むためのクエリ条件をJSON形式で指定します。使用できるキーは task_id, status, phase, phase_stage です。"
289
+ " ``file://`` を先頭に付けると、JSON形式のファイルを指定できます。",
290
+ )
291
+ argument_parser.add_task_id(required=False)
292
+
293
+ parser.add_argument(
294
+ "--latest",
295
+ action="store_true",
296
+ help="``--annotation`` を指定しないとき、最新のアノテーションzipを参照します。このオプションを指定すると、アノテーションzipを更新するのに数分待ちます。", # noqa: E501
297
+ )
298
+
299
+ parser.add_argument(
300
+ "--temp_dir",
301
+ type=Path,
302
+ help="指定したディレクトリに、アノテーションZIPなどの一時ファイルをダウンロードします。",
303
+ )
304
+
305
+ parser.set_defaults(subcommand_func=main)
306
+
307
+
308
+ def main(args: argparse.Namespace) -> None:
309
+ service = build_annofabapi_resource_and_login(args)
310
+ facade = AnnofabApiFacade(service)
311
+ ListAnnotationArea(service, facade, args).main()
312
+
313
+
314
+ def add_parser(subparsers: Optional[argparse._SubParsersAction] = None) -> argparse.ArgumentParser:
315
+ subcommand_name = "list_annotation_area"
316
+ subcommand_help = "塗りつぶしアノテーションまたは矩形アノテーションの面積を出力します。"
317
+ epilog = "オーナロールまたはアノテーションユーザロールを持つユーザで実行してください。"
318
+ parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, subcommand_help, description=subcommand_help, epilog=epilog)
319
+ parse_args(parser)
320
+ return parser
@@ -4,6 +4,7 @@ from typing import Optional
4
4
  import annofabcli
5
5
  import annofabcli.common.cli
6
6
  import annofabcli.stat_visualization.merge_visualization_dir
7
+ import annofabcli.statistics.list_annotation_area
7
8
  import annofabcli.statistics.list_annotation_attribute
8
9
  import annofabcli.statistics.list_annotation_attribute_filled_count
9
10
  import annofabcli.statistics.list_annotation_count
@@ -27,6 +28,7 @@ def parse_args(parser: argparse.ArgumentParser) -> None:
27
28
  annofabcli.statistics.list_annotation_attribute_filled_count.add_parser(subparsers)
28
29
  annofabcli.statistics.list_annotation_count.add_parser(subparsers)
29
30
  annofabcli.statistics.list_annotation_duration.add_parser(subparsers)
31
+ annofabcli.statistics.list_annotation_area.add_parser(subparsers)
30
32
 
31
33
  annofabcli.statistics.list_video_duration.add_parser(subparsers)
32
34
  annofabcli.statistics.list_worktime.add_parser(subparsers)
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: annofabcli
3
- Version: 1.101.0
3
+ Version: 1.102.0
4
4
  Summary: Utility Command Line Interface for AnnoFab
5
5
  Author: Kurusugawa Computer Inc.
6
6
  License: MIT
@@ -135,6 +135,7 @@ annofabcli/stat_visualization/write_performance_rating_csv.py,sha256=TDn7-poyFt2
135
135
  annofabcli/statistics/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
136
136
  annofabcli/statistics/histogram.py,sha256=CvzDxT2cKLSnBGSqkZE6p92PayGxYYja1YyB24M4ALU,3245
137
137
  annofabcli/statistics/linegraph.py,sha256=0kr7jVBNMiM2ECYhv3Ry5RitElKerSl9ZKxbKzfiplI,12494
138
+ annofabcli/statistics/list_annotation_area.py,sha256=1LFYqc1JodVuHKM2ceTwWriDCabMmkOnHiNptvEyXAw,12325
138
139
  annofabcli/statistics/list_annotation_attribute.py,sha256=87jjNCOXJUbWnmswMCLN7GTjGsBfqpFJ6hViWmnj8Y4,12557
139
140
  annofabcli/statistics/list_annotation_attribute_filled_count.py,sha256=vwWeFHwTnEMdrLBauIKPFDkUCa6lXXd0GQgUAQ0LCqU,28890
140
141
  annofabcli/statistics/list_annotation_count.py,sha256=nzmlHRCWt5mjeksZkeQyWqm4UaCa9SrdbNtuX9TPP5w,52907
@@ -142,7 +143,7 @@ annofabcli/statistics/list_annotation_duration.py,sha256=N7nnVUDfX_thIapqe6-z_Mq
142
143
  annofabcli/statistics/list_video_duration.py,sha256=uNeMteRBX2JG_AWmcgMJj0Jzbq_qF7bvAwr25GmeIiw,9124
143
144
  annofabcli/statistics/list_worktime.py,sha256=C7Yu3IOW2EvhkJJv6gY3hNdS9_TOLmT_9LZsB7vLJ1o,6493
144
145
  annofabcli/statistics/scatter.py,sha256=IUCwXix9GbZb6V82wjjb5q2eamrT5HQsU_bzDTjAFnM,11011
145
- annofabcli/statistics/subcommand_statistics.py,sha256=mx18Fgxz2eG4LrF-x0vISw2qh9aLommxuQLD8cfoZhw,2416
146
+ annofabcli/statistics/subcommand_statistics.py,sha256=Pvd7s0vvDU9tSpAphPrv94IDhhR1p8iFH2tjdt7I7ZU,2536
146
147
  annofabcli/statistics/summarize_task_count.py,sha256=8OH6BBRYRjHJkWRTjU0A0OfXa7f3NIRHrxPNFlRt_hM,9707
147
148
  annofabcli/statistics/summarize_task_count_by_task_id_group.py,sha256=TSSmcFv615NLcq6uqXmg3ilYqSHl3A5qp90msVQM1gE,8646
148
149
  annofabcli/statistics/summarize_task_count_by_user.py,sha256=TRoJXpt2HOVb8QO2YtRejkOAxyK80_NsPt3Vk9es9C8,6948
@@ -206,8 +207,8 @@ annofabcli/task_history_event/download_task_history_event_json.py,sha256=hQLVbQ0
206
207
  annofabcli/task_history_event/list_all_task_history_event.py,sha256=JQEgwOIXbbTIfeX23AVaoySHViOR9UGm9uoXuhVEBqo,6446
207
208
  annofabcli/task_history_event/list_worktime.py,sha256=9jsRYa2C9bva8E1Aqxv9CCKDuCP0MvbiaIyQFTDpjqY,13150
208
209
  annofabcli/task_history_event/subcommand_task_history_event.py,sha256=mJVJoT4RXk4HWnY7-Nrsl4If-gtaIIEXd2z7eFZwM2I,1260
209
- annofabcli-1.101.0.dist-info/METADATA,sha256=zlNu7ev7fOJfYzE4Dx2Gs6B_tT2_kvIOwBBI10WAzyg,5286
210
- annofabcli-1.101.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
211
- annofabcli-1.101.0.dist-info/entry_points.txt,sha256=C2uSUc-kkLJpoK_mDL5FEMAdorLEMPfwSf8VBMYnIFM,56
212
- annofabcli-1.101.0.dist-info/licenses/LICENSE,sha256=pcqWYfxFtxBzhvKp3x9MXNM4xciGb2eFewaRhXUNHlo,1081
213
- annofabcli-1.101.0.dist-info/RECORD,,
210
+ annofabcli-1.102.0.dist-info/METADATA,sha256=nxOd0drqHnw28-7Kf3aA0qxKodxsNDV8HgqjX5Nnqxw,5286
211
+ annofabcli-1.102.0.dist-info/WHEEL,sha256=qtCwoSJWgHk21S1Kb4ihdzI2rlJ1ZKaIurTj_ngOhyQ,87
212
+ annofabcli-1.102.0.dist-info/entry_points.txt,sha256=C2uSUc-kkLJpoK_mDL5FEMAdorLEMPfwSf8VBMYnIFM,56
213
+ annofabcli-1.102.0.dist-info/licenses/LICENSE,sha256=pcqWYfxFtxBzhvKp3x9MXNM4xciGb2eFewaRhXUNHlo,1081
214
+ annofabcli-1.102.0.dist-info/RECORD,,