Using annotation data
This package contains a sole component named
dpa.DashPictureAnnotation
. To use this
component, we can start from a minimal example:
import dash
from dash import html
import dash_picture_annotation as dpa
app = dash.Dash()
app.layout = html.Div(
(
dpa.DashPictureAnnotation(
id="annotator",
data=None,
image="/site-address/to/an/image",
),
)
)
Here, we let the data
initialized by None
, which means that no annotations are
defined when the component is loaded.
Users can interact with the annotator with
- Dragging and dropping for creating new annotation boxes;
- Selecting an annotation box and specify its comment;
- Selecting an annotation box, resizing, and relocating it;
- Delete any annotation box.
Catch the update of the annotation data
Every time when the mouse is released, the annotation data will be updated. This update will fire a dash callback. Use the following codes to catch the callback.
app.layout = html.Div(
(
dpa.DashPictureAnnotation(id="annotator", ...),
html.Div(id="output"),
)
)
@app.callback(
Output("output", "children"), Input("annotator", "data"), prevent_initial_call=False
)
def get_annotation(data: dpa.Annotations | None) -> Optional[str]:
if data is None:
return None
return json.dumps(data)
Typically, the data
returned by the annotator is typed by
dpa.Annotations
. However
there are two exceptions:
data
can be initialized by None
or an unexpected value. Passing a data
that
cannot be typed by dpa.Annotations
to the
initialization of DashPictureAnnotation
is allowed. In this case, the firstly loaded
value captured by the callback will be exactly the same as the value used for
initializing the component.
data
can be updated by a callback using Output("annotator", "data")
. In this case,
users may provide a value not typed by
dpa.Annotations
. The subsequently fired
callback using this value as the input will be influenced.
Fortunately, the above example will not cause such issues because
- The
data
is initialized byNone
, such a value is already correctly handled by the callback. - The
data
would not be updated by any callback. In other words, it is only updated by user interactions. All updates fired by user interactions provide adata
typed bydpa.Annotations
.
Sanitization
The robustness of the callbacks capturing or updating the annotation can be improved by sanitization. Take the above codes as an example, we make a few modifications to let the data sanitized.
@app.callback(
Output("output", "children"), Input("annotator", "data"), prevent_initial_call=False
)
def get_annotation(data: dpa.Annotations | None) -> Optional[str]:
if data is None:
return None
if not dpa.is_annotations(data):
return json.dumps(dpa.sanitize_data(data))
return json.dumps(data)
This modification ensures that the returned value is always serializable even if
another callback modifies data
without following the typehint dpa.Annotations
.
The added lines provide the following functionalities:
- Check whether the
data
is formatted asdpa.Annotations
. If not, usedpa.sanitize_data(...)
to sanitize the data. - If the
data
is already formatted asdpa.Annotation
, return the serialized JSON string directly.
To verify this, users can modify the initialization and let data=1
which supposes
to be not JSON-serializable. With the sanitization, the callback stills works.
When defining a callback updating data
, or saving the data
as a file, the method
dpa.sanitize_data
can be used, too. It can
exempt the inappropriate modifications made by users.
Add a reset button
The property data
can be set by either the user interaction or the callback.
By adding another callback fired by a reset button, we can implement the feature of
clearing all annotations.
app.layout = html.Div(
(
html.Div(html.Button(id="btn-reset", children="Reset")),
dpa.DashPictureAnnotation(id="annotator", ...),
html.Div(id="output"),
)
)
@app.callback(
Output("annotator", "data"),
Input("btn-reset", "n_clicks"),
prevent_initial_call=True,
)
def reset_data(n_clicks: Optional[int]):
if n_clicks:
return dpa.sanitize_data([])
return dash.no_update
In this example, we provides a button triggering the updating of the data. By using
dpa.sanitize_data
, the updated value will
be sanitized and typed by dpa.Annotations
.
Clicking this button will remove all annotations.
Get or change data by IDs
Each annotation box created by user interactions will by marked by a randomly-generated
ID. These unique IDs can be used for locating a specific annotation box. Suppose that
a new button is added for getting an annotation box for with a specific ID, and update
the annotation's comment. We can use
dpa.get_data_item
to locate the item by
its ID.
app.layout = html.Div(
(
html.Div(html.Button(id="btn-getbyid", children="Get by ID")),
dpa.DashPictureAnnotation(id="annotator", ...),
html.Div(id="output"),
)
)
@app.callback(
Output("annotator", "data"),
Input("btn-getbyid", "n_clicks"),
State("annotator", "data"),
prevent_initial_call=True,
)
def change_data_by_id(n_clicks: Optional[int], data: dpa.Annotations):
if n_clicks:
all_ids = dpa.get_all_ids(data) # A sequence of IDs.
data_item = dpa.get_data_item(data, all_ids[-1])
data_item["comment"] = "new-comment"
return data
return dash.no_update
In this example, we modify the located data item. Any modification on the item will make the data changed in place. Therefore, returning the data directly will make the annotations changed.
Get or search data by comment
Another useful functionality is to query the data by the comment. Since different
annotation data items may have the same comment, the method
dpa.get_data_items
is used for locating
all annotation items with a specific comment, for example
app.layout = html.Div(
(
html.Div(html.Button(id="btn-getbycomment", children="Get by comment")),
dpa.DashPictureAnnotation(id="annotator", ...),
html.Div(id="output"),
html.Div(id="comment-ids"),
)
)
@app.callback(
Output("comment-ids", "children"),
Input("btn-getbycomment", "n_clicks"),
State("annotator", "data"),
prevent_initial_call=True,
)
def get_data_by_comment(n_clicks: Optional[int], data: dpa.Annotations):
if n_clicks:
all_comments = dpa.get_all_comments(data) # A frozen set of comments.
data_items = dpa.get_data_items(data, next(iter(all_comments)))
comment_ids = dpa.get_all_ids(data_items) # A sequence of ids of `data_items`.
return json.dumps(comment_ids)
return dash.no_update
dpa.get_data_items
can only locate the
items with a specific comment. There is another version used for searching the comments
by the regular expression, see
dpa.get_data_items_by_regex
.
@app.callback(
Output("comment-ids", "children"),
Input("btn-getbycomment", "n_clicks"),
State("annotator", "data"),
prevent_initial_call=True,
)
def get_data_by_comment(n_clicks: Optional[int], data: dpa.Annotations):
if n_clicks:
all_comments = dpa.get_all_comments(data) # A frozen set of comments.
data_items = dpa.get_data_items_by_regex(data, r"^type-.*"))
comment_ids = dpa.get_all_ids(data_items) # A sequence of ids of `data_items`.
return json.dumps(comment_ids)
return dash.no_update
The above version will find all items with a comment starting with type-
.
Certainly, changing data_items
will make the data
changed in place. The usages are
the same as the example of using get_data_item
.