Skip to content

Polygons

Draw COCO-style outlines and DOTA-style oriented boxes with the same 8-or-more-number flat format, with numbered vertices so a model can correct one point instead of restating the shape.

from annotools import (
    PolygonObject,
    RotatedBox,
    draw_polygons,
    is_rectangle,
    load_image,
    normalize_coordinates,
    preview,
    rotated_box_to_corners,
)

view = preview(load_image("photo.jpg"), max_width=768, max_height=768)
width, height = view.metadata["output_size"]

answer = [[180, 113, 454, 171, 420, 327, 146, 269]]  # 4 corners in pixels of the view
print("rectangle:", is_rectangle(answer[0]))  # pixels are square, so the test means something here
outline = normalize_coordinates(answer, width, height, crop=view.metadata["crop"])

corners = rotated_box_to_corners(
    RotatedBox(cx=0.72, cy=0.68, w=0.26, h=0.24, theta=15),
    aspect_ratio=view.metadata["original_width"] / view.metadata["original_height"],
)

overlay = draw_polygons(
    view,
    [
        PolygonObject(points=outline[0], label="roof"),
        PolygonObject(points=corners, label="sign", color="orange"),
    ],
)

The test prints True, so that answer could be stored as a rotated box. Run it on the pixel answer, not on normalized coordinates: normalized units are stretched by the aspect ratio, so a genuine rectangle on a 4:3 image is a parallelogram there. rotated_box_to_corners takes aspect_ratio for the same reason, and does not clip — a box touching the border can produce values outside 0-1, which draw_polygons rejects.

normalize_coordinates on the model's outline, rotated_bbox_to_polygon for the oriented box, then preview_image_polygons to draw both:

{
  "coordinates": [[180, 113, 454, 171, 420, 327, 146, 269]],
  "base_width": 768,
  "base_height": 576,
  "crop": [0, 0, 1, 1]
}
{
  "boxes": [{ "cx": 0.72, "cy": 0.68, "w": 0.26, "h": 0.24, "theta": 15 }],
  "angle_unit": "degrees",
  "aspect_ratio": 1.3333
}
{
  "source": "photo.jpg",
  "objects": [
    { "points": [0.2344, 0.1962, 0.5911, 0.2969, 0.5469, 0.5677, 0.1901, 0.467], "label": "roof" },
    { "points": [0.618, 0.519, 0.869, 0.609, 0.822, 0.841, 0.571, 0.751], "label": "sign" }
  ],
  "max_width": 768,
  "max_height": 768
}

aspect_ratio is the source's original_width / original_height from any preview's metadata — 1600/1200 here — and leaving it at the default 1.0 shears the box on a non-square image. Both conversions answer with full-precision numbers, rounded above for readability. The overlay returns the image plus metadata with objects; vertex indices are 1-based and drawn unless show_point_index is turned off, which is what lets a correction round say "move point 3 left" instead of resending eight numbers. There is no MCP tool for the rectangle test — deciding whether a 4-point answer can collapse into a rotated box is a library-side call.

Then: preview_image_polygons and rotated_bbox_to_polygon for the parameters; draw_polygons, rotated_box_to_corners and is_rectangle for the library contract. Choosing between a polygon and a rotated box, and the correction loop, are in skills/localization-annotation-guide; committing the accepted shapes is skills/sqlite-annotation-store.