EasyAnnotate
Image annotation with nothing to set up. Open a folder, draw a box, type a class name — that is the whole workflow. Load a model's predictions and review them at a few images a minute. Export a dataset that is correct by construction.
pip install easyannotate
easyannotate demo # a sample project opens in your browser
easyannotate annotate ./my-images # your own folder: labels are written in place
easyannotate app # or a desktop window, with an Open-folder button
Three ways to run it, one codebase:
- Local web app —
easyannotate serveopens a page in your browser that talks to a server on your own machine. Paste a folder path (or Browse) and work on it in place; nothing is uploaded unless you upload a zip yourself. The link the launcher opens carries a one-time key, so no other website can reach your folders through it. - Headless —
import easyannotate as eafor scripts and pipelines, theeasyannotateCLI, oreasyannotate serve --headlesson a GPU box, reached through an SSH tunnel. - Desktop app — its own window, an Open-folder button, everything included except the heavy models, which download only when you ask.
Or download the desktop app for Windows, macOS or Linux from the releases page — no Python needed.
No account, no server, no Docker, no project template, no label schema. Your images never leave your machine. Python 3.12+, Windows, macOS and Linux.
Author: Shivpratap Singh Panwar.
Why this exists
CVAT makes you create an organisation, then a project, then labels, then a task and jobs before you can draw one box. Label Studio wants a labelling config. Roboflow wants your images in its cloud. EasyAnnotate's rule, which every feature is subordinate to:
If a feature requires you to configure something before you can draw a box, it does not ship.
What you get instead:
- A class is created by using it. Draw, type a name, Enter. Typing
openwhenOpenexists picksOpen— theClose/closesplit that ruins real datasets cannot happen by accident. - No Save button. Edits are saved 400ms after you stop and before you navigate. Reopening a project resumes on the first unfinished image. Export… downloads the dataset in any format, from the editor.
- The model loop is the product. Import predictions from any YOLO, COCO,
VOC or LabelMe run; they arrive dashed with a confidence slider;
Aaccepts an image. An agreement figure tells you when to retrain and when to stop. - Exports are correct. YOLO class indices never move when you rename or retire a class. Export defaults to reviewed images only, so unreviewed model output can never silently become training data.
- Five shape kinds, every format. Boxes, polygons, rotated boxes, lines and points. Rotated boxes export as YOLO-OBB and DOTA, points as COCO keypoints, everything to LabelMe — and a dataset of any of them still comes out as plain boxes when a detector wants boxes.
- AI assist that feeds the review loop.
easyannotate tools installadds SAM 2 (drag a box or click: an editable outline) and text-prompted detection: typehandle, and every handle on every unreviewed image arrives as a prediction to review, dashed, with a confidence slider and one key to accept. The models run in their own environment, so the editor's install never depends on PyTorch, and without them it works unchanged. - Dataset tooling built in. 28 operations — convert between five formats,
find duplicates and train/val leakage, health reports, augmentation that
moves the labels with the pixels — one
pip installaway.
One core, four front ends
The original Power_tools/ folder held fifteen scripts. Each hardcoded its
paths at the top, each re-implemented "find the image that matches this XML",
and they had quietly drifted apart — one moved sidecar labels alongside
duplicates, another had that code commented out. Same job, different answers.
Everything here runs through one core. Pairing an image with its label, moving annotations when the pixels move, previewing before writing, and recording what happened are solved once, so no operation can forget them.
data_factory/
core/ pure logic — no printing, no argparse, no hardcoded paths
types.py OpResult / FileAction / Context (progress + cancellation)
registry.py every operation declares its own parameters
pairs.py the image + sidecar abstraction all 15 scripts re-invented
fs.py the single choke point every file mutation passes through
imaging.py image I/O, and geometry that moves annotations with pixels
writer.py streaming dataset writer, so augmentation is not memory-bound
stats.py dataset measurement, and the judgements drawn from it
parallel.py ordered parallel map for the decode-bound stages
provenance.py per-dataset lineage, recorded automatically
bktree.py metric tree behind fast perceptual matching
annotations/ neutral model + VOC / YOLO / COCO / LabelMe / CSV
ops/ the operations themselves
cli/ argparse commands, interactive menu, terminal rendering
pipeline.py YAML/JSON recipes
An operation declares its parameters once. The CLI subcommand, the menu prompts, the recipe validator and the web form schema are all generated from that declaration — adding an operation means writing one file, and it appears everywhere.
Install
pip install easyannotate # everything: editor, CLI, service
uv tool install easyannotate # isolated, on PATH
uvx easyannotate demo # run once, install nothing
Python 3.12+. Needs only numpy, Pillow, OpenCV, imagehash, PyYAML and FastAPI — deliberately no PyTorch, so it installs in seconds anywhere.
OpenCV is the headless build by default. The ordinary opencv-python wheel
links against libGL.so.1, which slim containers, Cloud Run and most CI images
do not have — it installs fine and then throws ImportError on exactly the
machines a backend runs on. Only collect.webcam's preview needs a GUI, and it
falls back to timer capture with a warning. For the preview:
pip install "easyannotate[gui]".
There is deliberately no sam extra. Model-backed tools run in a separate
process with their own interpreter, discovered at runtime and absent without
complaint — so torch's version matrix never has to agree with this package's.
Point DATAFACTORY_TOOLS_MODULE at one to enable it; with none installed,
GET /api/tools returns an empty list and the editor is fully usable.
The Python package is data_factory, and easyannotate still works as a
command — the tool was named EasyAnnotate before the editor became the point.
Four ways to drive it
1. Interactive menu — run it bare. Every path is a prompt, and destructive operations preview before they ask.
easyannotate
2. Command line — for scripting and repeatability.
easyannotate list # every operation
easyannotate explain augment.dataset # full help for one
easyannotate convert.detect --folder ./mystery-dataset
easyannotate inspect.health --folder ./data
easyannotate convert.dataset --folder ./voc --output ./yolo --to yolo
easyannotate augment.dataset --folder ./train --output ./aug --multiplier 3 --dry-run
3. Recipes — a dataset build written down, so it is reproducible.
easyannotate run recipes/train_ready_yolo.yaml --set root=D:/data --set out=D:/build
4. Browser — a local web interface, which doubles as the desktop app.
easyannotate serve # opens http://localhost:8765
Drop in a zip, it detects the format, and every operation appears as a form
generated from the same declaration the CLI reads. Binds to 127.0.0.1 only.
The same code hosts the service — see DEPLOYING.md.
Annotating
easyannotate serve, drop in a folder, click Start annotating. That is the
whole setup. There is no organisation to create, no project template, no task,
no label schema — a class is created by drawing a box and typing its name.
drag draw a box in the current class -- inside other boxes too
click select a shape; drag it to move, its handles to resize
/ find a class by typing (never makes a Close/close variant)
P polygon: click points, Enter or double-click to close
O rotated box: drag, then turn it by its handle, or [ and ]
L K line: click points, Enter · point: one click
1–0 pick a class — or re-class what is selected
Alt+click on a vertex: delete it. On an edge midpoint: insert one
Ctrl+D copy the previous image's shapes (worth more than a model on video)
R mark reviewed and move on
S skip — "I can't tell". Never exported
arrows nudge the selection 1px, Shift 10px
Tab select the next shape, even one buried under another
Ctrl+C / Ctrl+V copy shapes, paste onto this image or the next
H hide every shape to see the bare image
? every shortcut
There is no Save button: edits are saved 400ms after you stop and before you navigate. Reopening a project resumes on the first unfinished image, not at the beginning.
Existing work comes with you. Promoting an upload imports its VOC, YOLO, COCO
or LabelMe annotations, and if it carried a data.yaml the class order in it is
adopted rather than recomputed — those indices are already baked into label
files that exist in the world.
Export goes back through an ordinary operation, so it has a dry run, progress and recipe support like everything else:
easyannotate annotate.export --database ~/.easyannotate/annotations.db \
--project <id> --output ./train-set --to yolo
It exports reviewed images only unless told otherwise. Feeding a model's own unreviewed predictions back in as ground truth is how a model poisons itself, and nothing about the output looks wrong when it happens.
Annotating a folder in place
easyannotate annotate ./my-dataset
No copy is made. The folder's own label files are updated as you draw —
VOC .xml, LabelMe .json or YOLO .txt, whichever the folder already uses,
detected on open. A YOLO folder's data.yaml is kept in step, and its existing
class order is inherited rather than recomputed.
Writes are atomic, so a save landing while something else reads the folder
cannot produce a truncated label file. COCO and CSV keep one file for the whole
dataset, so those open read-only for write-back — it says so, and
annotate.export produces the updated dataset instead.
This mode is CLI-only by design: it takes a path, and no HTTP route is ever allowed to.
The model loop
Annotate a few hundred by hand → train → run inference → load the predictions back in → fix the wrong ones → export → retrain.
Load a zip of YOLO .txt (with --save-conf), VOC, LabelMe, or a COCO json —
including the detection-results form, the bare JSON list that torchvision,
mmdetection and Ultralytics evaluation scripts actually emit.
Predictions render dashed, and fade with confidence. A slider hides the weak
ones without deleting them. A accepts everything visible, marks the image
reviewed and moves on — a thousand images in about fifteen minutes when the
model is mostly right.
The import always reports what didn't match. Predictions pair with images by
filename stem, and silently importing zero shapes because img_001.txt met
IMG_001.jpg looks exactly like a model that predicted nothing.
Model agreement is the number to watch: it tells you when to retrain and when to stop. It counts only predictions on images a person has actually reviewed — otherwise importing a fresh run would make the model look better — and it counts deletions, because deleting a false positive is the commonest correction there is and ignoring it would score a model that hallucinates boxes at 100%.
One turn of the loop is written down as a recipe:
easyannotate run recipes/model_loop.yaml --set project=<id> --set out=./round-2
Export reviewed → check classes and health → split → check leakage → convert → resize → augment train only → report. Compare this round's report against the last one to see whether the loop is actually working.
AI assist
easyannotate tools install # ~/.datafactory/tools, chosen for this machine
easyannotate tools install --cpu # skip the GPU build
easyannotate tools status # what this machine gets, and what is installed
From a source checkout, before the first PyPI release:
easyannotate tools install --source . The desktop app has the same thing
as an "Install AI tools" button.
The installer reads the GPU, its memory and its driver. NVIDIA cards get the PyTorch CUDA build their driver was made for (12.8 or 12.6; PyTorch 2.7 with CUDA 11.8 for drivers older than 560), Apple silicon gets Metal, anything else the CPU build. Model sizes follow memory: SAM 2.1 base, small or tiny, YOLO-World large, medium or small. Then it runs real work on the device and believes only that: on an RTX 2050 with driver 529, PyTorch's CUDA 12.6 build reported the GPU available and failed every allocation. Measured there on CUDA 11.8: SAM 0.044 s a click, a new text prompt about 1 s.
Then the Assist panel appears in the editor:
- Outline (SAM) — drag a box, or click, on an object: SAM 2.1 traces it and you get a simplified polygon with draggable vertices.
- Find every… — type a class name (or several:
door, handle) and run it on this image, or on every unreviewed image as a job. The results are predictions: dashed, scored, filtered by the slider, accepted withA, counted by the agreement figure, and never exported until reviewed.
Weights download on first use. A slow machine is fine: a tool that is still working says so and the editor waits, while you keep drawing.
The loop: train, predict, review hardest first
easyannotate train ./doors # reviewed images -> a YOLO model -> predictions on the rest
easyannotate predict ./doors --model best.pt # or bring your own Ultralytics model
train exports exactly what export would (reviewed images, the project's own
class order, so index 3 means in the model what it means in your files),
trains in the tools environment, saves the model under
~/.datafactory/models, and predicts every unreviewed image with it. Then pick
predicted, least sure first in the editor's image list: images are ordered
by how likely their predictions are wrong (ambiguous confidences, and boxes of
two classes on one object), and N, E and A walk that queue. Each turn of
the loop needs less of you than the last.
Custom models run from the command line only: a .pt file is code, and no
web route will ever load one.
Model-assisted shapes
Segmentation models can propose an outline; the editor shows a button for one
only when a tool is installed and healthy, so there are never dead controls.
With nothing installed, GET /api/tools returns an empty list and everything
else works — that is the default and it is a tested state.
Heavy models run in a separate process. A CUDA out-of-memory or a native segfault kills that process, the tool goes unavailable, and you carry on drawing boxes. Turning a mask into an editable outline is done here, not there: contours, Douglas-Peucker simplification, then a hard vertex cap. A 512×512 mask has a boundary thousands of pixels long, and nobody edits a two-thousand-vertex polygon — which is exactly what the script this replaces used to write.
Class indices do not move
A YOLO label file stores a class index, never a name, so the index-to-name
table is external state that every exported .txt silently depends on. Deriving
it by sorting the names present at export time means adding one class shifts
every index at or after it:
Monday [Closed, Open] a file reading `1` means Open
Friday [Closed, Handle, Open] the same file now means Handle
Nothing errors. The files still parse. A project's label table allocates an
index once and never moves it: renaming keeps the index, retiring keeps the
slot, a new class is appended. For datasets without a project, pass the same
--classes list to convert.dataset on every export and the indices hold.
Operations
Formats — every direction, because they meet in the middle at one model.
| Operation | What it does |
|---|---|
convert.dataset |
VOC ↔ YOLO ↔ COCO ↔ LabelMe ↔ CSV, boxes and polygons |
convert.detect |
Identify an unknown folder and summarise what is in it |
export.coco |
Pascal VOC → COCO with segmentation and hard negatives |
Preprocessing — the pixels move, and the annotations move with them.
| Operation | What it does |
|---|---|
image.resize |
Fixed size by letterbox, stretch, crop or fit |
image.autoorient |
Bake EXIF rotation into the pixels, so every reader agrees |
image.tile |
Slice big images into an overlapping grid, reassigning objects |
augment.dataset |
Seeded variants: flips, rotation, colour, blur, noise, cutout |
Analysis — whether this dataset will train into something that works.
| Operation | What it does |
|---|---|
inspect.health |
Class balance, object size, geometry — ranked findings with fixes |
inspect.leakage |
Near-duplicate images shared between train and val |
inspect.dataset |
Pairing, classes, geometry, corrupt files, duplicates |
inspect.history |
What was done to this dataset, in order, with its settings |
inspect.coco |
Summarise an instances.json, flag case-collided categories |
report.html |
One self-contained HTML report, no external assets |
labels.list |
Count classes and objects, surface case-variant class names |
verify.visualize |
Draw boxes and polygons onto copies, colour-coded by class |
Dataset algebra
| Operation | What it does |
|---|---|
dataset.merge |
Combine datasets, reconciling class drift and name collisions |
dataset.filter |
Subset by class, object count, object size or split |
dataset.sample |
Seeded subset — random, stratified, or capped per class |
dataset.split |
Reproducible, stratified train/val/test split |
dataset.sort |
Group pairs into one folder per class |
Cleaning and collection
| Operation | What it does |
|---|---|
clean.dedup |
Exact or perceptual duplicates, sidecar labels moved too |
clean.orphans |
Report, quarantine or delete unpaired images and labels |
clean.unannotated |
Move images with no label into an "still to annotate" queue |
clean.rename |
Sequential renaming, pair-safe, retargets the label's filename |
labels.rename |
Bulk rename/drop classes; --fold-case unifies case drift |
collect.frames |
Video (or a folder of videos) → numbered JPEG frames |
collect.webcam |
Capture stills from a camera, manually or on a timer |
Operations marked ! in easyannotate list write to disk. All accept --dry-run.
Five ideas worth knowing
Pairs. An image and its label are one thing. Every operation moves, renames and splits them together — a property of the core, not something each operation remembers to do.
Dry run is the same code path. Operations build their full list of
FileActions either way; --dry-run only stops the last step from executing. A
preview is a faithful description of the real run, not an approximation.
Formats meet in the middle. Readers parse into a neutral Annotation;
writers serialise out of it. Five formats cost five readers and five writers,
not twenty converters. Adding CreateML is one file, and it converts to and from
everything already here.
Geometry is solved once. Resize, tile and augment share one transform layer. A box is rebuilt from all four transformed corners, so rotation stays correct rather than degenerating into a box of a box; objects that fall out of frame or survive only as slivers are dropped rather than becoming three pixels labelled "door".
Results are data. Every operation returns an OpResult with counts, an
audit trail, warnings and structured payload. The terminal renderer is one
consumer; a web API is another.
What it catches
Run on the real Project_Door V3 set (3,686 images, 3,871 objects):
$ easyannotate inspect.health --folder ./pascal_voc
[error] Classes differing only by capitalisation
Open / open; Close / close
fix: These train as separate classes and split your data in half.
Fix with labels.rename --fold-case.
[note] Mixed image sizes — 811 distinct resolutions.
[note] No train/val split.
Open: 1385 objects / 1316 images
Close: 1341 objects / 1206 images
close: 645 objects / 614 images
open: 500 objects / 499 images
That is a dataset with two real classes training as four, and no one had
noticed. inspect.leakage finds the other expensive one — the same photo in
both train and val, which makes a validation score measure memorisation and
looks excellent right up until deployment.
Running recipes/train_ready_yolo.yaml over that same folder end to end:
| Step | Result |
|---|---|
labels.rename --fold-case |
1,145 objects renamed across 1,088 files — 4 classes → 2 |
dataset.split |
2,948 / 369 / 369, stratified and seeded |
inspect.leakage |
0 leaked pairs across 3 splits |
convert.dataset --to yolo |
3,686 images, 3,871 objects, 2 classes |
image.resize 640 letterbox |
3,686 resized, 0 objects dropped |
augment.dataset ×2, train only |
9,582 total images, 10,067 objects |
inspect.health on the result |
0 errors, 0 warnings, 1 resolution |
The finished dataset is what the health check now has nothing to say about — which is the point of the health check.
As a library, and in a web app
import easyannotate as ea
project = ea.open_folder("./doors") # the same project the editor shows
print(project.counts()) # {'todo': 812, 'reviewed': 200, ...}
project.import_predictions("runs/predict/labels")
hardest = project.images(state="predicted", order="uncertain")[:20]
project.export("./train-set", to="yolo") # reviewed only; class indices never move
project.train(epochs=30) # needs: easyannotate tools install
result = ea.run("inspect.health", {"folder": "./data"})
for finding in result.data["findings"]:
print(finding["severity"], finding["issue"], "->", finding["fix"])
payload = result.as_dict() # JSON-serialisable, ready to return over HTTP
Long runs report progress and can be cancelled — designed for a request that outlives a single HTTP round trip:
from data_factory import Context
ctx = Context(
dry_run=False,
progress=lambda done, total, msg: socket.send({"done": done, "total": total}),
cancel_check=lambda: job.is_cancelled(),
)
df.run("augment.dataset", {"folder": "./train", "output": "./aug"}, ctx)
A front-end can build its own forms without importing Python at all:
easyannotate schema --output schema.json
That file describes every operation, every parameter, its kind (dir, int,
choice, …), defaults, and whether it is destructive. See
examples/web_api.py for a working FastAPI wrapper in about eighty lines.
Provenance
Every operation that writes a dataset appends one line to
<output>/.easyannotate/history.jsonl. It is recorded in Operation.run, not by
the operations themselves, so no operation can forget and a new one gets it
without asking.
easyannotate inspect.history --folder ./final
It answers the questions that are otherwise unanswerable six weeks later: was this augmented, at what seed, was it resized before or after the split, which folders went into the merge. Plain JSON Lines — readable without this tool, and appended to rather than rewritten, so a build that dies halfway still leaves a truthful record.
Performance
The editor at 20,000 images (uv run python docs/bench_scale.py, a laptop,
medians; gates set in docs/PLAN.md before the editor existed):
| What a person waits for | Time | Gate |
|---|---|---|
| Editor open to first image, in a browser | 1.6 s | 2 s |
| Image list page, first or last of 20,000 | 5–7 ms | 200 ms |
| "Least sure first" queue page | 6 ms | 200 ms |
| Open an image's shapes | 2 ms | 300 ms |
| Save a shape, round trip | 7 ms | 100 ms |
| Scroll the strip to the last image | 139 ms | 1 s |
| Strip rows in the DOM | 28 | — |
| Open a 20,000-image folder, first time | 3.7 s | — |
AI assist on a CPU (no GPU used), measured in the editor: SAM outline 0.17 s
per click once the image is encoded (encoding happens ahead, as you look);
"find every person" 4–5 s per image warm, 46 s the very first time (CLIP
downloads); easyannotate train on 4 reviewed images, 3 epochs, then
predicting 20 more: 16 s end to end.
Decode-bound stages run across threads, in input order, so results stay reproducible. Measured on the real Project_Door set:
| Stage | Serial | Parallel | |
|---|---|---|---|
image.resize — 3,686 images |
186.6s | 24.2s | 7.7× |
clean.dedup — perceptual, 2,783 images |
95.6s | 33.2s | 2.9× |
augment.dataset — 5,896 variants |
288.4s | 124.0s | 2.3× |
inspect.health — 9,582 images |
217.7s | 112.4s | 1.9× |
Output is byte-identical either way. Order is preserved deliberately: dedup keeps the first image it sees, and COCO image ids follow write order, so a result that depended on which thread finished first would not be reproducible. Parallelism is confined to pure functions; writing stays on the calling thread.
The map used for image work is bounded — the hashing map submits every task up front, which is right when a result is eight bytes and catastrophic when it is a decoded frame, so image stages keep only a few in flight and memory stays flat regardless of dataset size.
The BK-tree handles the other half — comparing every hash to every other is O(n²), about 112 million comparisons on a 15k-image set.
Set --workers N to override; 0 (the default) picks a sensible count.
Adding an operation
One file, one decorator. It then appears in the CLI, the menu, the schema and the recipe validator with no further wiring.
from ..registry import Param, operation
from ..types import Context, OpResult
@operation(
"clean.mything",
title="My thing",
category="clean",
summary="One line shown in listings.",
params=(Param("folder", "dir", "Folder to work on.", required=True, must_exist=True),),
destructive=True,
safe_when=lambda p: p.get("action") == "report",
)
def mything(params: dict, ctx: Context) -> OpResult:
result = OpResult(op="clean.mything", dry_run=ctx.dry_run)
...
return result
Then add it to the imports in data_factory/core/ops/__init__.py.
Adding a format is two functions — a reader producing Annotation and a
writer consuming it — plus an entry in annotations/formats.py. It then
converts to and from every other format for free.
Where the old scripts went
| Original script | Now |
|---|---|
CrossFolder_Duplicate_Removal_WithXML.py |
clean.dedup --method exact |
Remove_Duplicates_OneFolderOnly.py |
clean.dedup --method phash |
del_orphan_img_xml.py |
clean.orphans --action delete |
rename_labels_andFindOrphans.py |
clean.orphans + labels.rename |
Move_pairs.py |
clean.unannotated |
rename_files_and_xmls.py |
clean.rename |
Find_label_PVOC.py |
labels.list |
video_to_frames.py |
collect.frames |
live_webcame_image_saver.txt (unfinished) |
collect.webcam |
coco_dataset_generators.py |
export.coco / convert.dataset --to coco |
draw_BB_from_xml_toNewFolder.py |
verify.visualize --draw bbox |
Crosscheck_polygon_coco_data.py |
verify.visualize --split-by-type |
Crosscheck_finishedData_pascalvoc.py |
dataset.sort + verify.visualize |
The annotation GUIs (Polygon_Advanced_Annotation_Tool.py,
Sam2.1_segmentation.py) stay as they are — they are interactive OpenCV apps,
not batch operations.
Bugs fixed along the way
- COCO exported four classes where there were two. Categories were keyed on
the raw XML string, so
Openandopenbecame separate classes — splitting Project_Door V3 as 1341/1385/645/500. Export now folds case by default and reports every merge;labels.rename --fold-casefixes it at the source, andinspect.healthnow flags it as an error before you get that far. - SAM polygons exported as empty masks. The two annotation tools write
different polygon XML (
<x1>vs<pt><x>) and the converter only read one. The reader now accepts both. - Renaming could clobber files. Renaming through per-file temporary names still collides when a target belongs to a file later in the queue. It is now done in two global passes.
- Renaming left labels pointing at the old image. The
<filename>and<path>fields inside the XML are now retargeted. - Perceptual dedup was O(n²). ~112 million comparisons on a 15k-image set. A BK-tree cuts it to roughly logarithmic per lookup, and decoding now runs across threads.
- Duplicate removal could discard the annotated copy.
keep=annotatednow keeps the labelled twin by default. - COCO
areawas always the box area, even for polygons. It is now the true polygon area. - Train/test splitting was documented but never implemented. It exists now, stratified and seeded.
Tests
uv run pytest # 621 tests
Ten of those drive the annotation editor in a real browser and are skipped
unless Playwright is installed (pip install playwright && playwright install chromium). A canvas tool with no automated test is one that quietly stops
working.
Fixtures build a small dataset carrying every defect the real one had: case variant classes, both polygon dialects, a hard negative, an exact duplicate, orphans of both kinds and a corrupt XML.
The format tests convert and read back, asserting on what survived — a converter's failure mode is not a crash, it is arriving with fewer objects than it left with. The geometry tests assert the annotation lands where the pixels landed, which is the failure that silently ruins a dataset.
Datasets in this repository
Projects/Project_Door/ holds the door-state dataset (V3: 3686 images, 3871
objects) and Models/Sam2.1_tiny/ the SAM 2.1 tiny checkpoint. Neither is
tracked by git.
Metadata
Release files for easyannotate 0.4.0
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | Provenance | Actions |
|---|---|---|---|---|
| easyannotate-0.4.0.tar.gz | 400.8 kB | With provenance | Details for easyannotate-0.4.0.tar.gz |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Actions Reset |
|---|---|---|---|---|
| easyannotate-0.4.0-py3-none-any.whl | Python 3 | none | any | Details for easyannotate-0.4.0-py3-none-any.whl |
Total release size: 752.9 kB
Release files / easyannotate-0.4.0.tar.gz
| Download URL | easyannotate-0.4.0.tar.gz |
|---|---|
| Size | 400.8 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
43fdbdc5d4fb6ad428c4219bac3850292796c18a8ef26fdfccd8a6aa67f8e351
|
|
BLAKE2b-256 checksum How to use checksums |
04d214eb7afd37514abff8f9f05e0ffae8bac54aab8fc30612352b523cb84809
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 15, 2026.
Transparency logRelease files / easyannotate-0.4.0-py3-none-any.whl
| Download URL | easyannotate-0.4.0-py3-none-any.whl |
|---|---|
| Size | 352.0 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
06b80c309213dbc56a56c9b0f61fe74e57fa68ac4156f6c25889b692817a6cd8
|
|
BLAKE2b-256 checksum How to use checksums |
90a9152117f9a0dbad41be559d081c37bdd2da48cf2a5e219e141ae729316b6a
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/7.0.0 CPython/3.13.14
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 15, 2026.
Transparency logSecurity for easyannotate 0.4.0
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
Full provenance
All 2 files in this release have been uploaded with provenance metadata.
Attestation for easyannotate-0.4.0.tar.gz
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Oct 15, 2026.
Transparency logA note on package security
PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.
To learn more about protecting yourself from malicious actors, visit our package security documentation.