annodesk 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
annodesk/__init__.py ADDED
@@ -0,0 +1,3 @@
1
+ """AnnoDesk image annotation workbench."""
2
+
3
+ __version__ = "0.1.0"
annodesk/__main__.py ADDED
@@ -0,0 +1,3 @@
1
+ from annodesk.cli import main
2
+
3
+ main()
annodesk/app.py ADDED
@@ -0,0 +1,83 @@
1
+ """Application factory for combined, API-only and UI-only deployment."""
2
+
3
+ from pathlib import Path
4
+ from typing import Literal
5
+
6
+ from fastapi import FastAPI, Request
7
+ from fastapi.middleware.cors import CORSMiddleware
8
+ from fastapi.responses import FileResponse, JSONResponse
9
+ from fastapi.staticfiles import StaticFiles
10
+
11
+ from annodesk import __version__
12
+ from annodesk.storage import RevisionConflict
13
+
14
+
15
+ class WorkbenchFiles(StaticFiles):
16
+ """Never reuse entry HTML across builds, including conditional requests."""
17
+
18
+ def file_response(self, full_path, stat_result, scope, status_code=200):
19
+ if Path(full_path).suffix.lower() == ".html":
20
+ response = FileResponse(
21
+ full_path,
22
+ status_code=status_code,
23
+ stat_result=stat_result,
24
+ headers={"Cache-Control": "no-store"},
25
+ )
26
+ # Avoid 304 responses that keep a browser's old asset manifest alive.
27
+ for name in ("etag", "last-modified"):
28
+ if name in response.headers:
29
+ del response.headers[name]
30
+ return response
31
+ return super().file_response(full_path, stat_result, scope, status_code)
32
+
33
+
34
+ def create_app(
35
+ mode: Literal["serve", "api", "ui"] = "serve",
36
+ api_url: str = "",
37
+ project: Path | None = None,
38
+ models_config: Path | None = None,
39
+ ) -> FastAPI:
40
+ app = FastAPI(title="AnnoDesk", version=__version__)
41
+ app.add_middleware(
42
+ CORSMiddleware, allow_origins=["*"], allow_methods=["*"], allow_headers=["*"]
43
+ )
44
+
45
+ @app.middleware("http")
46
+ async def fresh_json(request: Request, call_next):
47
+ response = await call_next(request)
48
+ if response.headers.get("content-type", "").startswith("application/json"):
49
+ response.headers["Cache-Control"] = "no-store"
50
+ return response
51
+
52
+ @app.exception_handler(ValueError)
53
+ async def invalid(_request: Request, error: ValueError):
54
+ return JSONResponse(
55
+ {"detail": str(error)}, status_code=409 if isinstance(error, RevisionConflict) else 400
56
+ )
57
+
58
+ @app.exception_handler(KeyError)
59
+ async def missing(_request: Request, error: KeyError):
60
+ return JSONResponse({"detail": str(error)}, status_code=404)
61
+
62
+ if mode != "ui":
63
+ from annodesk.routes import project_routes
64
+
65
+ app.include_router(project_routes(project, models_config))
66
+
67
+ @app.get("/api/v1/info")
68
+ def info():
69
+ return {"version": __version__, "protocol": 1, "name": "AnnoDesk"}
70
+
71
+ if mode != "api":
72
+
73
+ @app.get("/ui-config.json")
74
+ def ui_config():
75
+ base = api_url.rstrip("/")
76
+ return JSONResponse(
77
+ {"apiBase": f"{base}/api/v1"}, headers={"Cache-Control": "no-store"}
78
+ )
79
+
80
+ app.mount(
81
+ "/", WorkbenchFiles(directory=Path(__file__).parent / "web_dist", html=True), name="web"
82
+ )
83
+ return app
annodesk/batch.py ADDED
@@ -0,0 +1,39 @@
1
+ """Synchronous box-to-mask conversion without an HTTP server or browser."""
2
+
3
+ import json
4
+ from pathlib import Path
5
+
6
+ from annodesk.inference import SegmentationService, SegmentRequest, UltralyticsAdapter
7
+ from annodesk.models import ModelRegistry
8
+ from annodesk.storage import Project, atomic_write
9
+
10
+
11
+ def segment_dataset(project: Path, model: str, registry: ModelRegistry) -> dict:
12
+ # Fail before touching dataset metadata if the selected checkpoint is missing.
13
+ registry.weights(model)
14
+ dataset = Project(project)
15
+ service = SegmentationService(dataset, adapter=UltralyticsAdapter(registry))
16
+ report = {"model": registry.entry(model).id, "processed": 0, "skipped": 0, "failed": []}
17
+ report_path = dataset.private / "segment-report.json"
18
+ try:
19
+ for image_id in dataset.entries:
20
+ try:
21
+ annotation = dataset.annotation(image_id)
22
+ if annotation.excluded or not any(
23
+ o.box and not o.mask and not o.polygons and not o.locked
24
+ for o in annotation.instances
25
+ ):
26
+ report["skipped"] += 1
27
+ else:
28
+ service.process(image_id, SegmentRequest(model=model))
29
+ report["processed"] += 1
30
+ except Exception as error:
31
+ report["failed"].append(
32
+ {"image": dataset.entries[image_id]["path"], "error": str(error)}
33
+ )
34
+ atomic_write(report_path, json.dumps(report, indent=2))
35
+ print(json.dumps({"image": dataset.entries[image_id]["path"], **report}), flush=True)
36
+ finally:
37
+ service.close()
38
+ atomic_write(report_path, json.dumps(report, indent=2))
39
+ return report
annodesk/cli.py ADDED
@@ -0,0 +1,136 @@
1
+ """Typed console commands. Heavy inference imports are intentionally lazy."""
2
+
3
+ import importlib.util
4
+ import json
5
+ import subprocess
6
+ import webbrowser
7
+ from pathlib import Path
8
+ from typing import Annotated, Literal
9
+
10
+ import tyro
11
+
12
+
13
+ def serve(
14
+ project: Path | None = None,
15
+ host: str = "127.0.0.1",
16
+ port: int = 8765,
17
+ open: bool = False,
18
+ models_config: Path | None = None,
19
+ ) -> None:
20
+ """Start the annotation API and bundled workbench."""
21
+ import uvicorn
22
+
23
+ from annodesk.app import create_app
24
+
25
+ if open:
26
+ webbrowser.open(f"http://{host}:{port}")
27
+ uvicorn.run(create_app(project=project, models_config=models_config), host=host, port=port)
28
+
29
+
30
+ def api(
31
+ project: Path | None = None,
32
+ host: str = "127.0.0.1",
33
+ port: int = 8765,
34
+ models_config: Path | None = None,
35
+ ) -> None:
36
+ """Start only the API; dataset paths refer to this machine."""
37
+ import uvicorn
38
+
39
+ from annodesk.app import create_app
40
+
41
+ uvicorn.run(
42
+ create_app(mode="api", project=project, models_config=models_config), host=host, port=port
43
+ )
44
+
45
+
46
+ def ui(api_url: str, host: str = "127.0.0.1", port: int = 8080) -> None:
47
+ """Host bundled UI pointing at a separately deployed API."""
48
+ import uvicorn
49
+
50
+ from annodesk.app import create_app
51
+
52
+ uvicorn.run(create_app(mode="ui", api_url=api_url), host=host, port=port)
53
+
54
+
55
+ def doctor() -> None:
56
+ """Report installed optional dependencies without importing the models."""
57
+ print(
58
+ json.dumps(
59
+ {name: bool(importlib.util.find_spec(name)) for name in ["torch", "ultralytics"]}
60
+ )
61
+ )
62
+
63
+
64
+ def build_ui() -> None:
65
+ """Install frontend dependencies and build bundled assets (requires Node/npm)."""
66
+ root = Path(__file__).resolve().parents[2]
67
+ frontend = root / "frontend"
68
+ if not (frontend / "package.json").exists():
69
+ raise SystemExit("Frontend source is not available in this installation.")
70
+ for args in (["npm", "ci"], ["npm", "run", "build"]):
71
+ subprocess.run(args, cwd=frontend, check=True)
72
+
73
+
74
+ def models(
75
+ action: Annotated[Literal["list"], tyro.conf.Positional] = "list",
76
+ models_config: Path | None = None,
77
+ ) -> None:
78
+ """List configured local models and resolution errors. Never download files."""
79
+ from annodesk.models import ModelRegistry
80
+
81
+ ModelRegistry(models_config).print_json()
82
+
83
+
84
+ def segment(project: Path, model: str = "", models_config: Path | None = None) -> None:
85
+ """Convert existing YOLO boxes to masks locally, without HTTP or a UI.
86
+
87
+ Existing masks and locked/excluded objects are skipped. Run again to continue.
88
+ Labels are written to labels_seg; original detection boxes remain in labels.
89
+ """
90
+ from annodesk.batch import segment_dataset
91
+ from annodesk.models import ModelRegistry
92
+
93
+ try:
94
+ report = segment_dataset(project, model, ModelRegistry(models_config))
95
+ except ValueError as error:
96
+ raise SystemExit(str(error)) from error
97
+ print(json.dumps(report, indent=2))
98
+ if report["failed"]:
99
+ raise SystemExit(1)
100
+
101
+
102
+ def docs(output: Path | None = None) -> None:
103
+ """Read the bundled user guide or export it to a new Markdown file."""
104
+ from importlib.resources import files
105
+
106
+ resource = files("annodesk").joinpath("docs/user-guide.md")
107
+ source = Path(__file__).resolve().parents[2] / "docs/user-guide.md"
108
+ content = (
109
+ resource.read_text(encoding="utf-8")
110
+ if resource.is_file()
111
+ else source.read_text(encoding="utf-8")
112
+ )
113
+ if output is None:
114
+ print(content, end="")
115
+ else:
116
+ try:
117
+ with output.expanduser().open("x", encoding="utf-8") as stream:
118
+ stream.write(content)
119
+ except FileExistsError as error:
120
+ raise SystemExit(f"File already exists; choose another output: {output}") from error
121
+ print(output.expanduser().resolve())
122
+
123
+
124
+ def main() -> None:
125
+ tyro.extras.subcommand_cli_from_dict(
126
+ {
127
+ "serve": serve,
128
+ "api": api,
129
+ "ui": ui,
130
+ "doctor": doctor,
131
+ "docs": docs,
132
+ "build-ui": build_ui,
133
+ "models": models,
134
+ "segment": segment,
135
+ }
136
+ )
annodesk/colors.py ADDED
@@ -0,0 +1,52 @@
1
+ """Stable perceptual color suggestions for independent class/tag palettes."""
2
+
3
+ import itertools
4
+
5
+ import numpy as np
6
+
7
+ BASE = ["#26835d", "#a16927", "#456cd6", "#9b4b88", "#287e96", "#817722"]
8
+
9
+
10
+ def oklab(colors: list[str]) -> np.ndarray:
11
+ rgb = np.array([[int(c[i : i + 2], 16) / 255 for i in (1, 3, 5)] for c in colors])
12
+ linear = np.where(rgb <= 0.04045, rgb / 12.92, ((rgb + 0.055) / 1.055) ** 2.4)
13
+ lms = (
14
+ linear
15
+ @ np.array(
16
+ [
17
+ [0.4122214708, 0.5363325363, 0.0514459929],
18
+ [0.2119034982, 0.6806995451, 0.1073969566],
19
+ [0.0883024619, 0.2817188376, 0.6299787005],
20
+ ]
21
+ ).T
22
+ )
23
+ return (
24
+ np.cbrt(lms)
25
+ @ np.array(
26
+ [
27
+ [0.2104542553, 0.7936177850, -0.0040720468],
28
+ [1.9779984951, -2.4285922050, 0.4505937099],
29
+ [0.0259040371, 0.7827717662, -0.8086757660],
30
+ ]
31
+ ).T
32
+ )
33
+
34
+
35
+ def choose_color(categories: list[dict]) -> str:
36
+ used = [c["color"] for c in categories]
37
+ for color in BASE:
38
+ if color not in used:
39
+ return color
40
+ candidates = [
41
+ "#" + "".join(f"{v:02x}" for v in rgb)
42
+ for rgb in itertools.product(range(35, 211, 35), repeat=3)
43
+ ]
44
+ lab = oklab(candidates)
45
+ chroma = np.linalg.norm(lab[:, 1:], axis=1)
46
+ valid = (lab[:, 0] > 0.43) & (lab[:, 0] < 0.69) & (chroma > 0.08) & (chroma < 0.23)
47
+ available = [c for c, keep in zip(candidates, valid, strict=True) if keep]
48
+ distances = np.full(len(available), np.inf)
49
+ coordinates = lab[valid]
50
+ for old in oklab(used):
51
+ distances = np.minimum(distances, np.linalg.norm(coordinates - old, axis=1))
52
+ return available[int(distances.argmax())]
@@ -0,0 +1,303 @@
1
+ # AnnoDesk 用户指南
2
+
3
+ AnnoDesk 用于标注图像中的检测框和实例分割 mask,可导入已有 YOLO 检测标签,再根据框生成分割结果并人工复核。界面为英文、浅色主题,在浏览器中使用;图片、模型和标注数据保存在运行后端的电脑上。
4
+
5
+ 本指南面向安装 wheel 的使用者,不需要源码仓库、Node.js 或前端构建工具。
6
+
7
+ ## 1. 安装
8
+
9
+ 需要 Python 3.11 或更新版本。建议使用独立 Python 环境:
10
+
11
+ ```sh
12
+ python -m venv .venv
13
+ ```
14
+
15
+ macOS / Linux 激活环境:
16
+
17
+ ```sh
18
+ source .venv/bin/activate
19
+ ```
20
+
21
+ Windows PowerShell 激活环境:
22
+
23
+ ```powershell
24
+ .\.venv\Scripts\Activate.ps1
25
+ ```
26
+
27
+ 安装拿到的 wheel;文件名中的版本号以实际发布文件为准:
28
+
29
+ ```sh
30
+ python -m pip install ./annodesk-0.1.0-py3-none-any.whl
31
+ ```
32
+
33
+ 基础安装即可使用框标注、类别、审核状态和数据管理。若需要 SAM 推理,显式安装可选依赖:
34
+
35
+ ```sh
36
+ python -m pip install "./annodesk-0.1.0-py3-none-any.whl[inference]"
37
+ annodesk doctor
38
+ ```
39
+
40
+ `doctor` 只检查 torch / ultralytics 是否安装,不验证权重、显卡驱动或推理结果。PyTorch 的 GPU 环境需根据实际机器自行准备;SAM 3 文本推理还需要对应的本地 tokenizer / runtime 依赖。
41
+
42
+ 安装依赖可能访问 Python 包索引;模型推理不会自动下载权重,也不会自动安装缺失依赖。
43
+
44
+ ## 2. 启动和打开数据集
45
+
46
+ ```sh
47
+ annodesk serve --project /data/my-dataset --open
48
+ ```
49
+
50
+ 默认地址为 `http://127.0.0.1:8765`。`--open` 请求打开浏览器;也可手动访问该地址。终端保持运行,按 Ctrl+C 停止服务。
51
+
52
+ 也可先启动,再在页面点击 **Open project…** 输入路径:
53
+
54
+ ```sh
55
+ annodesk serve
56
+ ```
57
+
58
+ 未打开项目时显示空白工作台属于正常状态。路径始终指向后端电脑,不是浏览器电脑。端口被占用时可换端口:
59
+
60
+ ```sh
61
+ annodesk serve --port 8766
62
+ ```
63
+
64
+ 同一数据集目前请只运行一个后端进程,避免多处同时修改。
65
+
66
+ ## 3. 准备数据集
67
+
68
+ 推荐工作目录结构:
69
+
70
+ ```text
71
+ my-dataset/
72
+ ├── images/
73
+ │ ├── train/camera_a/001.jpg
74
+ │ └── val/camera_b/002.jpg
75
+ ├── labels/ # YOLO detection 标签;可选
76
+ ├── labels_seg/ # YOLO segmentation 标签;可选
77
+ └── data.yaml # 类别名称;可选
78
+ ```
79
+
80
+ `train` / `val` / `test` 不是打开项目的必要条件,允许多级子目录。标签路径与图片在 `images/` 下的相对路径对应,例如:
81
+
82
+ ```text
83
+ images/train/camera_a/001.jpg
84
+ labels/train/camera_a/001.txt
85
+ labels_seg/train/camera_a/001.txt
86
+ ```
87
+
88
+ 图片可以尚无标签;没有 `data.yaml` 时,AnnoDesk 会创建它。已有标签但缺少类别名时初始化为 `class_0` 等占位名称。也允许直接打开图片目录;此时标签和 `.annodesk/` 会写到这个目录下,建议使用独立工作目录。
89
+
90
+ 同一目录下的 `001.jpg` 与 `001.png` 会争用同一个标签文件,需先重命名。导入已有 detection 和 segmentation 标签时,目前会分别创建实例,不自动匹配合并。
91
+
92
+ ### 工作文件与备份
93
+
94
+ - `labels/`:显式画出的检测框;mask-only 标注不会凭空生成框。
95
+ - `labels_seg/`:分割结果的 YOLO polygon 投影。
96
+ - `.annodesk/`:实例、无损 mask RLE、审核状态、提示、任务进度和恢复信息。
97
+ - `data.yaml`:类别名称等数据集配置。
98
+
99
+ 备份时保留整个工作目录,尤其不要遗漏 `.annodesk/`。YOLO polygon 不能完整表达孔洞,不能替代内部无损 mask 数据。项目已打开时不要同时用其他工具修改标签;检测到外部修改后,保存会报冲突,需要先备份并核对文件。
100
+
101
+ ## 4. 配置本地 SAM 模型
102
+
103
+ 创建自己的 `models.json`,例如:
104
+
105
+ ```json
106
+ {
107
+ "default_model": "sam2-small",
108
+ "models": [
109
+ {
110
+ "id": "sam2-small",
111
+ "name": "SAM 2.1 Small",
112
+ "backend": "sam2",
113
+ "source": "facebook/sam2.1-hiera-small"
114
+ },
115
+ {
116
+ "id": "mobile",
117
+ "name": "MobileSAM",
118
+ "backend": "mobile_sam",
119
+ "source": "/srv/models/MobileSAM"
120
+ }
121
+ ]
122
+ }
123
+ ```
124
+
125
+ - `id`:唯一标识,供 `--model` 和默认模型引用。
126
+ - `name`:页面显示名称。
127
+ - `backend`:`sam`、`sam2`、`mobile_sam` 或 `sam3`。
128
+ - `source`:模型的最终一级文件夹,不填写 `.pt` / `.safetensors` 文件,也不填写 `checkpoint` 字段。
129
+
130
+ ### 目录查找规则
131
+
132
+ 展开 `~` 和环境变量后:
133
+
134
+ 1. 绝对路径直接检查该目录,缺失则报错,不添加前缀。
135
+ 2. 相对路径先相对 JSON 所在目录解析,存在目录则使用。
136
+ 3. 相对目录不存在时,再检查 `$HF_MODELS/<source>`;`HF_MODELS` 未设置则不回退。
137
+
138
+ repo ID 与相对目录采用相同规则,不访问 Hugging Face。例如 JSON 位于 `/work/models.json`,`source` 为 `facebook/sam2.1-hiera-small`,先检查 `/work/facebook/sam2.1-hiera-small`,再检查 `$HF_MODELS/facebook/sam2.1-hiera-small`。
139
+
140
+ macOS / Linux 可设置:
141
+
142
+ ```sh
143
+ export HF_MODELS=/srv/models
144
+ ```
145
+
146
+ Windows PowerShell 可设置:
147
+
148
+ ```powershell
149
+ $env:HF_MODELS = "D:\models"
150
+ ```
151
+
152
+ 适配器在所选目录的顶层查找权重,优先匹配该 backend 的已知 `.pt` 文件名;没有已知命名时,只接受唯一的 `.pt` 文件。存在多个候选会报错,请每个目录只放一个模型。
153
+
154
+ **当前适配器使用 Ultralytics 兼容的 `.pt` 权重。** 目录来自 Hugging Face 并不代表格式一定兼容;仅含 safetensors 的目录暂不支持。既有目录中缺少兼容权重时不会偷偷换目录或下载。真实模型和加速设备兼容性需在目标机器验证。
155
+
156
+ ### 使用配置与切换模型
157
+
158
+ ```sh
159
+ annodesk models list --models-config ./models.json
160
+ annodesk serve --project /data/my-dataset --models-config ./models.json
161
+ ```
162
+
163
+ 配置查找优先级:`--models-config` > `ANNODESK_MODELS_CONFIG` 环境变量 > 当前目录 `models.json` > 源码仓库默认(若存在)> 安装包内置默认。
164
+
165
+ 建议始终显式传入自己的配置文件。页面顶部选择模型,修改 JSON 或放好模型后点击 **Reload**;`(missing)` 表示目录或兼容权重未就绪,指向提示可查看原因。模型目录存在不代表推理依赖已经全部安装。
166
+
167
+ ## 5. 在页面中标注
168
+
169
+ ### 浏览图片和设置类别
170
+
171
+ 左上图片列表支持 **Tree / Flat**、搜索和展开目录;底部缩略图可左右滚动,图片按需加载。拖动面板之间的分隔条可调整比例。
172
+
173
+ 左下 **Class palette** 是赋类入口:
174
+
175
+ - 没有选中实例时,点击类别设置新标注的默认类别。
176
+ - 点击 **Default class** 取消实例选择,回到默认类别设置。
177
+ - 选中框或 mask 后,点击类别直接修改该实例。
178
+ - 按 **C** 打开类别搜索,↑↓ 选择、Enter 确认、Esc 取消;选择模式下双击实例也可打开。
179
+
180
+ 顶部 **Manage classes** 用于增加类别、修改名称和颜色。类别 ID 平时保持稳定;**Reindex IDs…** 会修改整个项目的类别映射和标签文件,使用前备份项目。
181
+
182
+ ### 画框和生成 mask
183
+
184
+ 1. 在 Class palette 中选择类别。
185
+ 2. 使用 **Draw box** 拖出检测框,切回 **Select** 后可移动和拖动控制点调整;右侧也可编辑坐标。
186
+ 3. 选中框后按 Enter,或点击 **Refine mask**,使用当前模型生成 mask。
187
+ 4. **Segment current** 处理当前图片中尚无 mask 的未锁定检测框。
188
+
189
+ 仅需要 mask、不希望保存检测框时,使用 **Box prompt: mask only**,或用正负点建立提示,再执行分割。框提示与显式检测框分开保存。正点用于包含区域,负点用于排除区域。文本提示工具只适用于支持文本提示的 SAM 3 模型,需先设置默认类别。
190
+
191
+ 开启 **Auto-segment** 后,切换到含待处理检测框的图片会自动发起分割。**Batch…** 可对整个项目的已有框进行批量处理,在任务抽屉查看进度、错误和取消/继续操作。取消在当前图片完成后生效,继续只处理未处理图片;失败图片可在问题解决后重新发起批次。
192
+
193
+ 选中实例可在右侧删除、锁定或隐藏。锁定后先解锁才能改类;隐藏只影响显示,不等于从导出中移除。画布支持滚轮局部缩放、平移和 **Fit image**。
194
+
195
+ ### 保存、复核与标签
196
+
197
+ 编辑会自动保存,切换图片前也会保存。底部显示保存状态;失败时查看错误并使用 **Retry save**,确认保存成功后再关闭页面。
198
+
199
+ 右侧审核状态为:
200
+
201
+ | 状态 | 含义 |
202
+ | --- | --- |
203
+ | Not reviewed | 尚未人工复核 |
204
+ | Approved | 当前任务已复核通过 |
205
+ | Needs review | 当前任务需要再次检查 |
206
+ | Excluded | 整张图片不参与处理和导出;不直接删除图片 |
207
+
208
+ Detection 与 segmentation 的前三个审核状态分别记录;Excluded 作用于整张图片,可再次点击取消。编辑标注后可能恢复为未审核,需要重新确认。
209
+
210
+ 图片可添加自定义 tags(逗号分隔)和 notes。顶部 **Tags** 抽屉展示只读的四个内置审核状态,下方管理自定义 tag 名称和颜色。当前自定义 tag 改名不会批量迁移旧图片上的 tag 字符串,需自行核对。
211
+
212
+ ### 导出
213
+
214
+ 先选中 detection 或 segmentation 任务并完成复核,再点击 **Export**。当前页面导出当前任务的 YOLO 数据集,不是只导出当前图片。
215
+
216
+ 默认跳过未审核、Needs review、Excluded 和含未分配类别实例的图片;无对象图片需明确勾选 **Confirm no objects for this task** 并复核通过,才能作为空标签样本导出。
217
+
218
+ 结果位于后端项目的 `.annodesk/exports/<id>/`,页面返回路径。导出目录包含复制的图片、标准 `labels/`、类别配置及报告;分割工作目录中的 `labels_seg/` 在导出副本中转换为标准 `labels/`。导出类别 ID 可能连续重排,工作项目不受影响。
219
+
220
+ YOLO polygon 对孔洞等结构可能有损,请保留原始项目及内部 RLE。当前界面还没有 COCO 导出选项和回收站管理界面。
221
+
222
+ ## 6. 不打开页面,直接将检测框批量分割
223
+
224
+ ```sh
225
+ annodesk segment --project /data/my-dataset --models-config ./models.json --model sam2-small
226
+ ```
227
+
228
+ 省略 `--model` 使用 JSON 中的 `default_model`。该命令不启动 HTTP 服务或浏览器。
229
+
230
+ - 保留 detection 框及类别,分割写入 `labels_seg/`,内部保存无损 RLE。
231
+ - 跳过已有 mask、锁定实例和 Excluded 图片。
232
+ - 每张图片独立保存;中断后再次运行可跳过已完成结果。
233
+ - 控制台输出处理进度,错误报告位于 `.annodesk/segment-report.json`。
234
+ - 成功退出码为 0;有失败图片时为 1。解决原因后可重新运行。
235
+
236
+ 生成结果仍需在页面人工复核,命令不会自动标为 Approved。
237
+
238
+ ## 7. 快捷键
239
+
240
+ 输入框、类别搜索和 IME 输入期间不会触发工作台单键快捷键。页面 **Shortcuts ?** 可搜索快捷键;按钮提示约 150ms 后显示。
241
+
242
+ | 操作 | 按键 |
243
+ | --- | --- |
244
+ | 上一张 / 下一张 | A / D or ← / → |
245
+ | 选择 / 画框 | V / R |
246
+ | 正点 / 负点 | Q / E |
247
+ | mask 框提示 / 文本提示 | B / T |
248
+ | 执行当前实例分割 | Enter |
249
+ | 修改选中实例类别 | C or double-click |
250
+ | 删除选中实例 | X or Delete |
251
+ | 平移 | 按住 Space,或选择 Pan |
252
+ | 适应画布 | F |
253
+ | 放大 / 缩小 | + / - |
254
+ | Approved / Needs review / Excluded | 1 / 2 / 3 |
255
+ | 撤销 | Ctrl+Z or ⌘Z |
256
+ | 重做 | Ctrl+Shift+Z or ⌘Shift+Z |
257
+ | 保存 | Ctrl+S or ⌘S |
258
+ | 查看快捷键 | ? |
259
+
260
+ ## 8. 前后端分开部署
261
+
262
+ 在保存图片、标签及模型的电脑上:
263
+
264
+ ```sh
265
+ annodesk api --host 0.0.0.0 --port 8765 --models-config /config/models.json
266
+ ```
267
+
268
+ 在提供前端页面的电脑上(同样安装基础 wheel):
269
+
270
+ ```sh
271
+ annodesk ui --api-url http://gpu-host:8765 --port 8080
272
+ ```
273
+
274
+ 浏览器打开 `http://前端电脑:8080`。如果需要让其他电脑访问前端,请为 `ui` 增加 `--host 0.0.0.0`。`gpu-host` 必须能从浏览器所在电脑访问。
275
+
276
+ 所有数据路径、模型配置和 `HF_MODELS` 以 API 电脑为准,浏览器电脑上的目录不会自动上传。当前版本没有认证,HTTP 部署限于可信网络。
277
+
278
+ ## 9. 常见问题
279
+
280
+ | 现象 | 处理方式 |
281
+ | --- | --- |
282
+ | 升级后样式丢失、请求旧 CSS / JS 404,或只有某个浏览器异常 | 先保存编辑,重启为新版本后强制刷新(Windows / Linux:Ctrl+Shift+R;macOS:⌘Shift+R);必要时只清除此站点的缓存。不同浏览器的缓存相互独立 |
283
+ | 页面提示 Open project | 打开数据集,或启动时传入 `--project`;不表示服务故障 |
284
+ | 端口占用 | 换一个 `--port`,或停止原来的服务 |
285
+ | 模型显示 missing | 检查 JSON 路径、目录解析、backend 和目录顶层权重;点击 Reload |
286
+ | 提示 No compatible .pt checkpoint | 当前适配器不兼容该目录内容;准备兼容权重,不会自动下载或转换 |
287
+ | Multiple checkpoint candidates | 为每个模型使用独立目录,避免多个候选权重 |
288
+ | 缺少 ultralytics / torch / tokenizer | 在后端环境手动安装对应依赖;运行 `annodesk doctor` 辅助检查 |
289
+ | 保存冲突 | 保留页面和备份,核对是否有其他进程或工具修改同一项目;不要强行覆盖 |
290
+ | 推理完成时已有新编辑 | 页面会保留新编辑并提示需要核对;备份后比较结果,再决定如何重新载入 |
291
+ | 导出结果少于图片数 | 检查当前任务的审核状态、Excluded、未分配类别和空标签确认 |
292
+ | 模型不支持或推理异常 | 基础框标注仍可用;记录模型格式、依赖版本和控制台错误进行排查 |
293
+
294
+ ## 10. 查阅已安装的指南
295
+
296
+ ```sh
297
+ annodesk docs
298
+ annodesk docs --output ./AnnoDesk-user-guide.md
299
+ annodesk --help
300
+ annodesk segment --help
301
+ ```
302
+
303
+ `annodesk docs` 输出随当前安装版本发布的本指南;`--output` 导出 Markdown,目标文件已存在时不会覆盖。升级安装包后,可用该命令获取对应版本的说明。
annodesk/domain.py ADDED
@@ -0,0 +1,52 @@
1
+ """Validated persisted annotation types; geometry is always in original pixels."""
2
+
3
+ from typing import Literal
4
+ from uuid import uuid4
5
+
6
+ from pydantic import BaseModel, ConfigDict, Field, model_validator
7
+
8
+
9
+ def uid() -> str:
10
+ return str(uuid4())
11
+
12
+
13
+ class Point(BaseModel):
14
+ model_config = ConfigDict(allow_inf_nan=False)
15
+ x: float
16
+ y: float
17
+ positive: bool = True
18
+
19
+
20
+ class Instance(BaseModel):
21
+ model_config = ConfigDict(allow_inf_nan=False)
22
+ id: str = Field(default_factory=uid)
23
+ class_id: str | None = None
24
+ box: list[float] | None = None
25
+ polygons: list[list[list[float]]] = Field(default_factory=list)
26
+ mask: dict | None = None
27
+ points: list[Point] = Field(default_factory=list)
28
+ prompt_box: list[float] | None = None
29
+ hidden: bool = False
30
+ locked: bool = False
31
+ source: str = "manual"
32
+
33
+ @model_validator(mode="after")
34
+ def geometry(self):
35
+ for box in (self.box, self.prompt_box):
36
+ if box is not None and (len(box) != 4 or box[2] <= box[0] or box[3] <= box[1]):
37
+ raise ValueError("A box must have four coordinates and positive area")
38
+ for polygon in self.polygons:
39
+ if len(polygon) < 3 or any(len(point) != 2 for point in polygon):
40
+ raise ValueError("A polygon needs at least three xy vertices")
41
+ return self
42
+
43
+
44
+ class Annotation(BaseModel):
45
+ revision: int = Field(default=0, ge=0)
46
+ instances: list[Instance] = Field(default_factory=list)
47
+ detection_review: Literal["pending", "ok", "uncertain"] = "pending"
48
+ segmentation_review: Literal["pending", "ok", "uncertain"] = "pending"
49
+ excluded: bool = False
50
+ tags: list[str] = Field(default_factory=list)
51
+ notes: str = ""
52
+ explicit_empty: dict[str, bool] = Field(default_factory=dict)