Offline image annotation for object detection and segmentation. Point it at a folder, draw boxes and polygons, export COCO / YOLO / Pascal VOC.
Your images never leave your machine. There is no upload, no account, no
database, and no network traffic beyond 127.0.0.1.
pip install -r requirements.txt
python app.py /path/to/photosYour browser opens on http://127.0.0.1:8000. That is the whole setup.
Most annotation tools want you to upload a dataset to a server or stand up a database before you can draw a single box. That is overkill when you are one person labelling a folder of images on your own laptop, and a non-starter when the images are private, sensitive, or simply too large to upload.
Local Label reads the files straight off disk, writes annotations next to them, and gets out of the way.
- Nothing is copied or uploaded. Images are streamed from their original location and never duplicated.
- Annotations are plain JSON, one small file per image. Readable, diffable, and trivial to parse yourself.
- Move the folder and the labels come with it. Everything lives in a
.labeller/directory beside your images. - Exports are real training files, not a JSON blob you have to convert.
- Bounding boxes and polygons
- Zoom, pan, and crosshair guides for precise edges
- Keyboard-driven, so the whole workflow works without the mouse
- Multi-select, bulk relabel, copy and paste shapes between images
- Repeat the previous image's shapes with one key, for sequential footage
- Autosave, undo and redo, and resume exactly where you left off
- Brightness and contrast sliders for dark or flat images (view only, your files are never modified)
- Light and dark themes, plus a neutral canvas surround tuned for long sessions
- Dataset summary: class balance, shape sizes, unlabelled-class warnings
- Export to COCO, YOLO (Ultralytics-ready), and Pascal VOC
Python 3.9 or newer, and a modern browser. Four dependencies, no database, no build step, no Node.
git clone https://github.com/diptiman-mohanta/Local-Label.git
cd Local-Label
pip install -r requirements.txtA virtual environment is recommended but not required:
python -m venv .venv
# Windows
.venv\Scripts\activate
# macOS / Linux
source .venv/bin/activate
pip install -r requirements.txtpython app.py # start, then pick a folder in the UI
python app.py /path/to/photos # open that folder immediately
python app.py /path/to/photos --recursive
python app.py /path/to/photos --port 8080 --no-browser| Flag | Default | What it does |
|---|---|---|
folder |
none | Folder of images to open. Optional; you can pick one in the UI. |
--recursive |
off | Include images in subfolders. |
--port |
8000 |
Port to serve on. |
--host |
127.0.0.1 |
Bind address. Read the security note before changing this. |
--no-browser |
off | Do not open a browser window on start. |
--version |
none | Print the version and exit. |
Supported image types: .jpg .jpeg .png .bmp .webp .tif .tiff
Press ? in the app to see this list at any time.
| Key | Action |
|---|---|
| A / D | Previous / next image |
| Tab | Jump to next unannotated image |
| V B P | Select, Box, Polygon |
| 1 to 9 | Pick label (also relabels the selection) |
| Enter | Close the polygon you are drawing |
| Right-click or Backspace | Undo the last polygon point |
| Esc | Cancel the draft, or deselect |
| Shift+click | Add to selection |
| Ctrl+A | Select all shapes |
| Arrow keys | Nudge selection (Shift for larger steps) |
| Del | Delete selection |
| Ctrl+C / Ctrl+V | Copy and paste shapes across images |
| R | Repeat the previous image's shapes |
| H | Hide all shapes |
| Ctrl+Z / Ctrl+Y | Undo / redo |
| Ctrl+S | Save now (it also autosaves) |
| F | Fit image to view |
| Scroll, middle-drag, Space-drag | Zoom, pan, pan |
| Alt+click | Remove a polygon vertex |
photos/
cat1.jpg
cat2.jpg
.labeller/
classes.txt one label per line; order sets the class id
annotations/
cat1.jpg.json one sidecar per image
exports/
coco/annotations.json
yolo/images/ labels/ data.yaml
voc/cat1.xml
An image with no shapes has no sidecar. Delete .labeller/ and you are back to
a plain folder of photos.
Coordinates are original image pixels, so they stay correct at any zoom level or window size.
{
"version": 1,
"image": "cat1.jpg",
"width": 1920,
"height": 1080,
"shapes": [
{ "id": "a1b2c3d4", "type": "bbox", "label": "cat", "points": [[120, 80], [640, 700]] },
{ "id": "e5f6a7b8", "type": "polygon", "label": "dog", "points": [[10, 10], [90, 20], [50, 95]] }
],
"updated_at": "2026-09-03T00:42:05"
}A bbox is two corner points, top-left and bottom-right. A polygon is three
or more points in order.
Exports are written into .labeller/exports/<format>/ and never touch your
images. Re-running an export replaces the previous one, including removing
files for images whose annotations you have since cleared.
COCO — a single annotations.json. Polygons carry a segmentation array
alongside a derived bbox and shoelace area.
YOLO — a ready-to-train Ultralytics dataset:
yolo/
images/ hard links to your originals, so no extra disk is used
labels/ one .txt per image
data.yaml
yolo detect train data=".labeller/exports/yolo/data.yaml" model=yolo11n.ptBoth folders are required: Ultralytics locates a label by swapping the
/images/ segment of an image path for /labels/. Boxes use cls xc yc w h;
polygons use YOLO-seg (cls x1 y1 x2 y2 ...), normalised to 0-1. Only
annotated images are included, and val points at the same set, so split it
yourself before publishing a result.
Pascal VOC — one .xml per image. Polygons get a <bndbox> plus a
<polygon> element holding the real vertices.
EXIF orientation is handled. Camera JPEGs store portrait shots as landscape pixels plus a rotation tag. Local Label reads that tag so annotations and exports use the dimensions you actually see, which is the same convention Ultralytics uses. Tools that ignore it place boxes wrongly on rotated photos.
Renaming a label migrates existing shapes. If you rename a class, shapes using it are updated. Any label that matches no class is reported rather than silently exported as class 0.
The image folder must be writable, since .labeller/ lives inside it. A
read-only or network location is rejected with a clear message.
Security. --host defaults to loopback for a reason: the folder-open
endpoint can read any directory the process can see. That is correct for a
personal tool on 127.0.0.1 and unsafe on a routable address. Binding
elsewhere prints a warning.
Local Label is deliberately small. Planned next:
- Import existing COCO / YOLO / VOC datasets
- Model-assisted pre-labelling
- One-click segmentation (SAM)
- Per-shape attributes: occluded, truncated, difficult
- Train / val / test splitting on export
Ideas and disagreement both welcome. Open an issue.
Bug reports, feature requests, and pull requests are all welcome. Start with CONTRIBUTING.md.
Found a bug, or something confusing? Open an issue and include your OS, your Python version, and what you expected to happen.
MIT © Diptiman Mohanta