Upload folder using huggingface_hub
Browse files- README.md +29 -20
- app.py +3 -0
- space.py +19 -6
- src/README.md +29 -3
- src/backend/gradio_image_annotation/image_annotator.py +21 -6
- src/backend/gradio_image_annotation/templates/component/index.js +0 -0
- src/backend/gradio_image_annotation/templates/component/style.css +1 -1
- src/demo/app.py +3 -0
- src/demo/space.py +19 -6
- src/frontend/Index.svelte +2 -0
- src/frontend/shared/AnnotatedImageData.ts +1 -0
- src/frontend/shared/Box.ts +158 -104
- src/frontend/shared/Canvas.svelte +173 -13
- src/frontend/shared/ImageAnnotator.svelte +2 -0
- src/frontend/shared/ImageCanvas.svelte +2 -0
- src/frontend/shared/ModalBox.svelte +35 -0
- src/frontend/shared/WindowViewer.ts +95 -0
- src/frontend/shared/icons/Label.svelte +15 -0
- src/frontend/shared/icons/Lock.svelte +14 -0
- src/frontend/shared/icons/Unlock.svelte +14 -0
- src/frontend/shared/icons/index.ts +7 -4
- src/images/demo.png +2 -2
- src/pyproject.toml +1 -1
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:
|
572 |
-
) ->
|
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 |
+

|
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': '
|
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:
|
173 |
-
) ->
|
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:
|
555 |
-
) ->
|
556 |
return value
|
557 |
```
|
558 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
4 |
|
5 |
A Gradio component that can be used to annotate images with bounding boxes.
|
6 |
|
7 |
+

|
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) ->
|
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:
|
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 |
-
|
|
|
|
|
|
|
|
|
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': '
|
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:
|
173 |
-
) ->
|
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.
|
84 |
-
this.
|
85 |
-
this.
|
86 |
-
this.
|
|
|
|
|
|
|
|
|
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.
|
107 |
-
ymin: this.
|
108 |
-
xmax: this.
|
109 |
-
ymax: this.
|
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.
|
122 |
-
this.
|
123 |
-
this.
|
124 |
-
this.
|
125 |
-
this.
|
|
|
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.
|
281 |
-
this.offsetMouseY = event.clientY - this.
|
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.
|
295 |
-
let deltaY = event.clientY - this.offsetMouseY - this.
|
296 |
-
|
297 |
-
const
|
298 |
-
|
299 |
-
|
300 |
-
this.
|
301 |
-
this.
|
302 |
-
this.
|
303 |
-
this.
|
304 |
-
this.
|
|
|
|
|
|
|
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
|
348 |
-
y
|
349 |
|
350 |
-
if (x > this.
|
351 |
if (this.creatingAnchorX == "xmax") {
|
352 |
-
this.
|
353 |
}
|
354 |
-
this.
|
355 |
this.creatingAnchorX = "xmin";
|
356 |
-
} else if (x > this.
|
357 |
-
this.
|
358 |
-
} else if (x > this.
|
359 |
-
this.
|
360 |
-
} else if (x < this.
|
361 |
if (this.creatingAnchorX == "xmin") {
|
362 |
-
this.
|
363 |
}
|
364 |
-
this.
|
365 |
this.creatingAnchorX = "xmax";
|
366 |
}
|
367 |
|
368 |
-
if (y > this.
|
369 |
if (this.creatingAnchorY == "ymax") {
|
370 |
-
this.
|
371 |
}
|
372 |
-
this.
|
373 |
this.creatingAnchorY = "ymin";
|
374 |
-
} else if (y > this.
|
375 |
-
this.
|
376 |
-
} else if (y > this.
|
377 |
-
this.
|
378 |
-
} else if (y < this.
|
379 |
if (this.creatingAnchorY == "ymin") {
|
380 |
-
this.
|
381 |
}
|
382 |
-
this.
|
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.
|
400 |
-
this.
|
401 |
-
this.
|
402 |
-
this.
|
403 |
|
404 |
if (this.minSize > 0) {
|
405 |
-
if (this.getWidth() < this.minSize) {
|
406 |
if (this.creatingAnchorX == "xmin") {
|
407 |
-
this.
|
408 |
} else {
|
409 |
-
this.
|
410 |
}
|
411 |
}
|
412 |
-
if (this.getHeight() < this.minSize) {
|
413 |
if (this.creatingAnchorY == "ymin") {
|
414 |
-
this.
|
415 |
} else {
|
416 |
-
this.
|
417 |
}
|
418 |
}
|
419 |
-
if (this.
|
420 |
-
this.
|
421 |
-
this.
|
422 |
-
} else if (this.
|
423 |
-
this.
|
424 |
-
this.
|
425 |
}
|
426 |
-
if (this.
|
427 |
-
this.
|
428 |
-
this.
|
429 |
-
} else if (this.
|
430 |
-
this.
|
431 |
-
this.
|
432 |
}
|
433 |
}
|
434 |
-
this.
|
|
|
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
|
454 |
-
const deltaY = mouseY - this.resizeHandles[this.resizingHandleIndex].ymin
|
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.
|
460 |
-
this.
|
461 |
-
this.
|
462 |
-
this.
|
463 |
break;
|
464 |
case 1: // Top-right handle
|
465 |
-
this.
|
466 |
-
this.
|
467 |
-
this.
|
468 |
-
this.
|
469 |
break;
|
470 |
case 2: // Bottom-right handle
|
471 |
-
this.
|
472 |
-
this.
|
473 |
-
this.
|
474 |
-
this.
|
475 |
break;
|
476 |
case 3: // Bottom-left handle
|
477 |
-
this.
|
478 |
-
this.
|
479 |
-
this.
|
480 |
-
this.
|
481 |
break;
|
482 |
case 4: // Top center handle
|
483 |
-
this.
|
484 |
-
this.
|
485 |
break;
|
486 |
case 5: // Right center handle
|
487 |
-
this.
|
488 |
-
this.
|
489 |
break;
|
490 |
case 6: // Bottom center handle
|
491 |
-
this.
|
492 |
-
this.
|
493 |
break;
|
494 |
case 7: // Left center handle
|
495 |
-
this.
|
496 |
-
this.
|
497 |
break;
|
498 |
}
|
|
|
499 |
// Update the resize handles
|
500 |
-
this.
|
|
|
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 =
|
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 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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 -
|
186 |
-
const y = (event.clientY - rect.top -
|
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 |
-
|
|
|
|
|
|
|
|
|
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 (
|
324 |
-
scaleFactor = canvas.width /
|
325 |
-
imageWidth =
|
326 |
-
imageHeight =
|
327 |
canvasXmin = 0;
|
328 |
canvasYmin = 0;
|
329 |
canvasXmax = imageWidth;
|
330 |
canvasYmax = imageHeight;
|
331 |
canvas.height = imageHeight;
|
332 |
} else {
|
333 |
-
imageWidth =
|
334 |
-
imageHeight =
|
335 |
var x = (canvas.width - imageWidth) / 2;
|
336 |
canvasXmin = x;
|
337 |
canvasYmin = 0;
|
338 |
canvasXmax = x + imageWidth;
|
339 |
-
canvasYmax =
|
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
|
![]() |
Git LFS Details
|
src/pyproject.toml
CHANGED
@@ -8,7 +8,7 @@ build-backend = "hatchling.build"
|
|
8 |
|
9 |
[project]
|
10 |
name = "gradio_image_annotation"
|
11 |
-
version = "0.2.
|
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"
|