annofabcli 1.111.2__py3-none-any.whl → 1.113.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.
Files changed (186) hide show
  1. annofabcli/__main__.py +1 -2
  2. annofabcli/annotation/annotation_query.py +10 -10
  3. annofabcli/annotation/change_annotation_attributes.py +10 -10
  4. annofabcli/annotation/change_annotation_attributes_per_annotation.py +4 -5
  5. annofabcli/annotation/change_annotation_properties.py +14 -14
  6. annofabcli/annotation/copy_annotation.py +6 -6
  7. annofabcli/annotation/create_classification_annotation.py +7 -7
  8. annofabcli/annotation/delete_annotation.py +9 -9
  9. annofabcli/annotation/download_annotation_zip.py +1 -3
  10. annofabcli/annotation/dump_annotation.py +8 -8
  11. annofabcli/annotation/import_annotation.py +13 -13
  12. annofabcli/annotation/list_annotation.py +9 -9
  13. annofabcli/annotation/list_annotation_count.py +2 -3
  14. annofabcli/annotation/merge_segmentation.py +6 -6
  15. annofabcli/annotation/remove_segmentation_overlap.py +5 -5
  16. annofabcli/annotation/restore_annotation.py +7 -7
  17. annofabcli/annotation/subcommand_annotation.py +1 -2
  18. annofabcli/annotation_specs/add_attribute_restriction.py +4 -5
  19. annofabcli/annotation_specs/attribute_restriction.py +8 -8
  20. annofabcli/annotation_specs/export_annotation_specs.py +4 -5
  21. annofabcli/annotation_specs/get_annotation_specs_with_attribute_id_replaced.py +3 -4
  22. annofabcli/annotation_specs/get_annotation_specs_with_choice_id_replaced.py +3 -4
  23. annofabcli/annotation_specs/get_annotation_specs_with_label_id_replaced.py +3 -4
  24. annofabcli/annotation_specs/list_annotation_specs_attribute.py +9 -10
  25. annofabcli/annotation_specs/list_annotation_specs_choice.py +9 -10
  26. annofabcli/annotation_specs/list_annotation_specs_history.py +3 -3
  27. annofabcli/annotation_specs/list_annotation_specs_label.py +8 -9
  28. annofabcli/annotation_specs/list_annotation_specs_label_attribute.py +10 -11
  29. annofabcli/annotation_specs/list_attribute_restriction.py +2 -4
  30. annofabcli/annotation_specs/list_label_color.py +2 -3
  31. annofabcli/annotation_specs/put_label_color.py +3 -4
  32. annofabcli/annotation_specs/subcommand_annotation_specs.py +1 -3
  33. annofabcli/annotation_zip/list_annotation_3d_bounding_box.py +365 -0
  34. annofabcli/annotation_zip/list_annotation_bounding_box_2d.py +37 -38
  35. annofabcli/annotation_zip/list_polygon_annotation.py +390 -0
  36. annofabcli/annotation_zip/list_polyline_annotation.py +402 -0
  37. annofabcli/annotation_zip/list_range_annotation.py +25 -15
  38. annofabcli/annotation_zip/list_single_point_annotation.py +25 -34
  39. annofabcli/annotation_zip/subcommand_annotation_zip.py +7 -2
  40. annofabcli/annotation_zip/validate_annotation.py +8 -7
  41. annofabcli/comment/delete_comment.py +4 -6
  42. annofabcli/comment/download_comment_json.py +4 -6
  43. annofabcli/comment/list_all_comment.py +5 -6
  44. annofabcli/comment/list_comment.py +3 -4
  45. annofabcli/comment/put_comment.py +9 -10
  46. annofabcli/comment/put_comment_simply.py +5 -6
  47. annofabcli/comment/put_inspection_comment.py +1 -3
  48. annofabcli/comment/put_inspection_comment_simply.py +1 -3
  49. annofabcli/comment/put_onhold_comment.py +1 -3
  50. annofabcli/comment/put_onhold_comment_simply.py +1 -3
  51. annofabcli/comment/subcommand_comment.py +1 -3
  52. annofabcli/common/bokeh.py +4 -4
  53. annofabcli/common/cli.py +18 -17
  54. annofabcli/common/download.py +28 -29
  55. annofabcli/common/facade.py +37 -38
  56. annofabcli/common/image.py +14 -14
  57. annofabcli/common/utils.py +8 -8
  58. annofabcli/common/visualize.py +13 -13
  59. annofabcli/experimental/list_out_of_range_annotation_for_movie.py +3 -4
  60. annofabcli/experimental/subcommand_experimental.py +1 -3
  61. annofabcli/filesystem/draw_annotation.py +27 -27
  62. annofabcli/filesystem/filter_annotation.py +9 -10
  63. annofabcli/filesystem/mask_user_info.py +15 -15
  64. annofabcli/filesystem/merge_annotation.py +9 -9
  65. annofabcli/filesystem/subcommand_filesystem.py +1 -3
  66. annofabcli/input_data/copy_input_data.py +8 -9
  67. annofabcli/input_data/delete_input_data.py +3 -3
  68. annofabcli/input_data/delete_metadata_key_of_input_data.py +3 -5
  69. annofabcli/input_data/download_input_data_json.py +4 -6
  70. annofabcli/input_data/list_all_input_data.py +9 -9
  71. annofabcli/input_data/list_all_input_data_merged_task.py +5 -5
  72. annofabcli/input_data/list_input_data.py +5 -5
  73. annofabcli/input_data/put_input_data.py +6 -6
  74. annofabcli/input_data/put_input_data_with_zip.py +3 -4
  75. annofabcli/input_data/subcommand_input_data.py +1 -3
  76. annofabcli/input_data/update_input_data.py +6 -8
  77. annofabcli/input_data/update_metadata_of_input_data.py +3 -5
  78. annofabcli/instruction/copy_instruction.py +5 -6
  79. annofabcli/instruction/download_instruction.py +5 -6
  80. annofabcli/instruction/list_instruction_history.py +3 -3
  81. annofabcli/instruction/subcommand_instruction.py +1 -3
  82. annofabcli/instruction/upload_instruction.py +3 -4
  83. annofabcli/job/delete_job.py +2 -3
  84. annofabcli/job/list_job.py +5 -5
  85. annofabcli/job/list_last_job.py +4 -4
  86. annofabcli/job/subcommand_job.py +1 -3
  87. annofabcli/job/wait_job.py +4 -5
  88. annofabcli/my_account/get_my_account.py +2 -3
  89. annofabcli/my_account/subcommand_my_account.py +1 -3
  90. annofabcli/organization/list_organization.py +2 -3
  91. annofabcli/organization/subcommand_organization.py +1 -3
  92. annofabcli/organization_member/change_organization_member.py +3 -4
  93. annofabcli/organization_member/delete_organization_member.py +3 -4
  94. annofabcli/organization_member/invite_organization_member.py +1 -3
  95. annofabcli/organization_member/list_organization_member.py +3 -3
  96. annofabcli/organization_member/subcommand_organization_member.py +1 -3
  97. annofabcli/project/change_organization_of_project.py +4 -4
  98. annofabcli/project/change_project_status.py +4 -4
  99. annofabcli/project/copy_project.py +5 -5
  100. annofabcli/project/create_project.py +8 -8
  101. annofabcli/project/diff_projects.py +4 -5
  102. annofabcli/project/list_project.py +5 -5
  103. annofabcli/project/put_project.py +2 -3
  104. annofabcli/project/subcommand_project.py +1 -2
  105. annofabcli/project/update_configuration.py +4 -4
  106. annofabcli/project/update_project.py +6 -8
  107. annofabcli/project_member/change_project_members.py +8 -8
  108. annofabcli/project_member/copy_project_members.py +4 -4
  109. annofabcli/project_member/drop_project_members.py +2 -3
  110. annofabcli/project_member/invite_project_members.py +1 -3
  111. annofabcli/project_member/list_users.py +2 -3
  112. annofabcli/project_member/put_project_members.py +6 -6
  113. annofabcli/project_member/subcommand_project_member.py +1 -3
  114. annofabcli/stat_visualization/mask_visualization_dir.py +8 -9
  115. annofabcli/stat_visualization/merge_visualization_dir.py +6 -7
  116. annofabcli/stat_visualization/subcommand_stat_visualization.py +1 -2
  117. annofabcli/stat_visualization/summarize_whole_performance_csv.py +1 -2
  118. annofabcli/stat_visualization/write_graph.py +2 -3
  119. annofabcli/stat_visualization/write_performance_rating_csv.py +20 -27
  120. annofabcli/statistics/histogram.py +5 -6
  121. annofabcli/statistics/linegraph.py +13 -14
  122. annofabcli/statistics/list_annotation_area.py +38 -13
  123. annofabcli/statistics/list_annotation_attribute.py +9 -10
  124. annofabcli/statistics/list_annotation_attribute_filled_count.py +30 -31
  125. annofabcli/statistics/list_annotation_count.py +57 -58
  126. annofabcli/statistics/list_annotation_duration.py +33 -34
  127. annofabcli/statistics/list_video_duration.py +4 -5
  128. annofabcli/statistics/list_worktime.py +4 -4
  129. annofabcli/statistics/scatter.py +9 -8
  130. annofabcli/statistics/subcommand_statistics.py +1 -4
  131. annofabcli/statistics/summarize_task_count.py +4 -6
  132. annofabcli/statistics/summarize_task_count_by_task_id_group.py +2 -4
  133. annofabcli/statistics/summarize_task_count_by_user.py +1 -3
  134. annofabcli/statistics/visualization/dataframe/annotation_count.py +5 -4
  135. annofabcli/statistics/visualization/dataframe/annotation_duration.py +2 -3
  136. annofabcli/statistics/visualization/dataframe/cumulative_productivity.py +15 -17
  137. annofabcli/statistics/visualization/dataframe/productivity_per_date.py +17 -19
  138. annofabcli/statistics/visualization/dataframe/project_performance.py +3 -12
  139. annofabcli/statistics/visualization/dataframe/task.py +11 -12
  140. annofabcli/statistics/visualization/dataframe/task_worktime_by_phase_user.py +9 -10
  141. annofabcli/statistics/visualization/dataframe/user_performance.py +21 -19
  142. annofabcli/statistics/visualization/dataframe/whole_performance.py +3 -4
  143. annofabcli/statistics/visualization/dataframe/whole_productivity_per_date.py +12 -14
  144. annofabcli/statistics/visualization/dataframe/worktime_per_date.py +11 -13
  145. annofabcli/statistics/visualization/filtering_query.py +7 -7
  146. annofabcli/statistics/visualization/project_dir.py +27 -14
  147. annofabcli/statistics/visualize_annotation_count.py +22 -23
  148. annofabcli/statistics/visualize_annotation_duration.py +21 -22
  149. annofabcli/statistics/visualize_statistics.py +36 -33
  150. annofabcli/statistics/visualize_video_duration.py +18 -20
  151. annofabcli/supplementary/delete_supplementary_data.py +5 -5
  152. annofabcli/supplementary/list_supplementary_data.py +4 -4
  153. annofabcli/supplementary/put_supplementary_data.py +9 -9
  154. annofabcli/supplementary/subcommand_supplementary.py +1 -3
  155. annofabcli/task/cancel_acceptance.py +16 -17
  156. annofabcli/task/change_operator.py +10 -12
  157. annofabcli/task/change_status_to_break.py +7 -9
  158. annofabcli/task/change_status_to_on_hold.py +10 -12
  159. annofabcli/task/complete_tasks.py +17 -18
  160. annofabcli/task/copy_tasks.py +3 -5
  161. annofabcli/task/delete_metadata_key_of_task.py +4 -6
  162. annofabcli/task/delete_tasks.py +7 -7
  163. annofabcli/task/download_task_json.py +4 -6
  164. annofabcli/task/list_all_tasks.py +8 -8
  165. annofabcli/task/list_all_tasks_added_task_history.py +14 -13
  166. annofabcli/task/list_tasks.py +7 -7
  167. annofabcli/task/list_tasks_added_task_history.py +10 -10
  168. annofabcli/task/put_tasks.py +5 -6
  169. annofabcli/task/put_tasks_by_count.py +2 -3
  170. annofabcli/task/reject_tasks.py +18 -20
  171. annofabcli/task/subcommand_task.py +1 -3
  172. annofabcli/task/update_metadata_of_task.py +5 -6
  173. annofabcli/task_history/download_task_history_json.py +4 -6
  174. annofabcli/task_history/list_all_task_history.py +6 -7
  175. annofabcli/task_history/list_task_history.py +4 -5
  176. annofabcli/task_history/subcommand_task_history.py +1 -3
  177. annofabcli/task_history_event/download_task_history_event_json.py +4 -6
  178. annofabcli/task_history_event/list_all_task_history_event.py +7 -7
  179. annofabcli/task_history_event/list_worktime.py +17 -16
  180. annofabcli/task_history_event/subcommand_task_history_event.py +1 -2
  181. {annofabcli-1.111.2.dist-info → annofabcli-1.113.0.dist-info}/METADATA +9 -15
  182. annofabcli-1.113.0.dist-info/RECORD +231 -0
  183. {annofabcli-1.111.2.dist-info → annofabcli-1.113.0.dist-info}/WHEEL +1 -1
  184. annofabcli-1.111.2.dist-info/RECORD +0 -228
  185. {annofabcli-1.111.2.dist-info → annofabcli-1.113.0.dist-info}/entry_points.txt +0 -0
  186. {annofabcli-1.111.2.dist-info → annofabcli-1.113.0.dist-info}/licenses/LICENSE +0 -0
@@ -7,13 +7,12 @@ import tempfile
7
7
  from collections.abc import Collection
8
8
  from dataclasses import dataclass
9
9
  from pathlib import Path
10
- from typing import Any, Optional
10
+ from typing import Any
11
11
 
12
12
  import pandas
13
13
  from annofabapi.models import InputDataType, ProjectMemberRole
14
14
  from dataclasses_json import DataClassJsonMixin
15
15
 
16
- import annofabcli
17
16
  import annofabcli.common.cli
18
17
  from annofabcli.common.annofab.annotation_zip import lazy_parse_simple_annotation_by_input_data
19
18
  from annofabcli.common.cli import COMMAND_LINE_ERROR_STATUS_CODE, ArgumentParser, CommandLine, build_annofabapi_resource_and_login, get_list_from_args
@@ -40,18 +39,23 @@ class AnnotationBoundingBoxInfo(DataClassJsonMixin):
40
39
  input_data_id: str
41
40
  input_data_name: str
42
41
 
43
- updated_datetime: Optional[str]
42
+ updated_datetime: str | None
44
43
  """アノテーションJSONに格納されているアノテーションの更新日時"""
45
44
 
46
45
  label: str
47
46
  annotation_id: str
48
47
  left_top: dict[str, int]
49
48
  right_bottom: dict[str, int]
49
+ center: dict[str, float]
50
+ """バウンディングボックスの中心座標"""
50
51
  width: int
51
52
  height: int
53
+ area: int
54
+ """バウンディングボックスの面積"""
55
+ attributes: dict[str, str | int | bool]
52
56
 
53
57
 
54
- def get_annotation_bounding_box_info_list(simple_annotation: dict[str, Any], *, target_label_names: Optional[Collection[str]] = None) -> list[AnnotationBoundingBoxInfo]:
58
+ def get_annotation_bounding_box_info_list(simple_annotation: dict[str, Any], *, target_label_names: Collection[str] | None = None) -> list[AnnotationBoundingBoxInfo]:
55
59
  result = []
56
60
  target_label_names_set = set(target_label_names) if target_label_names is not None else None
57
61
  for detail in simple_annotation["details"]:
@@ -65,6 +69,9 @@ def get_annotation_bounding_box_info_list(simple_annotation: dict[str, Any], *,
65
69
  right_bottom = detail["data"]["right_bottom"]
66
70
  width = abs(right_bottom["x"] - left_top["x"])
67
71
  height = abs(right_bottom["y"] - left_top["y"])
72
+ center_x = (left_top["x"] + right_bottom["x"]) / 2
73
+ center_y = (left_top["y"] + right_bottom["y"]) / 2
74
+ area = width * height
68
75
 
69
76
  result.append(
70
77
  AnnotationBoundingBoxInfo(
@@ -79,9 +86,12 @@ def get_annotation_bounding_box_info_list(simple_annotation: dict[str, Any], *,
79
86
  annotation_id=detail["annotation_id"],
80
87
  left_top=left_top,
81
88
  right_bottom=right_bottom,
89
+ center={"x": center_x, "y": center_y},
82
90
  width=width,
83
91
  height=height,
92
+ area=area,
84
93
  updated_datetime=simple_annotation["updated_datetime"],
94
+ attributes=detail["attributes"],
85
95
  )
86
96
  )
87
97
 
@@ -91,9 +101,9 @@ def get_annotation_bounding_box_info_list(simple_annotation: dict[str, Any], *,
91
101
  def get_annotation_bounding_box_info_list_from_annotation_path(
92
102
  annotation_path: Path,
93
103
  *,
94
- target_task_ids: Optional[Collection[str]] = None,
95
- task_query: Optional[TaskQuery] = None,
96
- target_label_names: Optional[Collection[str]] = None,
104
+ target_task_ids: Collection[str] | None = None,
105
+ task_query: TaskQuery | None = None,
106
+ target_label_names: Collection[str] | None = None,
97
107
  ) -> list[AnnotationBoundingBoxInfo]:
98
108
  annotation_bbox_list = []
99
109
  target_task_ids = set(target_task_ids) if target_task_ids is not None else None
@@ -115,7 +125,7 @@ def get_annotation_bounding_box_info_list_from_annotation_path(
115
125
  def create_df(
116
126
  annotation_bbox_list: list[AnnotationBoundingBoxInfo],
117
127
  ) -> pandas.DataFrame:
118
- columns = [
128
+ base_columns = [
119
129
  "project_id",
120
130
  "task_id",
121
131
  "task_status",
@@ -130,33 +140,22 @@ def create_df(
130
140
  "left_top.y",
131
141
  "right_bottom.x",
132
142
  "right_bottom.y",
143
+ "center.x",
144
+ "center.y",
133
145
  "width",
134
146
  "height",
147
+ "area",
135
148
  ]
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
- )
149
+
150
+ if not annotation_bbox_list:
151
+ # 空のリストの場合は、base_columnsのみで空のDataFrameを返す
152
+ return pandas.DataFrame(columns=base_columns)
153
+
154
+ tmp_annotation_bbox_list = [e.to_dict(encode_json=True) for e in annotation_bbox_list]
155
+ df = pandas.json_normalize(tmp_annotation_bbox_list)
156
+
157
+ attribute_columns = sorted(col for col in df.columns if col.startswith("attributes."))
158
+ columns = base_columns + attribute_columns
160
159
 
161
160
  return df[columns]
162
161
 
@@ -166,9 +165,9 @@ def print_annotation_bounding_box(
166
165
  output_file: Path,
167
166
  output_format: FormatArgument,
168
167
  *,
169
- target_task_ids: Optional[Collection[str]] = None,
170
- task_query: Optional[TaskQuery] = None,
171
- target_label_names: Optional[Collection[str]] = None,
168
+ target_task_ids: Collection[str] | None = None,
169
+ task_query: TaskQuery | None = None,
170
+ target_label_names: Collection[str] | None = None,
172
171
  ) -> None:
173
172
  annotation_bbox_list = get_annotation_bounding_box_info_list_from_annotation_path(
174
173
  annotation_path,
@@ -214,7 +213,7 @@ class ListAnnotationBoundingBox2d(CommandLine):
214
213
  if not self.validate(args):
215
214
  sys.exit(COMMAND_LINE_ERROR_STATUS_CODE)
216
215
 
217
- project_id: Optional[str] = args.project_id
216
+ project_id: str | None = args.project_id
218
217
  if project_id is not None:
219
218
  super().validate_project(project_id, project_member_roles=[ProjectMemberRole.OWNER, ProjectMemberRole.TRAINING_DATA_USER])
220
219
  project, _ = self.service.api.get_project(project_id)
@@ -302,7 +301,7 @@ def parse_args(parser: argparse.ArgumentParser) -> None:
302
301
  parser.add_argument(
303
302
  "--label_name",
304
303
  type=str,
305
- nargs="*",
304
+ nargs="+",
306
305
  help="指定したラベル名のバウンディングボックスアノテーションのみを対象にします。複数指定できます。",
307
306
  )
308
307
 
@@ -327,7 +326,7 @@ def main(args: argparse.Namespace) -> None:
327
326
  ListAnnotationBoundingBox2d(service, facade, args).main()
328
327
 
329
328
 
330
- def add_parser(subparsers: Optional[argparse._SubParsersAction] = None) -> argparse.ArgumentParser:
329
+ def add_parser(subparsers: argparse._SubParsersAction | None = None) -> argparse.ArgumentParser:
331
330
  subcommand_name = "list_bounding_box_annotation"
332
331
  subcommand_help = "アノテーションZIPからバウンディングボックス(矩形)アノテーションの座標情報を出力します。"
333
332
  epilog = "アノテーションZIPをダウンロードする場合は、オーナロールまたはアノテーションユーザロールを持つユーザで実行してください。"
@@ -0,0 +1,390 @@
1
+ import argparse
2
+ import logging
3
+ import sys
4
+ import tempfile
5
+ from collections.abc import Collection
6
+ from pathlib import Path
7
+ from typing import Any
8
+
9
+ import pandas
10
+ from annofabapi.models import InputDataType, ProjectMemberRole
11
+ from pydantic import BaseModel, ConfigDict
12
+ from shapely.errors import ShapelyError
13
+ from shapely.geometry import Polygon
14
+
15
+ import annofabcli.common.cli
16
+ from annofabcli.common.annofab.annotation_zip import lazy_parse_simple_annotation_by_input_data
17
+ from annofabcli.common.cli import COMMAND_LINE_ERROR_STATUS_CODE, ArgumentParser, CommandLine, build_annofabapi_resource_and_login, get_list_from_args
18
+ from annofabcli.common.download import DownloadingFile
19
+ from annofabcli.common.enums import FormatArgument
20
+ from annofabcli.common.facade import (
21
+ AnnofabApiFacade,
22
+ TaskQuery,
23
+ match_annotation_with_task_query,
24
+ )
25
+ from annofabcli.common.utils import print_csv, print_json
26
+
27
+ logger = logging.getLogger(__name__)
28
+
29
+
30
+ class AnnotationPolygonInfo(BaseModel):
31
+ """
32
+ ポリゴンアノテーションの情報
33
+ """
34
+
35
+ model_config = ConfigDict(frozen=True)
36
+
37
+ project_id: str
38
+ task_id: str
39
+ task_status: str
40
+ task_phase: str
41
+ task_phase_stage: int
42
+
43
+ input_data_id: str
44
+ input_data_name: str
45
+
46
+ updated_datetime: str | None
47
+ """アノテーションJSONに格納されているアノテーションの更新日時"""
48
+
49
+ label: str
50
+ annotation_id: str
51
+ point_count: int
52
+ area: float | None
53
+ """ポリゴンの面積。2点のポリラインの場合はNone"""
54
+ centroid: dict[str, float] | None
55
+ """ポリゴンの重心座標。2点のポリラインの場合はNone"""
56
+ bounding_box_width: float | None
57
+ """外接矩形の幅。2点のポリラインの場合はNone"""
58
+ bounding_box_height: float | None
59
+ """外接矩形の高さ。2点のポリラインの場合はNone"""
60
+ attributes: dict[str, Any]
61
+ points: list[dict[str, int]]
62
+ """ポリゴンの頂点リスト。各頂点は整数座標 {"x": int, "y": int} の形式。
63
+ """
64
+
65
+
66
+ def calculate_polygon_properties(points: list[dict[str, int]]) -> tuple[float | None, dict[str, float] | None, float | None, float | None]:
67
+ """
68
+ ポリゴンの面積、重心、外接矩形のサイズを計算する。
69
+
70
+ Args:
71
+ points: ポリゴンの頂点リスト。各頂点は整数座標 {"x": int, "y": int} の形式。
72
+
73
+ Returns:
74
+ (面積, 重心, 外接矩形の幅, 外接矩形の高さ) のタプル。
75
+ 2点以下の場合はポリラインなので、(None, None, None, None) を返す。
76
+ 無効なポリゴン(自己交差など)の場合も (None, None, None, None) を返す。
77
+ """
78
+ if len(points) < 3:
79
+ # 2点以下の場合はポリラインなので、NA扱い
80
+ return None, None, None, None
81
+
82
+ try:
83
+ # shapelyのPolygonオブジェクトを作成
84
+ coords = [(p["x"], p["y"]) for p in points]
85
+ polygon = Polygon(coords)
86
+
87
+ # 面積を計算
88
+ area = polygon.area
89
+
90
+ # 重心を計算
91
+ centroid = polygon.centroid
92
+ centroid_dict = {"x": centroid.x, "y": centroid.y}
93
+
94
+ # 外接矩形を取得
95
+ minx, miny, maxx, maxy = polygon.bounds
96
+ bbox_width = maxx - minx
97
+ bbox_height = maxy - miny
98
+ except (ValueError, ShapelyError):
99
+ # 無効なポリゴン(例:自己交差など)の場合はNA扱い
100
+ return None, None, None, None
101
+ else:
102
+ return area, centroid_dict, bbox_width, bbox_height
103
+
104
+
105
+ def get_annotation_polygon_info_list(simple_annotation: dict[str, Any], *, target_label_names: Collection[str] | None = None) -> list[AnnotationPolygonInfo]:
106
+ result = []
107
+ target_label_names_set = set(target_label_names) if target_label_names is not None else None
108
+ for detail in simple_annotation["details"]:
109
+ if detail["data"]["_type"] == "Points":
110
+ label = detail["label"]
111
+ # ラベル名によるフィルタリング
112
+ if target_label_names_set is not None and label not in target_label_names_set:
113
+ continue
114
+
115
+ points = detail["data"]["points"]
116
+ point_count = len(points)
117
+
118
+ # ポリゴンのプロパティを計算
119
+ area, centroid, bbox_width, bbox_height = calculate_polygon_properties(points)
120
+
121
+ result.append(
122
+ AnnotationPolygonInfo(
123
+ project_id=simple_annotation["project_id"],
124
+ task_id=simple_annotation["task_id"],
125
+ task_phase=simple_annotation["task_phase"],
126
+ task_phase_stage=simple_annotation["task_phase_stage"],
127
+ task_status=simple_annotation["task_status"],
128
+ input_data_id=simple_annotation["input_data_id"],
129
+ input_data_name=simple_annotation["input_data_name"],
130
+ label=label,
131
+ annotation_id=detail["annotation_id"],
132
+ point_count=point_count,
133
+ area=area,
134
+ centroid=centroid,
135
+ bounding_box_width=bbox_width,
136
+ bounding_box_height=bbox_height,
137
+ attributes=detail["attributes"],
138
+ points=points,
139
+ updated_datetime=simple_annotation["updated_datetime"],
140
+ )
141
+ )
142
+
143
+ return result
144
+
145
+
146
+ def get_annotation_polygon_info_list_from_annotation_path(
147
+ annotation_path: Path,
148
+ *,
149
+ target_task_ids: Collection[str] | None = None,
150
+ task_query: TaskQuery | None = None,
151
+ target_label_names: Collection[str] | None = None,
152
+ ) -> list[AnnotationPolygonInfo]:
153
+ annotation_polygon_list = []
154
+ target_task_ids = set(target_task_ids) if target_task_ids is not None else None
155
+ iter_parser = lazy_parse_simple_annotation_by_input_data(annotation_path)
156
+ logger.info(f"アノテーションZIPまたはディレクトリ'{annotation_path}'を読み込みます。")
157
+ for index, parser in enumerate(iter_parser):
158
+ if (index + 1) % 10000 == 0:
159
+ logger.info(f"{index + 1} 件目のJSONを読み込み中")
160
+ if target_task_ids is not None and parser.task_id not in target_task_ids:
161
+ continue
162
+ dict_simple_annotation = parser.load_json()
163
+ if task_query is not None and not match_annotation_with_task_query(dict_simple_annotation, task_query):
164
+ continue
165
+ sub_annotation_polygon_list = get_annotation_polygon_info_list(dict_simple_annotation, target_label_names=target_label_names)
166
+ annotation_polygon_list.extend(sub_annotation_polygon_list)
167
+ return annotation_polygon_list
168
+
169
+
170
+ def create_df(
171
+ annotation_polygon_list: list[AnnotationPolygonInfo],
172
+ ) -> pandas.DataFrame:
173
+ """
174
+ CSV出力用のDataFrameを作成する。
175
+
176
+ Notes:
177
+ points列は含めない。CSVに含めると列の長さが非常に大きくなるため。
178
+ attributes列は、キーごとに別々の列(attributes.<key>の形式)として出力する。
179
+ pandas.json_normalizeを使用してネストした辞書を自動的に展開する。
180
+
181
+ """
182
+ # 基本列の定義
183
+ base_columns = [
184
+ "project_id",
185
+ "task_id",
186
+ "task_status",
187
+ "task_phase",
188
+ "task_phase_stage",
189
+ "input_data_id",
190
+ "input_data_name",
191
+ "updated_datetime",
192
+ "label",
193
+ "annotation_id",
194
+ "point_count",
195
+ "area",
196
+ "centroid.x",
197
+ "centroid.y",
198
+ "bounding_box_width",
199
+ "bounding_box_height",
200
+ ]
201
+
202
+ if len(annotation_polygon_list) == 0:
203
+ # 件数が0件のときも列ヘッダを出力する
204
+ return pandas.DataFrame(columns=base_columns)
205
+
206
+ # pandas.json_normalizeを使用してネストした辞書を展開
207
+ # centroid(辞書)とattributes(辞書)が自動的に展開される
208
+ df = pandas.json_normalize([e.model_dump() for e in annotation_polygon_list])
209
+
210
+ # attributes列を抽出してソート
211
+ attributes_columns = sorted([col for col in df.columns if col.startswith("attributes.")])
212
+ # 列の順序を設定
213
+ columns = base_columns + attributes_columns
214
+
215
+ return df[columns]
216
+
217
+
218
+ def print_annotation_polygon(
219
+ annotation_path: Path,
220
+ output_file: Path,
221
+ output_format: FormatArgument,
222
+ *,
223
+ target_task_ids: Collection[str] | None = None,
224
+ task_query: TaskQuery | None = None,
225
+ target_label_names: Collection[str] | None = None,
226
+ ) -> None:
227
+ annotation_polygon_list = get_annotation_polygon_info_list_from_annotation_path(
228
+ annotation_path,
229
+ target_task_ids=target_task_ids,
230
+ task_query=task_query,
231
+ target_label_names=target_label_names,
232
+ )
233
+
234
+ logger.info(f"{len(annotation_polygon_list)} 件のポリゴンアノテーションの情報を出力します。 :: output='{output_file}'")
235
+
236
+ if output_format == FormatArgument.CSV:
237
+ df = create_df(annotation_polygon_list)
238
+ print_csv(df, output_file)
239
+
240
+ elif output_format in [FormatArgument.PRETTY_JSON, FormatArgument.JSON]:
241
+ json_is_pretty = output_format == FormatArgument.PRETTY_JSON
242
+ # Pydantic BaseModelを使用したJSON処理
243
+ print_json(
244
+ [e.model_dump() for e in annotation_polygon_list],
245
+ is_pretty=json_is_pretty,
246
+ output=output_file,
247
+ )
248
+
249
+ else:
250
+ raise ValueError(f"出力形式 '{output_format}' はサポートされていません。")
251
+
252
+
253
+ class ListAnnotationPolygon(CommandLine):
254
+ COMMON_MESSAGE = "annofabcli annotation_zip list_polygon_annotation: error:"
255
+
256
+ def validate(self, args: argparse.Namespace) -> bool:
257
+ if args.project_id is None and args.annotation is None:
258
+ print( # noqa: T201
259
+ f"{self.COMMON_MESSAGE} argument --project_id: '--annotation'が未指定のときは、'--project_id' を指定してください。",
260
+ file=sys.stderr,
261
+ )
262
+ return False
263
+ return True
264
+
265
+ def main(self) -> None:
266
+ args = self.args
267
+
268
+ if not self.validate(args):
269
+ sys.exit(COMMAND_LINE_ERROR_STATUS_CODE)
270
+
271
+ project_id: str | None = args.project_id
272
+ if project_id is not None:
273
+ super().validate_project(project_id, project_member_roles=[ProjectMemberRole.OWNER, ProjectMemberRole.TRAINING_DATA_USER])
274
+ project, _ = self.service.api.get_project(project_id)
275
+ if project["input_data_type"] != InputDataType.IMAGE.value:
276
+ print(f"project_id='{project_id}'であるプロジェクトは画像プロジェクトでないので、終了します", file=sys.stderr) # noqa: T201
277
+ sys.exit(COMMAND_LINE_ERROR_STATUS_CODE)
278
+
279
+ annotation_path = Path(args.annotation) if args.annotation is not None else None
280
+
281
+ task_id_list = get_list_from_args(args.task_id) if args.task_id is not None else None
282
+ task_query = TaskQuery.from_dict(annofabcli.common.cli.get_json_from_args(args.task_query)) if args.task_query is not None else None
283
+ label_name_list = get_list_from_args(args.label_name) if args.label_name is not None else None
284
+
285
+ output_file: Path = args.output
286
+ output_format = FormatArgument(args.format)
287
+
288
+ downloading_obj = DownloadingFile(self.service)
289
+
290
+ def download_and_print_annotation_polygon(project_id: str, temp_dir: Path, *, is_latest: bool) -> None:
291
+ local_annotation_path = temp_dir / f"{project_id}__annotation.zip"
292
+ downloading_obj.download_annotation_zip(
293
+ project_id,
294
+ dest_path=local_annotation_path,
295
+ is_latest=is_latest,
296
+ )
297
+ print_annotation_polygon(
298
+ local_annotation_path,
299
+ output_file,
300
+ output_format,
301
+ target_task_ids=task_id_list,
302
+ task_query=task_query,
303
+ target_label_names=label_name_list,
304
+ )
305
+
306
+ if project_id is not None:
307
+ if args.temp_dir is not None:
308
+ download_and_print_annotation_polygon(project_id=project_id, temp_dir=args.temp_dir, is_latest=args.latest)
309
+ else:
310
+ with tempfile.TemporaryDirectory() as str_temp_dir:
311
+ download_and_print_annotation_polygon(
312
+ project_id=project_id,
313
+ temp_dir=Path(str_temp_dir),
314
+ is_latest=args.latest,
315
+ )
316
+ else:
317
+ assert annotation_path is not None
318
+ print_annotation_polygon(
319
+ annotation_path,
320
+ output_file,
321
+ output_format,
322
+ target_task_ids=task_id_list,
323
+ task_query=task_query,
324
+ target_label_names=label_name_list,
325
+ )
326
+
327
+
328
+ def parse_args(parser: argparse.ArgumentParser) -> None:
329
+ argument_parser = ArgumentParser(parser)
330
+
331
+ group = parser.add_mutually_exclusive_group(required=True)
332
+ group.add_argument(
333
+ "--annotation",
334
+ type=str,
335
+ help="アノテーションzip、またはzipを展開したディレクトリを指定します。",
336
+ )
337
+
338
+ group.add_argument("-p", "--project_id", type=str, help="project_id。アノテーションZIPをダウンロードします。")
339
+
340
+ argument_parser.add_format(
341
+ choices=[FormatArgument.CSV, FormatArgument.JSON, FormatArgument.PRETTY_JSON],
342
+ default=FormatArgument.CSV,
343
+ )
344
+
345
+ argument_parser.add_output()
346
+
347
+ parser.add_argument(
348
+ "-tq",
349
+ "--task_query",
350
+ type=str,
351
+ help="集計対象タスクを絞り込むためのクエリ条件をJSON形式で指定します。使用できるキーは task_id, status, phase, phase_stage です。"
352
+ " ``file://`` を先頭に付けると、JSON形式のファイルを指定できます。",
353
+ )
354
+ argument_parser.add_task_id(required=False)
355
+
356
+ parser.add_argument(
357
+ "--label_name",
358
+ type=str,
359
+ nargs="+",
360
+ help="指定したラベル名のポリゴンアノテーションのみを対象にします。複数指定できます。",
361
+ )
362
+
363
+ parser.add_argument(
364
+ "--latest",
365
+ action="store_true",
366
+ help="``--annotation`` を指定しないとき、最新のアノテーションzipを参照します。このオプションを指定すると、アノテーションzipを更新するのに数分待ちます。",
367
+ )
368
+
369
+ parser.add_argument(
370
+ "--temp_dir",
371
+ type=Path,
372
+ help="指定したディレクトリに、アノテーションZIPなどの一時ファイルをダウンロードします。",
373
+ )
374
+
375
+ parser.set_defaults(subcommand_func=main)
376
+
377
+
378
+ def main(args: argparse.Namespace) -> None:
379
+ service = build_annofabapi_resource_and_login(args)
380
+ facade = AnnofabApiFacade(service)
381
+ ListAnnotationPolygon(service, facade, args).main()
382
+
383
+
384
+ def add_parser(subparsers: argparse._SubParsersAction | None = None) -> argparse.ArgumentParser:
385
+ subcommand_name = "list_polygon_annotation"
386
+ subcommand_help = "アノテーションZIPからポリゴンアノテーションの座標情報と属性情報を出力します。"
387
+ epilog = "アノテーションZIPをダウンロードする場合は、オーナロールまたはアノテーションユーザロールを持つユーザで実行してください。"
388
+ parser = annofabcli.common.cli.add_parser(subparsers, subcommand_name, subcommand_help, description=subcommand_help, epilog=epilog)
389
+ parse_args(parser)
390
+ return parser