Preserve aspect ratio in false-positive review sheets
GeoIntel CI / docs-smoke (push) Canceled after 0s
GeoIntel CI / contract-smoke (push) Canceled after 0s

This commit is contained in:
Codex
2026-07-13 17:20:40 +02:00
parent 1322a5d66a
commit e2ddf84c74
4 changed files with 88 additions and 30 deletions
@@ -141,7 +141,7 @@ def require_dependencies() -> dict[str, Any]:
import rasterio
from PIL import Image, ImageDraw, ImageFont
from pyproj import Geod, Transformer
from shapely.geometry import box, shape
from shapely.geometry import Polygon, box, shape
from shapely.ops import transform
except ImportError as exc:
raise SystemExit(
@@ -156,6 +156,7 @@ def require_dependencies() -> dict[str, Any]:
"Geod": Geod,
"Transformer": Transformer,
"box": box,
"Polygon": Polygon,
"shape": shape,
"transform": transform,
}
@@ -212,15 +213,23 @@ def draw_geometry(
draw: Any,
geometry: Any,
inverse_transform: Any,
scale_x: float,
scale_y: float,
scale: float,
offset_x: float,
offset_y: float,
crop_left: float,
crop_top: float,
color: tuple[int, int, int],
) -> None:
for ring in polygon_rings(geometry):
points = []
for x, y in ring.coords:
column, row = inverse_transform * (x, y)
points.append((column * scale_x, row * scale_y))
points.append(
(
(column - crop_left) * scale + offset_x,
(row - crop_top) * scale + offset_y,
)
)
if len(points) >= 2:
draw.line(points, fill=color, width=2, joint="curve")
@@ -239,18 +248,44 @@ def render_card(
Transformer = dependencies["Transformer"]
shape = dependencies["shape"]
transform_geometry = dependencies["transform"]
box = dependencies["box"]
Polygon = dependencies["Polygon"]
header_height = 88
with rasterio.open(record["resolved_source_tile_path"]) as source:
pixels = normalize_raster(source.read(), numpy)
image = Image.fromarray(pixels, mode="RGB").resize(
(thumb_size, thumb_size), Image.Resampling.BILINEAR
bbox = record["bbox_json"]
if (
float(bbox["x_min"]) < 0
or float(bbox["y_min"]) < 0
or float(bbox["x_max"]) > source.width
or float(bbox["y_max"]) > source.height
):
raise SystemExit(
f"Detection bbox_json exceeds source tile dimensions: {record['candidate_feature_id']}"
)
bbox_width = float(bbox["x_max"]) - float(bbox["x_min"])
bbox_height = float(bbox["y_max"]) - float(bbox["y_min"])
crop_width = min(source.width, max(128, math.ceil(bbox_width * 4)))
crop_height = min(source.height, max(128, math.ceil(bbox_height * 4)))
center_x = (float(bbox["x_min"]) + float(bbox["x_max"])) / 2
center_y = (float(bbox["y_min"]) + float(bbox["y_max"])) / 2
crop_left = max(0, min(source.width - crop_width, round(center_x - crop_width / 2)))
crop_top = max(0, min(source.height - crop_height, round(center_y - crop_height / 2)))
crop_right = crop_left + crop_width
crop_bottom = crop_top + crop_height
image = Image.fromarray(pixels, mode="RGB").crop(
(crop_left, crop_top, crop_right, crop_bottom)
)
scale = min(thumb_size / image.width, thumb_size / image.height)
render_width = max(1, round(image.width * scale))
render_height = max(1, round(image.height * scale))
image = image.resize((render_width, render_height), Image.Resampling.BILINEAR)
image_offset_x = (thumb_size - render_width) // 2
image_offset_y = (thumb_size - render_height) // 2
card = Image.new(
"RGB", (thumb_size, thumb_size + header_height), color=(242, 245, 247)
)
card.paste(image, (0, header_height))
card.paste(image, (image_offset_x, header_height + image_offset_y))
draw = ImageDraw.Draw(card)
font = ImageFont.load_default()
draw.rectangle((0, 0, thumb_size, header_height), fill=(22, 29, 38))
@@ -279,29 +314,33 @@ def render_card(
(6, 72), "green ref | blue miss", fill=(235, 238, 241), font=font
)
scale_x = thumb_size / source.width
scale_y = thumb_size / source.height
bbox = record["bbox_json"]
draw.rectangle(
(
max(0, float(bbox["x_min"]) * scale_x),
header_height + max(0, float(bbox["y_min"]) * scale_y),
min(thumb_size - 1, float(bbox["x_max"]) * scale_x),
header_height + min(thumb_size - 1, float(bbox["y_max"]) * scale_y),
),
outline=(231, 76, 60),
width=3,
candidate_box = (
(float(bbox["x_min"]) - crop_left) * scale + image_offset_x,
header_height
+ (float(bbox["y_min"]) - crop_top) * scale
+ image_offset_y,
(float(bbox["x_max"]) - crop_left) * scale + image_offset_x,
header_height
+ (float(bbox["y_max"]) - crop_top) * scale
+ image_offset_y,
)
overlay_count = 0
if source.crs:
transformer = Transformer.from_crs("EPSG:4326", source.crs, always_xy=True)
bounds = box(*source.bounds)
crop_bounds = Polygon(
[
source.transform * (crop_left, crop_top),
source.transform * (crop_right, crop_top),
source.transform * (crop_right, crop_bottom),
source.transform * (crop_left, crop_bottom),
]
)
overlay = Image.new("RGBA", (thumb_size, thumb_size), (0, 0, 0, 0))
overlay_draw = ImageDraw.Draw(overlay)
for reference in references:
geometry = transform_geometry(transformer.transform, shape(reference["geometry"]))
if geometry.is_empty or not geometry.intersects(bounds):
if geometry.is_empty or not geometry.intersects(crop_bounds):
continue
color = (
(39, 174, 96)
@@ -312,12 +351,16 @@ def render_card(
overlay_draw,
geometry,
~source.transform,
scale_x,
scale_y,
scale,
image_offset_x,
image_offset_y,
crop_left,
crop_top,
color,
)
overlay_count += 1
card.paste(overlay, (0, header_height), overlay)
draw.rectangle(candidate_box, outline=(231, 76, 60), width=3)
return card, overlay_count