使用标记数据
此模块包内含一个单独的组件dpa.DashPictureAnnotation。要使用该组件,可以从以下最小范例入手:
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",
        ),
    )
)
此处,令data初始化为None,从而在组件加载的时候,不设置任何标记。
用户可以透过以下方式和标记器交互:
- 拖拽创建标记标记框;
 - 选中某个标记框,并为其设置文字标注(comment);
 - 选中某个标记框,修改其大小或位置;
 - 删除某个标记框。
 
捕获标记数据的更新
每当鼠标松开时,标记数据就会更新。该更新会触发一个dash callback。使用以下代码来捕获该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)
通常,标记器返回的属性data,其类型可以标记为dpa.Annotations。然而,此处有两个值得关注的例外:
data可以初始化为None或者某个预期之外的值。换言之,初始化DashPictureAnnotation时,其实允许向data传入一个无法标记为dpa.Annotations类型的值。在这种情况下,callback初次捕获到的数据,将会和初始化组件时、传入的数据完全相同。
可以透过在某callback设置Output("annotator", "data"),来更新属性data。在这种情况下,用户可能会提供一个不可标记为dpa.Annotations的值。而透过这次更新触发的其他callback,将会受到这种不标准的输入值的影响。
万幸的是,上例并不会引发这样的问题。这乃是由于
- 属性
data初始化为None,该特殊情况已经在callback的定义里进行了妥善处理。 - 属性
data不会受到来自其他callback的更新。换言之,上例中该值仅仅会受到用户交互的更新。所有由用户交互触发的callback,其data值都是妥善标记为dpa.Annotations的。 
数据清理
为了提高那些用来捕获、或是更新标记数据的callback的可靠性,可以使用数据清理功能。援引上例,可以透过一些微调,来确保数据总是整理完备的。
@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)
此处对代码的修改,保证了即便在data会被某个未知的callback修改为不符合dpa.Annotations标准的情况下,返回的值也总是可序列化的(serializable)。新追加的几行代码,提供了以下功能:
- 检验
data的格式是否符合dpa.Annotations。若不符,则透过dpa.sanitize_data(...)清理数据。 - 若
data已经符合dpa.Annotation的格式,则直接返回其JSON序列化后的字符串。 
要验证其效果,用户只需在初始化的时候,设置参数data=1即可。该值无法直接JSON序列化。但透过数据清理,callback仍然能正常工作。
每当要定义一个更新属性data的callback,或需要将data保存为文件时,可以使用dpa.sanitize_data方法。它可以确保排除那些用户做出的、不适当的修改。
追加重置按钮
来自用户的交互、和callback,均可以更新属性data。透过添加一个由重置按钮触发的callback,可以实现“清除所有标记数据”的功能。
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
上例中,提供了一个由按钮触发、并更新数据的callback。透过使用dpa.sanitize_data,会清理callback更新的数据,并确保其类型为dpa.Annotations。按下按钮会删去标记器上的所有标记。
透过ID来获取或修改数据
每次用户交互创建一个新的标记框时,其都会自带一个随机生成的ID。这些唯一ID可以用来获取某个标记框。考虑这样的场景:添加一个按钮,按下时使用某ID获取某标记框的数据,并修改其文字标注的内容。要实现它,可以使用dpa.get_data_item来完成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)  # 一个IDs构成的序列。
        data_item = dpa.get_data_item(data, all_ids[-1])
        data_item["comment"] = "new-comment"
        return data
    return dash.no_update
上例中,修改了所定位到的数据项。任何针对所获取的数据项的修改,都是会引起原数据改变的“原处修改”。因此,可以直接返回原处修改后的数据本身,从而修改标记器上的标记。
透过文字标注获取或搜索数据
另一有用的功能是透过文字标注来查询数据。由于不同的标记数据项、可能有相同的文字标注,方法dpa.get_data_items总是会返回具备某个特定文字标注的所有标记数据项。例如,
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)  # 一个文字标注构成的不可变集合。
        data_items = dpa.get_data_items(data, next(iter(all_comments)))
        comment_ids = dpa.get_all_ids(data_items)  # 一个由`data_items`下ID构成的序列。
        return json.dumps(comment_ids)
    return dash.no_update
方法dpa.get_data_items只能用来定位具有某个特定文字标注的数据项。还有另一个、使用正则表达式搜索某些文字标注的版本,参见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)  # 一个文字标注构成的不可变集合。
        data_items = dpa.get_data_items_by_regex(data, r"^type-.*"))
        comment_ids = dpa.get_all_ids(data_items)  # 一个由`data_items`下ID构成的序列。
        return json.dumps(comment_ids)
    return dash.no_update
以上版本将会搜索所有“具备以type-开头的文字标注”的标记数据项。
当然,修改data_items同样会使得data原处修改。这种用法和上面使用get_data_item的例子相同。