fix: calibrate submenu tent lights from asset coordinates
This commit is contained in:
@@ -0,0 +1 @@
|
|||||||
|
uid://bk0kwrttwfdl3
|
||||||
@@ -0,0 +1,103 @@
|
|||||||
|
shader_type canvas_item;
|
||||||
|
// Shader per i menu di secondo livello P2A e P3A.
|
||||||
|
// Coordinate UV relative al TextureRect del mockup (2796x1290).
|
||||||
|
// Tracciate e verificate ad alto contrasto con fondo nero:
|
||||||
|
// 1. Stella centrata sul numero 1
|
||||||
|
// 2. 20 lucine lungo le due falde del tetto a cono
|
||||||
|
// 3. 4 grandi punti di ancoraggio sulla balza ondulata
|
||||||
|
|
||||||
|
uniform float lights_strength : hint_range(0.0, 1.5) = 1.0;
|
||||||
|
uniform float star_strength : hint_range(0.0, 1.5) = 1.0;
|
||||||
|
uniform vec2 star_center = vec2(0.269671, 0.155039);
|
||||||
|
uniform float garland_fps : hint_range(1.0, 60.0) = 12.5;
|
||||||
|
|
||||||
|
const vec3 GOLD_CORE = vec3(0.945, 0.80, 0.385);
|
||||||
|
const vec3 GOLD_HALO = vec3(1.0, 0.78, 0.35);
|
||||||
|
|
||||||
|
// 20 lampadine lungo le due falde del tetto a cono
|
||||||
|
// Coordinate UV nella texture del mockup (2796x1290), z = fase del loop (4, 2, 0)
|
||||||
|
// Coordinate rilevate automaticamente dal PNG 2a_tendone.png:
|
||||||
|
// dodici lampadine reali per lato, non dieci e non interpolate su una retta.
|
||||||
|
const vec3 p2a_bulbs[24] = vec3[24](
|
||||||
|
// Sinistra, dal colletto verso il bordo esterno
|
||||||
|
vec3(0.251808, 0.249698, 4.0), // L0
|
||||||
|
vec3(0.234388, 0.289171, 2.0), // L1
|
||||||
|
vec3(0.214765, 0.320653, 0.0), // L2
|
||||||
|
vec3(0.194605, 0.356896, 4.0), // L3
|
||||||
|
vec3(0.181462, 0.375494, 2.0), // L4
|
||||||
|
vec3(0.167428, 0.398533, 0.0), // L5
|
||||||
|
vec3(0.150578, 0.433674, 4.0), // L6
|
||||||
|
vec3(0.129082, 0.459304, 2.0), // L7
|
||||||
|
vec3(0.107434, 0.492707, 0.0), // L8
|
||||||
|
vec3(0.088943, 0.512666, 4.0), // L9
|
||||||
|
vec3(0.070246, 0.536780, 2.0), // L10
|
||||||
|
vec3(0.042324, 0.560880, 0.0), // L11
|
||||||
|
// Destra, dal colletto verso il bordo esterno
|
||||||
|
vec3(0.285755, 0.241321, 0.0), // R0
|
||||||
|
vec3(0.300670, 0.272138, 2.0), // R1
|
||||||
|
vec3(0.320168, 0.303235, 4.0), // R2
|
||||||
|
vec3(0.339147, 0.339426, 0.0), // R3
|
||||||
|
vec3(0.357906, 0.370273, 2.0), // R4
|
||||||
|
vec3(0.376152, 0.403834, 4.0), // R5
|
||||||
|
vec3(0.393310, 0.431054, 0.0), // R6
|
||||||
|
vec3(0.413618, 0.461451, 2.0), // R7
|
||||||
|
vec3(0.439441, 0.496182, 4.0), // R8
|
||||||
|
vec3(0.462399, 0.522573, 0.0), // R9
|
||||||
|
vec3(0.482340, 0.544497, 2.0), // R10
|
||||||
|
vec3(0.500324, 0.560728, 4.0) // R11
|
||||||
|
);
|
||||||
|
|
||||||
|
// 4 grandi punti di ancoraggio lungo la balza ondulata del tendone
|
||||||
|
const vec3 p2a_anchor_bulbs[4] = vec3[4](
|
||||||
|
vec3(0.08727, 0.46822, 4.0), // A0 (Estremità sinistra della balza)
|
||||||
|
vec3(0.19170, 0.51783, 0.0), // A1 (Angolo ingresso SX)
|
||||||
|
vec3(0.27182, 0.51783, 2.0), // A2 (Angolo ingresso DX)
|
||||||
|
vec3(0.37697, 0.46822, 4.0) // A3 (Estremità destra della balza)
|
||||||
|
);
|
||||||
|
|
||||||
|
float chase_intensity(float d) {
|
||||||
|
if (d < 1.0) return 0.30;
|
||||||
|
if (d < 2.0) return 0.60;
|
||||||
|
if (d < 3.0) return 1.00;
|
||||||
|
if (d < 4.0) return 0.95;
|
||||||
|
return 0.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
void fragment() {
|
||||||
|
vec4 color = texture(TEXTURE, UV);
|
||||||
|
|
||||||
|
// 1. Stella grande in cima al tendone
|
||||||
|
if (star_strength > 0.001) {
|
||||||
|
vec2 aspect_diff = (UV - star_center) * vec2(2.1674, 1.0);
|
||||||
|
float star_dist = length(aspect_diff);
|
||||||
|
float star_area = 1.0 - smoothstep(0.020, 0.060, star_dist);
|
||||||
|
float pulse = 0.88 + 0.12 * sin(TIME * 5.0);
|
||||||
|
vec3 gold_glow = vec3(1.0, 0.75, 0.20) * star_area * star_strength * pulse;
|
||||||
|
color.rgb += gold_glow;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Lucine lungo le falde del tetto e punti sulla balza
|
||||||
|
if (lights_strength > 0.001) {
|
||||||
|
float frame_time = TIME * garland_fps;
|
||||||
|
float cycle = mod(floor(frame_time), 6.0);
|
||||||
|
|
||||||
|
// 20 Lucine lungo le due falde del tetto a cono
|
||||||
|
for (int i = 0; i < 24; i++) {
|
||||||
|
float d = mod(cycle - p2a_bulbs[i].z + 6.0, 6.0);
|
||||||
|
float intensity = chase_intensity(d);
|
||||||
|
if (intensity <= 0.0) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
vec2 delta = (UV - p2a_bulbs[i].xy) * vec2(2796.0, 1290.0);
|
||||||
|
float r = length(delta);
|
||||||
|
float core = 1.0 - smoothstep(4.0, 10.0, r);
|
||||||
|
float halo = exp(-r / 15.0);
|
||||||
|
float ga = clamp(intensity * lights_strength, 0.0, 1.0);
|
||||||
|
color.rgb = mix(color.rgb, GOLD_CORE, core * ga);
|
||||||
|
color.rgb += GOLD_HALO * (halo * ga * 0.75 * (1.0 - core));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
color.rgb = min(color.rgb, vec3(1.0));
|
||||||
|
COLOR = color;
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://cw1cvmopgvx1n
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
uid://c06koaxcymtib
|
||||||
@@ -7,6 +7,7 @@ const MENU_SHADER := preload("res://menus/assets/shaders/ambient_drift.gdshader"
|
|||||||
const TENT_STAR_SHADER := preload("res://menus/assets/shaders/tent_star_glow.gdshader")
|
const TENT_STAR_SHADER := preload("res://menus/assets/shaders/tent_star_glow.gdshader")
|
||||||
const TENT_LIGHTS_SHADER := preload("res://menus/assets/shaders/tent_lights.gdshader")
|
const TENT_LIGHTS_SHADER := preload("res://menus/assets/shaders/tent_lights.gdshader")
|
||||||
const SIGN_LIGHTS_SHADER := preload("res://menus/assets/shaders/circus_sign_lights.gdshader")
|
const SIGN_LIGHTS_SHADER := preload("res://menus/assets/shaders/circus_sign_lights.gdshader")
|
||||||
|
const SUBMENU_LIGHTS_SHADER := preload("res://menus/assets/shaders/submenu_tent_lights.gdshader")
|
||||||
|
|
||||||
# Posizione e ingombro condivisi della freccia indietro in tutte le schermate.
|
# Posizione e ingombro condivisi della freccia indietro in tutte le schermate.
|
||||||
const BACK_BUTTON_CENTER := Vector2(0.12, 0.14)
|
const BACK_BUTTON_CENTER := Vector2(0.12, 0.14)
|
||||||
@@ -60,6 +61,14 @@ static func sign_lights_material() -> ShaderMaterial:
|
|||||||
return material
|
return material
|
||||||
|
|
||||||
|
|
||||||
|
static func submenu_lights_material() -> ShaderMaterial:
|
||||||
|
var material := ShaderMaterial.new()
|
||||||
|
material.shader = SUBMENU_LIGHTS_SHADER
|
||||||
|
material.set_shader_parameter("lights_strength", 1.0)
|
||||||
|
material.set_shader_parameter("star_strength", 1.0)
|
||||||
|
return material
|
||||||
|
|
||||||
|
|
||||||
static func add_image(parent: Control, texture: Texture2D, center: Vector2, normalized_size: Vector2, material: Material = null) -> TextureRect:
|
static func add_image(parent: Control, texture: Texture2D, center: Vector2, normalized_size: Vector2, material: Material = null) -> TextureRect:
|
||||||
var image := TextureRect.new()
|
var image := TextureRect.new()
|
||||||
image.texture = texture
|
image.texture = texture
|
||||||
|
|||||||
@@ -28,6 +28,7 @@ func _build() -> void:
|
|||||||
artwork.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
artwork.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||||||
artwork.stretch_mode = TextureRect.STRETCH_SCALE
|
artwork.stretch_mode = TextureRect.STRETCH_SCALE
|
||||||
artwork.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
artwork.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
artwork.material = MenuUI.submenu_lights_material()
|
||||||
add_child(artwork)
|
add_child(artwork)
|
||||||
|
|
||||||
_add_hotspot(MenuUI.BACK_BUTTON_CENTER, MenuUI.BACK_BUTTON_SIZE, func(): back_requested.emit())
|
_add_hotspot(MenuUI.BACK_BUTTON_CENTER, MenuUI.BACK_BUTTON_SIZE, func(): back_requested.emit())
|
||||||
|
|||||||
@@ -25,6 +25,7 @@ func _build() -> void:
|
|||||||
artwork.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
artwork.expand_mode = TextureRect.EXPAND_IGNORE_SIZE
|
||||||
artwork.stretch_mode = TextureRect.STRETCH_SCALE
|
artwork.stretch_mode = TextureRect.STRETCH_SCALE
|
||||||
artwork.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
artwork.mouse_filter = Control.MOUSE_FILTER_IGNORE
|
||||||
|
artwork.material = MenuUI.submenu_lights_material()
|
||||||
add_child(artwork)
|
add_child(artwork)
|
||||||
|
|
||||||
_add_hotspot(MenuUI.BACK_BUTTON_CENTER, MenuUI.BACK_BUTTON_SIZE, func(): back_requested.emit())
|
_add_hotspot(MenuUI.BACK_BUTTON_CENTER, MenuUI.BACK_BUTTON_SIZE, func(): back_requested.emit())
|
||||||
|
|||||||
@@ -0,0 +1,297 @@
|
|||||||
|
#!/usr/bin/env python3
|
||||||
|
"""Rileva con precisione stella e lampadine nel PNG RGBA del tendone.
|
||||||
|
|
||||||
|
Il rilevamento non stima rette: segmenta direttamente il colore oro, ricompone
|
||||||
|
le pennellate di ogni lampadina e usa il centroide del blob reale. Può inoltre
|
||||||
|
registrare il PNG su un mockup tramite SIFT+RANSAC e trasferire le coordinate.
|
||||||
|
|
||||||
|
Esempio:
|
||||||
|
python3 tools/analyze_tent_lights.py \
|
||||||
|
menus/assets/p2a/2a_tendone.png \
|
||||||
|
--reference menus/assets/p2a/p2a_menu_mockup.jpg.bak \
|
||||||
|
--output-dir /tmp/tent-lights-analysis \
|
||||||
|
--debug-scene-asset menus/assets/p2a/p2a_menu_mockup.jpg
|
||||||
|
"""
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import argparse
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import cv2
|
||||||
|
import numpy as np
|
||||||
|
from PIL import Image, ImageDraw
|
||||||
|
|
||||||
|
|
||||||
|
GOLD_HSV_LOW = np.array((12, 45, 90), dtype=np.uint8)
|
||||||
|
GOLD_HSV_HIGH = np.array((45, 255, 255), dtype=np.uint8)
|
||||||
|
|
||||||
|
|
||||||
|
def gold_mask(rgba: np.ndarray) -> np.ndarray:
|
||||||
|
hsv = cv2.cvtColor(rgba[:, :, :3], cv2.COLOR_RGB2HSV)
|
||||||
|
color = cv2.inRange(hsv, GOLD_HSV_LOW, GOLD_HSV_HIGH) > 0
|
||||||
|
return (color & (rgba[:, :, 3] > 15)).astype(np.uint8) * 255
|
||||||
|
|
||||||
|
|
||||||
|
def connected_gold_blobs(mask: np.ndarray) -> list[dict]:
|
||||||
|
# Chiude soltanto le micro-interruzioni della pennellata, senza fondere
|
||||||
|
# lampadine adiacenti. La dilatazione rende stabile il centro del blob.
|
||||||
|
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (7, 7))
|
||||||
|
merged = cv2.morphologyEx(mask, cv2.MORPH_CLOSE, kernel)
|
||||||
|
merged = cv2.dilate(
|
||||||
|
merged, cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3))
|
||||||
|
)
|
||||||
|
count, _, stats, centroids = cv2.connectedComponentsWithStats(merged, 8)
|
||||||
|
blobs: list[dict] = []
|
||||||
|
for label in range(1, count):
|
||||||
|
x, y, width, height, area = map(int, stats[label])
|
||||||
|
blobs.append(
|
||||||
|
{
|
||||||
|
"x": float(centroids[label][0]),
|
||||||
|
"y": float(centroids[label][1]),
|
||||||
|
"bbox": [x, y, width, height],
|
||||||
|
"area": area,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return blobs
|
||||||
|
|
||||||
|
|
||||||
|
def detect_star(mask: np.ndarray) -> dict:
|
||||||
|
height, _ = mask.shape
|
||||||
|
top = mask[: int(height * 0.25)].copy()
|
||||||
|
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (11, 11))
|
||||||
|
top = cv2.morphologyEx(top, cv2.MORPH_CLOSE, kernel)
|
||||||
|
blobs = connected_gold_blobs(top)
|
||||||
|
if not blobs:
|
||||||
|
raise RuntimeError("Stella non rilevata nella parte alta dell'asset")
|
||||||
|
star = max(blobs, key=lambda blob: blob["area"])
|
||||||
|
x, y, width, height = star["bbox"]
|
||||||
|
# Per il bagliore serve il centro geometrico della stella, non il centroide
|
||||||
|
# colorimetrico (spostato dalle cinque punte e dal numero nero interno).
|
||||||
|
star["x"] = x + (width - 1) / 2.0
|
||||||
|
star["y"] = y + (height - 1) / 2.0
|
||||||
|
return star
|
||||||
|
|
||||||
|
|
||||||
|
def detect_bulbs(mask: np.ndarray, star: dict) -> tuple[list[dict], list[dict]]:
|
||||||
|
height, width = mask.shape
|
||||||
|
roi = np.zeros_like(mask)
|
||||||
|
# Le garlande finiscono prima della modanatura dorata della balza. Fermarsi
|
||||||
|
# al 59% evita che frammenti orizzontali del bordo vengano scambiati per
|
||||||
|
# lampadine (nel PNG 2796x1290 il bordo comincia attorno a y=768).
|
||||||
|
roi[int(height * 0.16) : int(height * 0.59), : int(width * 0.60)] = 255
|
||||||
|
blobs = connected_gold_blobs(cv2.bitwise_and(mask, roi))
|
||||||
|
|
||||||
|
candidates = []
|
||||||
|
for blob in blobs:
|
||||||
|
_, _, blob_width, blob_height = blob["bbox"]
|
||||||
|
if (
|
||||||
|
6 <= blob_width <= 80
|
||||||
|
and 6 <= blob_height <= 80
|
||||||
|
and 10 <= blob["area"] <= 3000
|
||||||
|
and blob["y"] > star["bbox"][1] + star["bbox"][3]
|
||||||
|
):
|
||||||
|
candidates.append(blob)
|
||||||
|
|
||||||
|
center_x = star["x"]
|
||||||
|
left = sorted((b for b in candidates if b["x"] < center_x), key=lambda b: b["y"])
|
||||||
|
right = sorted((b for b in candidates if b["x"] > center_x), key=lambda b: b["y"])
|
||||||
|
|
||||||
|
if len(left) != 12 or len(right) != 12:
|
||||||
|
raise RuntimeError(
|
||||||
|
"Rilevamento ambiguo: attese 12 lampadine per lato, "
|
||||||
|
f"rilevate {len(left)} a sinistra e {len(right)} a destra"
|
||||||
|
)
|
||||||
|
return left, right
|
||||||
|
|
||||||
|
|
||||||
|
def estimate_transform(source_rgba: np.ndarray, reference_rgb: np.ndarray) -> tuple[np.ndarray, dict]:
|
||||||
|
source_gray = cv2.cvtColor(source_rgba[:, :, :3], cv2.COLOR_RGB2GRAY)
|
||||||
|
reference_gray = cv2.cvtColor(reference_rgb, cv2.COLOR_RGB2GRAY)
|
||||||
|
source_mask = (source_rgba[:, :, 3] > 128).astype(np.uint8) * 255
|
||||||
|
reference_mask = np.zeros_like(reference_gray)
|
||||||
|
reference_mask[:, : int(reference_gray.shape[1] * 0.60)] = 255
|
||||||
|
|
||||||
|
sift = cv2.SIFT_create(nfeatures=10000, contrastThreshold=0.02)
|
||||||
|
source_keys, source_desc = sift.detectAndCompute(source_gray, source_mask)
|
||||||
|
reference_keys, reference_desc = sift.detectAndCompute(reference_gray, reference_mask)
|
||||||
|
if source_desc is None or reference_desc is None:
|
||||||
|
raise RuntimeError("Feature insufficienti per registrare asset e mockup")
|
||||||
|
|
||||||
|
matches = cv2.BFMatcher().knnMatch(source_desc, reference_desc, k=2)
|
||||||
|
good = [first for first, second in matches if first.distance < 0.70 * second.distance]
|
||||||
|
if len(good) < 20:
|
||||||
|
raise RuntimeError(f"Registrazione instabile: soltanto {len(good)} match validi")
|
||||||
|
|
||||||
|
source_points = np.float32([source_keys[m.queryIdx].pt for m in good])
|
||||||
|
reference_points = np.float32([reference_keys[m.trainIdx].pt for m in good])
|
||||||
|
matrix, inliers = cv2.estimateAffine2D(
|
||||||
|
source_points,
|
||||||
|
reference_points,
|
||||||
|
method=cv2.RANSAC,
|
||||||
|
ransacReprojThreshold=3.0,
|
||||||
|
maxIters=10000,
|
||||||
|
confidence=0.999,
|
||||||
|
)
|
||||||
|
if matrix is None or inliers is None:
|
||||||
|
raise RuntimeError("RANSAC non ha trovato una trasformazione affine")
|
||||||
|
|
||||||
|
inlier_count = int(inliers.sum())
|
||||||
|
if inlier_count < 100 or inlier_count / len(good) < 0.80:
|
||||||
|
raise RuntimeError(
|
||||||
|
f"Registrazione non affidabile: {inlier_count}/{len(good)} inlier"
|
||||||
|
)
|
||||||
|
return matrix, {
|
||||||
|
"source_features": len(source_keys),
|
||||||
|
"reference_features": len(reference_keys),
|
||||||
|
"matches": len(good),
|
||||||
|
"inliers": inlier_count,
|
||||||
|
"inlier_ratio": inlier_count / len(good),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def transform_point(point: dict, matrix: np.ndarray) -> dict:
|
||||||
|
mapped = matrix @ np.array((point["x"], point["y"], 1.0))
|
||||||
|
result = dict(point)
|
||||||
|
result["x"] = float(mapped[0])
|
||||||
|
result["y"] = float(mapped[1])
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def normalized(point: dict, width: int, height: int) -> list[float]:
|
||||||
|
return [point["x"] / width, point["y"] / height]
|
||||||
|
|
||||||
|
|
||||||
|
def draw_marker(draw: ImageDraw.ImageDraw, point: dict, label: str, radius: int) -> None:
|
||||||
|
x, y = point["x"], point["y"]
|
||||||
|
draw.ellipse(
|
||||||
|
(x - radius, y - radius, x + radius, y + radius),
|
||||||
|
outline=(255, 0, 0, 255),
|
||||||
|
width=max(4, radius // 5),
|
||||||
|
)
|
||||||
|
cross = max(7, radius // 3)
|
||||||
|
draw.line((x - cross, y, x + cross, y), fill=(0, 255, 255, 255), width=3)
|
||||||
|
draw.line((x, y - cross, x, y + cross), fill=(0, 255, 255, 255), width=3)
|
||||||
|
draw.text((x + radius + 4, y - radius), label, fill=(255, 255, 255, 255), stroke_width=2, stroke_fill=(0, 0, 0, 255))
|
||||||
|
|
||||||
|
|
||||||
|
def source_overlay(rgba: np.ndarray, star: dict, left: list[dict], right: list[dict]) -> Image.Image:
|
||||||
|
alpha = rgba[:, :, 3:4].astype(np.float32) / 255.0
|
||||||
|
black = np.zeros_like(rgba[:, :, :3], dtype=np.float32)
|
||||||
|
composited = rgba[:, :, :3] * alpha + black * (1.0 - alpha)
|
||||||
|
image = Image.fromarray(np.uint8(np.clip(composited, 0, 255)), "RGB").convert("RGBA")
|
||||||
|
draw = ImageDraw.Draw(image)
|
||||||
|
draw_marker(draw, star, "STAR", 60)
|
||||||
|
for index, point in enumerate(left):
|
||||||
|
draw_marker(draw, point, f"L{index:02d}", 25)
|
||||||
|
for index, point in enumerate(right):
|
||||||
|
draw_marker(draw, point, f"R{index:02d}", 25)
|
||||||
|
return image
|
||||||
|
|
||||||
|
|
||||||
|
def reference_overlay(
|
||||||
|
reference_rgb: np.ndarray,
|
||||||
|
source_rgba: np.ndarray,
|
||||||
|
star: dict,
|
||||||
|
left: list[dict],
|
||||||
|
right: list[dict],
|
||||||
|
) -> Image.Image:
|
||||||
|
# Nero fuori dalla sagoma del PNG: impedisce al cielo di mascherare gli errori.
|
||||||
|
image = reference_rgb.copy()
|
||||||
|
foreground = source_rgba[:, :, 3] > 15
|
||||||
|
image[~foreground] = 0
|
||||||
|
result = Image.fromarray(image, "RGB").convert("RGBA")
|
||||||
|
draw = ImageDraw.Draw(result)
|
||||||
|
draw_marker(draw, star, "STAR", 60)
|
||||||
|
for index, point in enumerate(left):
|
||||||
|
draw_marker(draw, point, f"L{index:02d}", 25)
|
||||||
|
for index, point in enumerate(right):
|
||||||
|
draw_marker(draw, point, f"R{index:02d}", 25)
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
|
def main() -> None:
|
||||||
|
parser = argparse.ArgumentParser()
|
||||||
|
parser.add_argument("asset", type=Path, help="PNG RGBA contenente il tendone")
|
||||||
|
parser.add_argument("--reference", type=Path, help="Mockup sul quale trasferire i punti")
|
||||||
|
parser.add_argument("--output-dir", type=Path, default=Path("/tmp/tent-lights-analysis"))
|
||||||
|
parser.add_argument(
|
||||||
|
"--debug-scene-asset",
|
||||||
|
type=Path,
|
||||||
|
help="Scrive una copia JPEG di debug per la scena Godot (sfondo nero e marker)",
|
||||||
|
)
|
||||||
|
args = parser.parse_args()
|
||||||
|
|
||||||
|
source_rgba = np.array(Image.open(args.asset).convert("RGBA"))
|
||||||
|
height, width = source_rgba.shape[:2]
|
||||||
|
mask = gold_mask(source_rgba)
|
||||||
|
star = detect_star(mask)
|
||||||
|
left, right = detect_bulbs(mask, star)
|
||||||
|
|
||||||
|
args.output_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
overlay = source_overlay(source_rgba, star, left, right)
|
||||||
|
overlay_path = args.output_dir / "detected_on_source.png"
|
||||||
|
overlay.save(overlay_path)
|
||||||
|
|
||||||
|
report: dict = {
|
||||||
|
"asset": str(args.asset),
|
||||||
|
"size": [width, height],
|
||||||
|
"star": {"pixel": [star["x"], star["y"]], "uv": normalized(star, width, height)},
|
||||||
|
"left": [
|
||||||
|
{"pixel": [p["x"], p["y"]], "uv": normalized(p, width, height), "bbox": p["bbox"]}
|
||||||
|
for p in left
|
||||||
|
],
|
||||||
|
"right": [
|
||||||
|
{"pixel": [p["x"], p["y"]], "uv": normalized(p, width, height), "bbox": p["bbox"]}
|
||||||
|
for p in right
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
if args.reference:
|
||||||
|
reference_rgb = np.array(Image.open(args.reference).convert("RGB"))
|
||||||
|
matrix, diagnostics = estimate_transform(source_rgba, reference_rgb)
|
||||||
|
mapped_star = transform_point(star, matrix)
|
||||||
|
mapped_left = [transform_point(p, matrix) for p in left]
|
||||||
|
mapped_right = [transform_point(p, matrix) for p in right]
|
||||||
|
ref_height, ref_width = reference_rgb.shape[:2]
|
||||||
|
mapped_overlay = reference_overlay(
|
||||||
|
reference_rgb, source_rgba, mapped_star, mapped_left, mapped_right
|
||||||
|
)
|
||||||
|
reference_path = args.output_dir / "detected_on_reference.png"
|
||||||
|
mapped_overlay.save(reference_path)
|
||||||
|
if args.debug_scene_asset:
|
||||||
|
mapped_overlay.convert("RGB").save(args.debug_scene_asset, quality=96)
|
||||||
|
|
||||||
|
report["reference"] = {
|
||||||
|
"path": str(args.reference),
|
||||||
|
"size": [ref_width, ref_height],
|
||||||
|
"affine_matrix": matrix.tolist(),
|
||||||
|
"diagnostics": diagnostics,
|
||||||
|
"star": {
|
||||||
|
"pixel": [mapped_star["x"], mapped_star["y"]],
|
||||||
|
"uv": normalized(mapped_star, ref_width, ref_height),
|
||||||
|
},
|
||||||
|
"left": [
|
||||||
|
{"pixel": [p["x"], p["y"]], "uv": normalized(p, ref_width, ref_height)}
|
||||||
|
for p in mapped_left
|
||||||
|
],
|
||||||
|
"right": [
|
||||||
|
{"pixel": [p["x"], p["y"]], "uv": normalized(p, ref_width, ref_height)}
|
||||||
|
for p in mapped_right
|
||||||
|
],
|
||||||
|
}
|
||||||
|
|
||||||
|
report_path = args.output_dir / "coordinates.json"
|
||||||
|
report_path.write_text(json.dumps(report, indent=2) + "\n", encoding="utf-8")
|
||||||
|
|
||||||
|
print(f"Asset: {width}x{height}")
|
||||||
|
print(f"Stella: ({star['x']:.2f}, {star['y']:.2f})")
|
||||||
|
print("Sinistra:", ", ".join(f"({p['x']:.2f},{p['y']:.2f})" for p in left))
|
||||||
|
print("Destra:", ", ".join(f"({p['x']:.2f},{p['y']:.2f})" for p in right))
|
||||||
|
print(f"Overlay: {overlay_path}")
|
||||||
|
print(f"Coordinate: {report_path}")
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == "__main__":
|
||||||
|
main()
|
||||||
Reference in New Issue
Block a user