Composer l'image de la planche en Lambert 93 depuis les sources de la pyramide
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@ -117,3 +117,78 @@ def frame(lat, lon, paper, orient, scale):
|
||||
return {"cx": cx, "cy": cy, "bbox_l93": list(b),
|
||||
"corners": [[round(a, 7), round(o, 7)] for a, o in corners],
|
||||
"width_m": round(b[2] - b[0]), "height_m": round(b[3] - b[1])}
|
||||
|
||||
|
||||
# Couleur des pixels sans donnée du relief orienté (recopie de
|
||||
# visualizations.RELIEF_NODATA_RGB : ce module importe numpy, absent de
|
||||
# l'image légère ; égalité vérifiée par les tests).
|
||||
NODATA_RGB = (38, 38, 41)
|
||||
_NODATA_TOLERANCE = 3 # écart par canal toléré (rééchantillonnage)
|
||||
_HATCH_STEP_PX = 14
|
||||
|
||||
|
||||
def _paste_l93(canvas, mask, src, bbox, px):
|
||||
"""Recadre et rééchantillonne une source L93 dans l'image de la planche."""
|
||||
from PIL import Image, ImageChops
|
||||
from . import tiles
|
||||
|
||||
img = tiles.load_source(src)
|
||||
if img is None:
|
||||
return False
|
||||
w, h = img.size
|
||||
sx0, sy0, sx1, sy1 = src.bounds
|
||||
ix0, ix1 = max(bbox[0], sx0), min(bbox[2], sx1)
|
||||
iy0, iy1 = max(bbox[1], sy0), min(bbox[3], sy1)
|
||||
if ix1 <= ix0 or iy1 <= iy0:
|
||||
return False
|
||||
dx0 = int(round((ix0 - bbox[0]) / px)); dx1 = int(round((ix1 - bbox[0]) / px))
|
||||
dy0 = int(round((bbox[3] - iy1) / px)); dy1 = int(round((bbox[3] - iy0) / px))
|
||||
if dx1 <= dx0 or dy1 <= dy0:
|
||||
return False
|
||||
rx, ry = (sx1 - sx0) / w, (sy1 - sy0) / h
|
||||
gx0, gx1 = bbox[0] + dx0 * px, bbox[0] + dx1 * px
|
||||
gy1, gy0 = bbox[3] - dy0 * px, bbox[3] - dy1 * px
|
||||
box = (max(0.0, (gx0 - sx0) / rx), max(0.0, (sy1 - gy1) / ry),
|
||||
min(float(w), (gx1 - sx0) / rx), min(float(h), (sy1 - gy0) / ry))
|
||||
part = img.resize((dx1 - dx0, dy1 - dy0), Image.LANCZOS, box=box)
|
||||
rgb = part.convert("RGB")
|
||||
diff = ImageChops.difference(rgb, Image.new("RGB", rgb.size, NODATA_RGB))
|
||||
r, g, b = diff.split()
|
||||
valid = ImageChops.lighter(ImageChops.lighter(r, g), b).point(
|
||||
lambda v: 255 if v > _NODATA_TOLERANCE else 0)
|
||||
if part.mode == "RGBA":
|
||||
valid = ImageChops.multiply(valid, part.getchannel("A").point(
|
||||
lambda v: 255 if v >= 128 else 0))
|
||||
canvas.paste(rgb, (dx0, dy0), valid)
|
||||
mask.paste(255, (dx0, dy0, dx1, dy1), valid)
|
||||
return True
|
||||
|
||||
|
||||
def _hatch(size):
|
||||
"""Motif blanc à hachures grises (zones sans donnée)."""
|
||||
from PIL import Image, ImageDraw
|
||||
w, h = size
|
||||
pat = Image.new("RGB", size, (255, 255, 255))
|
||||
draw = ImageDraw.Draw(pat)
|
||||
for k in range(-h, w, _HATCH_STEP_PX):
|
||||
draw.line([(k, h), (k + h, 0)], fill=(200, 200, 200), width=2)
|
||||
return pat
|
||||
|
||||
|
||||
def compose_l93(output_dir, bbox, px_size, layer=LAYER):
|
||||
"""Image RGB de l'emprise L93 à px_size m/px, masque des pixels peints et
|
||||
dalles contributrices. Hors données : blanc hachuré."""
|
||||
from PIL import Image, ImageOps
|
||||
from . import tiles
|
||||
|
||||
width = max(1, int(round((bbox[2] - bbox[0]) / px_size)))
|
||||
height = max(1, int(round((bbox[3] - bbox[1]) / px_size)))
|
||||
canvas = Image.new("RGB", (width, height), (255, 255, 255))
|
||||
mask = Image.new("L", (width, height), 0)
|
||||
cells = set()
|
||||
for cell, src in tiles.sources_in_bbox(output_dir, layer, bbox, px_size):
|
||||
if _paste_l93(canvas, mask, src, bbox, px_size):
|
||||
cells.add(cell)
|
||||
if mask.getextrema() != (255, 255):
|
||||
canvas.paste(_hatch(canvas.size), (0, 0), ImageOps.invert(mask))
|
||||
return canvas, mask, sorted(cells)
|
||||
|
||||
Reference in New Issue
Block a user