annotools.image¶
The preview pipeline: crop and downscale a source into a token-bounded view, then draw grids and annotations on it. Overlays take coordinates in source space and map them through the preview's crop and scale, so the same objects can be drawn on a full view and on a zoomed crop.
preview ¶
Crop and resize a source image into a token-bounded preview.
PreviewResult
dataclass
¶
PreviewResult(
image: Image,
metadata: dict[str, Any] = dict(),
crop_pixels: tuple[int, int, int, int] | None = None,
)
A rendered preview plus the metadata agents need to map it back to the source.
Attributes:
| Name | Type | Description |
|---|---|---|
image |
Image
|
The rendered PIL image (what the model will see). |
metadata |
dict[str, Any]
|
|
crop_pixels |
tuple[int, int, int, int] | None
|
The applied crop in source pixels ( |
Examples:
>>> from PIL import Image
>>> from annotools import preview
>>> preview(Image.new("RGB", (1600, 1200)), max_width=384, max_height=384).metadata[
... "output_size"
... ]
[384, 288]
crop_pixels
class-attribute
instance-attribute
¶
The applied crop in source pixels (None = full frame); exact, unlike re-deriving it from crop.
preview ¶
preview(
image: Image,
*,
crop: Box | None = None,
target_pixels: int | None = None,
max_width: int | None = None,
max_height: int | None = None,
allow_upscale: bool = False,
) -> PreviewResult
Crop image to a normalized box and shrink it to fit a token budget.
The result is what an MLLM will see: agents localize on this view, so the returned metadata carries
everything needed to map their answers back to the uncropped source (see
normalize_coordinates). Downscaling uses LANCZOS; upscaling (only with
allow_upscale) uses BICUBIC. EXIF orientation must already be applied (load_image
does).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
image
|
Image
|
Source image; not modified. |
required |
crop
|
Box | None
|
|
None
|
target_pixels
|
int | None
|
Cap on the output area in pixels; combined with the size limits (smallest wins). |
None
|
max_width
|
int | None
|
Maximum output width in pixels; |
None
|
max_height
|
int | None
|
Maximum output height in pixels; |
None
|
allow_upscale
|
bool
|
Enlarge small images or crops up to the limits instead of returning them as is. |
False
|
Returns:
| Type | Description |
|---|---|
PreviewResult
|
A |
PreviewResult
|
|
PreviewResult
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
|
Examples:
>>> from PIL import Image
>>> from annotools import preview
>>> result = preview(
... Image.new("RGB", (1600, 1200)),
... crop=(0.5, 0.5, 1.0, 1.0),
... max_width=384,
... max_height=384,
... )
>>> result.image.size, result.metadata["scale"]
((384, 288), 0.48)
References
- Spec:
.agents/knowledge/spec/preview-image.mdandmcp-overview.md(annotools repository). - Default 384 px: the largest size Gemini bills as one 258-token unit,
https://ai.google.dev/gemini-api/docs/image-understanding (verified 2026-08-27); Claude and GPT
bill by area, so pass larger limits for them (
ARCHITECTURE.md, Decisions).
Source code in src/annotools/image/preview.py
encode ¶
Encode image as jpeg, png, or webp bytes.
JPEG is the default because it is the cheapest to send; alpha is flattened onto white for JPEG and
the quality comes from Settings.jpeg_quality (90).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
image
|
Image
|
The image to encode (any mode; converted as the format requires). |
required |
output_format
|
str | None
|
|
None
|
Returns:
| Type | Description |
|---|---|
bytes
|
The encoded bytes. |
Raises:
| Type | Description |
|---|---|
ValueError
|
For an unknown |
Examples:
>>> from PIL import Image
>>> from annotools import encode
>>> encode(Image.new("RGB", (10, 10)), "png")[:4]
b'\x89PNG'
References
- Spec:
.agents/knowledge/spec/mcp-overview.md(annotools repository),output_format.
Source code in src/annotools/image/preview.py
grid ¶
Semi-transparent grid overlay that helps an MLLM anchor positions.
GridOptions ¶
Bases: BaseModel
Grid parameters shared by every tool that accepts grid.
None fields take their value from Settings when the grid is drawn
(resolved). A 10x10 ratio grid is the default because MLLMs
anchor positions far better against visible cells than on a bare image, and 10 cells keep the labels legible at 384
px.
Examples:
References
- Spec:
.agents/knowledge/spec/mcp-overview.md(annotools repository),GridOptionstable.
resolved ¶
Return a copy with every None filled from Settings.
Source code in src/annotools/image/grid.py
draw_grid ¶
Blend grid lines onto image and return it with grid metadata.
Lines are blended at options.opacity in white, black, or the inverted underlying pixels so the
grid stays visible on any background without hiding it. Metadata reports the cell layout so
coordinates can be reasoned about in cells and converted back.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
image
|
Image
|
The (already previewed) PIL image; RGBA input keeps its alpha channel. |
required |
options
|
GridOptions
|
Grid layout; |
required |
Returns:
| Type | Description |
|---|---|
PreviewResult
|
A |
PreviewResult
|
/ |
Raises:
| Type | Description |
|---|---|
ValueError
|
Via |
Examples:
>>> from PIL import Image
>>> from annotools import GridOptions, draw_grid
>>> draw_grid(
... Image.new("RGB", (200, 100)), GridOptions(columns=4, rows=2)
... ).metadata["grid"]["columns"]
4
References
- Spec:
.agents/knowledge/spec/preview-image-grid.md(annotools repository).
Source code in src/annotools/image/grid.py
overlay ¶
Annotation overlays (bounding boxes, keypoints, polygons) drawn on a rendered preview.
BBoxObject ¶
KeypointObject ¶
PolygonObject ¶
draw_bboxes ¶
draw_bboxes(
result: PreviewResult,
objects: Sequence[BBoxObject],
line_width: int | None = None,
) -> PreviewResult
Draw objects on result.image (in place) and add the objects count to the metadata.
Boxes are given in source coordinates and mapped through the preview's crop and scale, so
the same objects can be drawn on the full view and on a zoomed crop. Boxes entirely outside the
view are skipped but still counted; partially visible ones are clipped.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
result
|
PreviewResult
|
A preview from |
required |
objects
|
Sequence[BBoxObject]
|
At least one box; |
required |
line_width
|
int | None
|
Outline width in output pixels; |
None
|
Returns:
| Type | Description |
|---|---|
PreviewResult
|
The same image wrapped with |
Raises:
| Type | Description |
|---|---|
ValueError
|
For empty |
Examples:
>>> from PIL import Image
>>> from annotools import BBoxObject, draw_bboxes, preview
>>> result = preview(
... Image.new("RGB", (400, 300), "white"), max_width=400, max_height=400
... )
>>> draw_bboxes(
... result, [BBoxObject(bbox=(0.1, 0.1, 0.5, 0.5), label="cat")]
... ).metadata["objects"]
1
References
- Spec:
.agents/knowledge/spec/preview-image-bboxes.md(annotools repository).
Source code in src/annotools/image/overlay.py
draw_keypoints ¶
draw_keypoints(
result: PreviewResult,
objects: Sequence[KeypointObject],
point_diameter: int | None = None,
) -> PreviewResult
Draw objects as filled dots (optional labels) and add the objects count to the metadata.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
result
|
PreviewResult
|
A preview from |
required |
objects
|
Sequence[KeypointObject]
|
At least one point; |
required |
point_diameter
|
int | None
|
Dot diameter in output pixels; |
None
|
Returns:
| Type | Description |
|---|---|
PreviewResult
|
The same image wrapped with |
Raises:
| Type | Description |
|---|---|
ValueError
|
For empty |
Examples:
>>> from PIL import Image
>>> from annotools import KeypointObject, draw_keypoints, preview
>>> result = preview(
... Image.new("RGB", (400, 300), "white"), max_width=400, max_height=400
... )
>>> draw_keypoints(result, [KeypointObject(point=(0.5, 0.5))]).metadata["objects"]
1
References
- Spec:
.agents/knowledge/spec/preview-image-keypoints.md(annotools repository).
Source code in src/annotools/image/overlay.py
draw_polygons ¶
draw_polygons(
result: PreviewResult,
objects: Sequence[PolygonObject],
line_width: int | None = None,
point_diameter: int | None = None,
show_point_index: bool = True,
) -> PreviewResult
Draw closed polygons with vertex dots and optional 1-based vertex indices; add objects to metadata.
Vertex indices let a model refer to "point 3" when correcting a polygon, which is why they are on by default. Polygons with no vertex inside the view are skipped but still counted.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
result
|
PreviewResult
|
A preview from |
required |
objects
|
Sequence[PolygonObject]
|
At least one polygon ( |
required |
line_width
|
int | None
|
Outline width in output pixels; |
None
|
point_diameter
|
int | None
|
Vertex dot diameter; |
None
|
show_point_index
|
bool
|
Draw the 1-based vertex number next to each vertex. |
True
|
Returns:
| Type | Description |
|---|---|
PreviewResult
|
The same image wrapped with |
Raises:
| Type | Description |
|---|---|
ValueError
|
For empty |
Examples:
>>> from PIL import Image
>>> from annotools import PolygonObject, draw_polygons, preview
>>> result = preview(
... Image.new("RGB", (400, 300), "white"), max_width=400, max_height=400
... )
>>> draw_polygons(
... result, [PolygonObject(points=[0.1, 0.1, 0.5, 0.1, 0.1, 0.5])]
... ).metadata["objects"]
1
References
- Spec:
.agents/knowledge/spec/preview-image-polygons.md(annotools repository).
Source code in src/annotools/image/overlay.py
segmentation ¶
ID-mask overlays for instance, panoptic, and semantic segmentation previews.
MASK_MODES
module-attribute
¶
Pillow modes accepted for an ID mask: single-channel integer images only.
load_mask ¶
Load a single-channel ID mask (0 = background) as an integer array.
Masks are exchanged as ID images rather than color images so instance, panoptic, and semantic
outputs share one contract (ARCHITECTURE.md, Decisions); the accepted modes are MASK_MODES.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
uri
|
str
|
Local path or fsspec URL of an |
required |
Returns:
| Type | Description |
|---|---|
ndarray
|
A 2-D |
Raises:
| Type | Description |
|---|---|
ValueError
|
Naming |
FileNotFoundError
|
When the URI does not exist (from |
OSError
|
For other read failures (from |
Examples:
References
- Spec:
.agents/knowledge/spec/preview-image-segmentation.md(annotools repository).
Source code in src/annotools/image/segmentation.py
overlay_mask ¶
overlay_mask(
result: PreviewResult,
mask: ndarray,
*,
annotation: Literal["label", "legend"] | str = "label",
id_names: dict[int, str] | None = None,
alpha: float = 0.5,
line_width: int | None = None,
max_width: int | None = None,
max_height: int | None = None,
target_pixels: int | None = None,
) -> PreviewResult
Blend the ID mask over result.image and annotate regions with labels or a legend.
mask is in source pixel space (any size; resized to the source with nearest neighbour), then
follows the preview's crop and scale. Each ID gets a stable color from
color_from_text, so the same instance keeps its color across previews.
annotation="legend" appends a color strip below the image and re-fits the composite to the size limits.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
result
|
PreviewResult
|
A preview from |
required |
mask
|
ndarray
|
2-D integer array of IDs, 0 = background (from |
required |
annotation
|
Literal['label', 'legend'] | str
|
|
'label'
|
id_names
|
dict[int, str] | None
|
Optional |
None
|
alpha
|
float
|
Blend strength of the region color in [0, 1]. |
0.5
|
line_width
|
int | None
|
Boundary width in output pixels (0 = no boundary); |
None
|
max_width
|
int | None
|
Size limit for the legend composite, which is re-fitted after the strip is appended;
|
None
|
max_height
|
int | None
|
Likewise; |
None
|
target_pixels
|
int | None
|
Optional area cap for the legend composite. |
None
|
Returns:
| Type | Description |
|---|---|
PreviewResult
|
A new |
PreviewResult
|
legend, |
PreviewResult
|
inverse mapping applies to, above the strip) and size keys refreshed for the re-fitted composite — |
PreviewResult
|
|
Raises:
| Type | Description |
|---|---|
ValueError
|
For |
Examples:
>>> import numpy as np
>>> from PIL import Image
>>> from annotools import overlay_mask, preview
>>> result = preview(
... Image.new("RGB", (100, 100), "white"), max_width=100, max_height=100
... )
>>> mask = np.zeros((100, 100), dtype=np.uint8)
>>> mask[20:60, 20:60] = 1
>>> overlay_mask(result, mask).metadata["ids"]
1
References
- Spec:
.agents/knowledge/spec/preview-image-segmentation.md(annotools repository).
Source code in src/annotools/image/segmentation.py
106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 | |