bbannotate 1.0.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.
- bbannotate-1.0.0.dist-info/METADATA +182 -0
- bbannotate-1.0.0.dist-info/RECORD +21 -0
- bbannotate-1.0.0.dist-info/WHEEL +4 -0
- bbannotate-1.0.0.dist-info/entry_points.txt +2 -0
- bbannotate-1.0.0.dist-info/licenses/LICENSE +21 -0
- src/__init__.py +4 -0
- src/api/__init__.py +5 -0
- src/api/routes.py +339 -0
- src/cli.py +263 -0
- src/frontend_dist/assets/index-DFfgJzXo.css +1 -0
- src/frontend_dist/assets/index-DezlWFbC.js +89 -0
- src/frontend_dist/index.html +20 -0
- src/frontend_dist/logo.png +0 -0
- src/main.py +90 -0
- src/models/__init__.py +21 -0
- src/models/annotations.py +62 -0
- src/py.typed +0 -0
- src/services/__init__.py +6 -0
- src/services/annotation_service.py +307 -0
- src/services/export_service.py +488 -0
- src/services/project_service.py +203 -0
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: bbannotate
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: A lightweight bounding box annotation tool for image datasets with YOLO/COCO/Pascal VOC export
|
|
5
|
+
Project-URL: Homepage, https://github.com/seba2390/bbannotate
|
|
6
|
+
Project-URL: Documentation, https://github.com/seba2390/bbannotate#readme
|
|
7
|
+
Project-URL: Repository, https://github.com/seba2390/bbannotate
|
|
8
|
+
Project-URL: Issues, https://github.com/seba2390/bbannotate/issues
|
|
9
|
+
Author: Sebastian Yde Madsen
|
|
10
|
+
License-Expression: MIT
|
|
11
|
+
License-File: LICENSE
|
|
12
|
+
Keywords: annotation,bounding-box,coco,computer-vision,image-annotation,machine-learning,object-detection,pascal-voc,yolo
|
|
13
|
+
Classifier: Development Status :: 4 - Beta
|
|
14
|
+
Classifier: Environment :: Web Environment
|
|
15
|
+
Classifier: Framework :: FastAPI
|
|
16
|
+
Classifier: Intended Audience :: Developers
|
|
17
|
+
Classifier: Intended Audience :: Science/Research
|
|
18
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
19
|
+
Classifier: Operating System :: OS Independent
|
|
20
|
+
Classifier: Programming Language :: Python :: 3
|
|
21
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
22
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
23
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
24
|
+
Classifier: Topic :: Scientific/Engineering :: Image Recognition
|
|
25
|
+
Classifier: Typing :: Typed
|
|
26
|
+
Requires-Python: >=3.12
|
|
27
|
+
Requires-Dist: aiofiles>=23.2.1
|
|
28
|
+
Requires-Dist: fastapi>=0.109.0
|
|
29
|
+
Requires-Dist: pillow>=10.2.0
|
|
30
|
+
Requires-Dist: pydantic>=2.5.0
|
|
31
|
+
Requires-Dist: python-multipart>=0.0.6
|
|
32
|
+
Requires-Dist: rich>=13.0.0
|
|
33
|
+
Requires-Dist: typer>=0.9.0
|
|
34
|
+
Requires-Dist: uvicorn[standard]>=0.27.0
|
|
35
|
+
Provides-Extra: dev
|
|
36
|
+
Requires-Dist: httpx; extra == 'dev'
|
|
37
|
+
Requires-Dist: pyright; extra == 'dev'
|
|
38
|
+
Requires-Dist: pytest; extra == 'dev'
|
|
39
|
+
Requires-Dist: pytest-cov; extra == 'dev'
|
|
40
|
+
Requires-Dist: ruff; extra == 'dev'
|
|
41
|
+
Description-Content-Type: text/markdown
|
|
42
|
+
|
|
43
|
+
<p align="center">
|
|
44
|
+
<img src="frontend/public/logo.png" alt="Bbannotate Logo" width="400">
|
|
45
|
+
</p>
|
|
46
|
+
|
|
47
|
+
<p align="center">
|
|
48
|
+
<a href="https://pypi.org/project/bbannotate/"><img src="https://img.shields.io/pypi/v/bbannotate" alt="PyPI version"></a>
|
|
49
|
+
<a href="https://pypi.org/project/bbannotate/"><img src="https://img.shields.io/pypi/pyversions/bbannotate" alt="Python versions"></a>
|
|
50
|
+
<a href="https://github.com/sebastianydemadsen/bbannotate/blob/main/LICENSE"><img src="https://img.shields.io/github/license/sebastianydemadsen/bbannotate" alt="License"></a>
|
|
51
|
+
</p>
|
|
52
|
+
|
|
53
|
+
A lightweight bounding box annotation tool for image datasets. Built with React/TypeScript frontend and FastAPI backend. Export to YOLO, COCO, Pascal VOC, and more.
|
|
54
|
+
|
|
55
|
+
## Features
|
|
56
|
+
|
|
57
|
+
- 🖼️ **Multi-format support** — PNG, JPEG, WebP, BMP
|
|
58
|
+
- 📁 **Project management** — Organize annotations by project
|
|
59
|
+
- 🏷️ **Custom labels** — Define your own class labels
|
|
60
|
+
- ⌨️ **Keyboard shortcuts** — Fast annotation workflow
|
|
61
|
+
- 📤 **Multiple export formats** — YOLO, COCO, Pascal VOC, CreateML, CSV
|
|
62
|
+
- 🔄 **Train/Val/Test split** — Automatic dataset splitting for YOLO export
|
|
63
|
+
|
|
64
|
+
## Installation
|
|
65
|
+
|
|
66
|
+
```bash
|
|
67
|
+
pip install bbannotate
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### Requirements
|
|
71
|
+
|
|
72
|
+
- Python 3.12+
|
|
73
|
+
- Node.js (only for frontend development)
|
|
74
|
+
|
|
75
|
+
## Quick Start
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
# Start the annotation server
|
|
79
|
+
bbannotate start
|
|
80
|
+
|
|
81
|
+
# Opens http://127.0.0.1:8000 in your browser
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
### CLI Options
|
|
85
|
+
|
|
86
|
+
```bash
|
|
87
|
+
bbannotate start [OPTIONS]
|
|
88
|
+
|
|
89
|
+
Options:
|
|
90
|
+
-h, --host TEXT Host to bind the server to [default: 127.0.0.1]
|
|
91
|
+
-p, --port INTEGER Port to bind the server to [default: 8000]
|
|
92
|
+
--no-browser Don't open browser automatically
|
|
93
|
+
-r, --reload Enable auto-reload for development
|
|
94
|
+
-d, --data-dir PATH Directory for storing data [default: ./data]
|
|
95
|
+
--projects-dir PATH Directory for storing projects [default: ./projects]
|
|
96
|
+
-v, --version Show version and exit
|
|
97
|
+
--help Show help and exit
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### Other Commands
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
bbannotate info # Show installation info
|
|
104
|
+
bbannotate build-frontend # Build frontend assets (development)
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
## Keyboard Shortcuts
|
|
108
|
+
|
|
109
|
+
| Key | Action |
|
|
110
|
+
|-----|--------|
|
|
111
|
+
| `D` | Draw mode |
|
|
112
|
+
| `S` | Select mode |
|
|
113
|
+
| `Space` | Pan mode |
|
|
114
|
+
| `←` `→` | Navigate images |
|
|
115
|
+
| `1-9` | Select label by index |
|
|
116
|
+
| `Del` / `Backspace` | Delete annotation |
|
|
117
|
+
| `Esc` | Deselect / Cancel |
|
|
118
|
+
|
|
119
|
+
## Export Formats
|
|
120
|
+
|
|
121
|
+
| Format | Description |
|
|
122
|
+
|--------|-------------|
|
|
123
|
+
| **YOLO** | ZIP with train/val/test split, `data.yaml`, normalized coordinates |
|
|
124
|
+
| **COCO** | COCO JSON format with categories, images, and annotations |
|
|
125
|
+
| **Pascal VOC** | XML files per image with absolute coordinates |
|
|
126
|
+
| **CreateML** | Apple CreateML JSON format |
|
|
127
|
+
| **CSV** | Simple CSV with image, label, and bbox columns |
|
|
128
|
+
|
|
129
|
+
## Configuration
|
|
130
|
+
|
|
131
|
+
### Environment Variables
|
|
132
|
+
|
|
133
|
+
| Variable | Description |
|
|
134
|
+
|----------|-------------|
|
|
135
|
+
| `BBANNOTATE_DATA_DIR` | Override default data directory |
|
|
136
|
+
| `BBANNOTATE_PROJECTS_DIR` | Override default projects directory |
|
|
137
|
+
|
|
138
|
+
## Development
|
|
139
|
+
|
|
140
|
+
### Setup
|
|
141
|
+
|
|
142
|
+
```bash
|
|
143
|
+
git clone https://github.com/sebastianydemadsen/bbannotate.git
|
|
144
|
+
cd bbannotate
|
|
145
|
+
make install # Install with dev dependencies
|
|
146
|
+
make frontend-install # Install frontend dependencies
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
### Development Commands
|
|
150
|
+
|
|
151
|
+
| Command | Description |
|
|
152
|
+
|---------|-------------|
|
|
153
|
+
| `make run` | Start full application (backend + frontend) |
|
|
154
|
+
| `make backend-dev` | Start backend only with auto-reload |
|
|
155
|
+
| `make frontend-dev` | Start frontend dev server |
|
|
156
|
+
| `make stop` | Stop all servers |
|
|
157
|
+
| `make test` | Run tests |
|
|
158
|
+
| `make test-cov` | Run tests with coverage report |
|
|
159
|
+
| `make type-check` | Run pyright type checking |
|
|
160
|
+
| `make format` | Format code with ruff |
|
|
161
|
+
| `make check-all` | Run all checks (lint, type, test) |
|
|
162
|
+
| `make build` | Build package for distribution |
|
|
163
|
+
| `make clean` | Remove build artifacts |
|
|
164
|
+
|
|
165
|
+
### Project Structure
|
|
166
|
+
|
|
167
|
+
```
|
|
168
|
+
src/ # Python package (FastAPI backend)
|
|
169
|
+
api/ # API routes
|
|
170
|
+
models/ # Pydantic models
|
|
171
|
+
services/ # Business logic
|
|
172
|
+
cli.py # CLI entry point
|
|
173
|
+
frontend/ # React/TypeScript frontend
|
|
174
|
+
src/
|
|
175
|
+
components/ # UI components
|
|
176
|
+
hooks/ # React hooks
|
|
177
|
+
tests/ # Test suite (145 tests)
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
## License
|
|
181
|
+
|
|
182
|
+
[MIT](LICENSE)
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
src/__init__.py,sha256=Eg75iuRICqJS9qBThEk9OQ4l7sDRagmKrjuc4tGCVns,117
|
|
2
|
+
src/cli.py,sha256=q2u1j1Q4A1F1yMAWPzNmnw053CKqxxnHVGzhUGSJKzI,7533
|
|
3
|
+
src/main.py,sha256=M2yuPRxkRDYCwNw7jPBwDm6R-Egk6EFbdUeh3W3YHss,2346
|
|
4
|
+
src/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
src/api/__init__.py,sha256=rin7NuwATcUVSpd-oncHr4VPxCj2je9kPpsjMdwQAJ0,99
|
|
6
|
+
src/api/routes.py,sha256=iLliNvNMXFFpm5SxCQ5fj-txaWurIGy-kuchx9v-mYM,11085
|
|
7
|
+
src/models/__init__.py,sha256=V3IlbnQ_PWn43HCKlOi-IRTmNj1GX7iA79ZbCns_yts,368
|
|
8
|
+
src/models/annotations.py,sha256=NiCBjmejQbgoMKkDiNqFZBKK6-QggcU1ikXwXFoQWgc,1951
|
|
9
|
+
src/services/__init__.py,sha256=jDAHHd3p_fYdT3Txzakd2I0fxwEQ6irMQ1nNELhuIEY,207
|
|
10
|
+
src/services/annotation_service.py,sha256=_OiUlm42LXIMrcbTVjGWZPOovhppUJMX3v8rZuq6WVU,10495
|
|
11
|
+
src/services/export_service.py,sha256=-NhO6jCQtOpIgFL3cVDk86KQSQBBiogN3jVpESJQVac,17180
|
|
12
|
+
src/services/project_service.py,sha256=6l2mih32z5_tqLj8orF5szNhuEJYkboNeUwNqDEs9iA,7125
|
|
13
|
+
src/frontend_dist/index.html,sha256=opKxjiCx3PXtHQsAsn7iQ3zkOUMr-fTgZ_l85E4X7cc,733
|
|
14
|
+
src/frontend_dist/logo.png,sha256=hiuri3y0lYBsavGECeXh8VPVJvhA8nOlHCDeTSfqk8s,198758
|
|
15
|
+
src/frontend_dist/assets/index-DFfgJzXo.css,sha256=v9Ez3rvpb4mWv3JtVFrke3ubOwyOfCBSl0vbIbekCC0,20719
|
|
16
|
+
src/frontend_dist/assets/index-DezlWFbC.js,sha256=S8frLZGZ7cm5ugjS7bLjPrJBRGeoJnx1lDyPQVBRQg4,518021
|
|
17
|
+
bbannotate-1.0.0.dist-info/METADATA,sha256=r7SrH-rMJ-TJKqHcWm11Hy-ee3QbeNevgzAA9Pj6zqM,6046
|
|
18
|
+
bbannotate-1.0.0.dist-info/WHEEL,sha256=WLgqFyCfm_KASv4WHyYy0P3pM_m7J5L9k2skdKLirC8,87
|
|
19
|
+
bbannotate-1.0.0.dist-info/entry_points.txt,sha256=nu0u4lG6-pWhDUmquxeusVDJzhsWQOUczupOWmKH01c,44
|
|
20
|
+
bbannotate-1.0.0.dist-info/licenses/LICENSE,sha256=rkJbDOYRrUX8k3S55pPiEquJXZGh07KvD01F1sBBppw,1077
|
|
21
|
+
bbannotate-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Sebastian Yde Madsen
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
src/__init__.py
ADDED
src/api/__init__.py
ADDED
src/api/routes.py
ADDED
|
@@ -0,0 +1,339 @@
|
|
|
1
|
+
"""FastAPI routes for the annotation API."""
|
|
2
|
+
|
|
3
|
+
import os
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
from typing import Annotated
|
|
6
|
+
|
|
7
|
+
from fastapi import APIRouter, Depends, File, HTTPException, Query, UploadFile
|
|
8
|
+
from fastapi.responses import FileResponse
|
|
9
|
+
|
|
10
|
+
from src.models.annotations import (
|
|
11
|
+
Annotation,
|
|
12
|
+
AnnotationCreate,
|
|
13
|
+
AnnotationUpdate,
|
|
14
|
+
ImageInfo,
|
|
15
|
+
ProjectInfo,
|
|
16
|
+
)
|
|
17
|
+
from src.services.annotation_service import AnnotationService
|
|
18
|
+
from src.services.export_service import ExportService
|
|
19
|
+
from src.services.project_service import Project, ProjectCreate, ProjectService
|
|
20
|
+
|
|
21
|
+
router = APIRouter()
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_projects_dir() -> Path:
|
|
25
|
+
"""Get the projects directory from environment or default."""
|
|
26
|
+
env_path = os.environ.get("BBANNOTATE_PROJECTS_DIR")
|
|
27
|
+
if env_path:
|
|
28
|
+
return Path(env_path)
|
|
29
|
+
return Path.cwd() / "projects"
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
def get_data_dir() -> Path:
|
|
33
|
+
"""Get the data directory from environment or default."""
|
|
34
|
+
env_path = os.environ.get("BBANNOTATE_DATA_DIR")
|
|
35
|
+
if env_path:
|
|
36
|
+
return Path(env_path)
|
|
37
|
+
return Path.cwd() / "data"
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
# Default directories (use functions for dynamic resolution)
|
|
41
|
+
PROJECTS_DIR = get_projects_dir()
|
|
42
|
+
DATA_DIR = get_data_dir() # Legacy fallback
|
|
43
|
+
|
|
44
|
+
# Global state for current project
|
|
45
|
+
_current_project_id: str | None = None
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
def get_project_service() -> ProjectService:
|
|
49
|
+
"""Dependency for project service."""
|
|
50
|
+
return ProjectService(get_projects_dir())
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
def get_annotation_service() -> AnnotationService:
|
|
54
|
+
"""Dependency for annotation service - uses current project's data directory."""
|
|
55
|
+
global _current_project_id
|
|
56
|
+
if _current_project_id:
|
|
57
|
+
project_service = ProjectService(get_projects_dir())
|
|
58
|
+
data_dir = project_service.get_project_data_dir(_current_project_id)
|
|
59
|
+
if data_dir:
|
|
60
|
+
return AnnotationService(data_dir)
|
|
61
|
+
# Fallback to legacy data directory
|
|
62
|
+
return AnnotationService(get_data_dir())
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
def get_export_service(
|
|
66
|
+
annotation_service: Annotated[AnnotationService, Depends(get_annotation_service)],
|
|
67
|
+
) -> ExportService:
|
|
68
|
+
"""Dependency for export service."""
|
|
69
|
+
return ExportService(annotation_service)
|
|
70
|
+
|
|
71
|
+
|
|
72
|
+
# Project management endpoints
|
|
73
|
+
@router.get("/projects", response_model=list[Project])
|
|
74
|
+
def list_projects(
|
|
75
|
+
service: Annotated[ProjectService, Depends(get_project_service)],
|
|
76
|
+
) -> list[Project]:
|
|
77
|
+
"""List all projects, sorted by last opened."""
|
|
78
|
+
return service.list_projects()
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
@router.post("/projects", response_model=Project)
|
|
82
|
+
def create_project(
|
|
83
|
+
create: ProjectCreate,
|
|
84
|
+
service: Annotated[ProjectService, Depends(get_project_service)],
|
|
85
|
+
) -> Project:
|
|
86
|
+
"""Create a new project."""
|
|
87
|
+
return service.create_project(create)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
@router.get("/projects/current", response_model=Project | None)
|
|
91
|
+
def get_current_project(
|
|
92
|
+
service: Annotated[ProjectService, Depends(get_project_service)],
|
|
93
|
+
) -> Project | None:
|
|
94
|
+
"""Get the currently active project."""
|
|
95
|
+
global _current_project_id
|
|
96
|
+
if not _current_project_id:
|
|
97
|
+
return None
|
|
98
|
+
return service.get_project(_current_project_id)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@router.post("/projects/{project_id}/open", response_model=Project)
|
|
102
|
+
def open_project(
|
|
103
|
+
project_id: str,
|
|
104
|
+
service: Annotated[ProjectService, Depends(get_project_service)],
|
|
105
|
+
) -> Project:
|
|
106
|
+
"""Open a project (set as current and update last_opened)."""
|
|
107
|
+
global _current_project_id
|
|
108
|
+
project = service.open_project(project_id)
|
|
109
|
+
if not project:
|
|
110
|
+
raise HTTPException(status_code=404, detail="Project not found")
|
|
111
|
+
_current_project_id = project_id
|
|
112
|
+
return project
|
|
113
|
+
|
|
114
|
+
|
|
115
|
+
@router.post("/projects/close")
|
|
116
|
+
def close_project() -> dict[str, bool]:
|
|
117
|
+
"""Close the current project."""
|
|
118
|
+
global _current_project_id
|
|
119
|
+
_current_project_id = None
|
|
120
|
+
return {"success": True}
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
@router.delete("/projects/{project_id}")
|
|
124
|
+
def delete_project(
|
|
125
|
+
project_id: str,
|
|
126
|
+
service: Annotated[ProjectService, Depends(get_project_service)],
|
|
127
|
+
) -> dict[str, bool]:
|
|
128
|
+
"""Delete a project and all its data."""
|
|
129
|
+
global _current_project_id
|
|
130
|
+
success = service.delete_project(project_id)
|
|
131
|
+
if not success:
|
|
132
|
+
raise HTTPException(status_code=404, detail="Project not found")
|
|
133
|
+
if _current_project_id == project_id:
|
|
134
|
+
_current_project_id = None
|
|
135
|
+
return {"success": True}
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
# Project endpoints (existing)
|
|
139
|
+
@router.get("/project", response_model=ProjectInfo)
|
|
140
|
+
def get_project_info(
|
|
141
|
+
service: Annotated[AnnotationService, Depends(get_annotation_service)],
|
|
142
|
+
) -> ProjectInfo:
|
|
143
|
+
"""Get project statistics and label information."""
|
|
144
|
+
return service.get_project_info()
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
# Image endpoints
|
|
148
|
+
@router.get("/images", response_model=list[str])
|
|
149
|
+
def list_images(
|
|
150
|
+
service: Annotated[AnnotationService, Depends(get_annotation_service)],
|
|
151
|
+
) -> list[str]:
|
|
152
|
+
"""List all images in the project."""
|
|
153
|
+
return service.list_images()
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
@router.post("/images", response_model=ImageInfo)
|
|
157
|
+
async def upload_image(
|
|
158
|
+
file: Annotated[UploadFile, File(...)],
|
|
159
|
+
service: Annotated[AnnotationService, Depends(get_annotation_service)],
|
|
160
|
+
) -> ImageInfo:
|
|
161
|
+
"""Upload a new image."""
|
|
162
|
+
if not file.filename:
|
|
163
|
+
raise HTTPException(status_code=400, detail="Filename is required")
|
|
164
|
+
|
|
165
|
+
content = await file.read()
|
|
166
|
+
try:
|
|
167
|
+
return service.upload_image(file.filename, content)
|
|
168
|
+
except ValueError as e:
|
|
169
|
+
raise HTTPException(status_code=400, detail=str(e)) from e
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
@router.get("/images/{filename}")
|
|
173
|
+
def get_image(
|
|
174
|
+
filename: str,
|
|
175
|
+
service: Annotated[AnnotationService, Depends(get_annotation_service)],
|
|
176
|
+
) -> FileResponse:
|
|
177
|
+
"""Get an image file."""
|
|
178
|
+
path = service.get_image_path(filename)
|
|
179
|
+
if path is None:
|
|
180
|
+
raise HTTPException(status_code=404, detail="Image not found")
|
|
181
|
+
return FileResponse(path)
|
|
182
|
+
|
|
183
|
+
|
|
184
|
+
@router.delete("/images/{filename}")
|
|
185
|
+
def delete_image(
|
|
186
|
+
filename: str,
|
|
187
|
+
service: Annotated[AnnotationService, Depends(get_annotation_service)],
|
|
188
|
+
) -> dict[str, bool]:
|
|
189
|
+
"""Delete an image and its annotations."""
|
|
190
|
+
success = service.delete_image(filename)
|
|
191
|
+
if not success:
|
|
192
|
+
raise HTTPException(status_code=404, detail="Image not found")
|
|
193
|
+
return {"success": True}
|
|
194
|
+
|
|
195
|
+
|
|
196
|
+
# Annotation endpoints
|
|
197
|
+
@router.get("/images/{filename}/annotations", response_model=list[Annotation])
|
|
198
|
+
def get_annotations(
|
|
199
|
+
filename: str,
|
|
200
|
+
service: Annotated[AnnotationService, Depends(get_annotation_service)],
|
|
201
|
+
) -> list[Annotation]:
|
|
202
|
+
"""Get all annotations for an image."""
|
|
203
|
+
return service.get_annotations(filename)
|
|
204
|
+
|
|
205
|
+
|
|
206
|
+
@router.post("/images/{filename}/annotations", response_model=Annotation)
|
|
207
|
+
def add_annotation(
|
|
208
|
+
filename: str,
|
|
209
|
+
annotation: AnnotationCreate,
|
|
210
|
+
service: Annotated[AnnotationService, Depends(get_annotation_service)],
|
|
211
|
+
) -> Annotation:
|
|
212
|
+
"""Add a new annotation to an image."""
|
|
213
|
+
try:
|
|
214
|
+
return service.add_annotation(filename, annotation)
|
|
215
|
+
except FileNotFoundError as e:
|
|
216
|
+
raise HTTPException(status_code=404, detail=str(e)) from e
|
|
217
|
+
|
|
218
|
+
|
|
219
|
+
@router.put("/images/{filename}/annotations/{annotation_id}", response_model=Annotation)
|
|
220
|
+
def update_annotation(
|
|
221
|
+
filename: str,
|
|
222
|
+
annotation_id: str,
|
|
223
|
+
update: AnnotationUpdate,
|
|
224
|
+
service: Annotated[AnnotationService, Depends(get_annotation_service)],
|
|
225
|
+
) -> Annotation:
|
|
226
|
+
"""Update an existing annotation."""
|
|
227
|
+
result = service.update_annotation(filename, annotation_id, update)
|
|
228
|
+
if result is None:
|
|
229
|
+
raise HTTPException(status_code=404, detail="Annotation not found")
|
|
230
|
+
return result
|
|
231
|
+
|
|
232
|
+
|
|
233
|
+
@router.delete("/images/{filename}/annotations/{annotation_id}")
|
|
234
|
+
def delete_annotation(
|
|
235
|
+
filename: str,
|
|
236
|
+
annotation_id: str,
|
|
237
|
+
service: Annotated[AnnotationService, Depends(get_annotation_service)],
|
|
238
|
+
) -> dict[str, bool]:
|
|
239
|
+
"""Delete an annotation."""
|
|
240
|
+
success = service.delete_annotation(filename, annotation_id)
|
|
241
|
+
if not success:
|
|
242
|
+
raise HTTPException(status_code=404, detail="Annotation not found")
|
|
243
|
+
return {"success": True}
|
|
244
|
+
|
|
245
|
+
|
|
246
|
+
@router.delete("/images/{filename}/annotations")
|
|
247
|
+
def clear_annotations(
|
|
248
|
+
filename: str,
|
|
249
|
+
service: Annotated[AnnotationService, Depends(get_annotation_service)],
|
|
250
|
+
) -> dict[str, int]:
|
|
251
|
+
"""Clear all annotations for an image."""
|
|
252
|
+
count = service.clear_annotations(filename)
|
|
253
|
+
return {"deleted": count}
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
@router.post("/images/{filename}/annotations/copy-from/{source_filename}")
|
|
257
|
+
def copy_annotations(
|
|
258
|
+
filename: str,
|
|
259
|
+
source_filename: str,
|
|
260
|
+
service: Annotated[AnnotationService, Depends(get_annotation_service)],
|
|
261
|
+
) -> dict[str, int]:
|
|
262
|
+
"""Copy annotations from another image."""
|
|
263
|
+
count = service.copy_annotations(source_filename, filename)
|
|
264
|
+
return {"copied": count}
|
|
265
|
+
|
|
266
|
+
|
|
267
|
+
# Export endpoints
|
|
268
|
+
@router.post("/export/yolo")
|
|
269
|
+
def export_yolo(
|
|
270
|
+
export_service: Annotated[ExportService, Depends(get_export_service)],
|
|
271
|
+
train_split: Annotated[float, Query(ge=0.1, le=0.98)] = 0.7,
|
|
272
|
+
val_split: Annotated[float, Query(ge=0.01, le=0.5)] = 0.2,
|
|
273
|
+
test_split: Annotated[float, Query(ge=0.0, le=0.5)] = 0.1,
|
|
274
|
+
) -> FileResponse:
|
|
275
|
+
"""Export annotations in YOLO format as a ZIP file."""
|
|
276
|
+
zip_path = export_service.export_yolo_zip(train_split, val_split, test_split)
|
|
277
|
+
return FileResponse(
|
|
278
|
+
zip_path,
|
|
279
|
+
media_type="application/zip",
|
|
280
|
+
filename="yolo_dataset.zip",
|
|
281
|
+
)
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
@router.post("/export/coco")
|
|
285
|
+
def export_coco(
|
|
286
|
+
export_service: Annotated[ExportService, Depends(get_export_service)],
|
|
287
|
+
annotation_service: Annotated[AnnotationService, Depends(get_annotation_service)],
|
|
288
|
+
) -> FileResponse:
|
|
289
|
+
"""Export annotations in COCO JSON format."""
|
|
290
|
+
output_path = annotation_service.data_dir / "coco_annotations.json"
|
|
291
|
+
export_service.export_coco(output_path)
|
|
292
|
+
return FileResponse(
|
|
293
|
+
output_path,
|
|
294
|
+
media_type="application/json",
|
|
295
|
+
filename="coco_annotations.json",
|
|
296
|
+
)
|
|
297
|
+
|
|
298
|
+
|
|
299
|
+
@router.post("/export/pascal-voc")
|
|
300
|
+
def export_pascal_voc(
|
|
301
|
+
export_service: Annotated[ExportService, Depends(get_export_service)],
|
|
302
|
+
) -> FileResponse:
|
|
303
|
+
"""Export annotations in Pascal VOC XML format as a ZIP file."""
|
|
304
|
+
zip_path = export_service.export_pascal_voc_zip()
|
|
305
|
+
return FileResponse(
|
|
306
|
+
zip_path,
|
|
307
|
+
media_type="application/zip",
|
|
308
|
+
filename="pascal_voc_dataset.zip",
|
|
309
|
+
)
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
@router.post("/export/createml")
|
|
313
|
+
def export_createml(
|
|
314
|
+
export_service: Annotated[ExportService, Depends(get_export_service)],
|
|
315
|
+
annotation_service: Annotated[AnnotationService, Depends(get_annotation_service)],
|
|
316
|
+
) -> FileResponse:
|
|
317
|
+
"""Export annotations in Apple CreateML JSON format."""
|
|
318
|
+
output_path = annotation_service.data_dir / "createml_annotations.json"
|
|
319
|
+
export_service.export_createml(output_path)
|
|
320
|
+
return FileResponse(
|
|
321
|
+
output_path,
|
|
322
|
+
media_type="application/json",
|
|
323
|
+
filename="createml_annotations.json",
|
|
324
|
+
)
|
|
325
|
+
|
|
326
|
+
|
|
327
|
+
@router.post("/export/csv")
|
|
328
|
+
def export_csv(
|
|
329
|
+
export_service: Annotated[ExportService, Depends(get_export_service)],
|
|
330
|
+
annotation_service: Annotated[AnnotationService, Depends(get_annotation_service)],
|
|
331
|
+
) -> FileResponse:
|
|
332
|
+
"""Export annotations in CSV format."""
|
|
333
|
+
output_path = annotation_service.data_dir / "annotations.csv"
|
|
334
|
+
export_service.export_csv(output_path)
|
|
335
|
+
return FileResponse(
|
|
336
|
+
output_path,
|
|
337
|
+
media_type="text/csv",
|
|
338
|
+
filename="annotations.csv",
|
|
339
|
+
)
|