edgargg commited on
Commit
01ec9b6
·
verified ·
1 Parent(s): 94da247

Upload folder using huggingface_hub

Browse files
README.md CHANGED
@@ -1,19 +1,3 @@
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.3.1
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
  # `gradio_image_annotation`
19
  <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>
@@ -22,6 +6,15 @@ A Gradio component that can be used to annotate images with bounding boxes.
22
 
23
  ![Demo preview](images/demo.png)
24
 
 
 
 
 
 
 
 
 
 
25
  ## Installation
26
 
27
  ```bash
@@ -126,6 +119,17 @@ with gr.Blocks() as demo:
126
  button_crop.click(crop, annotator_crop, image_crop)
127
 
128
  gr.Examples(examples_crop, annotator_crop)
 
 
 
 
 
 
 
 
 
 
 
129
 
130
  if __name__ == "__main__":
131
  demo.launch()
@@ -558,6 +562,19 @@ bool | None
558
  <td align="left"><code>False</code></td>
559
  <td align="left">If True, the first item in label_list will be used as the default label when creating boxes.</td>
560
  </tr>
 
 
 
 
 
 
 
 
 
 
 
 
 
561
  </tbody></table>
562
 
563
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>
 
6
 
7
  ![Demo preview](images/demo.png)
8
 
9
+ ## Keyboard Shortcuts:
10
+ - `C`: Create mode
11
+ - `D`: Drag mode
12
+ - `E`: Edit selected box (same as double-click a box)
13
+ - `Delete`: Remove selected box
14
+ - `Space`: Reset view (zoom/pan)
15
+ - `Enter`: Confirm modal dialog
16
+ - `Escape`: Cancel/close modal dialog
17
+
18
  ## Installation
19
 
20
  ```bash
 
119
  button_crop.click(crop, annotator_crop, image_crop)
120
 
121
  gr.Examples(examples_crop, annotator_crop)
122
+
123
+ with gr.Accordion("Keyboard Shortcuts"):
124
+ gr.Markdown("""
125
+ - ``C``: Create mode
126
+ - ``D``: Drag mode
127
+ - ``E``: Edit selected box (same as double-click a box)
128
+ - ``Delete``: Remove selected box
129
+ - ``Space``: Reset view (zoom/pan)
130
+ - ``Enter``: Confirm modal dialog
131
+ - ``Escape``: Cancel/close modal dialog
132
+ """)
133
 
134
  if __name__ == "__main__":
135
  demo.launch()
 
562
  <td align="left"><code>False</code></td>
563
  <td align="left">If True, the first item in label_list will be used as the default label when creating boxes.</td>
564
  </tr>
565
+
566
+ <tr>
567
+ <td align="left"><code>enable_keyboard_shortcuts</code></td>
568
+ <td align="left" style="width: 25%;">
569
+
570
+ ```python
571
+ bool
572
+ ```
573
+
574
+ </td>
575
+ <td align="left"><code>True</code></td>
576
+ <td align="left">If True, the component will respond to keyboard events.</td>
577
+ </tr>
578
  </tbody></table>
579
 
580
 
app.py CHANGED
@@ -94,5 +94,16 @@ with gr.Blocks() as demo:
94
 
95
  gr.Examples(examples_crop, annotator_crop)
96
 
 
 
 
 
 
 
 
 
 
 
 
97
  if __name__ == "__main__":
98
  demo.launch()
 
94
 
95
  gr.Examples(examples_crop, annotator_crop)
96
 
97
+ with gr.Accordion("Keyboard Shortcuts"):
98
+ gr.Markdown("""
99
+ - ``C``: Create mode
100
+ - ``D``: Drag mode
101
+ - ``E``: Edit selected box (same as double-click a box)
102
+ - ``Delete``: Remove selected box
103
+ - ``Space``: Reset view (zoom/pan)
104
+ - ``Enter``: Confirm modal dialog
105
+ - ``Escape``: Cancel/close modal dialog
106
+ """)
107
+
108
  if __name__ == "__main__":
109
  demo.launch()
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)`). 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 | None', '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
 
@@ -133,6 +133,17 @@ with gr.Blocks() as demo:
133
  button_crop.click(crop, annotator_crop, image_crop)
134
 
135
  gr.Examples(examples_crop, annotator_crop)
 
 
 
 
 
 
 
 
 
 
 
136
 
137
  if __name__ == "__main__":
138
  demo.launch()
 
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 | None', 'default': 'False', 'description': 'If True, the first item in label_list will be used as the default label when creating boxes.'}, 'enable_keyboard_shortcuts': {'type': 'bool', 'default': 'True', 'description': 'If True, the component will respond to keyboard events.'}}, '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
 
 
133
  button_crop.click(crop, annotator_crop, image_crop)
134
 
135
  gr.Examples(examples_crop, annotator_crop)
136
+
137
+ with gr.Accordion("Keyboard Shortcuts"):
138
+ gr.Markdown(\"\"\"
139
+ - ``C``: Create mode
140
+ - ``D``: Drag mode
141
+ - ``E``: Edit selected box (same as double-click a box)
142
+ - ``Delete``: Remove selected box
143
+ - ``Space``: Reset view (zoom/pan)
144
+ - ``Enter``: Confirm modal dialog
145
+ - ``Escape``: Cancel/close modal dialog
146
+ \"\"\")
147
 
148
  if __name__ == "__main__":
149
  demo.launch()
src/README.md CHANGED
@@ -6,6 +6,15 @@ 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
@@ -110,6 +119,17 @@ with gr.Blocks() as demo:
110
  button_crop.click(crop, annotator_crop, image_crop)
111
 
112
  gr.Examples(examples_crop, annotator_crop)
 
 
 
 
 
 
 
 
 
 
 
113
 
114
  if __name__ == "__main__":
115
  demo.launch()
@@ -542,6 +562,19 @@ bool | None
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
 
 
6
 
7
  ![Demo preview](images/demo.png)
8
 
9
+ ## Keyboard Shortcuts:
10
+ - `C`: Create mode
11
+ - `D`: Drag mode
12
+ - `E`: Edit selected box (same as double-click a box)
13
+ - `Delete`: Remove selected box
14
+ - `Space`: Reset view (zoom/pan)
15
+ - `Enter`: Confirm modal dialog
16
+ - `Escape`: Cancel/close modal dialog
17
+
18
  ## Installation
19
 
20
  ```bash
 
119
  button_crop.click(crop, annotator_crop, image_crop)
120
 
121
  gr.Examples(examples_crop, annotator_crop)
122
+
123
+ with gr.Accordion("Keyboard Shortcuts"):
124
+ gr.Markdown("""
125
+ - ``C``: Create mode
126
+ - ``D``: Drag mode
127
+ - ``E``: Edit selected box (same as double-click a box)
128
+ - ``Delete``: Remove selected box
129
+ - ``Space``: Reset view (zoom/pan)
130
+ - ``Enter``: Confirm modal dialog
131
+ - ``Escape``: Cancel/close modal dialog
132
+ """)
133
 
134
  if __name__ == "__main__":
135
  demo.launch()
 
562
  <td align="left"><code>False</code></td>
563
  <td align="left">If True, the first item in label_list will be used as the default label when creating boxes.</td>
564
  </tr>
565
+
566
+ <tr>
567
+ <td align="left"><code>enable_keyboard_shortcuts</code></td>
568
+ <td align="left" style="width: 25%;">
569
+
570
+ ```python
571
+ bool
572
+ ```
573
+
574
+ </td>
575
+ <td align="left"><code>True</code></td>
576
+ <td align="left">If True, the component will respond to keyboard events.</td>
577
+ </tr>
578
  </tbody></table>
579
 
580
 
src/backend/gradio_image_annotation/image_annotator.py CHANGED
@@ -89,6 +89,7 @@ class image_annotator(Component):
89
  show_remove_button: bool | None = None,
90
  handles_cursor: bool | None = True,
91
  use_default_label: bool | None = False,
 
92
  ):
93
  """
94
  Parameters:
@@ -123,6 +124,7 @@ class image_annotator(Component):
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"]
@@ -158,6 +160,7 @@ class image_annotator(Component):
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
 
89
  show_remove_button: bool | None = None,
90
  handles_cursor: bool | None = True,
91
  use_default_label: bool | None = False,
92
+ enable_keyboard_shortcuts: bool = True,
93
  ):
94
  """
95
  Parameters:
 
124
  show_remove_button: If True, will show a button to remove the selected bounding box.
125
  handles_cursor: If True, the cursor will change when hovering over box handles in drag mode. Can be CPU-intensive.
126
  use_default_label: If True, the first item in label_list will be used as the default label when creating boxes.
127
+ enable_keyboard_shortcuts: If True, the component will respond to keyboard events.
128
  """
129
 
130
  valid_types = ["numpy", "pil", "filepath"]
 
160
  self.show_remove_button = show_remove_button
161
  self.handles_cursor = handles_cursor
162
  self.use_default_label = use_default_label
163
+ self.enable_keyboard_shortcuts = enable_keyboard_shortcuts
164
 
165
  self.boxes_alpha = boxes_alpha
166
  self.box_min_size = box_min_size
src/backend/gradio_image_annotation/templates/component/index.js CHANGED
The diff for this file is too large to render. See raw diff
 
src/demo/app.py CHANGED
@@ -94,5 +94,16 @@ with gr.Blocks() as demo:
94
 
95
  gr.Examples(examples_crop, annotator_crop)
96
 
 
 
 
 
 
 
 
 
 
 
 
97
  if __name__ == "__main__":
98
  demo.launch()
 
94
 
95
  gr.Examples(examples_crop, annotator_crop)
96
 
97
+ with gr.Accordion("Keyboard Shortcuts"):
98
+ gr.Markdown("""
99
+ - ``C``: Create mode
100
+ - ``D``: Drag mode
101
+ - ``E``: Edit selected box (same as double-click a box)
102
+ - ``Delete``: Remove selected box
103
+ - ``Space``: Reset view (zoom/pan)
104
+ - ``Enter``: Confirm modal dialog
105
+ - ``Escape``: Cancel/close modal dialog
106
+ """)
107
+
108
  if __name__ == "__main__":
109
  demo.launch()
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)`). 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 | None', '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
 
@@ -133,6 +133,17 @@ with gr.Blocks() as demo:
133
  button_crop.click(crop, annotator_crop, image_crop)
134
 
135
  gr.Examples(examples_crop, annotator_crop)
 
 
 
 
 
 
 
 
 
 
 
136
 
137
  if __name__ == "__main__":
138
  demo.launch()
 
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 | None', 'default': 'False', 'description': 'If True, the first item in label_list will be used as the default label when creating boxes.'}, 'enable_keyboard_shortcuts': {'type': 'bool', 'default': 'True', 'description': 'If True, the component will respond to keyboard events.'}}, '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
 
 
133
  button_crop.click(crop, annotator_crop, image_crop)
134
 
135
  gr.Examples(examples_crop, annotator_crop)
136
+
137
+ with gr.Accordion("Keyboard Shortcuts"):
138
+ gr.Markdown(\"\"\"
139
+ - ``C``: Create mode
140
+ - ``D``: Drag mode
141
+ - ``E``: Edit selected box (same as double-click a box)
142
+ - ``Delete``: Remove selected box
143
+ - ``Space``: Reset view (zoom/pan)
144
+ - ``Enter``: Confirm modal dialog
145
+ - ``Escape``: Cancel/close modal dialog
146
+ \"\"\")
147
 
148
  if __name__ == "__main__":
149
  demo.launch()
src/frontend/Index.svelte CHANGED
@@ -50,6 +50,7 @@
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;
@@ -129,6 +130,7 @@
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" />
 
50
  export let show_remove_button: boolean;
51
  export let handles_cursor: boolean;
52
  export let use_default_label: boolean;
53
+ export let enable_keyboard_shortcuts: boolean;
54
 
55
  export let gradio: Gradio<{
56
  change: never;
 
130
  showRemoveButton={show_remove_button}
131
  handlesCursor={handles_cursor}
132
  useDefaultLabel={use_default_label}
133
+ enableKeyboardShortcuts={enable_keyboard_shortcuts}
134
  >
135
  {#if active_source === "upload"}
136
  <UploadText i18n={gradio.i18n} type="image" />
src/frontend/shared/Canvas.svelte CHANGED
@@ -1,5 +1,5 @@
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";
@@ -27,12 +27,14 @@
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);
33
  }
34
 
35
  let canvas: HTMLCanvasElement;
 
36
  let ctx: CanvasRenderingContext2D;
37
  let image = null;
38
  let selectedBox = -1;
@@ -309,16 +311,44 @@
309
  }
310
  }
311
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
312
  function handleKeyPress(event: KeyboardEvent) {
313
- if (!interactive) {
314
  return;
315
  }
 
 
 
316
 
317
- switch (event.key) {
318
- case "Delete":
319
- onDeleteBox();
320
- break;
321
  }
 
 
 
 
 
 
 
 
 
 
 
 
322
  }
323
 
324
  function handleMouseWheel(event: WheelEvent) {
@@ -439,6 +469,7 @@
439
 
440
  function onModalEditChange(event) {
441
  editModalVisible = false;
 
442
  const { detail } = event;
443
  let label = detail.label;
444
  let color = detail.color;
@@ -458,6 +489,7 @@
458
 
459
  function onModalNewChange(event) {
460
  newModalVisible = false;
 
461
  const { detail } = event;
462
  let label = detail.label;
463
  let color = detail.color;
@@ -481,6 +513,7 @@
481
 
482
  function onDefaultLabelEditChange(event) {
483
  editDefaultLabelVisible = false;
 
484
  const { detail } = event;
485
  let label = detail.label;
486
  let color = detail.color;
@@ -670,84 +703,84 @@
670
  if (selectedBox < 0 && value !== null && value.boxes.length > 0) {
671
  selectBox(0);
672
  }
 
673
  setImage();
674
  resize();
675
  draw();
676
  });
677
-
678
- function handleCanvasFocus() {
679
- document.addEventListener("keydown", handleKeyPress);
680
- }
681
-
682
- function handleCanvasBlur() {
683
- document.removeEventListener("keydown", handleKeyPress);
684
- }
685
-
686
- onDestroy(() => {
687
- document.removeEventListener("keydown", handleKeyPress);
688
- });
689
 
690
  </script>
691
 
 
 
692
  <div
693
- class="canvas-container"
694
- tabindex="-1"
695
- on:focusin={handleCanvasFocus}
696
- on:focusout={handleCanvasBlur}
 
697
  >
698
- <canvas
699
- bind:this={canvas}
700
- on:pointerdown={handlePointerDown}
701
- on:pointerup={handlePointerUp}
702
- on:pointermove={handlePointerMove}
703
- on:pointercancel={handlePointerCancel}
704
- on:dblclick={handleDoubleClick}
705
- on:wheel={handleMouseWheel}
706
- style="height: {height}; width: {width};"
707
- class="canvas-annotator"
708
- ></canvas>
709
- </div>
710
-
711
- {#if interactive}
712
- <span class="canvas-control">
713
- <button
714
- class="icon"
715
- class:selected={mode === Mode.creation}
716
- aria-label="Create box"
717
- on:click={() => setCreateMode()}><BoundingBox/></button
718
- >
719
- <button
720
- class="icon"
721
- class:selected={mode === Mode.drag}
722
- aria-label="Edit boxes"
723
- on:click={() => setDragMode()}><Hand/></button
724
- >
725
- {#if showRemoveButton}
726
  <button
727
  class="icon"
728
- aria-label="Remove boxes"
729
- on:click={() => onDeleteBox()}><Trash/></button
 
 
730
  >
731
- {/if}
732
- {#if !disableEditBoxes && labelDetailLock}
733
  <button
734
  class="icon"
735
- aria-label="Edit label"
736
- on:click={() => editDefaultLabelVisible = true}><Label/></button
 
 
737
  >
738
- {/if}
739
- <button
740
- class="icon"
741
- aria-label="Rotate counterclockwise"
742
- on:click={() => onRotateImage(-1)}><Undo/></button
743
- >
744
- <button
745
- class="icon"
746
- aria-label="Rotate clockwise"
747
- on:click={() => onRotateImage(1)}><Redo/></button
748
- >
749
- </span>
750
- {/if}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
751
 
752
  {#if editModalVisible}
753
  <ModalBox
 
1
  <script lang="ts">
2
+ import { onMount, createEventDispatcher } from "svelte";
3
  import { BoundingBox, Hand, Trash, Label } from "./icons/index";
4
  import ModalBox from "./ModalBox.svelte";
5
  import Box from "./Box";
 
27
  export let showRemoveButton: boolean = null;
28
  export let handlesCursor: boolean = true;
29
  export let useDefaultLabel: boolean = false;
30
+ export let enableKeyboardShortcuts: boolean = true;
31
 
32
  if (showRemoveButton === null) {
33
  showRemoveButton = (disableEditBoxes);
34
  }
35
 
36
  let canvas: HTMLCanvasElement;
37
+ let annotatorContainerDiv: HTMLDivElement;
38
  let ctx: CanvasRenderingContext2D;
39
  let image = null;
40
  let selectedBox = -1;
 
311
  }
312
  }
313
 
314
+ function resetView() {
315
+ // Calculate minimum scale to fit image
316
+ const scaleX = canvas.width / imageWidth;
317
+ const scaleY = canvas.height / imageHeight;
318
+ const minScale = Math.min(scaleX, scaleY);
319
+
320
+ // Set scale and center
321
+ canvasWindow.scale = minScale;
322
+ canvasWindow.offsetX = (canvas.width - imageWidth * minScale) / 2;
323
+ canvasWindow.offsetY = (canvas.height - imageHeight * minScale) / 2;
324
+
325
+ draw();
326
+ }
327
+
328
  function handleKeyPress(event: KeyboardEvent) {
329
+ if (!enableKeyboardShortcuts || event.target !== annotatorContainerDiv || !interactive) {
330
  return;
331
  }
332
+
333
+ const key = event.key.toLowerCase();
334
+ const blockedKeys = new Set(['delete', 'c', 'd', 'e', ' ']);
335
 
336
+ if (blockedKeys.has(key)) {
337
+ event.preventDefault();
338
+ event.stopPropagation();
 
339
  }
340
+
341
+ switch (key) {
342
+ case 'delete': onDeleteBox(); break;
343
+ case 'c': setCreateMode(); break;
344
+ case 'd': setDragMode(); break;
345
+ case 'e': onEditBox(); break;
346
+ case ' ': resetView(); break;
347
+ }
348
+ }
349
+
350
+ function focusAnnotator() {
351
+ setTimeout(() => {annotatorContainerDiv?.focus();}, 0);
352
  }
353
 
354
  function handleMouseWheel(event: WheelEvent) {
 
469
 
470
  function onModalEditChange(event) {
471
  editModalVisible = false;
472
+ focusAnnotator();
473
  const { detail } = event;
474
  let label = detail.label;
475
  let color = detail.color;
 
489
 
490
  function onModalNewChange(event) {
491
  newModalVisible = false;
492
+ focusAnnotator();
493
  const { detail } = event;
494
  let label = detail.label;
495
  let color = detail.color;
 
513
 
514
  function onDefaultLabelEditChange(event) {
515
  editDefaultLabelVisible = false;
516
+ focusAnnotator();
517
  const { detail } = event;
518
  let label = detail.label;
519
  let color = detail.color;
 
703
  if (selectedBox < 0 && value !== null && value.boxes.length > 0) {
704
  selectBox(0);
705
  }
706
+
707
  setImage();
708
  resize();
709
  draw();
710
  });
 
 
 
 
 
 
 
 
 
 
 
 
711
 
712
  </script>
713
 
714
+ <!-- svelte-ignore a11y-no-noninteractive-tabindex -->
715
+ <!-- svelte-ignore a11y-no-static-element-interactions -->
716
  <div
717
+ class="annotator-container"
718
+ tabindex="0"
719
+ bind:this={annotatorContainerDiv}
720
+ on:keydown={handleKeyPress}
721
+ on:click={() => annotatorContainerDiv.focus()}
722
  >
723
+ <div class="canvas-container">
724
+ <canvas
725
+ bind:this={canvas}
726
+ on:pointerdown={handlePointerDown}
727
+ on:pointerup={handlePointerUp}
728
+ on:pointermove={handlePointerMove}
729
+ on:pointercancel={handlePointerCancel}
730
+ on:dblclick={handleDoubleClick}
731
+ on:wheel={handleMouseWheel}
732
+ style="height: {height}; width: {width};"
733
+ class="canvas-annotator"
734
+ ></canvas>
735
+ </div>
736
+
737
+ {#if interactive}
738
+ <span class="canvas-control">
 
 
 
 
 
 
 
 
 
 
 
 
739
  <button
740
  class="icon"
741
+ class:selected={mode === Mode.creation}
742
+ aria-label="Create box"
743
+ title="Create box (C)"
744
+ on:click={() => setCreateMode()}><BoundingBox/></button
745
  >
 
 
746
  <button
747
  class="icon"
748
+ class:selected={mode === Mode.drag}
749
+ aria-label="Drag boxes"
750
+ title="Drag boxes (D)"
751
+ on:click={() => setDragMode()}><Hand/></button
752
  >
753
+ {#if showRemoveButton}
754
+ <button
755
+ class="icon"
756
+ aria-label="Remove box"
757
+ title="Remove box (Del)"
758
+ on:click={() => onDeleteBox()}><Trash/></button
759
+ >
760
+ {/if}
761
+ {#if !disableEditBoxes && labelDetailLock}
762
+ <button
763
+ class="icon"
764
+ aria-label="Edit label"
765
+ title="Edit label"
766
+ on:click={() => editDefaultLabelVisible = true}><Label/></button
767
+ >
768
+ {/if}
769
+ <button
770
+ class="icon"
771
+ aria-label="Rotate counterclockwise"
772
+ title="Rotate counterclockwise"
773
+ on:click={() => onRotateImage(-1)}><Undo/></button
774
+ >
775
+ <button
776
+ class="icon"
777
+ aria-label="Rotate clockwise"
778
+ title="Rotate clockwise"
779
+ on:click={() => onRotateImage(1)}><Redo/></button
780
+ >
781
+ </span>
782
+ {/if}
783
+ </div>
784
 
785
  {#if editModalVisible}
786
  <ModalBox
src/frontend/shared/ImageAnnotator.svelte CHANGED
@@ -42,6 +42,7 @@
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;
@@ -189,6 +190,7 @@
189
  {handlesCursor}
190
  {boxSelectedThickness}
191
  {useDefaultLabel}
 
192
  src={value.image.url}
193
  />
194
  </div>
 
42
  export let cli_upload: Client["upload"];
43
  export let stream_handler: Client["stream_factory"];
44
  export let useDefaultLabel: boolean;
45
+ export let enableKeyboardShortcuts: boolean;
46
 
47
  let upload: Upload;
48
  let uploading = false;
 
190
  {handlesCursor}
191
  {boxSelectedThickness}
192
  {useDefaultLabel}
193
+ {enableKeyboardShortcuts}
194
  src={value.image.url}
195
  />
196
  </div>
src/frontend/shared/ImageCanvas.svelte CHANGED
@@ -26,6 +26,7 @@
26
  export let showRemoveButton: boolean;
27
  export let handlesCursor: boolean;
28
  export let useDefaultLabel: boolean;
 
29
 
30
  let resolved_src: typeof src;
31
 
@@ -74,5 +75,6 @@
74
  {showRemoveButton}
75
  {handlesCursor}
76
  {useDefaultLabel}
 
77
  imageUrl={resolved_src}
78
  />
 
26
  export let showRemoveButton: boolean;
27
  export let handlesCursor: boolean;
28
  export let useDefaultLabel: boolean;
29
+ export let enableKeyboardShortcuts: boolean;
30
 
31
  let resolved_src: typeof src;
32
 
 
75
  {showRemoveButton}
76
  {handlesCursor}
77
  {useDefaultLabel}
78
+ {enableKeyboardShortcuts}
79
  imageUrl={resolved_src}
80
  />
src/frontend/shared/ModalBox.svelte CHANGED
@@ -3,7 +3,7 @@
3
  import { BaseButton } from "@gradio/button";
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 = "";
@@ -59,10 +59,13 @@
59
  }
60
 
61
  function handleKeyPress(event: KeyboardEvent) {
62
- switch (event.key) {
63
- case "Enter":
64
  dispatchChange(1);
65
  break;
 
 
 
66
  }
67
  }
68
 
@@ -70,11 +73,11 @@
70
  document.addEventListener("keydown", handleKeyPress);
71
  currentLabel = label;
72
  currentColor = color;
 
 
 
 
73
  });
74
-
75
- onDestroy(() => {
76
- document.removeEventListener("keydown", handleKeyPress);
77
- });
78
 
79
  </script>
80
 
 
3
  import { BaseButton } from "@gradio/button";
4
  import { BaseDropdown } from "./patched_dropdown/Index.svelte";
5
  import { createEventDispatcher } from "svelte";
6
+ import { onMount } from "svelte";
7
  import { Lock, Unlock } from "./icons/index";
8
 
9
  export let label = "";
 
59
  }
60
 
61
  function handleKeyPress(event: KeyboardEvent) {
62
+ switch (event.key.toLowerCase()) {
63
+ case "enter":
64
  dispatchChange(1);
65
  break;
66
+ case "escape":
67
+ dispatchChange(0);
68
+ break;
69
  }
70
  }
71
 
 
73
  document.addEventListener("keydown", handleKeyPress);
74
  currentLabel = label;
75
  currentColor = color;
76
+
77
+ return () => {
78
+ document.removeEventListener("keydown", handleKeyPress);
79
+ };
80
  });
 
 
 
 
81
 
82
  </script>
83
 
src/pyproject.toml CHANGED
@@ -8,7 +8,7 @@ build-backend = "hatchling.build"
8
 
9
  [project]
10
  name = "gradio_image_annotation"
11
- version = "0.3.1"
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.4.0"
12
  description = "A Gradio component that can be used to annotate images with bounding boxes."
13
  readme = "README.md"
14
  license = "MIT"