edgargg commited on
Commit
c3fc836
·
verified ·
1 Parent(s): 23f71f6

Upload folder using huggingface_hub

Browse files
README.md CHANGED
@@ -1,26 +1,11 @@
1
- ---
2
- tags:
3
- - gradio-custom-component
4
- - gradio-template-Image
5
- - bounding box
6
- - annotator
7
- - annotate
8
- - boxes
9
- title: gradio_image_annotation V0.2.6
10
- colorFrom: yellow
11
- colorTo: green
12
- sdk: docker
13
- pinned: false
14
- license: apache-2.0
15
- short_description: A Gradio component for image annotation
16
- ---
17
-
18
 
19
  # `gradio_image_annotation`
20
  <a href="https://pypi.org/project/gradio_image_annotation/" target="_blank"><img alt="PyPI - Version" src="https://img.shields.io/pypi/v/gradio_image_annotation"></a>
21
 
22
  A Gradio component that can be used to annotate images with bounding boxes.
23
 
 
 
24
  ## Installation
25
 
26
  ```bash
@@ -32,6 +17,7 @@ pip install gradio_image_annotation
32
  ```python
33
  import gradio as gr
34
  from gradio_image_annotation import image_annotator
 
35
 
36
 
37
  example_annotation = {
@@ -85,6 +71,8 @@ examples_crop = [
85
 
86
 
87
  def crop(annotations):
 
 
88
  if annotations["boxes"]:
89
  box = annotations["boxes"][0]
90
  return annotations["image"][
@@ -152,7 +140,7 @@ dict | None
152
 
153
  </td>
154
  <td align="left"><code>None</code></td>
155
- <td align="left">A dict or None. The dictionary must contain a key 'image' with either an URL to an image, a numpy image or a PIL image. Optionally it may contain a key 'boxes' with a list of boxes. Each box must be a dict wit the keys: 'xmin', 'ymin', 'xmax' and 'ymax' with the absolute image coordinates of the box. Optionally can also include the keys 'label' and 'color' describing the label and color of the box. Color must be a tuple of RGB values (e.g. `(255,255,255)`).</td>
156
  </tr>
157
 
158
  <tr>
@@ -541,6 +529,19 @@ bool | None
541
  <td align="left"><code>True</code></td>
542
  <td align="left">If True, the cursor will change when hovering over box handles in drag mode. Can be CPU-intensive.</td>
543
  </tr>
 
 
 
 
 
 
 
 
 
 
 
 
 
544
  </tbody></table>
545
 
546
 
@@ -568,8 +569,16 @@ The code snippet below is accurate in cases where the component is used as both
568
 
569
  ```python
570
  def predict(
571
- value: dict | None
572
- ) -> dict | None:
573
  return value
574
  ```
575
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
 
2
  # `gradio_image_annotation`
3
  <a href="https://pypi.org/project/gradio_image_annotation/" target="_blank"><img alt="PyPI - Version" src="https://img.shields.io/pypi/v/gradio_image_annotation"></a>
4
 
5
  A Gradio component that can be used to annotate images with bounding boxes.
6
 
7
+ ![Demo preview](images/demo.png)
8
+
9
  ## Installation
10
 
11
  ```bash
 
17
  ```python
18
  import gradio as gr
19
  from gradio_image_annotation import image_annotator
20
+ import numpy as np
21
 
22
 
23
  example_annotation = {
 
71
 
72
 
73
  def crop(annotations):
74
+ if angle := annotations.get("orientation", None):
75
+ annotations["image"] = np.rot90(annotations["image"], k=-angle)
76
  if annotations["boxes"]:
77
  box = annotations["boxes"][0]
78
  return annotations["image"][
 
140
 
141
  </td>
142
  <td align="left"><code>None</code></td>
143
+ <td align="left">A dict or None. The dictionary must contain a key 'image' with either an URL to an image, a numpy image or a PIL image. Optionally it may contain a key 'boxes' with a list of boxes. Each box must be a dict wit the keys: 'xmin', 'ymin', 'xmax' and 'ymax' with the absolute image coordinates of the box. Optionally can also include the keys 'label' and 'color' describing the label and color of the box. Color must be a tuple of RGB values (e.g. `(255,255,255)`). Optionally can also include the keys 'orientation' with a integer between 0 and 3, describing the number of times the image is rotated by 90 degrees in frontend, the rotation is clockwise.</td>
144
  </tr>
145
 
146
  <tr>
 
529
  <td align="left"><code>True</code></td>
530
  <td align="left">If True, the cursor will change when hovering over box handles in drag mode. Can be CPU-intensive.</td>
531
  </tr>
532
+
533
+ <tr>
534
+ <td align="left"><code>use_default_label</code></td>
535
+ <td align="left" style="width: 25%;">
536
+
537
+ ```python
538
+ bool
539
+ ```
540
+
541
+ </td>
542
+ <td align="left"><code>False</code></td>
543
+ <td align="left">If True, the first item in label_list will be used as the default label when creating boxes.</td>
544
+ </tr>
545
  </tbody></table>
546
 
547
 
 
569
 
570
  ```python
571
  def predict(
572
+ value: AnnotatedImageValue | None
573
+ ) -> AnnotatedImageValue | None:
574
  return value
575
  ```
576
 
577
+
578
+ ## `AnnotatedImageValue`
579
+ ```python
580
+ class AnnotatedImageValue(TypedDict):
581
+ image: Optional[np.ndarray | PIL.Image.Image | str]
582
+ boxes: Optional[List[dict]]
583
+ orientation: Optional[int]
584
+ ```
app.py CHANGED
@@ -1,5 +1,6 @@
1
  import gradio as gr
2
  from gradio_image_annotation import image_annotator
 
3
 
4
 
5
  example_annotation = {
@@ -53,6 +54,8 @@ examples_crop = [
53
 
54
 
55
  def crop(annotations):
 
 
56
  if annotations["boxes"]:
57
  box = annotations["boxes"][0]
58
  return annotations["image"][
 
1
  import gradio as gr
2
  from gradio_image_annotation import image_annotator
3
+ import numpy as np
4
 
5
 
6
  example_annotation = {
 
54
 
55
 
56
  def crop(annotations):
57
+ if angle := annotations.get("orientation", None):
58
+ annotations["image"] = np.rot90(annotations["image"], k=-angle)
59
  if annotations["boxes"]:
60
  box = annotations["boxes"][0]
61
  return annotations["image"][
space.py CHANGED
@@ -3,7 +3,7 @@ import gradio as gr
3
  from app import demo as app
4
  import os
5
 
6
- _docs = {'image_annotator': {'description': 'Creates a component to annotate images with bounding boxes. The bounding boxes can be created and edited by the user or be passed by code.\nIt is also possible to predefine a set of valid classes and colors.', 'members': {'__init__': {'value': {'type': 'dict | None', 'default': 'None', 'description': "A dict or None. The dictionary must contain a key 'image' with either an URL to an image, a numpy image or a PIL image. Optionally it may contain a key 'boxes' with a list of boxes. Each box must be a dict wit the keys: 'xmin', 'ymin', 'xmax' and 'ymax' with the absolute image coordinates of the box. Optionally can also include the keys 'label' and 'color' describing the label and color of the box. Color must be a tuple of RGB values (e.g. `(255,255,255)`)."}, 'boxes_alpha': {'type': 'float | None', 'default': 'None', 'description': 'Opacity of the bounding boxes 0 and 1.'}, 'label_list': {'type': 'list[str] | None', 'default': 'None', 'description': 'List of valid labels.'}, 'label_colors': {'type': 'list[str] | None', 'default': 'None', 'description': 'Optional list of colors for each label when `label_list` is used. Colors must be a tuple of RGB values (e.g. `(255,255,255)`).'}, 'box_min_size': {'type': 'int | None', 'default': 'None', 'description': 'Minimum valid bounding box size.'}, 'handle_size': {'type': 'int | None', 'default': 'None', 'description': 'Size of the bounding box resize handles.'}, 'box_thickness': {'type': 'int | None', 'default': 'None', 'description': 'Thickness of the bounding box outline.'}, 'box_selected_thickness': {'type': 'int | None', 'default': 'None', 'description': 'Thickness of the bounding box outline when it is selected.'}, 'disable_edit_boxes': {'type': 'bool | None', 'default': 'None', 'description': 'Disables the ability to set and edit the label and color of the boxes.'}, 'single_box': {'type': 'bool', 'default': 'False', 'description': 'If True, at most one box can be drawn.'}, 'height': {'type': 'int | str | None', 'default': 'None', 'description': 'The height of the displayed image, specified in pixels if a number is passed, or in CSS units if a string is passed.'}, 'width': {'type': 'int | str | None', 'default': 'None', 'description': 'The width of the displayed image, specified in pixels if a number is passed, or in CSS units if a string is passed.'}, 'image_mode': {'type': '"1"\n | "L"\n | "P"\n | "RGB"\n | "RGBA"\n | "CMYK"\n | "YCbCr"\n | "LAB"\n | "HSV"\n | "I"\n | "F"', 'default': '"RGB"', 'description': '"RGB" if color, or "L" if black and white. See https://pillow.readthedocs.io/en/stable/handbook/concepts.html for other supported image modes and their meaning.'}, 'sources': {'type': 'list["upload" | "webcam" | "clipboard"] | None', 'default': '["upload", "webcam", "clipboard"]', 'description': 'List of sources for the image. "upload" creates a box where user can drop an image file, "webcam" allows user to take snapshot from their webcam, "clipboard" allows users to paste an image from the clipboard. If None, defaults to ["upload", "webcam", "clipboard"].'}, 'image_type': {'type': '"numpy" | "pil" | "filepath"', 'default': '"numpy"', 'description': 'The format the image is converted before being passed into the prediction function. "numpy" converts the image to a numpy array with shape (height, width, 3) and values from 0 to 255, "pil" converts the image to a PIL image object, "filepath" passes a str path to a temporary file containing the image. If the image is SVG, the `type` is ignored and the filepath of the SVG is returned.'}, 'label': {'type': 'str | None', 'default': 'None', 'description': 'The label for this component. Appears above the component and is also used as the header if there are a table of examples for this component. If None and used in a `gr.Interface`, the label will be the name of the parameter this component is assigned to.'}, 'container': {'type': 'bool', 'default': 'True', 'description': 'If True, will place the component in a container - providing some extra padding around the border.'}, 'scale': {'type': 'int | None', 'default': 'None', 'description': 'relative size compared to adjacent Components. For example if Components A and B are in a Row, and A has scale=2, and B has scale=1, A will be twice as wide as B. Should be an integer. scale applies in Rows, and to top-level Components in Blocks where fill_height=True.'}, 'min_width': {'type': 'int', 'default': '160', 'description': 'minimum pixel width, will wrap if not sufficient screen space to satisfy this value. If a certain scale value results in this Component being narrower than min_width, the min_width parameter will be respected first.'}, 'interactive': {'type': 'bool | None', 'default': 'True', 'description': 'if True, will allow users to upload and annotate an image; if False, can only be used to display annotated images.'}, 'visible': {'type': 'bool', 'default': 'True', 'description': 'If False, component will be hidden.'}, 'elem_id': {'type': 'str | None', 'default': 'None', 'description': 'An optional string that is assigned as the id of this component in the HTML DOM. Can be used for targeting CSS styles.'}, 'elem_classes': {'type': 'list[str] | str | None', 'default': 'None', 'description': 'An optional list of strings that are assigned as the classes of this component in the HTML DOM. Can be used for targeting CSS styles.'}, 'render': {'type': 'bool', 'default': 'True', 'description': 'If False, component will not render be rendered in the Blocks context. Should be used if the intention is to assign event listeners now but render the component later.'}, 'show_label': {'type': 'bool | None', 'default': 'None', 'description': 'if True, will display label.'}, 'show_download_button': {'type': 'bool', 'default': 'True', 'description': 'If True, will show a button to download the image.'}, 'show_share_button': {'type': 'bool | None', 'default': 'None', 'description': 'If True, will show a share icon in the corner of the component that allows user to share outputs to Hugging Face Spaces Discussions. If False, icon does not appear. If set to None (default behavior), then the icon appears if this Gradio app is launched on Spaces, but not otherwise.'}, 'show_clear_button': {'type': 'bool | None', 'default': 'True', 'description': 'If True, will show a button to clear the current image.'}, 'show_remove_button': {'type': 'bool | None', 'default': 'None', 'description': 'If True, will show a button to remove the selected bounding box.'}, 'handles_cursor': {'type': 'bool | None', 'default': 'True', 'description': 'If True, the cursor will change when hovering over box handles in drag mode. Can be CPU-intensive.'}}, 'postprocess': {'value': {'type': 'dict | None', 'description': 'A dict with an image and an optional list of boxes or None.'}}, 'preprocess': {'return': {'type': 'dict | None', 'description': 'A dict with the image and boxes or None.'}, 'value': None}}, 'events': {'clear': {'type': None, 'default': None, 'description': 'This listener is triggered when the user clears the image_annotator using the clear button for the component.'}, 'change': {'type': None, 'default': None, 'description': 'Triggered when the value of the image_annotator changes either because of user input (e.g. a user types in a textbox) OR because of a function update (e.g. an image receives a value from the output of an event trigger). See `.input()` for a listener that is only triggered by user input.'}, 'upload': {'type': None, 'default': None, 'description': 'This listener is triggered when the user uploads a file into the image_annotator.'}}}, '__meta__': {'additional_interfaces': {}, 'user_fn_refs': {'image_annotator': []}}}
7
 
8
  abs_path = os.path.join(os.path.dirname(__file__), "css.css")
9
 
@@ -40,6 +40,7 @@ pip install gradio_image_annotation
40
  ```python
41
  import gradio as gr
42
  from gradio_image_annotation import image_annotator
 
43
 
44
 
45
  example_annotation = {
@@ -93,6 +94,8 @@ examples_crop = [
93
 
94
 
95
  def crop(annotations):
 
 
96
  if annotations["boxes"]:
97
  box = annotations["boxes"][0]
98
  return annotations["image"][
@@ -144,7 +147,7 @@ if __name__ == "__main__":
144
  ### Initialization
145
  """, elem_classes=["md-custom"], header_links=True)
146
 
147
- gr.ParamViewer(value=_docs["image_annotator"]["members"]["__init__"], linkify=[])
148
 
149
 
150
  gr.Markdown("### Events")
@@ -169,8 +172,8 @@ The code snippet below is accurate in cases where the component is used as both
169
 
170
  ```python
171
  def predict(
172
- value: dict | None
173
- ) -> dict | None:
174
  return value
175
  ```
176
  """, elem_classes=["md-custom", "image_annotator-user-fn"], header_links=True)
@@ -178,10 +181,20 @@ def predict(
178
 
179
 
180
 
 
 
 
 
 
 
 
 
 
181
  demo.load(None, js=r"""function() {
182
- const refs = {};
 
183
  const user_fn_refs = {
184
- image_annotator: [], };
185
  requestAnimationFrame(() => {
186
 
187
  Object.entries(user_fn_refs).forEach(([key, refs]) => {
 
3
  from app import demo as app
4
  import os
5
 
6
+ _docs = {'image_annotator': {'description': 'Creates a component to annotate images with bounding boxes. The bounding boxes can be created and edited by the user or be passed by code.\nIt is also possible to predefine a set of valid classes and colors.', 'members': {'__init__': {'value': {'type': 'dict | None', 'default': 'None', 'description': "A dict or None. The dictionary must contain a key 'image' with either an URL to an image, a numpy image or a PIL image. Optionally it may contain a key 'boxes' with a list of boxes. Each box must be a dict wit the keys: 'xmin', 'ymin', 'xmax' and 'ymax' with the absolute image coordinates of the box. Optionally can also include the keys 'label' and 'color' describing the label and color of the box. Color must be a tuple of RGB values (e.g. `(255,255,255)`). Optionally can also include the keys 'orientation' with a integer between 0 and 3, describing the number of times the image is rotated by 90 degrees in frontend, the rotation is clockwise."}, 'boxes_alpha': {'type': 'float | None', 'default': 'None', 'description': 'Opacity of the bounding boxes 0 and 1.'}, 'label_list': {'type': 'list[str] | None', 'default': 'None', 'description': 'List of valid labels.'}, 'label_colors': {'type': 'list[str] | None', 'default': 'None', 'description': 'Optional list of colors for each label when `label_list` is used. Colors must be a tuple of RGB values (e.g. `(255,255,255)`).'}, 'box_min_size': {'type': 'int | None', 'default': 'None', 'description': 'Minimum valid bounding box size.'}, 'handle_size': {'type': 'int | None', 'default': 'None', 'description': 'Size of the bounding box resize handles.'}, 'box_thickness': {'type': 'int | None', 'default': 'None', 'description': 'Thickness of the bounding box outline.'}, 'box_selected_thickness': {'type': 'int | None', 'default': 'None', 'description': 'Thickness of the bounding box outline when it is selected.'}, 'disable_edit_boxes': {'type': 'bool | None', 'default': 'None', 'description': 'Disables the ability to set and edit the label and color of the boxes.'}, 'single_box': {'type': 'bool', 'default': 'False', 'description': 'If True, at most one box can be drawn.'}, 'height': {'type': 'int | str | None', 'default': 'None', 'description': 'The height of the displayed image, specified in pixels if a number is passed, or in CSS units if a string is passed.'}, 'width': {'type': 'int | str | None', 'default': 'None', 'description': 'The width of the displayed image, specified in pixels if a number is passed, or in CSS units if a string is passed.'}, 'image_mode': {'type': '"1"\n | "L"\n | "P"\n | "RGB"\n | "RGBA"\n | "CMYK"\n | "YCbCr"\n | "LAB"\n | "HSV"\n | "I"\n | "F"', 'default': '"RGB"', 'description': '"RGB" if color, or "L" if black and white. See https://pillow.readthedocs.io/en/stable/handbook/concepts.html for other supported image modes and their meaning.'}, 'sources': {'type': 'list["upload" | "webcam" | "clipboard"] | None', 'default': '["upload", "webcam", "clipboard"]', 'description': 'List of sources for the image. "upload" creates a box where user can drop an image file, "webcam" allows user to take snapshot from their webcam, "clipboard" allows users to paste an image from the clipboard. If None, defaults to ["upload", "webcam", "clipboard"].'}, 'image_type': {'type': '"numpy" | "pil" | "filepath"', 'default': '"numpy"', 'description': 'The format the image is converted before being passed into the prediction function. "numpy" converts the image to a numpy array with shape (height, width, 3) and values from 0 to 255, "pil" converts the image to a PIL image object, "filepath" passes a str path to a temporary file containing the image. If the image is SVG, the `type` is ignored and the filepath of the SVG is returned.'}, 'label': {'type': 'str | None', 'default': 'None', 'description': 'The label for this component. Appears above the component and is also used as the header if there are a table of examples for this component. If None and used in a `gr.Interface`, the label will be the name of the parameter this component is assigned to.'}, 'container': {'type': 'bool', 'default': 'True', 'description': 'If True, will place the component in a container - providing some extra padding around the border.'}, 'scale': {'type': 'int | None', 'default': 'None', 'description': 'relative size compared to adjacent Components. For example if Components A and B are in a Row, and A has scale=2, and B has scale=1, A will be twice as wide as B. Should be an integer. scale applies in Rows, and to top-level Components in Blocks where fill_height=True.'}, 'min_width': {'type': 'int', 'default': '160', 'description': 'minimum pixel width, will wrap if not sufficient screen space to satisfy this value. If a certain scale value results in this Component being narrower than min_width, the min_width parameter will be respected first.'}, 'interactive': {'type': 'bool | None', 'default': 'True', 'description': 'if True, will allow users to upload and annotate an image; if False, can only be used to display annotated images.'}, 'visible': {'type': 'bool', 'default': 'True', 'description': 'If False, component will be hidden.'}, 'elem_id': {'type': 'str | None', 'default': 'None', 'description': 'An optional string that is assigned as the id of this component in the HTML DOM. Can be used for targeting CSS styles.'}, 'elem_classes': {'type': 'list[str] | str | None', 'default': 'None', 'description': 'An optional list of strings that are assigned as the classes of this component in the HTML DOM. Can be used for targeting CSS styles.'}, 'render': {'type': 'bool', 'default': 'True', 'description': 'If False, component will not render be rendered in the Blocks context. Should be used if the intention is to assign event listeners now but render the component later.'}, 'show_label': {'type': 'bool | None', 'default': 'None', 'description': 'if True, will display label.'}, 'show_download_button': {'type': 'bool', 'default': 'True', 'description': 'If True, will show a button to download the image.'}, 'show_share_button': {'type': 'bool | None', 'default': 'None', 'description': 'If True, will show a share icon in the corner of the component that allows user to share outputs to Hugging Face Spaces Discussions. If False, icon does not appear. If set to None (default behavior), then the icon appears if this Gradio app is launched on Spaces, but not otherwise.'}, 'show_clear_button': {'type': 'bool | None', 'default': 'True', 'description': 'If True, will show a button to clear the current image.'}, 'show_remove_button': {'type': 'bool | None', 'default': 'None', 'description': 'If True, will show a button to remove the selected bounding box.'}, 'handles_cursor': {'type': 'bool | None', 'default': 'True', 'description': 'If True, the cursor will change when hovering over box handles in drag mode. Can be CPU-intensive.'}, 'use_default_label': {'type': 'bool', 'default': 'False', 'description': 'If True, the first item in label_list will be used as the default label when creating boxes.'}}, 'postprocess': {'value': {'type': 'AnnotatedImageValue | None', 'description': 'A dict with an image and an optional list of boxes or None.'}}, 'preprocess': {'return': {'type': 'AnnotatedImageValue | None', 'description': 'A dict with the image and boxes or None.'}, 'value': None}}, 'events': {'clear': {'type': None, 'default': None, 'description': 'This listener is triggered when the user clears the image_annotator using the clear button for the component.'}, 'change': {'type': None, 'default': None, 'description': 'Triggered when the value of the image_annotator changes either because of user input (e.g. a user types in a textbox) OR because of a function update (e.g. an image receives a value from the output of an event trigger). See `.input()` for a listener that is only triggered by user input.'}, 'upload': {'type': None, 'default': None, 'description': 'This listener is triggered when the user uploads a file into the image_annotator.'}}}, '__meta__': {'additional_interfaces': {'AnnotatedImageValue': {'source': 'class AnnotatedImageValue(TypedDict):\n image: Optional[np.ndarray | PIL.Image.Image | str]\n boxes: Optional[List[dict]]\n orientation: Optional[int]'}}, 'user_fn_refs': {'image_annotator': ['AnnotatedImageValue']}}}
7
 
8
  abs_path = os.path.join(os.path.dirname(__file__), "css.css")
9
 
 
40
  ```python
41
  import gradio as gr
42
  from gradio_image_annotation import image_annotator
43
+ import numpy as np
44
 
45
 
46
  example_annotation = {
 
94
 
95
 
96
  def crop(annotations):
97
+ if angle := annotations.get("orientation", None):
98
+ annotations["image"] = np.rot90(annotations["image"], k=-angle)
99
  if annotations["boxes"]:
100
  box = annotations["boxes"][0]
101
  return annotations["image"][
 
147
  ### Initialization
148
  """, elem_classes=["md-custom"], header_links=True)
149
 
150
+ gr.ParamViewer(value=_docs["image_annotator"]["members"]["__init__"], linkify=['AnnotatedImageValue'])
151
 
152
 
153
  gr.Markdown("### Events")
 
172
 
173
  ```python
174
  def predict(
175
+ value: AnnotatedImageValue | None
176
+ ) -> AnnotatedImageValue | None:
177
  return value
178
  ```
179
  """, elem_classes=["md-custom", "image_annotator-user-fn"], header_links=True)
 
181
 
182
 
183
 
184
+ code_AnnotatedImageValue = gr.Markdown("""
185
+ ## `AnnotatedImageValue`
186
+ ```python
187
+ class AnnotatedImageValue(TypedDict):
188
+ image: Optional[np.ndarray | PIL.Image.Image | str]
189
+ boxes: Optional[List[dict]]
190
+ orientation: Optional[int]
191
+ ```""", elem_classes=["md-custom", "AnnotatedImageValue"], header_links=True)
192
+
193
  demo.load(None, js=r"""function() {
194
+ const refs = {
195
+ AnnotatedImageValue: [], };
196
  const user_fn_refs = {
197
+ image_annotator: ['AnnotatedImageValue'], };
198
  requestAnimationFrame(() => {
199
 
200
  Object.entries(user_fn_refs).forEach(([key, refs]) => {
src/README.md CHANGED
@@ -4,6 +4,8 @@
4
 
5
  A Gradio component that can be used to annotate images with bounding boxes.
6
 
 
 
7
  ## Installation
8
 
9
  ```bash
@@ -15,6 +17,7 @@ pip install gradio_image_annotation
15
  ```python
16
  import gradio as gr
17
  from gradio_image_annotation import image_annotator
 
18
 
19
 
20
  example_annotation = {
@@ -68,6 +71,8 @@ examples_crop = [
68
 
69
 
70
  def crop(annotations):
 
 
71
  if annotations["boxes"]:
72
  box = annotations["boxes"][0]
73
  return annotations["image"][
@@ -135,7 +140,7 @@ dict | None
135
 
136
  </td>
137
  <td align="left"><code>None</code></td>
138
- <td align="left">A dict or None. The dictionary must contain a key 'image' with either an URL to an image, a numpy image or a PIL image. Optionally it may contain a key 'boxes' with a list of boxes. Each box must be a dict wit the keys: 'xmin', 'ymin', 'xmax' and 'ymax' with the absolute image coordinates of the box. Optionally can also include the keys 'label' and 'color' describing the label and color of the box. Color must be a tuple of RGB values (e.g. `(255,255,255)`).</td>
139
  </tr>
140
 
141
  <tr>
@@ -524,6 +529,19 @@ bool | None
524
  <td align="left"><code>True</code></td>
525
  <td align="left">If True, the cursor will change when hovering over box handles in drag mode. Can be CPU-intensive.</td>
526
  </tr>
 
 
 
 
 
 
 
 
 
 
 
 
 
527
  </tbody></table>
528
 
529
 
@@ -551,8 +569,16 @@ The code snippet below is accurate in cases where the component is used as both
551
 
552
  ```python
553
  def predict(
554
- value: dict | None
555
- ) -> dict | None:
556
  return value
557
  ```
558
 
 
 
 
 
 
 
 
 
 
4
 
5
  A Gradio component that can be used to annotate images with bounding boxes.
6
 
7
+ ![Demo preview](images/demo.png)
8
+
9
  ## Installation
10
 
11
  ```bash
 
17
  ```python
18
  import gradio as gr
19
  from gradio_image_annotation import image_annotator
20
+ import numpy as np
21
 
22
 
23
  example_annotation = {
 
71
 
72
 
73
  def crop(annotations):
74
+ if angle := annotations.get("orientation", None):
75
+ annotations["image"] = np.rot90(annotations["image"], k=-angle)
76
  if annotations["boxes"]:
77
  box = annotations["boxes"][0]
78
  return annotations["image"][
 
140
 
141
  </td>
142
  <td align="left"><code>None</code></td>
143
+ <td align="left">A dict or None. The dictionary must contain a key 'image' with either an URL to an image, a numpy image or a PIL image. Optionally it may contain a key 'boxes' with a list of boxes. Each box must be a dict wit the keys: 'xmin', 'ymin', 'xmax' and 'ymax' with the absolute image coordinates of the box. Optionally can also include the keys 'label' and 'color' describing the label and color of the box. Color must be a tuple of RGB values (e.g. `(255,255,255)`). Optionally can also include the keys 'orientation' with a integer between 0 and 3, describing the number of times the image is rotated by 90 degrees in frontend, the rotation is clockwise.</td>
144
  </tr>
145
 
146
  <tr>
 
529
  <td align="left"><code>True</code></td>
530
  <td align="left">If True, the cursor will change when hovering over box handles in drag mode. Can be CPU-intensive.</td>
531
  </tr>
532
+
533
+ <tr>
534
+ <td align="left"><code>use_default_label</code></td>
535
+ <td align="left" style="width: 25%;">
536
+
537
+ ```python
538
+ bool
539
+ ```
540
+
541
+ </td>
542
+ <td align="left"><code>False</code></td>
543
+ <td align="left">If True, the first item in label_list will be used as the default label when creating boxes.</td>
544
+ </tr>
545
  </tbody></table>
546
 
547
 
 
569
 
570
  ```python
571
  def predict(
572
+ value: AnnotatedImageValue | None
573
+ ) -> AnnotatedImageValue | None:
574
  return value
575
  ```
576
 
577
+
578
+ ## `AnnotatedImageValue`
579
+ ```python
580
+ class AnnotatedImageValue(TypedDict):
581
+ image: Optional[np.ndarray | PIL.Image.Image | str]
582
+ boxes: Optional[List[dict]]
583
+ orientation: Optional[int]
584
+ ```
src/backend/gradio_image_annotation/image_annotator.py CHANGED
@@ -3,7 +3,7 @@ from __future__ import annotations
3
  import re
4
  import warnings
5
  from pathlib import Path
6
- from typing import Any, List, Literal, cast
7
 
8
  import numpy as np
9
  import PIL.Image
@@ -20,6 +20,13 @@ PIL.Image.init() # fixes https://github.com/gradio-app/gradio/issues/2843
20
  class AnnotatedImageData(GradioModel):
21
  image: FileData
22
  boxes: List[dict] = []
 
 
 
 
 
 
 
23
 
24
 
25
  def rgb2hex(r,g,b):
@@ -81,10 +88,11 @@ class image_annotator(Component):
81
  show_clear_button: bool | None = True,
82
  show_remove_button: bool | None = None,
83
  handles_cursor: bool | None = True,
 
84
  ):
85
  """
86
  Parameters:
87
- value: A dict or None. The dictionary must contain a key 'image' with either an URL to an image, a numpy image or a PIL image. Optionally it may contain a key 'boxes' with a list of boxes. Each box must be a dict wit the keys: 'xmin', 'ymin', 'xmax' and 'ymax' with the absolute image coordinates of the box. Optionally can also include the keys 'label' and 'color' describing the label and color of the box. Color must be a tuple of RGB values (e.g. `(255,255,255)`).
88
  boxes_alpha: Opacity of the bounding boxes 0 and 1.
89
  label_list: List of valid labels.
90
  label_colors: Optional list of colors for each label when `label_list` is used. Colors must be a tuple of RGB values (e.g. `(255,255,255)`).
@@ -114,6 +122,7 @@ class image_annotator(Component):
114
  show_clear_button: If True, will show a button to clear the current image.
115
  show_remove_button: If True, will show a button to remove the selected bounding box.
116
  handles_cursor: If True, the cursor will change when hovering over box handles in drag mode. Can be CPU-intensive.
 
117
  """
118
 
119
  valid_types = ["numpy", "pil", "filepath"]
@@ -148,6 +157,7 @@ class image_annotator(Component):
148
  self.show_clear_button = show_clear_button
149
  self.show_remove_button = show_remove_button
150
  self.handles_cursor = handles_cursor
 
151
 
152
  self.boxes_alpha = boxes_alpha
153
  self.box_min_size = box_min_size
@@ -247,7 +257,7 @@ class image_annotator(Component):
247
  parsed_boxes.append(new_box)
248
  return parsed_boxes
249
 
250
- def preprocess(self, payload: AnnotatedImageData | None) -> dict | None:
251
  """
252
  Parameters:
253
  payload: an AnnotatedImageData object.
@@ -259,11 +269,12 @@ class image_annotator(Component):
259
 
260
  ret_value = {
261
  "image": self.preprocess_image(payload.image),
262
- "boxes": self.preprocess_boxes(payload.boxes)
 
263
  }
264
  return ret_value
265
 
266
- def postprocess(self, value: dict | None) -> AnnotatedImageData | None:
267
  """
268
  Parameters:
269
  value: A dict with an image and an optional list of boxes or None.
@@ -303,7 +314,11 @@ class image_annotator(Component):
303
  else:
304
  raise ValueError(f"An image must be provided. Got {value}")
305
 
306
- return AnnotatedImageData(image=image, boxes=boxes)
 
 
 
 
307
 
308
  def process_example(self, value: dict | None) -> FileData | None:
309
  if value is None:
 
3
  import re
4
  import warnings
5
  from pathlib import Path
6
+ from typing import Any, List, Optional, Literal, TypedDict, cast
7
 
8
  import numpy as np
9
  import PIL.Image
 
20
  class AnnotatedImageData(GradioModel):
21
  image: FileData
22
  boxes: List[dict] = []
23
+ orientation: int = 0
24
+
25
+
26
+ class AnnotatedImageValue(TypedDict):
27
+ image: Optional[np.ndarray | PIL.Image.Image | str]
28
+ boxes: Optional[List[dict]]
29
+ orientation: Optional[int]
30
 
31
 
32
  def rgb2hex(r,g,b):
 
88
  show_clear_button: bool | None = True,
89
  show_remove_button: bool | None = None,
90
  handles_cursor: bool | None = True,
91
+ use_default_label: bool = False,
92
  ):
93
  """
94
  Parameters:
95
+ value: A dict or None. The dictionary must contain a key 'image' with either an URL to an image, a numpy image or a PIL image. Optionally it may contain a key 'boxes' with a list of boxes. Each box must be a dict wit the keys: 'xmin', 'ymin', 'xmax' and 'ymax' with the absolute image coordinates of the box. Optionally can also include the keys 'label' and 'color' describing the label and color of the box. Color must be a tuple of RGB values (e.g. `(255,255,255)`). Optionally can also include the keys 'orientation' with a integer between 0 and 3, describing the number of times the image is rotated by 90 degrees in frontend, the rotation is clockwise.
96
  boxes_alpha: Opacity of the bounding boxes 0 and 1.
97
  label_list: List of valid labels.
98
  label_colors: Optional list of colors for each label when `label_list` is used. Colors must be a tuple of RGB values (e.g. `(255,255,255)`).
 
122
  show_clear_button: If True, will show a button to clear the current image.
123
  show_remove_button: If True, will show a button to remove the selected bounding box.
124
  handles_cursor: If True, the cursor will change when hovering over box handles in drag mode. Can be CPU-intensive.
125
+ use_default_label: If True, the first item in label_list will be used as the default label when creating boxes.
126
  """
127
 
128
  valid_types = ["numpy", "pil", "filepath"]
 
157
  self.show_clear_button = show_clear_button
158
  self.show_remove_button = show_remove_button
159
  self.handles_cursor = handles_cursor
160
+ self.use_default_label = use_default_label
161
 
162
  self.boxes_alpha = boxes_alpha
163
  self.box_min_size = box_min_size
 
257
  parsed_boxes.append(new_box)
258
  return parsed_boxes
259
 
260
+ def preprocess(self, payload: AnnotatedImageData | None) -> AnnotatedImageValue | None:
261
  """
262
  Parameters:
263
  payload: an AnnotatedImageData object.
 
269
 
270
  ret_value = {
271
  "image": self.preprocess_image(payload.image),
272
+ "boxes": self.preprocess_boxes(payload.boxes),
273
+ "orientation": payload.orientation,
274
  }
275
  return ret_value
276
 
277
+ def postprocess(self, value: AnnotatedImageValue | None) -> AnnotatedImageData | None:
278
  """
279
  Parameters:
280
  value: A dict with an image and an optional list of boxes or None.
 
314
  else:
315
  raise ValueError(f"An image must be provided. Got {value}")
316
 
317
+ orientation = value.setdefault("orientation", 0)
318
+ if orientation is None:
319
+ orientation = 0
320
+
321
+ return AnnotatedImageData(image=image, boxes=boxes, orientation=orientation)
322
 
323
  def process_example(self, value: dict | None) -> FileData | None:
324
  if value is None:
src/backend/gradio_image_annotation/templates/component/index.js CHANGED
The diff for this file is too large to render. See raw diff
 
src/backend/gradio_image_annotation/templates/component/style.css CHANGED
@@ -1 +1 @@
1
- .block.svelte-nl1om8{position:relative;margin:0;box-shadow:var(--block-shadow);border-width:var(--block-border-width);border-color:var(--block-border-color);border-radius:var(--block-radius);background:var(--block-background-fill);width:100%;line-height:var(--line-sm)}.block.border_focus.svelte-nl1om8{border-color:var(--color-accent)}.block.border_contrast.svelte-nl1om8{border-color:var(--body-text-color)}.padded.svelte-nl1om8{padding:var(--block-padding)}.hidden.svelte-nl1om8{display:none}.hide-container.svelte-nl1om8{margin:0;box-shadow:none;--block-border-width:0;background:transparent;padding:0;overflow:visible}div.svelte-1hnfib2{margin-bottom:var(--spacing-lg);color:var(--block-info-text-color);font-weight:var(--block-info-text-weight);font-size:var(--block-info-text-size);line-height:var(--line-sm)}span.has-info.svelte-22c38v{margin-bottom:var(--spacing-xs)}span.svelte-22c38v:not(.has-info){margin-bottom:var(--spacing-lg)}span.svelte-22c38v{display:inline-block;position:relative;z-index:var(--layer-4);border:solid var(--block-title-border-width) var(--block-title-border-color);border-radius:var(--block-title-radius);background:var(--block-title-background-fill);padding:var(--block-title-padding);color:var(--block-title-text-color);font-weight:var(--block-title-text-weight);font-size:var(--block-title-text-size);line-height:var(--line-sm)}.hide.svelte-22c38v{margin:0;height:0}label.svelte-9gxdi0{display:inline-flex;align-items:center;z-index:var(--layer-2);box-shadow:var(--block-label-shadow);border:var(--block-label-border-width) solid var(--border-color-primary);border-top:none;border-left:none;border-radius:var(--block-label-radius);background:var(--block-label-background-fill);padding:var(--block-label-padding);pointer-events:none;color:var(--block-label-text-color);font-weight:var(--block-label-text-weight);font-size:var(--block-label-text-size);line-height:var(--line-sm)}.gr-group label.svelte-9gxdi0{border-top-left-radius:0}label.float.svelte-9gxdi0{position:absolute;top:var(--block-label-margin);left:var(--block-label-margin)}label.svelte-9gxdi0:not(.float){position:static;margin-top:var(--block-label-margin);margin-left:var(--block-label-margin)}.hide.svelte-9gxdi0{height:0}span.svelte-9gxdi0{opacity:.8;margin-right:var(--size-2);width:calc(var(--block-label-text-size) - 1px);height:calc(var(--block-label-text-size) - 1px)}.hide-label.svelte-9gxdi0{box-shadow:none;border-width:0;background:transparent;overflow:visible}button.svelte-1lrphxw{display:flex;justify-content:center;align-items:center;gap:1px;z-index:var(--layer-2);border-radius:var(--radius-sm);color:var(--block-label-text-color);border:1px solid transparent}button[disabled].svelte-1lrphxw{opacity:.5;box-shadow:none}button[disabled].svelte-1lrphxw:hover{cursor:not-allowed}.padded.svelte-1lrphxw{padding:2px;background:var(--bg-color);box-shadow:var(--shadow-drop);border:1px solid var(--button-secondary-border-color)}button.svelte-1lrphxw:hover,button.highlight.svelte-1lrphxw{cursor:pointer;color:var(--color-accent)}.padded.svelte-1lrphxw:hover{border:2px solid var(--button-secondary-border-color-hover);padding:1px;color:var(--block-label-text-color)}span.svelte-1lrphxw{padding:0 1px;font-size:10px}div.svelte-1lrphxw{padding:2px;display:flex;align-items:flex-end}.small.svelte-1lrphxw{width:14px;height:14px}.medium.svelte-1lrphxw{width:20px;height:20px}.large.svelte-1lrphxw{width:22px;height:22px}.pending.svelte-1lrphxw{animation:svelte-1lrphxw-flash .5s infinite}@keyframes svelte-1lrphxw-flash{0%{opacity:.5}50%{opacity:1}to{opacity:.5}}.transparent.svelte-1lrphxw{background:transparent;border:none;box-shadow:none}.empty.svelte-3w3rth{display:flex;justify-content:center;align-items:center;margin-top:calc(0px - var(--size-6));height:var(--size-full)}.icon.svelte-3w3rth{opacity:.5;height:var(--size-5);color:var(--body-text-color)}.small.svelte-3w3rth{min-height:calc(var(--size-32) - 20px)}.large.svelte-3w3rth{min-height:calc(var(--size-64) - 20px)}.unpadded_box.svelte-3w3rth{margin-top:0}.small_parent.svelte-3w3rth{min-height:100%!important}.dropdown-arrow.svelte-145leq6{fill:currentColor}.wrap.svelte-kzcjhc{display:flex;flex-direction:column;justify-content:center;align-items:center;min-height:var(--size-60);color:var(--block-label-text-color);line-height:var(--line-md);height:100%;padding-top:var(--size-3)}.or.svelte-kzcjhc{color:var(--body-text-color-subdued);display:flex}.icon-wrap.svelte-kzcjhc{width:30px;margin-bottom:var(--spacing-lg)}@media (--screen-md){.wrap.svelte-kzcjhc{font-size:var(--text-lg)}}.hovered.svelte-kzcjhc{color:var(--color-accent)}div.svelte-q32hvf{border-top:1px solid transparent;display:flex;max-height:100%;justify-content:center;align-items:center;gap:var(--spacing-sm);height:auto;align-items:flex-end;color:var(--block-label-text-color);flex-shrink:0}.show_border.svelte-q32hvf{border-top:1px solid var(--block-border-color);margin-top:var(--spacing-xxl);box-shadow:var(--shadow-drop)}.source-selection.svelte-1jp3vgd{display:flex;align-items:center;justify-content:center;border-top:1px solid var(--border-color-primary);width:95%;bottom:0;left:0;right:0;margin-left:auto;margin-right:auto}.icon.svelte-1jp3vgd{width:22px;height:22px;margin:var(--spacing-lg) var(--spacing-xs);padding:var(--spacing-xs);color:var(--neutral-400);border-radius:var(--radius-md)}.selected.svelte-1jp3vgd{color:var(--color-accent)}.icon.svelte-1jp3vgd:hover,.icon.svelte-1jp3vgd:focus{color:var(--color-accent)}.wrap.svelte-16nch4a.svelte-16nch4a{display:flex;flex-direction:column;justify-content:center;align-items:center;z-index:var(--layer-2);transition:opacity .1s ease-in-out;border-radius:var(--block-radius);background:var(--block-background-fill);padding:0 var(--size-6);max-height:var(--size-screen-h);overflow:hidden}.wrap.center.svelte-16nch4a.svelte-16nch4a{top:0;right:0;left:0}.wrap.default.svelte-16nch4a.svelte-16nch4a{top:0;right:0;bottom:0;left:0}.hide.svelte-16nch4a.svelte-16nch4a{opacity:0;pointer-events:none}.generating.svelte-16nch4a.svelte-16nch4a{animation:svelte-16nch4a-pulseStart 1s cubic-bezier(.4,0,.6,1),svelte-16nch4a-pulse 2s cubic-bezier(.4,0,.6,1) 1s infinite;border:2px solid var(--color-accent);background:transparent;z-index:var(--layer-1);pointer-events:none}.translucent.svelte-16nch4a.svelte-16nch4a{background:none}@keyframes svelte-16nch4a-pulseStart{0%{opacity:0}to{opacity:1}}@keyframes svelte-16nch4a-pulse{0%,to{opacity:1}50%{opacity:.5}}.loading.svelte-16nch4a.svelte-16nch4a{z-index:var(--layer-2);color:var(--body-text-color)}.eta-bar.svelte-16nch4a.svelte-16nch4a{position:absolute;top:0;right:0;bottom:0;left:0;transform-origin:left;opacity:.8;z-index:var(--layer-1);transition:10ms;background:var(--background-fill-secondary)}.progress-bar-wrap.svelte-16nch4a.svelte-16nch4a{border:1px solid var(--border-color-primary);background:var(--background-fill-primary);width:55.5%;height:var(--size-4)}.progress-bar.svelte-16nch4a.svelte-16nch4a{transform-origin:left;background-color:var(--loader-color);width:var(--size-full);height:var(--size-full)}.progress-level.svelte-16nch4a.svelte-16nch4a{display:flex;flex-direction:column;align-items:center;gap:1;z-index:var(--layer-2);width:var(--size-full)}.progress-level-inner.svelte-16nch4a.svelte-16nch4a{margin:var(--size-2) auto;color:var(--body-text-color);font-size:var(--text-sm);font-family:var(--font-mono)}.meta-text.svelte-16nch4a.svelte-16nch4a{position:absolute;top:0;right:0;z-index:var(--layer-2);padding:var(--size-1) var(--size-2);font-size:var(--text-sm);font-family:var(--font-mono)}.meta-text-center.svelte-16nch4a.svelte-16nch4a{display:flex;position:absolute;top:0;right:0;justify-content:center;align-items:center;transform:translateY(var(--size-6));z-index:var(--layer-2);padding:var(--size-1) var(--size-2);font-size:var(--text-sm);font-family:var(--font-mono);text-align:center}.error.svelte-16nch4a.svelte-16nch4a{box-shadow:var(--shadow-drop);border:solid 1px var(--error-border-color);border-radius:var(--radius-full);background:var(--error-background-fill);padding-right:var(--size-4);padding-left:var(--size-4);color:var(--error-text-color);font-weight:var(--weight-semibold);font-size:var(--text-lg);line-height:var(--line-lg);font-family:var(--font)}.minimal.svelte-16nch4a .progress-text.svelte-16nch4a{background:var(--block-background-fill)}.border.svelte-16nch4a.svelte-16nch4a{border:1px solid var(--border-color-primary)}.clear-status.svelte-16nch4a.svelte-16nch4a{position:absolute;display:flex;top:var(--size-2);right:var(--size-2);justify-content:flex-end;gap:var(--spacing-sm);z-index:var(--layer-1)}.wrap.svelte-cr2edf.svelte-cr2edf{overflow-y:auto;transition:opacity .5s ease-in-out;background:var(--block-background-fill);position:relative;display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:var(--size-40);width:var(--size-full)}.wrap.svelte-cr2edf.svelte-cr2edf:after{content:"";position:absolute;top:0;left:0;width:var(--upload-progress-width);height:100%;transition:all .5s ease-in-out;z-index:1}.uploading.svelte-cr2edf.svelte-cr2edf{font-size:var(--text-lg);font-family:var(--font);z-index:2}.file-name.svelte-cr2edf.svelte-cr2edf{margin:var(--spacing-md);font-size:var(--text-lg);color:var(--body-text-color-subdued)}.file.svelte-cr2edf.svelte-cr2edf{font-size:var(--text-md);z-index:2;display:flex;align-items:center}.file.svelte-cr2edf progress.svelte-cr2edf{display:inline;height:var(--size-1);width:100%;transition:all .5s ease-in-out;color:var(--color-accent);border:none}.file.svelte-cr2edf progress[value].svelte-cr2edf::-webkit-progress-value{background-color:var(--color-accent);border-radius:20px}.file.svelte-cr2edf progress[value].svelte-cr2edf::-webkit-progress-bar{background-color:var(--border-color-accent);border-radius:20px}.progress-bar.svelte-cr2edf.svelte-cr2edf{width:14px;height:14px;border-radius:50%;background:radial-gradient(closest-side,var(--block-background-fill) 64%,transparent 53% 100%),conic-gradient(var(--color-accent) var(--upload-progress-width),var(--border-color-accent) 0);transition:all .5s ease-in-out}button.svelte-1s26xmt{cursor:pointer;width:var(--size-full)}.hidden.svelte-1s26xmt{display:none;height:0!important;position:absolute;width:0;flex-grow:0}.center.svelte-1s26xmt{display:flex;justify-content:center}.flex.svelte-1s26xmt{display:flex;flex-direction:column;justify-content:center;align-items:center}.disable_click.svelte-1s26xmt{cursor:default}input.svelte-1s26xmt{display:none}div.svelte-1wj0ocy{display:flex;top:var(--size-2);right:var(--size-2);justify-content:flex-end;gap:var(--spacing-sm);z-index:var(--layer-1)}.not-absolute.svelte-1wj0ocy{margin:var(--size-1)}img.svelte-kxeri3{object-fit:cover}.image-container.svelte-n22rtv img,button.svelte-n22rtv{width:var(--size-full);height:var(--size-full);object-fit:contain;display:block;border-radius:var(--radius-lg)}.selectable.svelte-n22rtv{cursor:crosshair}.icon-buttons.svelte-n22rtv{display:flex;position:absolute;top:6px;right:6px;gap:var(--size-1)}button.svelte-fjcd9c{cursor:pointer;width:var(--size-full)}.wrap.svelte-fjcd9c{display:flex;flex-direction:column;justify-content:center;align-items:center;min-height:var(--size-60);color:var(--block-label-text-color);height:100%;padding-top:var(--size-3)}.icon-wrap.svelte-fjcd9c{width:30px;margin-bottom:var(--spacing-lg)}@media (--screen-md){.wrap.svelte-fjcd9c{font-size:var(--text-lg)}}.wrap.svelte-8hqvb6.svelte-8hqvb6{position:relative;width:var(--size-full);height:var(--size-full)}.hide.svelte-8hqvb6.svelte-8hqvb6{display:none}video.svelte-8hqvb6.svelte-8hqvb6{width:var(--size-full);height:var(--size-full);object-fit:cover}.button-wrap.svelte-8hqvb6.svelte-8hqvb6{position:absolute;background-color:var(--block-background-fill);border:1px solid var(--border-color-primary);padding:var(--size-1-5);display:flex;bottom:var(--size-2);left:50%;transform:translate(-50%);box-shadow:var(--shadow-drop-lg);border-radius:var(--radius-xl);line-height:var(--size-3);color:var(--button-secondary-text-color)}@media (--screen-md){button.svelte-8hqvb6.svelte-8hqvb6{bottom:var(--size-4)}}@media (--screen-xl){button.svelte-8hqvb6.svelte-8hqvb6{bottom:var(--size-8)}}.icon.svelte-8hqvb6.svelte-8hqvb6{opacity:.8;width:18px;height:18px;display:flex;justify-content:space-between;align-items:center}.red.svelte-8hqvb6.svelte-8hqvb6{fill:red;stroke:red}.flip.svelte-8hqvb6.svelte-8hqvb6{transform:scaleX(-1)}.select-wrap.svelte-8hqvb6.svelte-8hqvb6{-webkit-appearance:none;-moz-appearance:none;appearance:none;color:var(--button-secondary-text-color);background-color:transparent;width:95%;font-size:var(--text-md);position:absolute;bottom:var(--size-2);background-color:var(--block-background-fill);box-shadow:var(--shadow-drop-lg);border-radius:var(--radius-xl);z-index:var(--layer-top);border:1px solid var(--border-color-primary);text-align:left;line-height:var(--size-4);white-space:nowrap;text-overflow:ellipsis;left:50%;transform:translate(-50%);max-width:var(--size-52)}.select-wrap.svelte-8hqvb6>option.svelte-8hqvb6{padding:.25rem .5rem;border-bottom:1px solid var(--border-color-accent);padding-right:var(--size-8);text-overflow:ellipsis;overflow:hidden}.select-wrap.svelte-8hqvb6>option.svelte-8hqvb6:hover{background-color:var(--color-accent)}.select-wrap.svelte-8hqvb6>option.svelte-8hqvb6:last-child{border:none}.inset-icon.svelte-8hqvb6.svelte-8hqvb6{position:absolute;top:5px;right:-6.5px;width:var(--size-10);height:var(--size-5);opacity:.8}@media (--screen-md){.wrap.svelte-8hqvb6.svelte-8hqvb6{font-size:var(--text-lg)}}div.svelte-1g74h68{display:flex;position:absolute;top:var(--size-2);right:var(--size-2);justify-content:flex-end;gap:var(--spacing-sm);z-index:var(--layer-5)}.image-frame.svelte-xgcoa0 img{width:var(--size-full);height:var(--size-full);object-fit:cover}.image-frame.svelte-xgcoa0{object-fit:cover;width:100%;height:100%}.upload-container.svelte-xgcoa0{height:100%;flex-shrink:1;max-height:100%}.image-container.svelte-xgcoa0{display:flex;height:100%;flex-direction:column;justify-content:center;align-items:center;max-height:100%}.selectable.svelte-xgcoa0{cursor:crosshair}input.svelte-16l8u73{display:block;position:relative;background:var(--background-fill-primary);line-height:var(--line-sm)}svg.svelte-43sxxs.svelte-43sxxs{width:var(--size-20);height:var(--size-20)}svg.svelte-43sxxs path.svelte-43sxxs{fill:var(--loader-color)}div.svelte-43sxxs.svelte-43sxxs{z-index:var(--layer-2)}.margin.svelte-43sxxs.svelte-43sxxs{margin:var(--size-4)}.wrap.svelte-1yserjw.svelte-1yserjw{display:flex;flex-direction:column;justify-content:center;align-items:center;z-index:var(--layer-top);transition:opacity .1s ease-in-out;border-radius:var(--block-radius);background:var(--block-background-fill);padding:0 var(--size-6);max-height:var(--size-screen-h);overflow:hidden;pointer-events:none}.wrap.center.svelte-1yserjw.svelte-1yserjw{top:0;right:0;left:0}.wrap.default.svelte-1yserjw.svelte-1yserjw{top:0;right:0;bottom:0;left:0}.hide.svelte-1yserjw.svelte-1yserjw{opacity:0;pointer-events:none}.generating.svelte-1yserjw.svelte-1yserjw{animation:svelte-1yserjw-pulse 2s cubic-bezier(.4,0,.6,1) infinite;border:2px solid var(--color-accent);background:transparent;z-index:var(--layer-1)}.translucent.svelte-1yserjw.svelte-1yserjw{background:none}@keyframes svelte-1yserjw-pulse{0%,to{opacity:1}50%{opacity:.5}}.loading.svelte-1yserjw.svelte-1yserjw{z-index:var(--layer-2);color:var(--body-text-color)}.eta-bar.svelte-1yserjw.svelte-1yserjw{position:absolute;top:0;right:0;bottom:0;left:0;transform-origin:left;opacity:.8;z-index:var(--layer-1);transition:10ms;background:var(--background-fill-secondary)}.progress-bar-wrap.svelte-1yserjw.svelte-1yserjw{border:1px solid var(--border-color-primary);background:var(--background-fill-primary);width:55.5%;height:var(--size-4)}.progress-bar.svelte-1yserjw.svelte-1yserjw{transform-origin:left;background-color:var(--loader-color);width:var(--size-full);height:var(--size-full)}.progress-level.svelte-1yserjw.svelte-1yserjw{display:flex;flex-direction:column;align-items:center;gap:1;z-index:var(--layer-2);width:var(--size-full)}.progress-level-inner.svelte-1yserjw.svelte-1yserjw{margin:var(--size-2) auto;color:var(--body-text-color);font-size:var(--text-sm);font-family:var(--font-mono)}.meta-text.svelte-1yserjw.svelte-1yserjw{position:absolute;top:0;right:0;z-index:var(--layer-2);padding:var(--size-1) var(--size-2);font-size:var(--text-sm);font-family:var(--font-mono)}.meta-text-center.svelte-1yserjw.svelte-1yserjw{display:flex;position:absolute;top:0;right:0;justify-content:center;align-items:center;transform:translateY(var(--size-6));z-index:var(--layer-2);padding:var(--size-1) var(--size-2);font-size:var(--text-sm);font-family:var(--font-mono);text-align:center}.error.svelte-1yserjw.svelte-1yserjw{box-shadow:var(--shadow-drop);border:solid 1px var(--error-border-color);border-radius:var(--radius-full);background:var(--error-background-fill);padding-right:var(--size-4);padding-left:var(--size-4);color:var(--error-text-color);font-weight:var(--weight-semibold);font-size:var(--text-lg);line-height:var(--line-lg);font-family:var(--font)}.minimal.svelte-1yserjw .progress-text.svelte-1yserjw{background:var(--block-background-fill)}.border.svelte-1yserjw.svelte-1yserjw{border:1px solid var(--border-color-primary)}.toast-body.svelte-solcu7{display:flex;position:relative;right:0;left:0;align-items:center;margin:var(--size-6) var(--size-4);margin:auto;border-radius:var(--container-radius);overflow:hidden;pointer-events:auto}.toast-body.error.svelte-solcu7{border:1px solid var(--color-red-700);background:var(--color-red-50)}.dark .toast-body.error.svelte-solcu7{border:1px solid var(--color-red-500);background-color:var(--color-grey-950)}.toast-body.warning.svelte-solcu7{border:1px solid var(--color-yellow-700);background:var(--color-yellow-50)}.dark .toast-body.warning.svelte-solcu7{border:1px solid var(--color-yellow-500);background-color:var(--color-grey-950)}.toast-body.info.svelte-solcu7{border:1px solid var(--color-grey-700);background:var(--color-grey-50)}.dark .toast-body.info.svelte-solcu7{border:1px solid var(--color-grey-500);background-color:var(--color-grey-950)}.toast-title.svelte-solcu7{display:flex;align-items:center;font-weight:var(--weight-bold);font-size:var(--text-lg);line-height:var(--line-sm);text-transform:capitalize}.toast-title.error.svelte-solcu7{color:var(--color-red-700)}.dark .toast-title.error.svelte-solcu7{color:var(--color-red-50)}.toast-title.warning.svelte-solcu7{color:var(--color-yellow-700)}.dark .toast-title.warning.svelte-solcu7{color:var(--color-yellow-50)}.toast-title.info.svelte-solcu7{color:var(--color-grey-700)}.dark .toast-title.info.svelte-solcu7{color:var(--color-grey-50)}.toast-close.svelte-solcu7{margin:0 var(--size-3);border-radius:var(--size-3);padding:0px var(--size-1-5);font-size:var(--size-5);line-height:var(--size-5)}.toast-close.error.svelte-solcu7{color:var(--color-red-700)}.dark .toast-close.error.svelte-solcu7{color:var(--color-red-500)}.toast-close.warning.svelte-solcu7{color:var(--color-yellow-700)}.dark .toast-close.warning.svelte-solcu7{color:var(--color-yellow-500)}.toast-close.info.svelte-solcu7{color:var(--color-grey-700)}.dark .toast-close.info.svelte-solcu7{color:var(--color-grey-500)}.toast-text.svelte-solcu7{font-size:var(--text-lg)}.toast-text.error.svelte-solcu7{color:var(--color-red-700)}.dark .toast-text.error.svelte-solcu7{color:var(--color-red-50)}.toast-text.warning.svelte-solcu7{color:var(--color-yellow-700)}.dark .toast-text.warning.svelte-solcu7{color:var(--color-yellow-50)}.toast-text.info.svelte-solcu7{color:var(--color-grey-700)}.dark .toast-text.info.svelte-solcu7{color:var(--color-grey-50)}.toast-details.svelte-solcu7{margin:var(--size-3) var(--size-3) var(--size-3) 0;width:100%}.toast-icon.svelte-solcu7{display:flex;position:absolute;position:relative;flex-shrink:0;justify-content:center;align-items:center;margin:var(--size-2);border-radius:var(--radius-full);padding:var(--size-1);padding-left:calc(var(--size-1) - 1px);width:35px;height:35px}.toast-icon.error.svelte-solcu7{color:var(--color-red-700)}.dark .toast-icon.error.svelte-solcu7{color:var(--color-red-500)}.toast-icon.warning.svelte-solcu7{color:var(--color-yellow-700)}.dark .toast-icon.warning.svelte-solcu7{color:var(--color-yellow-500)}.toast-icon.info.svelte-solcu7{color:var(--color-grey-700)}.dark .toast-icon.info.svelte-solcu7{color:var(--color-grey-500)}@keyframes svelte-solcu7-countdown{0%{transform:scaleX(1)}to{transform:scaleX(0)}}.timer.svelte-solcu7{position:absolute;bottom:0;left:0;transform-origin:0 0;animation:svelte-solcu7-countdown 10s linear forwards;width:100%;height:var(--size-1)}.timer.error.svelte-solcu7{background:var(--color-red-700)}.dark .timer.error.svelte-solcu7{background:var(--color-red-500)}.timer.warning.svelte-solcu7{background:var(--color-yellow-700)}.dark .timer.warning.svelte-solcu7{background:var(--color-yellow-500)}.timer.info.svelte-solcu7{background:var(--color-grey-700)}.dark .timer.info.svelte-solcu7{background:var(--color-grey-500)}.toast-wrap.svelte-gatr8h{display:flex;position:fixed;top:var(--size-4);right:var(--size-4);flex-direction:column;align-items:end;gap:var(--size-2);z-index:var(--layer-top);width:calc(100% - var(--size-8))}@media (--screen-sm){.toast-wrap.svelte-gatr8h{width:calc(var(--size-96) + var(--size-10))}}div.svelte-1vvnm05{width:var(--size-10);height:var(--size-10)}.table.svelte-1vvnm05{margin:0 auto}button.svelte-8huxfn,a.svelte-8huxfn{display:inline-flex;justify-content:center;align-items:center;transition:var(--button-transition);box-shadow:var(--button-shadow);padding:var(--size-0-5) var(--size-2);text-align:center}button.svelte-8huxfn:hover,button[disabled].svelte-8huxfn,a.svelte-8huxfn:hover,a.disabled.svelte-8huxfn{box-shadow:var(--button-shadow-hover)}button.svelte-8huxfn:active,a.svelte-8huxfn:active{box-shadow:var(--button-shadow-active)}button[disabled].svelte-8huxfn,a.disabled.svelte-8huxfn{opacity:.5;filter:grayscale(30%);cursor:not-allowed}.hidden.svelte-8huxfn{display:none}.primary.svelte-8huxfn{border:var(--button-border-width) solid var(--button-primary-border-color);background:var(--button-primary-background-fill);color:var(--button-primary-text-color)}.primary.svelte-8huxfn:hover,.primary[disabled].svelte-8huxfn{border-color:var(--button-primary-border-color-hover);background:var(--button-primary-background-fill-hover);color:var(--button-primary-text-color-hover)}.secondary.svelte-8huxfn{border:var(--button-border-width) solid var(--button-secondary-border-color);background:var(--button-secondary-background-fill);color:var(--button-secondary-text-color)}.secondary.svelte-8huxfn:hover,.secondary[disabled].svelte-8huxfn{border-color:var(--button-secondary-border-color-hover);background:var(--button-secondary-background-fill-hover);color:var(--button-secondary-text-color-hover)}.stop.svelte-8huxfn{border:var(--button-border-width) solid var(--button-cancel-border-color);background:var(--button-cancel-background-fill);color:var(--button-cancel-text-color)}.stop.svelte-8huxfn:hover,.stop[disabled].svelte-8huxfn{border-color:var(--button-cancel-border-color-hover);background:var(--button-cancel-background-fill-hover);color:var(--button-cancel-text-color-hover)}.sm.svelte-8huxfn{border-radius:var(--button-small-radius);padding:var(--button-small-padding);font-weight:var(--button-small-text-weight);font-size:var(--button-small-text-size)}.lg.svelte-8huxfn{border-radius:var(--button-large-radius);padding:var(--button-large-padding);font-weight:var(--button-large-text-weight);font-size:var(--button-large-text-size)}.button-icon.svelte-8huxfn{width:var(--text-xl);height:var(--text-xl);margin-right:var(--spacing-xl)}.options.svelte-yuohum{--window-padding:var(--size-8);position:fixed;z-index:var(--layer-top);margin-left:0;box-shadow:var(--shadow-drop-lg);border-radius:var(--container-radius);background:var(--background-fill-primary);min-width:fit-content;max-width:inherit;overflow:auto;color:var(--body-text-color);list-style:none}.item.svelte-yuohum{display:flex;cursor:pointer;padding:var(--size-2)}.item.svelte-yuohum:hover,.active.svelte-yuohum{background:var(--background-fill-secondary)}.inner-item.svelte-yuohum{padding-right:var(--size-1)}.hide.svelte-yuohum{visibility:hidden}.icon-wrap.svelte-xtjjyg.svelte-xtjjyg.svelte-xtjjyg{color:var(--body-text-color);margin-right:var(--size-2);width:var(--size-5)}label.svelte-xtjjyg.svelte-xtjjyg.svelte-xtjjyg:not(.container),label.svelte-xtjjyg:not(.container) .wrap.svelte-xtjjyg.svelte-xtjjyg,label.svelte-xtjjyg:not(.container) .wrap-inner.svelte-xtjjyg.svelte-xtjjyg,label.svelte-xtjjyg:not(.container) .secondary-wrap.svelte-xtjjyg.svelte-xtjjyg,label.svelte-xtjjyg:not(.container) .token.svelte-xtjjyg.svelte-xtjjyg,label.svelte-xtjjyg:not(.container) input.svelte-xtjjyg.svelte-xtjjyg{height:100%}.container.svelte-xtjjyg .wrap.svelte-xtjjyg.svelte-xtjjyg{box-shadow:var(--input-shadow);border:var(--input-border-width) solid var(--border-color-primary)}.wrap.svelte-xtjjyg.svelte-xtjjyg.svelte-xtjjyg{position:relative;border-radius:var(--input-radius);background:var(--input-background-fill)}.wrap.svelte-xtjjyg.svelte-xtjjyg.svelte-xtjjyg:focus-within{box-shadow:var(--input-shadow-focus);border-color:var(--input-border-color-focus)}.wrap-inner.svelte-xtjjyg.svelte-xtjjyg.svelte-xtjjyg{display:flex;position:relative;flex-wrap:wrap;align-items:center;gap:var(--checkbox-label-gap);padding:var(--checkbox-label-padding)}.token.svelte-xtjjyg.svelte-xtjjyg.svelte-xtjjyg{display:flex;align-items:center;transition:var(--button-transition);cursor:pointer;box-shadow:var(--checkbox-label-shadow);border:var(--checkbox-label-border-width) solid var(--checkbox-label-border-color);border-radius:var(--button-small-radius);background:var(--checkbox-label-background-fill);padding:var(--checkbox-label-padding);color:var(--checkbox-label-text-color);font-weight:var(--checkbox-label-text-weight);font-size:var(--checkbox-label-text-size);line-height:var(--line-md)}.token.svelte-xtjjyg>.svelte-xtjjyg+.svelte-xtjjyg{margin-left:var(--size-2)}.token-remove.svelte-xtjjyg.svelte-xtjjyg.svelte-xtjjyg{fill:var(--body-text-color);display:flex;justify-content:center;align-items:center;cursor:pointer;border:var(--checkbox-border-width) solid var(--border-color-primary);border-radius:var(--radius-full);background:var(--background-fill-primary);padding:var(--size-0-5);width:16px;height:16px}.secondary-wrap.svelte-xtjjyg.svelte-xtjjyg.svelte-xtjjyg{display:flex;flex:1 1 0%;align-items:center;border:none;min-width:min-content}input.svelte-xtjjyg.svelte-xtjjyg.svelte-xtjjyg{margin:var(--spacing-sm);outline:none;border:none;background:inherit;width:var(--size-full);color:var(--body-text-color);font-size:var(--input-text-size)}input.svelte-xtjjyg.svelte-xtjjyg.svelte-xtjjyg:disabled{-webkit-text-fill-color:var(--body-text-color);-webkit-opacity:1;opacity:1;cursor:not-allowed}.remove-all.svelte-xtjjyg.svelte-xtjjyg.svelte-xtjjyg{margin-left:var(--size-1);width:20px;height:20px}.subdued.svelte-xtjjyg.svelte-xtjjyg.svelte-xtjjyg{color:var(--body-text-color-subdued)}input[readonly].svelte-xtjjyg.svelte-xtjjyg.svelte-xtjjyg{cursor:pointer}.icon-wrap.svelte-1m1zvyj.svelte-1m1zvyj{color:var(--body-text-color);margin-right:var(--size-2);width:var(--size-5)}.container.svelte-1m1zvyj.svelte-1m1zvyj{height:100%}.container.svelte-1m1zvyj .wrap.svelte-1m1zvyj{box-shadow:var(--input-shadow);border:var(--input-border-width) solid var(--border-color-primary)}.wrap.svelte-1m1zvyj.svelte-1m1zvyj{position:relative;border-radius:var(--input-radius);background:var(--input-background-fill)}.wrap.svelte-1m1zvyj.svelte-1m1zvyj:focus-within{box-shadow:var(--input-shadow-focus);border-color:var(--input-border-color-focus)}.wrap-inner.svelte-1m1zvyj.svelte-1m1zvyj{display:flex;position:relative;flex-wrap:wrap;align-items:center;gap:var(--checkbox-label-gap);padding:var(--checkbox-label-padding);height:100%}.secondary-wrap.svelte-1m1zvyj.svelte-1m1zvyj{display:flex;flex:1 1 0%;align-items:center;border:none;min-width:min-content;height:100%}input.svelte-1m1zvyj.svelte-1m1zvyj{margin:var(--spacing-sm);outline:none;border:none;background:inherit;width:var(--size-full);color:var(--body-text-color);font-size:var(--input-text-size);height:100%}input.svelte-1m1zvyj.svelte-1m1zvyj:disabled{-webkit-text-fill-color:var(--body-text-color);-webkit-opacity:1;opacity:1;cursor:not-allowed}.subdued.svelte-1m1zvyj.svelte-1m1zvyj{color:var(--body-text-color-subdued)}input[readonly].svelte-1m1zvyj.svelte-1m1zvyj{cursor:pointer}.gallery.svelte-1gecy8w{padding:var(--size-1) var(--size-2)}.modal.svelte-hkn2q1{position:fixed;left:0;top:0;width:100%;height:100%;z-index:var(--layer-top);-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}.modal-container.svelte-hkn2q1{border-style:solid;border-width:var(--block-border-width);margin-top:10%;padding:20px;box-shadow:var(--block-shadow);border-color:var(--block-border-color);border-radius:var(--block-radius);background:var(--block-background-fill);position:fixed;left:50%;transform:translate(-50%);width:fit-content}.model-content.svelte-hkn2q1{display:flex;align-items:flex-end}.canvas-annotator.svelte-1m8vz1h{border-color:var(--block-border-color);width:100%;height:100%;display:block;touch-action:none}.canvas-control.svelte-1m8vz1h{display:flex;align-items:center;justify-content:center;border-top:1px solid var(--border-color-primary);width:95%;bottom:0;left:0;right:0;margin-left:auto;margin-right:auto;margin-top:var(--size-2)}.icon.svelte-1m8vz1h{width:22px;height:22px;margin:var(--spacing-lg) var(--spacing-xs);padding:var(--spacing-xs);color:var(--neutral-400);border-radius:var(--radius-md)}.icon.svelte-1m8vz1h:hover,.icon.svelte-1m8vz1h:focus{color:var(--color-accent)}.selected.svelte-1m8vz1h{color:var(--color-accent)}.canvas-container.svelte-1m8vz1h{display:flex;justify-content:center;align-items:center}.canvas-container.svelte-1m8vz1h:focus{outline:none}.image-frame.svelte-1gjdske img{width:var(--size-full);height:var(--size-full);object-fit:cover}.image-frame.svelte-1gjdske{object-fit:cover;width:100%}.upload-container.svelte-1gjdske{height:100%;width:100%;flex-shrink:1;max-height:100%}.image-container.svelte-1gjdske{display:flex;height:100%;flex-direction:column;justify-content:center;align-items:center;max-height:100%}.selectable.svelte-1gjdske{cursor:crosshair}.icon-buttons.svelte-1gjdske{display:flex;position:absolute;top:6px;right:6px;gap:var(--size-1)}.container.svelte-1sgcyba img{width:100%;height:100%}.container.selected.svelte-1sgcyba{border-color:var(--border-color-accent)}.border.table.svelte-1sgcyba{border:2px solid var(--border-color-primary)}.container.table.svelte-1sgcyba{margin:0 auto;border-radius:var(--radius-lg);overflow:hidden;width:var(--size-20);height:var(--size-20);object-fit:cover}.container.gallery.svelte-1sgcyba{width:var(--size-20);max-width:var(--size-20);object-fit:cover}
 
1
+ .block.svelte-nl1om8{position:relative;margin:0;box-shadow:var(--block-shadow);border-width:var(--block-border-width);border-color:var(--block-border-color);border-radius:var(--block-radius);background:var(--block-background-fill);width:100%;line-height:var(--line-sm)}.block.border_focus.svelte-nl1om8{border-color:var(--color-accent)}.block.border_contrast.svelte-nl1om8{border-color:var(--body-text-color)}.padded.svelte-nl1om8{padding:var(--block-padding)}.hidden.svelte-nl1om8{display:none}.hide-container.svelte-nl1om8{margin:0;box-shadow:none;--block-border-width:0;background:transparent;padding:0;overflow:visible}div.svelte-1hnfib2{margin-bottom:var(--spacing-lg);color:var(--block-info-text-color);font-weight:var(--block-info-text-weight);font-size:var(--block-info-text-size);line-height:var(--line-sm)}span.has-info.svelte-22c38v{margin-bottom:var(--spacing-xs)}span.svelte-22c38v:not(.has-info){margin-bottom:var(--spacing-lg)}span.svelte-22c38v{display:inline-block;position:relative;z-index:var(--layer-4);border:solid var(--block-title-border-width) var(--block-title-border-color);border-radius:var(--block-title-radius);background:var(--block-title-background-fill);padding:var(--block-title-padding);color:var(--block-title-text-color);font-weight:var(--block-title-text-weight);font-size:var(--block-title-text-size);line-height:var(--line-sm)}.hide.svelte-22c38v{margin:0;height:0}label.svelte-9gxdi0{display:inline-flex;align-items:center;z-index:var(--layer-2);box-shadow:var(--block-label-shadow);border:var(--block-label-border-width) solid var(--border-color-primary);border-top:none;border-left:none;border-radius:var(--block-label-radius);background:var(--block-label-background-fill);padding:var(--block-label-padding);pointer-events:none;color:var(--block-label-text-color);font-weight:var(--block-label-text-weight);font-size:var(--block-label-text-size);line-height:var(--line-sm)}.gr-group label.svelte-9gxdi0{border-top-left-radius:0}label.float.svelte-9gxdi0{position:absolute;top:var(--block-label-margin);left:var(--block-label-margin)}label.svelte-9gxdi0:not(.float){position:static;margin-top:var(--block-label-margin);margin-left:var(--block-label-margin)}.hide.svelte-9gxdi0{height:0}span.svelte-9gxdi0{opacity:.8;margin-right:var(--size-2);width:calc(var(--block-label-text-size) - 1px);height:calc(var(--block-label-text-size) - 1px)}.hide-label.svelte-9gxdi0{box-shadow:none;border-width:0;background:transparent;overflow:visible}button.svelte-1lrphxw{display:flex;justify-content:center;align-items:center;gap:1px;z-index:var(--layer-2);border-radius:var(--radius-sm);color:var(--block-label-text-color);border:1px solid transparent}button[disabled].svelte-1lrphxw{opacity:.5;box-shadow:none}button[disabled].svelte-1lrphxw:hover{cursor:not-allowed}.padded.svelte-1lrphxw{padding:2px;background:var(--bg-color);box-shadow:var(--shadow-drop);border:1px solid var(--button-secondary-border-color)}button.svelte-1lrphxw:hover,button.highlight.svelte-1lrphxw{cursor:pointer;color:var(--color-accent)}.padded.svelte-1lrphxw:hover{border:2px solid var(--button-secondary-border-color-hover);padding:1px;color:var(--block-label-text-color)}span.svelte-1lrphxw{padding:0 1px;font-size:10px}div.svelte-1lrphxw{padding:2px;display:flex;align-items:flex-end}.small.svelte-1lrphxw{width:14px;height:14px}.medium.svelte-1lrphxw{width:20px;height:20px}.large.svelte-1lrphxw{width:22px;height:22px}.pending.svelte-1lrphxw{animation:svelte-1lrphxw-flash .5s infinite}@keyframes svelte-1lrphxw-flash{0%{opacity:.5}50%{opacity:1}to{opacity:.5}}.transparent.svelte-1lrphxw{background:transparent;border:none;box-shadow:none}.empty.svelte-3w3rth{display:flex;justify-content:center;align-items:center;margin-top:calc(0px - var(--size-6));height:var(--size-full)}.icon.svelte-3w3rth{opacity:.5;height:var(--size-5);color:var(--body-text-color)}.small.svelte-3w3rth{min-height:calc(var(--size-32) - 20px)}.large.svelte-3w3rth{min-height:calc(var(--size-64) - 20px)}.unpadded_box.svelte-3w3rth{margin-top:0}.small_parent.svelte-3w3rth{min-height:100%!important}.dropdown-arrow.svelte-145leq6{fill:currentColor}.wrap.svelte-kzcjhc{display:flex;flex-direction:column;justify-content:center;align-items:center;min-height:var(--size-60);color:var(--block-label-text-color);line-height:var(--line-md);height:100%;padding-top:var(--size-3)}.or.svelte-kzcjhc{color:var(--body-text-color-subdued);display:flex}.icon-wrap.svelte-kzcjhc{width:30px;margin-bottom:var(--spacing-lg)}@media (--screen-md){.wrap.svelte-kzcjhc{font-size:var(--text-lg)}}.hovered.svelte-kzcjhc{color:var(--color-accent)}div.svelte-q32hvf{border-top:1px solid transparent;display:flex;max-height:100%;justify-content:center;align-items:center;gap:var(--spacing-sm);height:auto;align-items:flex-end;color:var(--block-label-text-color);flex-shrink:0}.show_border.svelte-q32hvf{border-top:1px solid var(--block-border-color);margin-top:var(--spacing-xxl);box-shadow:var(--shadow-drop)}.source-selection.svelte-1jp3vgd{display:flex;align-items:center;justify-content:center;border-top:1px solid var(--border-color-primary);width:95%;bottom:0;left:0;right:0;margin-left:auto;margin-right:auto}.icon.svelte-1jp3vgd{width:22px;height:22px;margin:var(--spacing-lg) var(--spacing-xs);padding:var(--spacing-xs);color:var(--neutral-400);border-radius:var(--radius-md)}.selected.svelte-1jp3vgd{color:var(--color-accent)}.icon.svelte-1jp3vgd:hover,.icon.svelte-1jp3vgd:focus{color:var(--color-accent)}.wrap.svelte-16nch4a.svelte-16nch4a{display:flex;flex-direction:column;justify-content:center;align-items:center;z-index:var(--layer-2);transition:opacity .1s ease-in-out;border-radius:var(--block-radius);background:var(--block-background-fill);padding:0 var(--size-6);max-height:var(--size-screen-h);overflow:hidden}.wrap.center.svelte-16nch4a.svelte-16nch4a{top:0;right:0;left:0}.wrap.default.svelte-16nch4a.svelte-16nch4a{top:0;right:0;bottom:0;left:0}.hide.svelte-16nch4a.svelte-16nch4a{opacity:0;pointer-events:none}.generating.svelte-16nch4a.svelte-16nch4a{animation:svelte-16nch4a-pulseStart 1s cubic-bezier(.4,0,.6,1),svelte-16nch4a-pulse 2s cubic-bezier(.4,0,.6,1) 1s infinite;border:2px solid var(--color-accent);background:transparent;z-index:var(--layer-1);pointer-events:none}.translucent.svelte-16nch4a.svelte-16nch4a{background:none}@keyframes svelte-16nch4a-pulseStart{0%{opacity:0}to{opacity:1}}@keyframes svelte-16nch4a-pulse{0%,to{opacity:1}50%{opacity:.5}}.loading.svelte-16nch4a.svelte-16nch4a{z-index:var(--layer-2);color:var(--body-text-color)}.eta-bar.svelte-16nch4a.svelte-16nch4a{position:absolute;top:0;right:0;bottom:0;left:0;transform-origin:left;opacity:.8;z-index:var(--layer-1);transition:10ms;background:var(--background-fill-secondary)}.progress-bar-wrap.svelte-16nch4a.svelte-16nch4a{border:1px solid var(--border-color-primary);background:var(--background-fill-primary);width:55.5%;height:var(--size-4)}.progress-bar.svelte-16nch4a.svelte-16nch4a{transform-origin:left;background-color:var(--loader-color);width:var(--size-full);height:var(--size-full)}.progress-level.svelte-16nch4a.svelte-16nch4a{display:flex;flex-direction:column;align-items:center;gap:1;z-index:var(--layer-2);width:var(--size-full)}.progress-level-inner.svelte-16nch4a.svelte-16nch4a{margin:var(--size-2) auto;color:var(--body-text-color);font-size:var(--text-sm);font-family:var(--font-mono)}.meta-text.svelte-16nch4a.svelte-16nch4a{position:absolute;top:0;right:0;z-index:var(--layer-2);padding:var(--size-1) var(--size-2);font-size:var(--text-sm);font-family:var(--font-mono)}.meta-text-center.svelte-16nch4a.svelte-16nch4a{display:flex;position:absolute;top:0;right:0;justify-content:center;align-items:center;transform:translateY(var(--size-6));z-index:var(--layer-2);padding:var(--size-1) var(--size-2);font-size:var(--text-sm);font-family:var(--font-mono);text-align:center}.error.svelte-16nch4a.svelte-16nch4a{box-shadow:var(--shadow-drop);border:solid 1px var(--error-border-color);border-radius:var(--radius-full);background:var(--error-background-fill);padding-right:var(--size-4);padding-left:var(--size-4);color:var(--error-text-color);font-weight:var(--weight-semibold);font-size:var(--text-lg);line-height:var(--line-lg);font-family:var(--font)}.minimal.svelte-16nch4a .progress-text.svelte-16nch4a{background:var(--block-background-fill)}.border.svelte-16nch4a.svelte-16nch4a{border:1px solid var(--border-color-primary)}.clear-status.svelte-16nch4a.svelte-16nch4a{position:absolute;display:flex;top:var(--size-2);right:var(--size-2);justify-content:flex-end;gap:var(--spacing-sm);z-index:var(--layer-1)}.wrap.svelte-cr2edf.svelte-cr2edf{overflow-y:auto;transition:opacity .5s ease-in-out;background:var(--block-background-fill);position:relative;display:flex;flex-direction:column;align-items:center;justify-content:center;min-height:var(--size-40);width:var(--size-full)}.wrap.svelte-cr2edf.svelte-cr2edf:after{content:"";position:absolute;top:0;left:0;width:var(--upload-progress-width);height:100%;transition:all .5s ease-in-out;z-index:1}.uploading.svelte-cr2edf.svelte-cr2edf{font-size:var(--text-lg);font-family:var(--font);z-index:2}.file-name.svelte-cr2edf.svelte-cr2edf{margin:var(--spacing-md);font-size:var(--text-lg);color:var(--body-text-color-subdued)}.file.svelte-cr2edf.svelte-cr2edf{font-size:var(--text-md);z-index:2;display:flex;align-items:center}.file.svelte-cr2edf progress.svelte-cr2edf{display:inline;height:var(--size-1);width:100%;transition:all .5s ease-in-out;color:var(--color-accent);border:none}.file.svelte-cr2edf progress[value].svelte-cr2edf::-webkit-progress-value{background-color:var(--color-accent);border-radius:20px}.file.svelte-cr2edf progress[value].svelte-cr2edf::-webkit-progress-bar{background-color:var(--border-color-accent);border-radius:20px}.progress-bar.svelte-cr2edf.svelte-cr2edf{width:14px;height:14px;border-radius:50%;background:radial-gradient(closest-side,var(--block-background-fill) 64%,transparent 53% 100%),conic-gradient(var(--color-accent) var(--upload-progress-width),var(--border-color-accent) 0);transition:all .5s ease-in-out}button.svelte-1s26xmt{cursor:pointer;width:var(--size-full)}.hidden.svelte-1s26xmt{display:none;height:0!important;position:absolute;width:0;flex-grow:0}.center.svelte-1s26xmt{display:flex;justify-content:center}.flex.svelte-1s26xmt{display:flex;flex-direction:column;justify-content:center;align-items:center}.disable_click.svelte-1s26xmt{cursor:default}input.svelte-1s26xmt{display:none}div.svelte-1wj0ocy{display:flex;top:var(--size-2);right:var(--size-2);justify-content:flex-end;gap:var(--spacing-sm);z-index:var(--layer-1)}.not-absolute.svelte-1wj0ocy{margin:var(--size-1)}img.svelte-kxeri3{object-fit:cover}.image-container.svelte-n22rtv img,button.svelte-n22rtv{width:var(--size-full);height:var(--size-full);object-fit:contain;display:block;border-radius:var(--radius-lg)}.selectable.svelte-n22rtv{cursor:crosshair}.icon-buttons.svelte-n22rtv{display:flex;position:absolute;top:6px;right:6px;gap:var(--size-1)}button.svelte-fjcd9c{cursor:pointer;width:var(--size-full)}.wrap.svelte-fjcd9c{display:flex;flex-direction:column;justify-content:center;align-items:center;min-height:var(--size-60);color:var(--block-label-text-color);height:100%;padding-top:var(--size-3)}.icon-wrap.svelte-fjcd9c{width:30px;margin-bottom:var(--spacing-lg)}@media (--screen-md){.wrap.svelte-fjcd9c{font-size:var(--text-lg)}}.wrap.svelte-8hqvb6.svelte-8hqvb6{position:relative;width:var(--size-full);height:var(--size-full)}.hide.svelte-8hqvb6.svelte-8hqvb6{display:none}video.svelte-8hqvb6.svelte-8hqvb6{width:var(--size-full);height:var(--size-full);object-fit:cover}.button-wrap.svelte-8hqvb6.svelte-8hqvb6{position:absolute;background-color:var(--block-background-fill);border:1px solid var(--border-color-primary);padding:var(--size-1-5);display:flex;bottom:var(--size-2);left:50%;transform:translate(-50%);box-shadow:var(--shadow-drop-lg);border-radius:var(--radius-xl);line-height:var(--size-3);color:var(--button-secondary-text-color)}@media (--screen-md){button.svelte-8hqvb6.svelte-8hqvb6{bottom:var(--size-4)}}@media (--screen-xl){button.svelte-8hqvb6.svelte-8hqvb6{bottom:var(--size-8)}}.icon.svelte-8hqvb6.svelte-8hqvb6{opacity:.8;width:18px;height:18px;display:flex;justify-content:space-between;align-items:center}.red.svelte-8hqvb6.svelte-8hqvb6{fill:red;stroke:red}.flip.svelte-8hqvb6.svelte-8hqvb6{transform:scaleX(-1)}.select-wrap.svelte-8hqvb6.svelte-8hqvb6{-webkit-appearance:none;-moz-appearance:none;appearance:none;color:var(--button-secondary-text-color);background-color:transparent;width:95%;font-size:var(--text-md);position:absolute;bottom:var(--size-2);background-color:var(--block-background-fill);box-shadow:var(--shadow-drop-lg);border-radius:var(--radius-xl);z-index:var(--layer-top);border:1px solid var(--border-color-primary);text-align:left;line-height:var(--size-4);white-space:nowrap;text-overflow:ellipsis;left:50%;transform:translate(-50%);max-width:var(--size-52)}.select-wrap.svelte-8hqvb6>option.svelte-8hqvb6{padding:.25rem .5rem;border-bottom:1px solid var(--border-color-accent);padding-right:var(--size-8);text-overflow:ellipsis;overflow:hidden}.select-wrap.svelte-8hqvb6>option.svelte-8hqvb6:hover{background-color:var(--color-accent)}.select-wrap.svelte-8hqvb6>option.svelte-8hqvb6:last-child{border:none}.inset-icon.svelte-8hqvb6.svelte-8hqvb6{position:absolute;top:5px;right:-6.5px;width:var(--size-10);height:var(--size-5);opacity:.8}@media (--screen-md){.wrap.svelte-8hqvb6.svelte-8hqvb6{font-size:var(--text-lg)}}div.svelte-1g74h68{display:flex;position:absolute;top:var(--size-2);right:var(--size-2);justify-content:flex-end;gap:var(--spacing-sm);z-index:var(--layer-5)}.image-frame.svelte-xgcoa0 img{width:var(--size-full);height:var(--size-full);object-fit:cover}.image-frame.svelte-xgcoa0{object-fit:cover;width:100%;height:100%}.upload-container.svelte-xgcoa0{height:100%;flex-shrink:1;max-height:100%}.image-container.svelte-xgcoa0{display:flex;height:100%;flex-direction:column;justify-content:center;align-items:center;max-height:100%}.selectable.svelte-xgcoa0{cursor:crosshair}input.svelte-16l8u73{display:block;position:relative;background:var(--background-fill-primary);line-height:var(--line-sm)}svg.svelte-43sxxs.svelte-43sxxs{width:var(--size-20);height:var(--size-20)}svg.svelte-43sxxs path.svelte-43sxxs{fill:var(--loader-color)}div.svelte-43sxxs.svelte-43sxxs{z-index:var(--layer-2)}.margin.svelte-43sxxs.svelte-43sxxs{margin:var(--size-4)}.wrap.svelte-1yserjw.svelte-1yserjw{display:flex;flex-direction:column;justify-content:center;align-items:center;z-index:var(--layer-top);transition:opacity .1s ease-in-out;border-radius:var(--block-radius);background:var(--block-background-fill);padding:0 var(--size-6);max-height:var(--size-screen-h);overflow:hidden;pointer-events:none}.wrap.center.svelte-1yserjw.svelte-1yserjw{top:0;right:0;left:0}.wrap.default.svelte-1yserjw.svelte-1yserjw{top:0;right:0;bottom:0;left:0}.hide.svelte-1yserjw.svelte-1yserjw{opacity:0;pointer-events:none}.generating.svelte-1yserjw.svelte-1yserjw{animation:svelte-1yserjw-pulse 2s cubic-bezier(.4,0,.6,1) infinite;border:2px solid var(--color-accent);background:transparent;z-index:var(--layer-1)}.translucent.svelte-1yserjw.svelte-1yserjw{background:none}@keyframes svelte-1yserjw-pulse{0%,to{opacity:1}50%{opacity:.5}}.loading.svelte-1yserjw.svelte-1yserjw{z-index:var(--layer-2);color:var(--body-text-color)}.eta-bar.svelte-1yserjw.svelte-1yserjw{position:absolute;top:0;right:0;bottom:0;left:0;transform-origin:left;opacity:.8;z-index:var(--layer-1);transition:10ms;background:var(--background-fill-secondary)}.progress-bar-wrap.svelte-1yserjw.svelte-1yserjw{border:1px solid var(--border-color-primary);background:var(--background-fill-primary);width:55.5%;height:var(--size-4)}.progress-bar.svelte-1yserjw.svelte-1yserjw{transform-origin:left;background-color:var(--loader-color);width:var(--size-full);height:var(--size-full)}.progress-level.svelte-1yserjw.svelte-1yserjw{display:flex;flex-direction:column;align-items:center;gap:1;z-index:var(--layer-2);width:var(--size-full)}.progress-level-inner.svelte-1yserjw.svelte-1yserjw{margin:var(--size-2) auto;color:var(--body-text-color);font-size:var(--text-sm);font-family:var(--font-mono)}.meta-text.svelte-1yserjw.svelte-1yserjw{position:absolute;top:0;right:0;z-index:var(--layer-2);padding:var(--size-1) var(--size-2);font-size:var(--text-sm);font-family:var(--font-mono)}.meta-text-center.svelte-1yserjw.svelte-1yserjw{display:flex;position:absolute;top:0;right:0;justify-content:center;align-items:center;transform:translateY(var(--size-6));z-index:var(--layer-2);padding:var(--size-1) var(--size-2);font-size:var(--text-sm);font-family:var(--font-mono);text-align:center}.error.svelte-1yserjw.svelte-1yserjw{box-shadow:var(--shadow-drop);border:solid 1px var(--error-border-color);border-radius:var(--radius-full);background:var(--error-background-fill);padding-right:var(--size-4);padding-left:var(--size-4);color:var(--error-text-color);font-weight:var(--weight-semibold);font-size:var(--text-lg);line-height:var(--line-lg);font-family:var(--font)}.minimal.svelte-1yserjw .progress-text.svelte-1yserjw{background:var(--block-background-fill)}.border.svelte-1yserjw.svelte-1yserjw{border:1px solid var(--border-color-primary)}.toast-body.svelte-solcu7{display:flex;position:relative;right:0;left:0;align-items:center;margin:var(--size-6) var(--size-4);margin:auto;border-radius:var(--container-radius);overflow:hidden;pointer-events:auto}.toast-body.error.svelte-solcu7{border:1px solid var(--color-red-700);background:var(--color-red-50)}.dark .toast-body.error.svelte-solcu7{border:1px solid var(--color-red-500);background-color:var(--color-grey-950)}.toast-body.warning.svelte-solcu7{border:1px solid var(--color-yellow-700);background:var(--color-yellow-50)}.dark .toast-body.warning.svelte-solcu7{border:1px solid var(--color-yellow-500);background-color:var(--color-grey-950)}.toast-body.info.svelte-solcu7{border:1px solid var(--color-grey-700);background:var(--color-grey-50)}.dark .toast-body.info.svelte-solcu7{border:1px solid var(--color-grey-500);background-color:var(--color-grey-950)}.toast-title.svelte-solcu7{display:flex;align-items:center;font-weight:var(--weight-bold);font-size:var(--text-lg);line-height:var(--line-sm);text-transform:capitalize}.toast-title.error.svelte-solcu7{color:var(--color-red-700)}.dark .toast-title.error.svelte-solcu7{color:var(--color-red-50)}.toast-title.warning.svelte-solcu7{color:var(--color-yellow-700)}.dark .toast-title.warning.svelte-solcu7{color:var(--color-yellow-50)}.toast-title.info.svelte-solcu7{color:var(--color-grey-700)}.dark .toast-title.info.svelte-solcu7{color:var(--color-grey-50)}.toast-close.svelte-solcu7{margin:0 var(--size-3);border-radius:var(--size-3);padding:0px var(--size-1-5);font-size:var(--size-5);line-height:var(--size-5)}.toast-close.error.svelte-solcu7{color:var(--color-red-700)}.dark .toast-close.error.svelte-solcu7{color:var(--color-red-500)}.toast-close.warning.svelte-solcu7{color:var(--color-yellow-700)}.dark .toast-close.warning.svelte-solcu7{color:var(--color-yellow-500)}.toast-close.info.svelte-solcu7{color:var(--color-grey-700)}.dark .toast-close.info.svelte-solcu7{color:var(--color-grey-500)}.toast-text.svelte-solcu7{font-size:var(--text-lg)}.toast-text.error.svelte-solcu7{color:var(--color-red-700)}.dark .toast-text.error.svelte-solcu7{color:var(--color-red-50)}.toast-text.warning.svelte-solcu7{color:var(--color-yellow-700)}.dark .toast-text.warning.svelte-solcu7{color:var(--color-yellow-50)}.toast-text.info.svelte-solcu7{color:var(--color-grey-700)}.dark .toast-text.info.svelte-solcu7{color:var(--color-grey-50)}.toast-details.svelte-solcu7{margin:var(--size-3) var(--size-3) var(--size-3) 0;width:100%}.toast-icon.svelte-solcu7{display:flex;position:absolute;position:relative;flex-shrink:0;justify-content:center;align-items:center;margin:var(--size-2);border-radius:var(--radius-full);padding:var(--size-1);padding-left:calc(var(--size-1) - 1px);width:35px;height:35px}.toast-icon.error.svelte-solcu7{color:var(--color-red-700)}.dark .toast-icon.error.svelte-solcu7{color:var(--color-red-500)}.toast-icon.warning.svelte-solcu7{color:var(--color-yellow-700)}.dark .toast-icon.warning.svelte-solcu7{color:var(--color-yellow-500)}.toast-icon.info.svelte-solcu7{color:var(--color-grey-700)}.dark .toast-icon.info.svelte-solcu7{color:var(--color-grey-500)}@keyframes svelte-solcu7-countdown{0%{transform:scaleX(1)}to{transform:scaleX(0)}}.timer.svelte-solcu7{position:absolute;bottom:0;left:0;transform-origin:0 0;animation:svelte-solcu7-countdown 10s linear forwards;width:100%;height:var(--size-1)}.timer.error.svelte-solcu7{background:var(--color-red-700)}.dark .timer.error.svelte-solcu7{background:var(--color-red-500)}.timer.warning.svelte-solcu7{background:var(--color-yellow-700)}.dark .timer.warning.svelte-solcu7{background:var(--color-yellow-500)}.timer.info.svelte-solcu7{background:var(--color-grey-700)}.dark .timer.info.svelte-solcu7{background:var(--color-grey-500)}.toast-wrap.svelte-gatr8h{display:flex;position:fixed;top:var(--size-4);right:var(--size-4);flex-direction:column;align-items:end;gap:var(--size-2);z-index:var(--layer-top);width:calc(100% - var(--size-8))}@media (--screen-sm){.toast-wrap.svelte-gatr8h{width:calc(var(--size-96) + var(--size-10))}}div.svelte-1vvnm05{width:var(--size-10);height:var(--size-10)}.table.svelte-1vvnm05{margin:0 auto}button.svelte-8huxfn,a.svelte-8huxfn{display:inline-flex;justify-content:center;align-items:center;transition:var(--button-transition);box-shadow:var(--button-shadow);padding:var(--size-0-5) var(--size-2);text-align:center}button.svelte-8huxfn:hover,button[disabled].svelte-8huxfn,a.svelte-8huxfn:hover,a.disabled.svelte-8huxfn{box-shadow:var(--button-shadow-hover)}button.svelte-8huxfn:active,a.svelte-8huxfn:active{box-shadow:var(--button-shadow-active)}button[disabled].svelte-8huxfn,a.disabled.svelte-8huxfn{opacity:.5;filter:grayscale(30%);cursor:not-allowed}.hidden.svelte-8huxfn{display:none}.primary.svelte-8huxfn{border:var(--button-border-width) solid var(--button-primary-border-color);background:var(--button-primary-background-fill);color:var(--button-primary-text-color)}.primary.svelte-8huxfn:hover,.primary[disabled].svelte-8huxfn{border-color:var(--button-primary-border-color-hover);background:var(--button-primary-background-fill-hover);color:var(--button-primary-text-color-hover)}.secondary.svelte-8huxfn{border:var(--button-border-width) solid var(--button-secondary-border-color);background:var(--button-secondary-background-fill);color:var(--button-secondary-text-color)}.secondary.svelte-8huxfn:hover,.secondary[disabled].svelte-8huxfn{border-color:var(--button-secondary-border-color-hover);background:var(--button-secondary-background-fill-hover);color:var(--button-secondary-text-color-hover)}.stop.svelte-8huxfn{border:var(--button-border-width) solid var(--button-cancel-border-color);background:var(--button-cancel-background-fill);color:var(--button-cancel-text-color)}.stop.svelte-8huxfn:hover,.stop[disabled].svelte-8huxfn{border-color:var(--button-cancel-border-color-hover);background:var(--button-cancel-background-fill-hover);color:var(--button-cancel-text-color-hover)}.sm.svelte-8huxfn{border-radius:var(--button-small-radius);padding:var(--button-small-padding);font-weight:var(--button-small-text-weight);font-size:var(--button-small-text-size)}.lg.svelte-8huxfn{border-radius:var(--button-large-radius);padding:var(--button-large-padding);font-weight:var(--button-large-text-weight);font-size:var(--button-large-text-size)}.button-icon.svelte-8huxfn{width:var(--text-xl);height:var(--text-xl);margin-right:var(--spacing-xl)}.options.svelte-yuohum{--window-padding:var(--size-8);position:fixed;z-index:var(--layer-top);margin-left:0;box-shadow:var(--shadow-drop-lg);border-radius:var(--container-radius);background:var(--background-fill-primary);min-width:fit-content;max-width:inherit;overflow:auto;color:var(--body-text-color);list-style:none}.item.svelte-yuohum{display:flex;cursor:pointer;padding:var(--size-2)}.item.svelte-yuohum:hover,.active.svelte-yuohum{background:var(--background-fill-secondary)}.inner-item.svelte-yuohum{padding-right:var(--size-1)}.hide.svelte-yuohum{visibility:hidden}.icon-wrap.svelte-xtjjyg.svelte-xtjjyg.svelte-xtjjyg{color:var(--body-text-color);margin-right:var(--size-2);width:var(--size-5)}label.svelte-xtjjyg.svelte-xtjjyg.svelte-xtjjyg:not(.container),label.svelte-xtjjyg:not(.container) .wrap.svelte-xtjjyg.svelte-xtjjyg,label.svelte-xtjjyg:not(.container) .wrap-inner.svelte-xtjjyg.svelte-xtjjyg,label.svelte-xtjjyg:not(.container) .secondary-wrap.svelte-xtjjyg.svelte-xtjjyg,label.svelte-xtjjyg:not(.container) .token.svelte-xtjjyg.svelte-xtjjyg,label.svelte-xtjjyg:not(.container) input.svelte-xtjjyg.svelte-xtjjyg{height:100%}.container.svelte-xtjjyg .wrap.svelte-xtjjyg.svelte-xtjjyg{box-shadow:var(--input-shadow);border:var(--input-border-width) solid var(--border-color-primary)}.wrap.svelte-xtjjyg.svelte-xtjjyg.svelte-xtjjyg{position:relative;border-radius:var(--input-radius);background:var(--input-background-fill)}.wrap.svelte-xtjjyg.svelte-xtjjyg.svelte-xtjjyg:focus-within{box-shadow:var(--input-shadow-focus);border-color:var(--input-border-color-focus)}.wrap-inner.svelte-xtjjyg.svelte-xtjjyg.svelte-xtjjyg{display:flex;position:relative;flex-wrap:wrap;align-items:center;gap:var(--checkbox-label-gap);padding:var(--checkbox-label-padding)}.token.svelte-xtjjyg.svelte-xtjjyg.svelte-xtjjyg{display:flex;align-items:center;transition:var(--button-transition);cursor:pointer;box-shadow:var(--checkbox-label-shadow);border:var(--checkbox-label-border-width) solid var(--checkbox-label-border-color);border-radius:var(--button-small-radius);background:var(--checkbox-label-background-fill);padding:var(--checkbox-label-padding);color:var(--checkbox-label-text-color);font-weight:var(--checkbox-label-text-weight);font-size:var(--checkbox-label-text-size);line-height:var(--line-md)}.token.svelte-xtjjyg>.svelte-xtjjyg+.svelte-xtjjyg{margin-left:var(--size-2)}.token-remove.svelte-xtjjyg.svelte-xtjjyg.svelte-xtjjyg{fill:var(--body-text-color);display:flex;justify-content:center;align-items:center;cursor:pointer;border:var(--checkbox-border-width) solid var(--border-color-primary);border-radius:var(--radius-full);background:var(--background-fill-primary);padding:var(--size-0-5);width:16px;height:16px}.secondary-wrap.svelte-xtjjyg.svelte-xtjjyg.svelte-xtjjyg{display:flex;flex:1 1 0%;align-items:center;border:none;min-width:min-content}input.svelte-xtjjyg.svelte-xtjjyg.svelte-xtjjyg{margin:var(--spacing-sm);outline:none;border:none;background:inherit;width:var(--size-full);color:var(--body-text-color);font-size:var(--input-text-size)}input.svelte-xtjjyg.svelte-xtjjyg.svelte-xtjjyg:disabled{-webkit-text-fill-color:var(--body-text-color);-webkit-opacity:1;opacity:1;cursor:not-allowed}.remove-all.svelte-xtjjyg.svelte-xtjjyg.svelte-xtjjyg{margin-left:var(--size-1);width:20px;height:20px}.subdued.svelte-xtjjyg.svelte-xtjjyg.svelte-xtjjyg{color:var(--body-text-color-subdued)}input[readonly].svelte-xtjjyg.svelte-xtjjyg.svelte-xtjjyg{cursor:pointer}.icon-wrap.svelte-1m1zvyj.svelte-1m1zvyj{color:var(--body-text-color);margin-right:var(--size-2);width:var(--size-5)}.container.svelte-1m1zvyj.svelte-1m1zvyj{height:100%}.container.svelte-1m1zvyj .wrap.svelte-1m1zvyj{box-shadow:var(--input-shadow);border:var(--input-border-width) solid var(--border-color-primary)}.wrap.svelte-1m1zvyj.svelte-1m1zvyj{position:relative;border-radius:var(--input-radius);background:var(--input-background-fill)}.wrap.svelte-1m1zvyj.svelte-1m1zvyj:focus-within{box-shadow:var(--input-shadow-focus);border-color:var(--input-border-color-focus)}.wrap-inner.svelte-1m1zvyj.svelte-1m1zvyj{display:flex;position:relative;flex-wrap:wrap;align-items:center;gap:var(--checkbox-label-gap);padding:var(--checkbox-label-padding);height:100%}.secondary-wrap.svelte-1m1zvyj.svelte-1m1zvyj{display:flex;flex:1 1 0%;align-items:center;border:none;min-width:min-content;height:100%}input.svelte-1m1zvyj.svelte-1m1zvyj{margin:var(--spacing-sm);outline:none;border:none;background:inherit;width:var(--size-full);color:var(--body-text-color);font-size:var(--input-text-size);height:100%}input.svelte-1m1zvyj.svelte-1m1zvyj:disabled{-webkit-text-fill-color:var(--body-text-color);-webkit-opacity:1;opacity:1;cursor:not-allowed}.subdued.svelte-1m1zvyj.svelte-1m1zvyj{color:var(--body-text-color-subdued)}input[readonly].svelte-1m1zvyj.svelte-1m1zvyj{cursor:pointer}.gallery.svelte-1gecy8w{padding:var(--size-1) var(--size-2)}.modal.svelte-d9x7u0{position:fixed;left:0;top:0;width:100%;height:100%;z-index:var(--layer-top);-webkit-backdrop-filter:blur(4px);backdrop-filter:blur(4px)}.modal-container.svelte-d9x7u0{border-style:solid;border-width:var(--block-border-width);margin-top:10%;padding:20px;box-shadow:var(--block-shadow);border-color:var(--block-border-color);border-radius:var(--block-radius);background:var(--block-background-fill);position:fixed;left:50%;transform:translate(-50%);width:fit-content}.model-content.svelte-d9x7u0{display:flex;align-items:flex-end}.icon.svelte-d9x7u0{width:22px;height:22px;margin:var(--spacing-lg) var(--spacing-xs);padding:var(--spacing-xs);color:var(--neutral-400);border-radius:var(--radius-md)}.icon.svelte-d9x7u0:hover,.selected.svelte-d9x7u0{color:var(--color-accent)}.canvas-annotator.svelte-1m8vz1h{border-color:var(--block-border-color);width:100%;height:100%;display:block;touch-action:none}.canvas-control.svelte-1m8vz1h{display:flex;align-items:center;justify-content:center;border-top:1px solid var(--border-color-primary);width:95%;bottom:0;left:0;right:0;margin-left:auto;margin-right:auto;margin-top:var(--size-2)}.icon.svelte-1m8vz1h{width:22px;height:22px;margin:var(--spacing-lg) var(--spacing-xs);padding:var(--spacing-xs);color:var(--neutral-400);border-radius:var(--radius-md)}.icon.svelte-1m8vz1h:hover,.icon.svelte-1m8vz1h:focus{color:var(--color-accent)}.selected.svelte-1m8vz1h{color:var(--color-accent)}.canvas-container.svelte-1m8vz1h{display:flex;justify-content:center;align-items:center}.canvas-container.svelte-1m8vz1h:focus{outline:none}.image-frame.svelte-1gjdske img{width:var(--size-full);height:var(--size-full);object-fit:cover}.image-frame.svelte-1gjdske{object-fit:cover;width:100%}.upload-container.svelte-1gjdske{height:100%;width:100%;flex-shrink:1;max-height:100%}.image-container.svelte-1gjdske{display:flex;height:100%;flex-direction:column;justify-content:center;align-items:center;max-height:100%}.selectable.svelte-1gjdske{cursor:crosshair}.icon-buttons.svelte-1gjdske{display:flex;position:absolute;top:6px;right:6px;gap:var(--size-1)}.container.svelte-1sgcyba img{width:100%;height:100%}.container.selected.svelte-1sgcyba{border-color:var(--border-color-accent)}.border.table.svelte-1sgcyba{border:2px solid var(--border-color-primary)}.container.table.svelte-1sgcyba{margin:0 auto;border-radius:var(--radius-lg);overflow:hidden;width:var(--size-20);height:var(--size-20);object-fit:cover}.container.gallery.svelte-1sgcyba{width:var(--size-20);max-width:var(--size-20);object-fit:cover}
src/demo/app.py CHANGED
@@ -1,5 +1,6 @@
1
  import gradio as gr
2
  from gradio_image_annotation import image_annotator
 
3
 
4
 
5
  example_annotation = {
@@ -53,6 +54,8 @@ examples_crop = [
53
 
54
 
55
  def crop(annotations):
 
 
56
  if annotations["boxes"]:
57
  box = annotations["boxes"][0]
58
  return annotations["image"][
 
1
  import gradio as gr
2
  from gradio_image_annotation import image_annotator
3
+ import numpy as np
4
 
5
 
6
  example_annotation = {
 
54
 
55
 
56
  def crop(annotations):
57
+ if angle := annotations.get("orientation", None):
58
+ annotations["image"] = np.rot90(annotations["image"], k=-angle)
59
  if annotations["boxes"]:
60
  box = annotations["boxes"][0]
61
  return annotations["image"][
src/demo/space.py CHANGED
@@ -3,7 +3,7 @@ import gradio as gr
3
  from app import demo as app
4
  import os
5
 
6
- _docs = {'image_annotator': {'description': 'Creates a component to annotate images with bounding boxes. The bounding boxes can be created and edited by the user or be passed by code.\nIt is also possible to predefine a set of valid classes and colors.', 'members': {'__init__': {'value': {'type': 'dict | None', 'default': 'None', 'description': "A dict or None. The dictionary must contain a key 'image' with either an URL to an image, a numpy image or a PIL image. Optionally it may contain a key 'boxes' with a list of boxes. Each box must be a dict wit the keys: 'xmin', 'ymin', 'xmax' and 'ymax' with the absolute image coordinates of the box. Optionally can also include the keys 'label' and 'color' describing the label and color of the box. Color must be a tuple of RGB values (e.g. `(255,255,255)`)."}, 'boxes_alpha': {'type': 'float | None', 'default': 'None', 'description': 'Opacity of the bounding boxes 0 and 1.'}, 'label_list': {'type': 'list[str] | None', 'default': 'None', 'description': 'List of valid labels.'}, 'label_colors': {'type': 'list[str] | None', 'default': 'None', 'description': 'Optional list of colors for each label when `label_list` is used. Colors must be a tuple of RGB values (e.g. `(255,255,255)`).'}, 'box_min_size': {'type': 'int | None', 'default': 'None', 'description': 'Minimum valid bounding box size.'}, 'handle_size': {'type': 'int | None', 'default': 'None', 'description': 'Size of the bounding box resize handles.'}, 'box_thickness': {'type': 'int | None', 'default': 'None', 'description': 'Thickness of the bounding box outline.'}, 'box_selected_thickness': {'type': 'int | None', 'default': 'None', 'description': 'Thickness of the bounding box outline when it is selected.'}, 'disable_edit_boxes': {'type': 'bool | None', 'default': 'None', 'description': 'Disables the ability to set and edit the label and color of the boxes.'}, 'single_box': {'type': 'bool', 'default': 'False', 'description': 'If True, at most one box can be drawn.'}, 'height': {'type': 'int | str | None', 'default': 'None', 'description': 'The height of the displayed image, specified in pixels if a number is passed, or in CSS units if a string is passed.'}, 'width': {'type': 'int | str | None', 'default': 'None', 'description': 'The width of the displayed image, specified in pixels if a number is passed, or in CSS units if a string is passed.'}, 'image_mode': {'type': '"1"\n | "L"\n | "P"\n | "RGB"\n | "RGBA"\n | "CMYK"\n | "YCbCr"\n | "LAB"\n | "HSV"\n | "I"\n | "F"', 'default': '"RGB"', 'description': '"RGB" if color, or "L" if black and white. See https://pillow.readthedocs.io/en/stable/handbook/concepts.html for other supported image modes and their meaning.'}, 'sources': {'type': 'list["upload" | "webcam" | "clipboard"] | None', 'default': '["upload", "webcam", "clipboard"]', 'description': 'List of sources for the image. "upload" creates a box where user can drop an image file, "webcam" allows user to take snapshot from their webcam, "clipboard" allows users to paste an image from the clipboard. If None, defaults to ["upload", "webcam", "clipboard"].'}, 'image_type': {'type': '"numpy" | "pil" | "filepath"', 'default': '"numpy"', 'description': 'The format the image is converted before being passed into the prediction function. "numpy" converts the image to a numpy array with shape (height, width, 3) and values from 0 to 255, "pil" converts the image to a PIL image object, "filepath" passes a str path to a temporary file containing the image. If the image is SVG, the `type` is ignored and the filepath of the SVG is returned.'}, 'label': {'type': 'str | None', 'default': 'None', 'description': 'The label for this component. Appears above the component and is also used as the header if there are a table of examples for this component. If None and used in a `gr.Interface`, the label will be the name of the parameter this component is assigned to.'}, 'container': {'type': 'bool', 'default': 'True', 'description': 'If True, will place the component in a container - providing some extra padding around the border.'}, 'scale': {'type': 'int | None', 'default': 'None', 'description': 'relative size compared to adjacent Components. For example if Components A and B are in a Row, and A has scale=2, and B has scale=1, A will be twice as wide as B. Should be an integer. scale applies in Rows, and to top-level Components in Blocks where fill_height=True.'}, 'min_width': {'type': 'int', 'default': '160', 'description': 'minimum pixel width, will wrap if not sufficient screen space to satisfy this value. If a certain scale value results in this Component being narrower than min_width, the min_width parameter will be respected first.'}, 'interactive': {'type': 'bool | None', 'default': 'True', 'description': 'if True, will allow users to upload and annotate an image; if False, can only be used to display annotated images.'}, 'visible': {'type': 'bool', 'default': 'True', 'description': 'If False, component will be hidden.'}, 'elem_id': {'type': 'str | None', 'default': 'None', 'description': 'An optional string that is assigned as the id of this component in the HTML DOM. Can be used for targeting CSS styles.'}, 'elem_classes': {'type': 'list[str] | str | None', 'default': 'None', 'description': 'An optional list of strings that are assigned as the classes of this component in the HTML DOM. Can be used for targeting CSS styles.'}, 'render': {'type': 'bool', 'default': 'True', 'description': 'If False, component will not render be rendered in the Blocks context. Should be used if the intention is to assign event listeners now but render the component later.'}, 'show_label': {'type': 'bool | None', 'default': 'None', 'description': 'if True, will display label.'}, 'show_download_button': {'type': 'bool', 'default': 'True', 'description': 'If True, will show a button to download the image.'}, 'show_share_button': {'type': 'bool | None', 'default': 'None', 'description': 'If True, will show a share icon in the corner of the component that allows user to share outputs to Hugging Face Spaces Discussions. If False, icon does not appear. If set to None (default behavior), then the icon appears if this Gradio app is launched on Spaces, but not otherwise.'}, 'show_clear_button': {'type': 'bool | None', 'default': 'True', 'description': 'If True, will show a button to clear the current image.'}, 'show_remove_button': {'type': 'bool | None', 'default': 'None', 'description': 'If True, will show a button to remove the selected bounding box.'}, 'handles_cursor': {'type': 'bool | None', 'default': 'True', 'description': 'If True, the cursor will change when hovering over box handles in drag mode. Can be CPU-intensive.'}}, 'postprocess': {'value': {'type': 'dict | None', 'description': 'A dict with an image and an optional list of boxes or None.'}}, 'preprocess': {'return': {'type': 'dict | None', 'description': 'A dict with the image and boxes or None.'}, 'value': None}}, 'events': {'clear': {'type': None, 'default': None, 'description': 'This listener is triggered when the user clears the image_annotator using the clear button for the component.'}, 'change': {'type': None, 'default': None, 'description': 'Triggered when the value of the image_annotator changes either because of user input (e.g. a user types in a textbox) OR because of a function update (e.g. an image receives a value from the output of an event trigger). See `.input()` for a listener that is only triggered by user input.'}, 'upload': {'type': None, 'default': None, 'description': 'This listener is triggered when the user uploads a file into the image_annotator.'}}}, '__meta__': {'additional_interfaces': {}, 'user_fn_refs': {'image_annotator': []}}}
7
 
8
  abs_path = os.path.join(os.path.dirname(__file__), "css.css")
9
 
@@ -40,6 +40,7 @@ pip install gradio_image_annotation
40
  ```python
41
  import gradio as gr
42
  from gradio_image_annotation import image_annotator
 
43
 
44
 
45
  example_annotation = {
@@ -93,6 +94,8 @@ examples_crop = [
93
 
94
 
95
  def crop(annotations):
 
 
96
  if annotations["boxes"]:
97
  box = annotations["boxes"][0]
98
  return annotations["image"][
@@ -144,7 +147,7 @@ if __name__ == "__main__":
144
  ### Initialization
145
  """, elem_classes=["md-custom"], header_links=True)
146
 
147
- gr.ParamViewer(value=_docs["image_annotator"]["members"]["__init__"], linkify=[])
148
 
149
 
150
  gr.Markdown("### Events")
@@ -169,8 +172,8 @@ The code snippet below is accurate in cases where the component is used as both
169
 
170
  ```python
171
  def predict(
172
- value: dict | None
173
- ) -> dict | None:
174
  return value
175
  ```
176
  """, elem_classes=["md-custom", "image_annotator-user-fn"], header_links=True)
@@ -178,10 +181,20 @@ def predict(
178
 
179
 
180
 
 
 
 
 
 
 
 
 
 
181
  demo.load(None, js=r"""function() {
182
- const refs = {};
 
183
  const user_fn_refs = {
184
- image_annotator: [], };
185
  requestAnimationFrame(() => {
186
 
187
  Object.entries(user_fn_refs).forEach(([key, refs]) => {
 
3
  from app import demo as app
4
  import os
5
 
6
+ _docs = {'image_annotator': {'description': 'Creates a component to annotate images with bounding boxes. The bounding boxes can be created and edited by the user or be passed by code.\nIt is also possible to predefine a set of valid classes and colors.', 'members': {'__init__': {'value': {'type': 'dict | None', 'default': 'None', 'description': "A dict or None. The dictionary must contain a key 'image' with either an URL to an image, a numpy image or a PIL image. Optionally it may contain a key 'boxes' with a list of boxes. Each box must be a dict wit the keys: 'xmin', 'ymin', 'xmax' and 'ymax' with the absolute image coordinates of the box. Optionally can also include the keys 'label' and 'color' describing the label and color of the box. Color must be a tuple of RGB values (e.g. `(255,255,255)`). Optionally can also include the keys 'orientation' with a integer between 0 and 3, describing the number of times the image is rotated by 90 degrees in frontend, the rotation is clockwise."}, 'boxes_alpha': {'type': 'float | None', 'default': 'None', 'description': 'Opacity of the bounding boxes 0 and 1.'}, 'label_list': {'type': 'list[str] | None', 'default': 'None', 'description': 'List of valid labels.'}, 'label_colors': {'type': 'list[str] | None', 'default': 'None', 'description': 'Optional list of colors for each label when `label_list` is used. Colors must be a tuple of RGB values (e.g. `(255,255,255)`).'}, 'box_min_size': {'type': 'int | None', 'default': 'None', 'description': 'Minimum valid bounding box size.'}, 'handle_size': {'type': 'int | None', 'default': 'None', 'description': 'Size of the bounding box resize handles.'}, 'box_thickness': {'type': 'int | None', 'default': 'None', 'description': 'Thickness of the bounding box outline.'}, 'box_selected_thickness': {'type': 'int | None', 'default': 'None', 'description': 'Thickness of the bounding box outline when it is selected.'}, 'disable_edit_boxes': {'type': 'bool | None', 'default': 'None', 'description': 'Disables the ability to set and edit the label and color of the boxes.'}, 'single_box': {'type': 'bool', 'default': 'False', 'description': 'If True, at most one box can be drawn.'}, 'height': {'type': 'int | str | None', 'default': 'None', 'description': 'The height of the displayed image, specified in pixels if a number is passed, or in CSS units if a string is passed.'}, 'width': {'type': 'int | str | None', 'default': 'None', 'description': 'The width of the displayed image, specified in pixels if a number is passed, or in CSS units if a string is passed.'}, 'image_mode': {'type': '"1"\n | "L"\n | "P"\n | "RGB"\n | "RGBA"\n | "CMYK"\n | "YCbCr"\n | "LAB"\n | "HSV"\n | "I"\n | "F"', 'default': '"RGB"', 'description': '"RGB" if color, or "L" if black and white. See https://pillow.readthedocs.io/en/stable/handbook/concepts.html for other supported image modes and their meaning.'}, 'sources': {'type': 'list["upload" | "webcam" | "clipboard"] | None', 'default': '["upload", "webcam", "clipboard"]', 'description': 'List of sources for the image. "upload" creates a box where user can drop an image file, "webcam" allows user to take snapshot from their webcam, "clipboard" allows users to paste an image from the clipboard. If None, defaults to ["upload", "webcam", "clipboard"].'}, 'image_type': {'type': '"numpy" | "pil" | "filepath"', 'default': '"numpy"', 'description': 'The format the image is converted before being passed into the prediction function. "numpy" converts the image to a numpy array with shape (height, width, 3) and values from 0 to 255, "pil" converts the image to a PIL image object, "filepath" passes a str path to a temporary file containing the image. If the image is SVG, the `type` is ignored and the filepath of the SVG is returned.'}, 'label': {'type': 'str | None', 'default': 'None', 'description': 'The label for this component. Appears above the component and is also used as the header if there are a table of examples for this component. If None and used in a `gr.Interface`, the label will be the name of the parameter this component is assigned to.'}, 'container': {'type': 'bool', 'default': 'True', 'description': 'If True, will place the component in a container - providing some extra padding around the border.'}, 'scale': {'type': 'int | None', 'default': 'None', 'description': 'relative size compared to adjacent Components. For example if Components A and B are in a Row, and A has scale=2, and B has scale=1, A will be twice as wide as B. Should be an integer. scale applies in Rows, and to top-level Components in Blocks where fill_height=True.'}, 'min_width': {'type': 'int', 'default': '160', 'description': 'minimum pixel width, will wrap if not sufficient screen space to satisfy this value. If a certain scale value results in this Component being narrower than min_width, the min_width parameter will be respected first.'}, 'interactive': {'type': 'bool | None', 'default': 'True', 'description': 'if True, will allow users to upload and annotate an image; if False, can only be used to display annotated images.'}, 'visible': {'type': 'bool', 'default': 'True', 'description': 'If False, component will be hidden.'}, 'elem_id': {'type': 'str | None', 'default': 'None', 'description': 'An optional string that is assigned as the id of this component in the HTML DOM. Can be used for targeting CSS styles.'}, 'elem_classes': {'type': 'list[str] | str | None', 'default': 'None', 'description': 'An optional list of strings that are assigned as the classes of this component in the HTML DOM. Can be used for targeting CSS styles.'}, 'render': {'type': 'bool', 'default': 'True', 'description': 'If False, component will not render be rendered in the Blocks context. Should be used if the intention is to assign event listeners now but render the component later.'}, 'show_label': {'type': 'bool | None', 'default': 'None', 'description': 'if True, will display label.'}, 'show_download_button': {'type': 'bool', 'default': 'True', 'description': 'If True, will show a button to download the image.'}, 'show_share_button': {'type': 'bool | None', 'default': 'None', 'description': 'If True, will show a share icon in the corner of the component that allows user to share outputs to Hugging Face Spaces Discussions. If False, icon does not appear. If set to None (default behavior), then the icon appears if this Gradio app is launched on Spaces, but not otherwise.'}, 'show_clear_button': {'type': 'bool | None', 'default': 'True', 'description': 'If True, will show a button to clear the current image.'}, 'show_remove_button': {'type': 'bool | None', 'default': 'None', 'description': 'If True, will show a button to remove the selected bounding box.'}, 'handles_cursor': {'type': 'bool | None', 'default': 'True', 'description': 'If True, the cursor will change when hovering over box handles in drag mode. Can be CPU-intensive.'}, 'use_default_label': {'type': 'bool', 'default': 'False', 'description': 'If True, the first item in label_list will be used as the default label when creating boxes.'}}, 'postprocess': {'value': {'type': 'AnnotatedImageValue | None', 'description': 'A dict with an image and an optional list of boxes or None.'}}, 'preprocess': {'return': {'type': 'AnnotatedImageValue | None', 'description': 'A dict with the image and boxes or None.'}, 'value': None}}, 'events': {'clear': {'type': None, 'default': None, 'description': 'This listener is triggered when the user clears the image_annotator using the clear button for the component.'}, 'change': {'type': None, 'default': None, 'description': 'Triggered when the value of the image_annotator changes either because of user input (e.g. a user types in a textbox) OR because of a function update (e.g. an image receives a value from the output of an event trigger). See `.input()` for a listener that is only triggered by user input.'}, 'upload': {'type': None, 'default': None, 'description': 'This listener is triggered when the user uploads a file into the image_annotator.'}}}, '__meta__': {'additional_interfaces': {'AnnotatedImageValue': {'source': 'class AnnotatedImageValue(TypedDict):\n image: Optional[np.ndarray | PIL.Image.Image | str]\n boxes: Optional[List[dict]]\n orientation: Optional[int]'}}, 'user_fn_refs': {'image_annotator': ['AnnotatedImageValue']}}}
7
 
8
  abs_path = os.path.join(os.path.dirname(__file__), "css.css")
9
 
 
40
  ```python
41
  import gradio as gr
42
  from gradio_image_annotation import image_annotator
43
+ import numpy as np
44
 
45
 
46
  example_annotation = {
 
94
 
95
 
96
  def crop(annotations):
97
+ if angle := annotations.get("orientation", None):
98
+ annotations["image"] = np.rot90(annotations["image"], k=-angle)
99
  if annotations["boxes"]:
100
  box = annotations["boxes"][0]
101
  return annotations["image"][
 
147
  ### Initialization
148
  """, elem_classes=["md-custom"], header_links=True)
149
 
150
+ gr.ParamViewer(value=_docs["image_annotator"]["members"]["__init__"], linkify=['AnnotatedImageValue'])
151
 
152
 
153
  gr.Markdown("### Events")
 
172
 
173
  ```python
174
  def predict(
175
+ value: AnnotatedImageValue | None
176
+ ) -> AnnotatedImageValue | None:
177
  return value
178
  ```
179
  """, elem_classes=["md-custom", "image_annotator-user-fn"], header_links=True)
 
181
 
182
 
183
 
184
+ code_AnnotatedImageValue = gr.Markdown("""
185
+ ## `AnnotatedImageValue`
186
+ ```python
187
+ class AnnotatedImageValue(TypedDict):
188
+ image: Optional[np.ndarray | PIL.Image.Image | str]
189
+ boxes: Optional[List[dict]]
190
+ orientation: Optional[int]
191
+ ```""", elem_classes=["md-custom", "AnnotatedImageValue"], header_links=True)
192
+
193
  demo.load(None, js=r"""function() {
194
+ const refs = {
195
+ AnnotatedImageValue: [], };
196
  const user_fn_refs = {
197
+ image_annotator: ['AnnotatedImageValue'], };
198
  requestAnimationFrame(() => {
199
 
200
  Object.entries(user_fn_refs).forEach(([key, refs]) => {
src/frontend/Index.svelte CHANGED
@@ -49,6 +49,7 @@
49
  export let single_box: boolean;
50
  export let show_remove_button: boolean;
51
  export let handles_cursor: boolean;
 
52
 
53
  export let gradio: Gradio<{
54
  change: never;
@@ -127,6 +128,7 @@
127
  singleBox={single_box}
128
  showRemoveButton={show_remove_button}
129
  handlesCursor={handles_cursor}
 
130
  >
131
  {#if active_source === "upload"}
132
  <UploadText i18n={gradio.i18n} type="image" />
 
49
  export let single_box: boolean;
50
  export let show_remove_button: boolean;
51
  export let handles_cursor: boolean;
52
+ export let use_default_label: boolean;
53
 
54
  export let gradio: Gradio<{
55
  change: never;
 
128
  singleBox={single_box}
129
  showRemoveButton={show_remove_button}
130
  handlesCursor={handles_cursor}
131
+ useDefaultLabel={use_default_label}
132
  >
133
  {#if active_source === "upload"}
134
  <UploadText i18n={gradio.i18n} type="image" />
src/frontend/shared/AnnotatedImageData.ts CHANGED
@@ -4,4 +4,5 @@ import Box from "./Box";
4
  export default class AnnotatedImageData {
5
  image: FileData;
6
  boxes: Box[] = [];
 
7
  }
 
4
  export default class AnnotatedImageData {
5
  image: FileData;
6
  boxes: Box[] = [];
7
+ orientation: number = 0;
8
  }
src/frontend/shared/Box.ts CHANGED
@@ -1,3 +1,4 @@
 
1
  const clamp = (num: number, min: number, max: number) => Math.min(Math.max(num, min), max)
2
 
3
 
@@ -20,6 +21,10 @@ export default class Box {
20
  ymin: number;
21
  xmax: number;
22
  ymax: number;
 
 
 
 
23
  color: string;
24
  alpha: number;
25
  isDragging: boolean;
@@ -49,10 +54,12 @@ export default class Box {
49
  ymax: number;
50
  cursor: string;
51
  }[];
 
52
 
53
  constructor(
54
  renderCallBack: () => void,
55
  onFinishCreation: () => void,
 
56
  canvasXmin: number,
57
  canvasYmin: number,
58
  canvasXmax: number,
@@ -72,6 +79,7 @@ export default class Box {
72
  ) {
73
  this.renderCallBack = renderCallBack;
74
  this.onFinishCreation = onFinishCreation;
 
75
  this.canvasXmin = canvasXmin;
76
  this.canvasYmin = canvasYmin;
77
  this.canvasXmax = canvasXmax;
@@ -80,10 +88,14 @@ export default class Box {
80
  this.label = label;
81
  this.isDragging = false;
82
  this.isCreating = false;
83
- this.xmin = xmin;
84
- this.ymin = ymin;
85
- this.xmax = xmax;
86
- this.ymax = ymax;
 
 
 
 
87
  this.isResizing = false;
88
  this.isSelected = false;
89
  this.offsetMouseX = 0;
@@ -103,10 +115,10 @@ export default class Box {
103
  toJSON() {
104
  return {
105
  label: this.label,
106
- xmin: this.xmin,
107
- ymin: this.ymin,
108
- xmax: this.xmax,
109
- ymax: this.ymax,
110
  color: this.color,
111
  scaleFactor: this.scaleFactor,
112
  };
@@ -118,11 +130,12 @@ export default class Box {
118
 
119
  setScaleFactor(scaleFactor: number) {
120
  let scale = scaleFactor / this.scaleFactor;
121
- this.xmin = Math.round(this.xmin * scale);
122
- this.ymin = Math.round(this.ymin * scale);
123
- this.xmax = Math.round(this.xmax * scale);
124
- this.ymax = Math.round(this.ymax * scale);
125
- this.updateHandles();
 
126
  this.scaleFactor = scaleFactor;
127
  }
128
 
@@ -222,9 +235,25 @@ export default class Box {
222
  return [x, y];
223
  }
224
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
225
  render(ctx: CanvasRenderingContext2D): void {
226
  let xmin: number, ymin: number;
227
 
 
228
  // Render the box and border
229
  ctx.beginPath();
230
  [xmin, ymin] = this.toCanvasCoordinates(this.xmin, this.ymin);
@@ -277,8 +306,8 @@ export default class Box {
277
 
278
  startDrag(event: MouseEvent): void {
279
  this.isDragging = true;
280
- this.offsetMouseX = event.clientX - this.xmin;
281
- this.offsetMouseY = event.clientY - this.ymin;
282
  document.addEventListener("pointermove", this.handleDrag);
283
  document.addEventListener("pointerup", this.stopDrag);
284
  }
@@ -291,17 +320,20 @@ export default class Box {
291
 
292
  handleDrag = (event: MouseEvent): void => {
293
  if (this.isDragging) {
294
- let deltaX = event.clientX - this.offsetMouseX - this.xmin;
295
- let deltaY = event.clientY - this.offsetMouseY - this.ymin;
296
- const canvasW = this.canvasXmax - this.canvasXmin;
297
- const canvasH = this.canvasYmax - this.canvasYmin;
298
- deltaX = clamp(deltaX, -this.xmin, canvasW-this.xmax);
299
- deltaY = clamp(deltaY, -this.ymin, canvasH-this.ymax);
300
- this.xmin += deltaX;
301
- this.ymin += deltaY;
302
- this.xmax += deltaX;
303
- this.ymax += deltaY;
304
- this.updateHandles();
 
 
 
305
  this.renderCallBack();
306
  }
307
  };
@@ -344,46 +376,46 @@ export default class Box {
344
  handleCreating = (event: MouseEvent): void => {
345
  if (this.isCreating) {
346
  let [x, y] = this.toBoxCoordinates(event.clientX, event.clientY);
347
- x -= this.offsetMouseX;
348
- y -= this.offsetMouseY;
349
 
350
- if (x > this.xmax) {
351
  if (this.creatingAnchorX == "xmax") {
352
- this.xmin = this.xmax;
353
  }
354
- this.xmax = x;
355
  this.creatingAnchorX = "xmin";
356
- } else if (x > this.xmin && x < this.xmax && this.creatingAnchorX == "xmin") {
357
- this.xmax = x;
358
- } else if (x > this.xmin && x < this.xmax && this.creatingAnchorX == "xmax") {
359
- this.xmin = x;
360
- } else if (x < this.xmin) {
361
  if (this.creatingAnchorX == "xmin") {
362
- this.xmax = this.xmin;
363
  }
364
- this.xmin = x;
365
  this.creatingAnchorX = "xmax";
366
  }
367
 
368
- if (y > this.ymax) {
369
  if (this.creatingAnchorY == "ymax") {
370
- this.ymin = this.ymax;
371
  }
372
- this.ymax = y;
373
  this.creatingAnchorY = "ymin";
374
- } else if (y > this.ymin && y < this.ymax && this.creatingAnchorY == "ymin") {
375
- this.ymax = y;
376
- } else if (y > this.ymin && y < this.ymax && this.creatingAnchorY == "ymax") {
377
- this.ymin = y;
378
- } else if (y < this.ymin) {
379
  if (this.creatingAnchorY == "ymin") {
380
- this.ymax = this.ymin;
381
  }
382
- this.ymin = y;
383
  this.creatingAnchorY = "ymax";
384
  }
385
-
386
- this.updateHandles();
387
  this.renderCallBack();
388
  }
389
  }
@@ -394,44 +426,45 @@ export default class Box {
394
  document.removeEventListener("pointerup", this.stopCreating);
395
 
396
  if (this.getArea() > 0) {
397
- const canvasW = this.canvasXmax - this.canvasXmin;
398
- const canvasH = this.canvasYmax - this.canvasYmin;
399
- this.xmin = clamp(this.xmin, 0, canvasW - this.minSize);
400
- this.ymin = clamp(this.ymin, 0, canvasH - this.minSize);
401
- this.xmax = clamp(this.xmax, this.minSize, canvasW);
402
- this.ymax = clamp(this.ymax, this.minSize, canvasH);
403
 
404
  if (this.minSize > 0) {
405
- if (this.getWidth() < this.minSize) {
406
  if (this.creatingAnchorX == "xmin") {
407
- this.xmax = this.xmin + this.minSize;
408
  } else {
409
- this.xmin = this.xmax - this.minSize;
410
  }
411
  }
412
- if (this.getHeight() < this.minSize) {
413
  if (this.creatingAnchorY == "ymin") {
414
- this.ymax = this.ymin + this.minSize;
415
  } else {
416
- this.ymin = this.ymax - this.minSize;
417
  }
418
  }
419
- if (this.xmax > canvasW) {
420
- this.xmin -= this.xmax - canvasW;
421
- this.xmax = canvasW;
422
- } else if (this.xmin < 0) {
423
- this.xmax -= this.xmin;
424
- this.xmin = 0;
425
  }
426
- if (this.ymax > canvasH) {
427
- this.ymin -= this.ymax - canvasH;
428
- this.ymax = canvasH;
429
- } else if (this.ymin < 0) {
430
- this.ymax -= this.ymin;
431
- this.ymin = 0;
432
  }
433
  }
434
- this.updateHandles();
 
435
  this.renderCallBack();
436
  }
437
  this.onFinishCreation();
@@ -450,54 +483,56 @@ export default class Box {
450
  if (this.isResizing) {
451
  const mouseX = event.clientX;
452
  const mouseY = event.clientY;
453
- const deltaX = mouseX - this.resizeHandles[this.resizingHandleIndex].xmin - this.offsetMouseX;
454
- const deltaY = mouseY - this.resizeHandles[this.resizingHandleIndex].ymin - this.offsetMouseY;
455
- const canvasW = this.canvasXmax - this.canvasXmin;
456
- const canvasH = this.canvasYmax - this.canvasYmin;
457
  switch (this.resizingHandleIndex) {
458
  case 0: // Top-left handle
459
- this.xmin += deltaX;
460
- this.ymin += deltaY;
461
- this.xmin = clamp(this.xmin, 0, this.xmax - this.minSize);
462
- this.ymin = clamp(this.ymin, 0, this.ymax - this.minSize);
463
  break;
464
  case 1: // Top-right handle
465
- this.xmax += deltaX;
466
- this.ymin += deltaY;
467
- this.xmax = clamp(this.xmax, this.xmin + this.minSize, canvasW);
468
- this.ymin = clamp(this.ymin, 0, this.ymax - this.minSize);
469
  break;
470
  case 2: // Bottom-right handle
471
- this.xmax += deltaX;
472
- this.ymax += deltaY;
473
- this.xmax = clamp(this.xmax, this.xmin + this.minSize, canvasW);
474
- this.ymax = clamp(this.ymax, this.ymin + this.minSize, canvasH);
475
  break;
476
  case 3: // Bottom-left handle
477
- this.xmin += deltaX;
478
- this.ymax += deltaY;
479
- this.xmin = clamp(this.xmin, 0, this.xmax - this.minSize);
480
- this.ymax = clamp(this.ymax, this.ymin + this.minSize, canvasH);
481
  break;
482
  case 4: // Top center handle
483
- this.ymin += deltaY;
484
- this.ymin = clamp(this.ymin, 0, this.ymax - this.minSize);
485
  break;
486
  case 5: // Right center handle
487
- this.xmax += deltaX;
488
- this.xmax = clamp(this.xmax, this.xmin + this.minSize, canvasW);
489
  break;
490
  case 6: // Bottom center handle
491
- this.ymax += deltaY;
492
- this.ymax = clamp(this.ymax, this.ymin + this.minSize, canvasH);
493
  break;
494
  case 7: // Left center handle
495
- this.xmin += deltaX;
496
- this.xmin = clamp(this.xmin, 0, this.xmax - this.minSize);
497
  break;
498
  }
 
499
  // Update the resize handles
500
- this.updateHandles();
 
501
  this.renderCallBack();
502
  }
503
  };
@@ -507,4 +542,23 @@ export default class Box {
507
  document.removeEventListener("pointermove", this.handleResize);
508
  document.removeEventListener("pointerup", this.stopResize);
509
  };
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
510
  }
 
1
+ import WindowViewer from "./WindowViewer";
2
  const clamp = (num: number, min: number, max: number) => Math.min(Math.max(num, min), max)
3
 
4
 
 
21
  ymin: number;
22
  xmax: number;
23
  ymax: number;
24
+ _xmin: number;
25
+ _ymin: number;
26
+ _xmax: number;
27
+ _ymax: number;
28
  color: string;
29
  alpha: number;
30
  isDragging: boolean;
 
54
  ymax: number;
55
  cursor: string;
56
  }[];
57
+ canvasWindow: WindowViewer;
58
 
59
  constructor(
60
  renderCallBack: () => void,
61
  onFinishCreation: () => void,
62
+ canvasWindow: WindowViewer,
63
  canvasXmin: number,
64
  canvasYmin: number,
65
  canvasXmax: number,
 
79
  ) {
80
  this.renderCallBack = renderCallBack;
81
  this.onFinishCreation = onFinishCreation;
82
+ this.canvasWindow = canvasWindow;
83
  this.canvasXmin = canvasXmin;
84
  this.canvasYmin = canvasYmin;
85
  this.canvasXmax = canvasXmax;
 
88
  this.label = label;
89
  this.isDragging = false;
90
  this.isCreating = false;
91
+ this._xmin = xmin;
92
+ this._ymin = ymin;
93
+ this._xmax = xmax;
94
+ this._ymax = ymax;
95
+ this.xmin = this._xmin * this.canvasWindow.scale;
96
+ this.ymin = this._ymin * this.canvasWindow.scale;
97
+ this.xmax = this._xmax * this.canvasWindow.scale;
98
+ this.ymax = this._ymax * this.canvasWindow.scale;
99
  this.isResizing = false;
100
  this.isSelected = false;
101
  this.offsetMouseX = 0;
 
115
  toJSON() {
116
  return {
117
  label: this.label,
118
+ xmin: this._xmin,
119
+ ymin: this._ymin,
120
+ xmax: this._xmax,
121
+ ymax: this._ymax,
122
  color: this.color,
123
  scaleFactor: this.scaleFactor,
124
  };
 
130
 
131
  setScaleFactor(scaleFactor: number) {
132
  let scale = scaleFactor / this.scaleFactor;
133
+ this._xmin = Math.round(this._xmin * scale);
134
+ this._ymin = Math.round(this._ymin * scale);
135
+ this._xmax = Math.round(this._xmax * scale);
136
+ this._ymax = Math.round(this._ymax * scale);
137
+ this.applyUserScale();
138
+ // this.updateHandles();
139
  this.scaleFactor = scaleFactor;
140
  }
141
 
 
235
  return [x, y];
236
  }
237
 
238
+ applyUserScale(): void {
239
+ this.xmin = this._xmin * this.canvasWindow.scale;
240
+ this.ymin = this._ymin * this.canvasWindow.scale;
241
+ this.xmax = this._xmax * this.canvasWindow.scale;
242
+ this.ymax = this._ymax * this.canvasWindow.scale;
243
+ this.updateHandles();
244
+ }
245
+
246
+ updateOffset(): void {
247
+ this.canvasXmin = this.canvasWindow.offsetX;
248
+ this.canvasYmin = this.canvasWindow.offsetY;
249
+ this.canvasXmax = this.canvasWindow.offsetX + this.canvasWindow.imageWidth * this.canvasWindow.scale;
250
+ this.canvasYmax = this.canvasWindow.offsetY + this.canvasWindow.imageHeight * this.canvasWindow.scale;
251
+ this.applyUserScale();
252
+ }
253
  render(ctx: CanvasRenderingContext2D): void {
254
  let xmin: number, ymin: number;
255
 
256
+ this.updateOffset()
257
  // Render the box and border
258
  ctx.beginPath();
259
  [xmin, ymin] = this.toCanvasCoordinates(this.xmin, this.ymin);
 
306
 
307
  startDrag(event: MouseEvent): void {
308
  this.isDragging = true;
309
+ this.offsetMouseX = event.clientX - this._xmin * this.canvasWindow.scale;
310
+ this.offsetMouseY = event.clientY - this._ymin * this.canvasWindow.scale;
311
  document.addEventListener("pointermove", this.handleDrag);
312
  document.addEventListener("pointerup", this.stopDrag);
313
  }
 
320
 
321
  handleDrag = (event: MouseEvent): void => {
322
  if (this.isDragging) {
323
+ let deltaX = (event.clientX - this.offsetMouseX) / this.canvasWindow.scale - this._xmin;
324
+ let deltaY = (event.clientY - this.offsetMouseY) / this.canvasWindow.scale - this._ymin;
325
+
326
+ const canvasW = (this.canvasXmax - this.canvasXmin) / this.canvasWindow.scale;
327
+ const canvasH = (this.canvasYmax - this.canvasYmin) / this.canvasWindow.scale;
328
+ deltaX = clamp(deltaX, -this._xmin, canvasW-this._xmax);
329
+ deltaY = clamp(deltaY, -this._ymin, canvasH-this._ymax);
330
+ this._xmin += deltaX;
331
+ this._ymin += deltaY;
332
+ this._xmax += deltaX;
333
+ this._ymax += deltaY;
334
+
335
+ this.applyUserScale();
336
+ // this.updateHandles();
337
  this.renderCallBack();
338
  }
339
  };
 
376
  handleCreating = (event: MouseEvent): void => {
377
  if (this.isCreating) {
378
  let [x, y] = this.toBoxCoordinates(event.clientX, event.clientY);
379
+ x = (x - this.offsetMouseX) / this.canvasWindow.scale;
380
+ y = (y - this.offsetMouseY) / this.canvasWindow.scale;
381
 
382
+ if (x > this._xmax) {
383
  if (this.creatingAnchorX == "xmax") {
384
+ this._xmin = this._xmax;
385
  }
386
+ this._xmax = x;
387
  this.creatingAnchorX = "xmin";
388
+ } else if (x > this._xmin && x < this._xmax && this.creatingAnchorX == "xmin") {
389
+ this._xmax = x;
390
+ } else if (x > this._xmin && x < this._xmax && this.creatingAnchorX == "xmax") {
391
+ this._xmin = x;
392
+ } else if (x < this._xmin) {
393
  if (this.creatingAnchorX == "xmin") {
394
+ this._xmax = this._xmin;
395
  }
396
+ this._xmin = x;
397
  this.creatingAnchorX = "xmax";
398
  }
399
 
400
+ if (y > this._ymax) {
401
  if (this.creatingAnchorY == "ymax") {
402
+ this._ymin = this._ymax;
403
  }
404
+ this._ymax = y;
405
  this.creatingAnchorY = "ymin";
406
+ } else if (y > this._ymin && y < this._ymax && this.creatingAnchorY == "ymin") {
407
+ this._ymax = y;
408
+ } else if (y > this._ymin && y < this._ymax && this.creatingAnchorY == "ymax") {
409
+ this._ymin = y;
410
+ } else if (y < this._ymin) {
411
  if (this.creatingAnchorY == "ymin") {
412
+ this._ymax = this._ymin;
413
  }
414
+ this._ymin = y;
415
  this.creatingAnchorY = "ymax";
416
  }
417
+ this.applyUserScale();
418
+ // this.updateHandles();
419
  this.renderCallBack();
420
  }
421
  }
 
426
  document.removeEventListener("pointerup", this.stopCreating);
427
 
428
  if (this.getArea() > 0) {
429
+ const canvasW = (this.canvasXmax - this.canvasXmin) / this.canvasWindow.scale;
430
+ const canvasH = (this.canvasYmax - this.canvasYmin) / this.canvasWindow.scale;
431
+ this._xmin = clamp(this._xmin, 0, canvasW - this.minSize);
432
+ this._ymin = clamp(this._ymin, 0, canvasH - this.minSize);
433
+ this._xmax = clamp(this._xmax, this.minSize, canvasW);
434
+ this._ymax = clamp(this._ymax, this.minSize, canvasH);
435
 
436
  if (this.minSize > 0) {
437
+ if (this.getWidth() / this.canvasWindow.scale < this.minSize) {
438
  if (this.creatingAnchorX == "xmin") {
439
+ this._xmax = this._xmin + this.minSize;
440
  } else {
441
+ this._xmin = this._xmax - this.minSize;
442
  }
443
  }
444
+ if (this.getHeight() / this.canvasWindow.scale < this.minSize) {
445
  if (this.creatingAnchorY == "ymin") {
446
+ this._ymax = this._ymin + this.minSize;
447
  } else {
448
+ this._ymin = this._ymax - this.minSize;
449
  }
450
  }
451
+ if (this._xmax > canvasW) {
452
+ this._xmin -= this._xmax - canvasW;
453
+ this._xmax = canvasW;
454
+ } else if (this._xmin < 0) {
455
+ this._xmax -= this._xmin;
456
+ this._xmin = 0;
457
  }
458
+ if (this._ymax > canvasH) {
459
+ this._ymin -= this._ymax - canvasH;
460
+ this._ymax = canvasH;
461
+ } else if (this._ymin < 0) {
462
+ this._ymax -= this._ymin;
463
+ this._ymin = 0;
464
  }
465
  }
466
+ this.applyUserScale();
467
+ // this.updateHandles();
468
  this.renderCallBack();
469
  }
470
  this.onFinishCreation();
 
483
  if (this.isResizing) {
484
  const mouseX = event.clientX;
485
  const mouseY = event.clientY;
486
+ const deltaX = (mouseX - this.offsetMouseX - this.resizeHandles[this.resizingHandleIndex].xmin) / this.canvasWindow.scale;
487
+ const deltaY = (mouseY - this.offsetMouseY - this.resizeHandles[this.resizingHandleIndex].ymin) / this.canvasWindow.scale;
488
+ const canvasW = (this.canvasXmax - this.canvasXmin) / this.canvasWindow.scale;
489
+ const canvasH = (this.canvasYmax - this.canvasYmin) / this.canvasWindow.scale;
490
  switch (this.resizingHandleIndex) {
491
  case 0: // Top-left handle
492
+ this._xmin += deltaX;
493
+ this._ymin += deltaY;
494
+ this._xmin = clamp(this._xmin, 0, this._xmax - this.minSize);
495
+ this._ymin = clamp(this._ymin, 0, this._ymax - this.minSize);
496
  break;
497
  case 1: // Top-right handle
498
+ this._xmax += deltaX;
499
+ this._ymin += deltaY;
500
+ this._xmax = clamp(this._xmax, this._xmin + this.minSize, canvasW);
501
+ this._ymin = clamp(this._ymin, 0, this._ymax - this.minSize);
502
  break;
503
  case 2: // Bottom-right handle
504
+ this._xmax += deltaX;
505
+ this._ymax += deltaY;
506
+ this._xmax = clamp(this._xmax, this._xmin + this.minSize, canvasW);
507
+ this._ymax = clamp(this._ymax, this._ymin + this.minSize, canvasH);
508
  break;
509
  case 3: // Bottom-left handle
510
+ this._xmin += deltaX;
511
+ this._ymax += deltaY;
512
+ this._xmin = clamp(this._xmin, 0, this._xmax - this.minSize);
513
+ this._ymax = clamp(this._ymax, this._ymin + this.minSize, canvasH);
514
  break;
515
  case 4: // Top center handle
516
+ this._ymin += deltaY;
517
+ this._ymin = clamp(this._ymin, 0, this._ymax - this.minSize);
518
  break;
519
  case 5: // Right center handle
520
+ this._xmax += deltaX;
521
+ this._xmax = clamp(this._xmax, this._xmin + this.minSize, canvasW);
522
  break;
523
  case 6: // Bottom center handle
524
+ this._ymax += deltaY;
525
+ this._ymax = clamp(this._ymax, this._ymin + this.minSize, canvasH);
526
  break;
527
  case 7: // Left center handle
528
+ this._xmin += deltaX;
529
+ this._xmin = clamp(this._xmin, 0, this._xmax - this.minSize);
530
  break;
531
  }
532
+
533
  // Update the resize handles
534
+ this.applyUserScale();
535
+ // this.updateHandles();
536
  this.renderCallBack();
537
  }
538
  };
 
542
  document.removeEventListener("pointermove", this.handleResize);
543
  document.removeEventListener("pointerup", this.stopResize);
544
  };
545
+
546
+ onRotate(op: number): void {
547
+ const [_xmin, _xmax, _ymin, _ymax] = [this._xmin, this._xmax, this._ymin, this._ymax];
548
+ switch (op) {
549
+ case 1:
550
+ this._xmin = this.canvasWindow.imageWidth - _ymax;
551
+ this._xmax = this.canvasWindow.imageWidth - _ymin;
552
+ this._ymin = _xmin;
553
+ this._ymax = _xmax;
554
+ break;
555
+ case -1:
556
+ this._xmin = _ymin;
557
+ this._xmax = _ymax;
558
+ this._ymin = this.canvasWindow.imageHeight - _xmax;
559
+ this._ymax = this.canvasWindow.imageHeight - _xmin;
560
+ break;
561
+ }
562
+ this.applyUserScale();
563
+ }
564
  }
src/frontend/shared/Canvas.svelte CHANGED
@@ -1,17 +1,19 @@
1
  <script lang="ts">
2
  import { onMount, onDestroy, createEventDispatcher } from "svelte";
3
- import { BoundingBox, Hand, Trash } from "./icons/index";
4
  import ModalBox from "./ModalBox.svelte";
5
  import Box from "./Box";
6
  import { Colors } from './Colors.js';
7
  import AnnotatedImageData from "./AnnotatedImageData";
 
 
8
 
9
  enum Mode {creation, drag}
10
 
11
  export let imageUrl: string | null = null;
12
  export let interactive: boolean;
13
  export let boxAlpha = 0.5;
14
- export let boxMinSize = 25;
15
  export let handleSize: number;
16
  export let boxThickness: number;
17
  export let boxSelectedThickness: number;
@@ -24,6 +26,7 @@
24
  export let singleBox: boolean = false;
25
  export let showRemoveButton: boolean = null;
26
  export let handlesCursor: boolean = true;
 
27
 
28
  if (showRemoveButton === null) {
29
  showRemoveButton = (disableEditBoxes);
@@ -34,6 +37,7 @@
34
  let image = null;
35
  let selectedBox = -1;
36
  let mode: Mode = Mode.drag;
 
37
 
38
  if (value !== null && value.boxes.length == 0) {
39
  mode = Mode.creation;
@@ -50,6 +54,13 @@
50
 
51
  let editModalVisible = false;
52
  let newModalVisible = false;
 
 
 
 
 
 
 
53
 
54
  const dispatch = createEventDispatcher<{
55
  change: undefined;
@@ -74,9 +85,36 @@
74
  function draw() {
75
  if (ctx) {
76
  ctx.clearRect(0, 0, canvas.width, canvas.height);
 
 
 
77
  if (image !== null){
78
- ctx.drawImage(image, canvasXmin, canvasYmin, imageWidth, imageHeight);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
79
  }
 
80
  for (const box of value.boxes.slice().reverse()) {
81
  box.render(ctx);
82
  }
@@ -115,11 +153,13 @@
115
  const rect = canvas.getBoundingClientRect();
116
  const mouseX = event.clientX - rect.left;
117
  const mouseY = event.clientY - rect.top;
 
118
 
119
  // Check if the mouse is over any of the resizing handles
120
  for (const [i, box] of value.boxes.entries()) {
121
  const handleIndex = box.indexOfPointInsideHandle(mouseX, mouseY);
122
  if (handleIndex >= 0) {
 
123
  selectBox(i);
124
  box.startResize(handleIndex, event);
125
  return;
@@ -129,6 +169,7 @@
129
  // Check if the mouse is inside a box
130
  for (const [i, box] of value.boxes.entries()) {
131
  if (box.isPointInsideBox(mouseX, mouseY)) {
 
132
  selectBox(i);
133
  box.startDrag(event);
134
  return;
@@ -138,6 +179,10 @@
138
  if (!singleBox) {
139
  selectBox(-1);
140
  }
 
 
 
 
141
  }
142
 
143
  function handlePointerUp(event: PointerEvent) {
@@ -180,10 +225,30 @@
180
  }
181
  }
182
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
183
  function createBox(event: PointerEvent) {
184
  const rect = canvas.getBoundingClientRect();
185
- const x = (event.clientX - rect.left - canvasXmin) / scaleFactor;
186
- const y = (event.clientY - rect.top - canvasYmin) / scaleFactor;
187
  let color;
188
  if (choicesColors.length > 0) {
189
  color = colorHexToRGB(choicesColors[0]);
@@ -200,6 +265,7 @@
200
  let box = new Box(
201
  draw,
202
  onBoxFinishCreation,
 
203
  canvasXmin,
204
  canvasYmin,
205
  canvasXmax,
@@ -243,7 +309,11 @@
243
  onDeleteBox();
244
  } else {
245
  if (!disableEditBoxes) {
246
- newModalVisible = true;
 
 
 
 
247
  }
248
  if (singleBox) {
249
  setDragMode();
@@ -291,9 +361,13 @@
291
  let label = detail.label;
292
  let color = detail.color;
293
  let ret = detail.ret;
 
294
  if (selectedBox >= 0 && selectedBox < value.boxes.length) {
295
  let box = value.boxes[selectedBox];
296
  if (ret == 1) {
 
 
 
297
  box.label = label;
298
  box.color = colorHexToRGB(color);
299
  draw();
@@ -304,6 +378,32 @@
304
  }
305
  }
306
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
307
  function onDeleteBox() {
308
  if (selectedBox >= 0 && selectedBox < value.boxes.length) {
309
  value.boxes.splice(selectedBox, 1);
@@ -314,31 +414,53 @@
314
  dispatch("change");
315
  }
316
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
317
 
318
  function resize() {
319
  if (canvas) {
320
  scaleFactor = 1;
321
  canvas.width = canvas.clientWidth;
 
 
 
322
  if (image !== null) {
323
- if (image.width > canvas.width) {
324
- scaleFactor = canvas.width / image.width;
325
- imageWidth = image.width * scaleFactor;
326
- imageHeight = image.height * scaleFactor;
327
  canvasXmin = 0;
328
  canvasYmin = 0;
329
  canvasXmax = imageWidth;
330
  canvasYmax = imageHeight;
331
  canvas.height = imageHeight;
332
  } else {
333
- imageWidth = image.width;
334
- imageHeight = image.height;
335
  var x = (canvas.width - imageWidth) / 2;
336
  canvasXmin = x;
337
  canvasYmin = 0;
338
  canvasXmax = x + imageWidth;
339
- canvasYmax = image.height;
340
  canvas.height = imageHeight;
341
  }
 
 
 
 
342
  } else {
343
  canvasXmin = 0;
344
  canvasYmin = 0;
@@ -346,6 +468,9 @@
346
  canvasYmax = canvas.height;
347
  canvas.height = canvas.clientHeight;
348
  }
 
 
 
349
  if (canvasXmax > 0 && canvasYmax > 0){
350
  for (const box of value.boxes) {
351
  box.canvasXmin = canvasXmin;
@@ -381,6 +506,7 @@
381
  box = new Box(
382
  draw,
383
  onBoxFinishCreation,
 
384
  canvasXmin,
385
  canvasYmin,
386
  canvasXmax,
@@ -431,6 +557,8 @@
431
  choicesColors.push(colorRGBAToHex(color));
432
  }
433
  }
 
 
434
  }
435
 
436
  ctx = canvas.getContext("2d");
@@ -470,6 +598,7 @@
470
  on:pointerup={handlePointerUp}
471
  on:pointermove={handlesCursor ? handlePointerMove : null}
472
  on:dblclick={handleDoubleClick}
 
473
  style="height: {height}; width: {width};"
474
  class="canvas-annotator"
475
  ></canvas>
@@ -496,6 +625,23 @@
496
  on:click={() => onDeleteBox()}><Trash/></button
497
  >
498
  {/if}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
499
  </span>
500
  {/if}
501
 
@@ -519,6 +665,20 @@
519
  choicesColors={choicesColors}
520
  label={selectedBox >= 0 && selectedBox < value.boxes.length ? value.boxes[selectedBox].label : ""}
521
  color={selectedBox >= 0 && selectedBox < value.boxes.length ? colorRGBAToHex(value.boxes[selectedBox].color) : ""}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
522
  />
523
  {/if}
524
 
 
1
  <script lang="ts">
2
  import { onMount, onDestroy, createEventDispatcher } from "svelte";
3
+ import { BoundingBox, Hand, Trash, Label } from "./icons/index";
4
  import ModalBox from "./ModalBox.svelte";
5
  import Box from "./Box";
6
  import { Colors } from './Colors.js';
7
  import AnnotatedImageData from "./AnnotatedImageData";
8
+ import { Undo, Redo } from "@gradio/icons";
9
+ import WindowViewer from "./WindowViewer";
10
 
11
  enum Mode {creation, drag}
12
 
13
  export let imageUrl: string | null = null;
14
  export let interactive: boolean;
15
  export let boxAlpha = 0.5;
16
+ export let boxMinSize = 10;
17
  export let handleSize: number;
18
  export let boxThickness: number;
19
  export let boxSelectedThickness: number;
 
26
  export let singleBox: boolean = false;
27
  export let showRemoveButton: boolean = null;
28
  export let handlesCursor: boolean = true;
29
+ export let useDefaultLabel: boolean = false;
30
 
31
  if (showRemoveButton === null) {
32
  showRemoveButton = (disableEditBoxes);
 
37
  let image = null;
38
  let selectedBox = -1;
39
  let mode: Mode = Mode.drag;
40
+ let canvasWindow: WindowViewer = new WindowViewer(draw);
41
 
42
  if (value !== null && value.boxes.length == 0) {
43
  mode = Mode.creation;
 
54
 
55
  let editModalVisible = false;
56
  let newModalVisible = false;
57
+ let editDefaultLabelVisible = false;
58
+
59
+ let labelDetailLock = useDefaultLabel;
60
+ let defaultLabelCache = {
61
+ label: "",
62
+ color: ""
63
+ };
64
 
65
  const dispatch = createEventDispatcher<{
66
  change: undefined;
 
85
  function draw() {
86
  if (ctx) {
87
  ctx.clearRect(0, 0, canvas.width, canvas.height);
88
+ ctx.save();
89
+ ctx.translate(canvasWindow.offsetX, canvasWindow.offsetY);
90
+ ctx.scale(canvasWindow.scale, canvasWindow.scale);
91
  if (image !== null){
92
+ switch (value.orientation) {
93
+ case 0:
94
+ ctx.drawImage(image, 0, 0, imageWidth, imageHeight);
95
+ break;
96
+ case 1:
97
+ ctx.translate(imageWidth, 0);
98
+ ctx.rotate(Math.PI / 2);
99
+ ctx.drawImage(image, 0, 0, imageHeight, imageWidth);
100
+ break;
101
+ case 2:
102
+ ctx.translate(imageWidth, imageHeight);
103
+ ctx.rotate(Math.PI);
104
+ ctx.drawImage(image, 0, 0, imageWidth, imageHeight);
105
+ break;
106
+ case 3:
107
+ ctx.translate(0, imageHeight);
108
+ ctx.rotate(-Math.PI / 2);
109
+ ctx.drawImage(image, 0, 0, imageHeight, imageWidth);
110
+ break;
111
+ }
112
+
113
+
114
+ ctx.restore();
115
+ // ctx.resetTransform();
116
  }
117
+
118
  for (const box of value.boxes.slice().reverse()) {
119
  box.render(ctx);
120
  }
 
153
  const rect = canvas.getBoundingClientRect();
154
  const mouseX = event.clientX - rect.left;
155
  const mouseY = event.clientY - rect.top;
156
+ let selectedBoxFlag = false;
157
 
158
  // Check if the mouse is over any of the resizing handles
159
  for (const [i, box] of value.boxes.entries()) {
160
  const handleIndex = box.indexOfPointInsideHandle(mouseX, mouseY);
161
  if (handleIndex >= 0) {
162
+ selectedBoxFlag = true;
163
  selectBox(i);
164
  box.startResize(handleIndex, event);
165
  return;
 
169
  // Check if the mouse is inside a box
170
  for (const [i, box] of value.boxes.entries()) {
171
  if (box.isPointInsideBox(mouseX, mouseY)) {
172
+ selectedBoxFlag = true;
173
  selectBox(i);
174
  box.startDrag(event);
175
  return;
 
179
  if (!singleBox) {
180
  selectBox(-1);
181
  }
182
+
183
+ if (!selectedBoxFlag) {
184
+ canvasWindow.startDrag(event);
185
+ }
186
  }
187
 
188
  function handlePointerUp(event: PointerEvent) {
 
225
  }
226
  }
227
 
228
+ function handleMouseWheel(event: WheelEvent) {
229
+ event.preventDefault();
230
+ const delta = 1 / (1 + (event.deltaY / 1000) * 0.5);
231
+
232
+ const newScaleTmp = parseFloat((canvasWindow.scale * delta).toFixed(2));
233
+ const newScale = newScaleTmp < 1 ? 1 : newScaleTmp;
234
+ const rect = canvas.getBoundingClientRect();
235
+ const mouseX = event.clientX - rect.left;
236
+ const mouseY = event.clientY - rect.top;
237
+
238
+ const worldX = (mouseX - canvasWindow.offsetX) / canvasWindow.scale;
239
+ const worldY = (mouseY - canvasWindow.offsetY) / canvasWindow.scale;
240
+
241
+ canvasWindow.offsetX = mouseX - worldX * newScale;
242
+ canvasWindow.offsetY = mouseY - worldY * newScale;
243
+
244
+ canvasWindow.scale = newScale;
245
+ draw();
246
+ }
247
+
248
  function createBox(event: PointerEvent) {
249
  const rect = canvas.getBoundingClientRect();
250
+ const x = (event.clientX - rect.left - canvasWindow.offsetX) / scaleFactor / canvasWindow.scale;
251
+ const y = (event.clientY - rect.top - canvasWindow.offsetY) / scaleFactor / canvasWindow.scale;
252
  let color;
253
  if (choicesColors.length > 0) {
254
  color = colorHexToRGB(choicesColors[0]);
 
265
  let box = new Box(
266
  draw,
267
  onBoxFinishCreation,
268
+ canvasWindow,
269
  canvasXmin,
270
  canvasYmin,
271
  canvasXmax,
 
309
  onDeleteBox();
310
  } else {
311
  if (!disableEditBoxes) {
312
+ if (labelDetailLock) {
313
+ onUseDefaultLabelModalNew();
314
+ } else{
315
+ newModalVisible = true;
316
+ }
317
  }
318
  if (singleBox) {
319
  setDragMode();
 
361
  let label = detail.label;
362
  let color = detail.color;
363
  let ret = detail.ret;
364
+ let lock = detail.lock;
365
  if (selectedBox >= 0 && selectedBox < value.boxes.length) {
366
  let box = value.boxes[selectedBox];
367
  if (ret == 1) {
368
+ labelDetailLock = lock;
369
+ defaultLabelCache.label = label;
370
+ defaultLabelCache.color = color;
371
  box.label = label;
372
  box.color = colorHexToRGB(color);
373
  draw();
 
378
  }
379
  }
380
 
381
+ function onDefaultLabelEditChange(event) {
382
+ editDefaultLabelVisible = false;
383
+ const { detail } = event;
384
+ let label = detail.label;
385
+ let color = detail.color;
386
+ let ret = detail.ret;
387
+ let lock = detail.lock;
388
+ if (ret == 1) {
389
+ labelDetailLock = lock;
390
+ defaultLabelCache.label = label;
391
+ defaultLabelCache.color = color;
392
+ }
393
+ }
394
+
395
+ function onUseDefaultLabelModalNew(){
396
+ if (selectedBox >= 0 && selectedBox < value.boxes.length) {
397
+ let box = value.boxes[selectedBox];
398
+ box.label = defaultLabelCache.label;
399
+ if (defaultLabelCache.color !== "") {
400
+ box.color = colorHexToRGB(defaultLabelCache.color);
401
+ }
402
+ draw();
403
+ dispatch("change");
404
+ }
405
+ }
406
+
407
  function onDeleteBox() {
408
  if (selectedBox >= 0 && selectedBox < value.boxes.length) {
409
  value.boxes.splice(selectedBox, 1);
 
414
  dispatch("change");
415
  }
416
  }
417
+
418
+ /**
419
+ * Rotate the image and all the boxes
420
+ * @param op 1: rotate clockwise, -1: rotate counterclockwise
421
+ */
422
+ function onRotateImage(op: number) {
423
+ value.orientation = (((value.orientation + op) % 4) + 4 ) % 4;
424
+ canvasWindow.orientation = value.orientation;
425
+
426
+ resize();
427
+ for (const box of value.boxes) {
428
+ box.onRotate(op);
429
+ }
430
+ draw();
431
+ }
432
 
433
  function resize() {
434
  if (canvas) {
435
  scaleFactor = 1;
436
  canvas.width = canvas.clientWidth;
437
+
438
+ canvasWindow.setRotatedImage(image);
439
+
440
  if (image !== null) {
441
+ if (canvasWindow.imageRotatedWidth > canvas.width) {
442
+ scaleFactor = canvas.width / canvasWindow.imageRotatedWidth;
443
+ imageWidth = Math.round(canvasWindow.imageRotatedWidth * scaleFactor);
444
+ imageHeight = Math.round(canvasWindow.imageRotatedHeight * scaleFactor);
445
  canvasXmin = 0;
446
  canvasYmin = 0;
447
  canvasXmax = imageWidth;
448
  canvasYmax = imageHeight;
449
  canvas.height = imageHeight;
450
  } else {
451
+ imageWidth = canvasWindow.imageRotatedWidth;
452
+ imageHeight = canvasWindow.imageRotatedHeight;
453
  var x = (canvas.width - imageWidth) / 2;
454
  canvasXmin = x;
455
  canvasYmin = 0;
456
  canvasXmax = x + imageWidth;
457
+ canvasYmax = imageHeight;
458
  canvas.height = imageHeight;
459
  }
460
+
461
+ canvasWindow.imageWidth = imageWidth;
462
+ canvasWindow.imageHeight = imageHeight;
463
+
464
  } else {
465
  canvasXmin = 0;
466
  canvasYmin = 0;
 
468
  canvasYmax = canvas.height;
469
  canvas.height = canvas.clientHeight;
470
  }
471
+
472
+ canvasWindow.resize(canvas.width, canvas.height, canvasXmin, canvasYmin);
473
+
474
  if (canvasXmax > 0 && canvasYmax > 0){
475
  for (const box of value.boxes) {
476
  box.canvasXmin = canvasXmin;
 
506
  box = new Box(
507
  draw,
508
  onBoxFinishCreation,
509
+ canvasWindow,
510
  canvasXmin,
511
  canvasYmin,
512
  canvasXmax,
 
557
  choicesColors.push(colorRGBAToHex(color));
558
  }
559
  }
560
+ defaultLabelCache.label = choices[0][0];
561
+ defaultLabelCache.color = choicesColors[0];
562
  }
563
 
564
  ctx = canvas.getContext("2d");
 
598
  on:pointerup={handlePointerUp}
599
  on:pointermove={handlesCursor ? handlePointerMove : null}
600
  on:dblclick={handleDoubleClick}
601
+ on:wheel={handleMouseWheel}
602
  style="height: {height}; width: {width};"
603
  class="canvas-annotator"
604
  ></canvas>
 
625
  on:click={() => onDeleteBox()}><Trash/></button
626
  >
627
  {/if}
628
+ {#if !disableEditBoxes && labelDetailLock}
629
+ <button
630
+ class="icon"
631
+ aria-label="Edit label"
632
+ on:click={() => editDefaultLabelVisible = true}><Label/></button
633
+ >
634
+ {/if}
635
+ <button
636
+ class="icon"
637
+ aria-label="Rotate counterclockwise"
638
+ on:click={() => onRotateImage(-1)}><Undo/></button
639
+ >
640
+ <button
641
+ class="icon"
642
+ aria-label="Rotate clockwise"
643
+ on:click={() => onRotateImage(1)}><Redo/></button
644
+ >
645
  </span>
646
  {/if}
647
 
 
665
  choicesColors={choicesColors}
666
  label={selectedBox >= 0 && selectedBox < value.boxes.length ? value.boxes[selectedBox].label : ""}
667
  color={selectedBox >= 0 && selectedBox < value.boxes.length ? colorRGBAToHex(value.boxes[selectedBox].color) : ""}
668
+ labelDetailLock = {labelDetailLock}
669
+ />
670
+ {/if}
671
+
672
+ {#if editDefaultLabelVisible}
673
+ <ModalBox
674
+ on:change={onDefaultLabelEditChange}
675
+ on:enter{onDefaultLabelEditChange}
676
+ choices={choices}
677
+ showRemove={false}
678
+ choicesColors={choicesColors}
679
+ label={selectedBox >= 0 && selectedBox < value.boxes.length ? value.boxes[selectedBox].label : ""}
680
+ color={selectedBox >= 0 && selectedBox < value.boxes.length ? colorRGBAToHex(value.boxes[selectedBox].color) : ""}
681
+ labelDetailLock = {labelDetailLock}
682
  />
683
  {/if}
684
 
src/frontend/shared/ImageAnnotator.svelte CHANGED
@@ -41,6 +41,7 @@
41
  export let max_file_size: number | null = null;
42
  export let cli_upload: Client["upload"];
43
  export let stream_handler: Client["stream_factory"];
 
44
 
45
  let upload: Upload;
46
  let uploading = false;
@@ -187,6 +188,7 @@
187
  {showRemoveButton}
188
  {handlesCursor}
189
  {boxSelectedThickness}
 
190
  src={value.image.url}
191
  />
192
  </div>
 
41
  export let max_file_size: number | null = null;
42
  export let cli_upload: Client["upload"];
43
  export let stream_handler: Client["stream_factory"];
44
+ export let useDefaultLabel: boolean;
45
 
46
  let upload: Upload;
47
  let uploading = false;
 
188
  {showRemoveButton}
189
  {handlesCursor}
190
  {boxSelectedThickness}
191
+ {useDefaultLabel}
192
  src={value.image.url}
193
  />
194
  </div>
src/frontend/shared/ImageCanvas.svelte CHANGED
@@ -25,6 +25,7 @@
25
  export let singleBox: boolean;
26
  export let showRemoveButton: boolean;
27
  export let handlesCursor: boolean;
 
28
 
29
  let resolved_src: typeof src;
30
 
@@ -72,5 +73,6 @@
72
  {singleBox}
73
  {showRemoveButton}
74
  {handlesCursor}
 
75
  imageUrl={resolved_src}
76
  />
 
25
  export let singleBox: boolean;
26
  export let showRemoveButton: boolean;
27
  export let handlesCursor: boolean;
28
+ export let useDefaultLabel: boolean;
29
 
30
  let resolved_src: typeof src;
31
 
 
73
  {singleBox}
74
  {showRemoveButton}
75
  {handlesCursor}
76
+ {useDefaultLabel}
77
  imageUrl={resolved_src}
78
  />
src/frontend/shared/ModalBox.svelte CHANGED
@@ -4,6 +4,7 @@
4
  import { BaseDropdown } from "./patched_dropdown/Index.svelte";
5
  import { createEventDispatcher } from "svelte";
6
  import { onMount, onDestroy } from "svelte";
 
7
 
8
  export let label = "";
9
  export let currentLabel = "";
@@ -12,6 +13,7 @@
12
  export let color = "";
13
  export let currentColor = "";
14
  export let showRemove = true;
 
15
 
16
  const dispatch = createEventDispatcher<{
17
  change: object;
@@ -21,6 +23,7 @@
21
  dispatch("change", {
22
  label: currentLabel,
23
  color: currentColor,
 
24
  ret: ret // -1: remove, 0: cancel, 1: change
25
  });
26
  }
@@ -51,6 +54,10 @@
51
  dispatchChange(1);
52
  }
53
 
 
 
 
 
54
  function handleKeyPress(event: KeyboardEvent) {
55
  switch (event.key) {
56
  case "Enter":
@@ -74,6 +81,17 @@
74
  <div class="modal" id="model-box-edit">
75
  <div class="modal-container">
76
  <span class="model-content">
 
 
 
 
 
 
 
 
 
 
 
77
  <div style="margin-right: 10px;">
78
  <BaseDropdown
79
  value={currentLabel}
@@ -148,4 +166,21 @@
148
  display: flex;
149
  align-items: flex-end;
150
  }
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
151
  </style>
 
4
  import { BaseDropdown } from "./patched_dropdown/Index.svelte";
5
  import { createEventDispatcher } from "svelte";
6
  import { onMount, onDestroy } from "svelte";
7
+ import { Lock, Unlock } from "./icons/index";
8
 
9
  export let label = "";
10
  export let currentLabel = "";
 
13
  export let color = "";
14
  export let currentColor = "";
15
  export let showRemove = true;
16
+ export let labelDetailLock = false;
17
 
18
  const dispatch = createEventDispatcher<{
19
  change: object;
 
23
  dispatch("change", {
24
  label: currentLabel,
25
  color: currentColor,
26
+ lock: labelDetailLock,
27
  ret: ret // -1: remove, 0: cancel, 1: change
28
  });
29
  }
 
54
  dispatchChange(1);
55
  }
56
 
57
+ function onLockClick(event) {
58
+ labelDetailLock = !labelDetailLock;
59
+ }
60
+
61
  function handleKeyPress(event: KeyboardEvent) {
62
  switch (event.key) {
63
  case "Enter":
 
81
  <div class="modal" id="model-box-edit">
82
  <div class="modal-container">
83
  <span class="model-content">
84
+ {#if !showRemove}
85
+ <div style="margin-right: 8px;">
86
+ <button
87
+ class="icon"
88
+ class:selected={labelDetailLock === true}
89
+ aria-label="Lock label detail"
90
+ on:click={onLockClick}>
91
+ {#if labelDetailLock}<Lock/>{:else}<Unlock/>{/if}</button
92
+ >
93
+ </div>
94
+ {/if}
95
  <div style="margin-right: 10px;">
96
  <BaseDropdown
97
  value={currentLabel}
 
166
  display: flex;
167
  align-items: flex-end;
168
  }
169
+
170
+ .icon {
171
+ width: 22px;
172
+ height: 22px;
173
+ margin: var(--spacing-lg) var(--spacing-xs);
174
+ padding: var(--spacing-xs);
175
+ color: var(--neutral-400);
176
+ border-radius: var(--radius-md);
177
+ }
178
+
179
+ .icon:hover{
180
+ color: var(--color-accent);
181
+ }
182
+
183
+ .selected {
184
+ color: var(--color-accent);
185
+ }
186
  </style>
src/frontend/shared/WindowViewer.ts ADDED
@@ -0,0 +1,95 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ const clamp = (num: number, min: number, max: number) => Math.min(Math.max(num, min), max)
2
+
3
+ export default class WindowViewer {
4
+ scale: number;
5
+ offsetX: number;
6
+ offsetY: number;
7
+ canvasWidth: number;
8
+ canvasHeight: number;
9
+ imageWidth: number;
10
+ imageHeight: number;
11
+ imageRotatedWidth: number;
12
+ imageRotatedHeight: number;
13
+ isDragging: boolean;
14
+ startDragX: number;
15
+ startDragY: number;
16
+ orientation: number;
17
+ renderCallBack: () => void;
18
+
19
+ constructor(renderCallBack: () => void) {
20
+ this.renderCallBack = renderCallBack;
21
+ this.scale = 1.0;
22
+ this.offsetX = 0;
23
+ this.offsetY = 0;
24
+ this.canvasWidth = 0;
25
+ this.canvasHeight = 0;
26
+ this.imageWidth = 0;
27
+ this.imageHeight = 0;
28
+ this.imageRotatedWidth = 0;
29
+ this.imageRotatedHeight = 0;
30
+ this.isDragging = false;
31
+ this.startDragX = 0;
32
+ this.startDragY = 0;
33
+ this.orientation = 0;
34
+ }
35
+ startDrag(event: MouseEvent): void {
36
+ this.isDragging = true;
37
+ this.startDragX = event.clientX - this.offsetX;
38
+ this.startDragY = event.clientY - this.offsetY;
39
+
40
+ document.addEventListener("pointermove", this.handleDrag);
41
+ document.addEventListener("pointerup", this.stopDrag);
42
+ }
43
+
44
+ stopDrag = (): void => {
45
+ this.isDragging = false;
46
+ document.removeEventListener("pointermove", this.handleDrag);
47
+ document.removeEventListener("pointerup", this.stopDrag);
48
+ };
49
+
50
+ handleDrag = (event: MouseEvent): void => {
51
+ if (this.isDragging) {
52
+
53
+ let deltaX = event.clientX - this.startDragX - this.offsetX;
54
+ let deltaY = event.clientY - this.startDragY - this.offsetY;
55
+
56
+ if (this.imageWidth * this.scale > this.canvasWidth){
57
+ deltaX = clamp(deltaX, this.canvasWidth-this.offsetX-(this.imageWidth*this.scale), -this.offsetX);
58
+ } else {
59
+ deltaX = clamp(deltaX, -this.offsetX, this.canvasWidth-this.offsetX-(this.imageWidth*this.scale));
60
+ }
61
+
62
+ if (this.imageHeight * this.scale > this.canvasHeight){
63
+ deltaY = clamp(deltaY, this.canvasHeight-this.offsetY-(this.imageHeight*this.scale), -this.offsetY);
64
+ } else {
65
+ deltaY = clamp(deltaY, -this.offsetY, this.canvasHeight-this.offsetY-(this.imageHeight*this.scale));
66
+ }
67
+
68
+ this.offsetX += deltaX;
69
+ this.offsetY += deltaY;
70
+ this.renderCallBack();
71
+ }
72
+ };
73
+
74
+ setRotatedImage(image: HTMLImageElement | null): void {
75
+ if (image !== null) {
76
+ if (this.orientation == 0 || this.orientation == 2) {
77
+ this.imageRotatedWidth = image.width;
78
+ this.imageRotatedHeight = image.height;
79
+ } else { // (this.orientation == 1 || this.orientation == 3)
80
+ this.imageRotatedWidth = image.height;
81
+ this.imageRotatedHeight = image.width;
82
+ }
83
+ }
84
+ }
85
+
86
+ resize(width: number, height: number, offsetX: number=0, offsetY: number=0): void {
87
+ if (this.canvasWidth == width && this.canvasHeight == height) return;
88
+ this.canvasWidth = width;
89
+ this.canvasHeight = height;
90
+
91
+ this.scale = 1.0;
92
+ this.offsetX = offsetX;
93
+ this.offsetY = offsetY;
94
+ }
95
+ }
src/frontend/shared/icons/Label.svelte ADDED
@@ -0,0 +1,15 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <svg
2
+ width="100%"
3
+ height="100%"
4
+ viewBox="0 0 24 24"
5
+ xmlns="http://www.w3.org/2000/svg"
6
+ fill="none"
7
+ stroke="currentColor"
8
+ stroke-width="2"
9
+ style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;"
10
+ >
11
+ <path
12
+ d="M12,2.5L2.5,12c-.7.7-.7,1.8,0,2.4l7.1,7.1c.7.7,1.8.7,2.4,0l9.5-9.5c.3-.3.5-.8.5-1.2V3.7c0-1-.8-1.7-1.7-1.7h-7.1c-.5,0-.9.2-1.2.5ZM7.3,14.1l4.7-4.7M9.9,16.7l2.2-2.2"
13
+ />
14
+ <path d="M18.5,6.3c0,.5-.4.9-.9.9s-.9-.4-.9-.9.4-.9.9-.9.9.4.9.9Z" />
15
+ </svg>
src/frontend/shared/icons/Lock.svelte ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <svg
2
+ width="100%"
3
+ height="100%"
4
+ viewBox="0 0 24 24"
5
+ fill="none"
6
+ stroke="currentColor"
7
+ stroke-width="2"
8
+ style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;"
9
+ xmlns="http://www.w3.org/2000/svg"
10
+ >
11
+ <path
12
+ d="M7 10.0288C7.47142 10 8.05259 10 8.8 10H15.2C15.9474 10 16.5286 10 17 10.0288M7 10.0288C6.41168 10.0647 5.99429 10.1455 5.63803 10.327C5.07354 10.6146 4.6146 11.0735 4.32698 11.638C4 12.2798 4 13.1198 4 14.8V16.2C4 17.8802 4 18.7202 4.32698 19.362C4.6146 19.9265 5.07354 20.3854 5.63803 20.673C6.27976 21 7.11984 21 8.8 21H15.2C16.8802 21 17.7202 21 18.362 20.673C18.9265 20.3854 19.3854 19.9265 19.673 19.362C20 18.7202 20 17.8802 20 16.2V14.8C20 13.1198 20 12.2798 19.673 11.638C19.3854 11.0735 18.9265 10.6146 18.362 10.327C18.0057 10.1455 17.5883 10.0647 17 10.0288M7 10.0288V8C7 5.23858 9.23858 3 12 3C14.7614 3 17 5.23858 17 8V10.0288"
13
+ ></path>
14
+ </svg>
src/frontend/shared/icons/Unlock.svelte ADDED
@@ -0,0 +1,14 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ <svg
2
+ width="100%"
3
+ height="100%"
4
+ viewBox="0 0 24 24"
5
+ fill="none"
6
+ stroke="currentColor"
7
+ stroke-width="2"
8
+ style="fill-rule:evenodd;clip-rule:evenodd;stroke-linecap:round;stroke-linejoin:round;"
9
+ xmlns="http://www.w3.org/2000/svg"
10
+ >
11
+ <path
12
+ d="M16.584 6C15.8124 4.2341 14.0503 3 12 3C9.23858 3 7 5.23858 7 8V10.0288M7 10.0288C7.47142 10 8.05259 10 8.8 10H15.2C16.8802 10 17.7202 10 18.362 10.327C18.9265 10.6146 19.3854 11.0735 19.673 11.638C20 12.2798 20 13.1198 20 14.8V16.2C20 17.8802 20 18.7202 19.673 19.362C19.3854 19.9265 18.9265 20.3854 18.362 20.673C17.7202 21 16.8802 21 15.2 21H8.8C7.11984 21 6.27976 21 5.63803 20.673C5.07354 20.3854 4.6146 19.9265 4.32698 19.362C4 18.7202 4 17.8802 4 16.2V14.8C4 13.1198 4 12.2798 4.32698 11.638C4.6146 11.0735 5.07354 10.6146 5.63803 10.327C5.99429 10.1455 6.41168 10.0647 7 10.0288Z"
13
+ ></path>
14
+ </svg>
src/frontend/shared/icons/index.ts CHANGED
@@ -1,4 +1,7 @@
1
- export { default as Add } from "./Add.svelte";
2
- export { default as BoundingBox } from "./BoundingBox.svelte";
3
- export { default as Hand } from "./Hand.svelte";
4
- export { default as Trash } from "./Trash.svelte";
 
 
 
 
1
+ export { default as Add } from "./Add.svelte";
2
+ export { default as BoundingBox } from "./BoundingBox.svelte";
3
+ export { default as Hand } from "./Hand.svelte";
4
+ export { default as Trash } from "./Trash.svelte";
5
+ export { default as Label } from "./Label.svelte";
6
+ export { default as Lock } from "./Lock.svelte";
7
+ export { default as Unlock } from "./Unlock.svelte";
src/images/demo.png CHANGED

Git LFS Details

  • SHA256: 751aaa0d35b883a2e87e0526634fdaf19c5810594d6cea423272953f3fc4f6c5
  • Pointer size: 132 Bytes
  • Size of remote file: 4.48 MB

Git LFS Details

  • SHA256: 59c43819317424902ad9a8878231dfa2898a74b0893233c2c9d83d96736e7140
  • Pointer size: 132 Bytes
  • Size of remote file: 1.58 MB
src/pyproject.toml CHANGED
@@ -8,7 +8,7 @@ build-backend = "hatchling.build"
8
 
9
  [project]
10
  name = "gradio_image_annotation"
11
- version = "0.2.6"
12
  description = "A Gradio component that can be used to annotate images with bounding boxes."
13
  readme = "README.md"
14
  license = "MIT"
 
8
 
9
  [project]
10
  name = "gradio_image_annotation"
11
+ version = "0.2.7"
12
  description = "A Gradio component that can be used to annotate images with bounding boxes."
13
  readme = "README.md"
14
  license = "MIT"