fix algining xynova texture to uv printing fixture.

This commit is contained in:
ykcheungab
2026-09-29 16:33:31 +08:00
parent 267334d543
commit 37e3e8400b
33 changed files with 758477 additions and 886701 deletions
+102 -6
View File
@@ -58,6 +58,7 @@ from texture_merge import ( # noqa: E402
validate_seam,
)
import seam_gap # noqa: E402
import page_sizing # noqa: E402
SCHEMA = "orisys-sensor-design/texture-job-multiview-v1"
GROUND_TRUTH_SCHEMA = "orisys-sensor-design/multiview-ground-truth-v1"
@@ -297,9 +298,20 @@ def _resolve_config(job: dict[str, Any]) -> dict[str, Any]:
f"job 'print.base_fill': must be 'none', 'inked' or 'owned' (got {base_fill!r})"
)
page_block = job.get("page") or {}
if not isinstance(page_block, dict):
raise ConfigError("job 'page' must be a mapping")
page_mode = str(page_block.get("mode", "auto"))
if page_mode not in ("auto", "fixed"):
raise ConfigError(f"job 'page.mode': must be 'auto' or 'fixed' (got {page_mode!r})")
px_per_mm = float(page_block.get("px_per_mm", page_sizing.DEFAULT_PX_PER_MM))
if px_per_mm <= 0.0:
raise ConfigError(f"job 'page.px_per_mm': must be > 0 (got {px_per_mm!r})")
return {
"mesh": str(objects.get("mesh") or DEFAULT_MESH),
"base_fill": base_fill,
"page": {"mode": page_mode, "px_per_mm": px_per_mm},
"gap": {
"enabled": gap_enabled,
"apply": gap_apply,
@@ -525,6 +537,34 @@ def _triangle_seam_edges(
return out
def _derive_pair_page(obj, camera, uv_name, depsgraph, px_per_mm):
"""Derive and verify one pair's page from the geometry its layout projects.
Uses the same scale-normalised view matrix `patch_projection` clips with, so the page
agrees with the geometry that is actually textured. Dividing out the camera's object scale
is not optional: `finger-xynova`'s cameras carry 0.014988, which would otherwise report a
1414 mm extent for a 19 mm part.
"""
evaluated = obj.evaluated_get(depsgraph)
mesh = evaluated.to_mesh(preserve_all_data_layers=True, depsgraph=depsgraph)
try:
layer = mesh.uv_layers.get(uv_name)
if layer is None:
raise SceneError(f"UV layer {uv_name!r} is missing from the evaluated mesh")
view = camera.matrix_world.normalized().inverted()
to_world = evaluated.matrix_world
uvs: list[tuple[float, float]] = []
planes: list[tuple[float, float]] = []
for loop in mesh.loops:
u, v = layer.data[loop.index].uv
local = view @ (to_world @ mesh.vertices[loop.vertex_index].co)
uvs.append((float(u), float(v)))
planes.append((float(local.x), float(local.y)))
return page_sizing.derive_page(uvs, planes, px_per_mm=px_per_mm)
finally:
evaluated.to_mesh_clear()
def _mesh_centroid_world(obj, depsgraph):
"""Area-weighted centroid of the evaluated surface, in world mm.
@@ -682,6 +722,20 @@ def main() -> int:
f"per view "
+ ", ".join(f"{view} {area:.3f}" for view, area in sorted(result['per_view_mm2'].items()))
)
if result["hole_fraction"] > policy.max_hole_fraction:
# An unowned region prints nothing, so it is a visible defect, not a statistic. This is
# the gate that turns "the surface has a bald patch" into a message naming the cause.
print(
f"ERROR: hole fraction {result['hole_fraction']:.4f} exceeds max_hole_fraction "
f"{policy.max_hole_fraction:g}: {result['hole_mm2']:.3f} mm^2 of the surface "
f"({meta['triangle_count'] - len(owner)} triangles) has no owning view, so it "
"would print nothing. The usual cause is the incidence limit: a view whose surface "
"is steeper than `incidence_limit_deg` cannot own it. Raise that limit (or re-pose "
f"the region cameras), or set `max_hole_fraction` above {result['hole_fraction']:.4f} "
"to accept the gap deliberately.",
file=sys.stderr,
)
return 1
# ---- 4. Pattern geometry, one pass per distinct pattern camera ------------------
# The seam is passed here (not to the region pass) because only the emitted geometry needs
@@ -758,6 +812,44 @@ def main() -> int:
detail(f" colour seed {pair['seed']}")
patch_colors = precompute_cell_colors(config["nx"], config["ny"], config["color_r"])
# ---- page: derived per view, or inherited from texture_patch ----------------
pair_page_mode = str(pair.get("page") or config["page"]["mode"])
if pair_page_mode == "auto":
try:
derived = _derive_pair_page(
obj, region_cam, uv_map, depsgraph, config["page"]["px_per_mm"]
)
except (page_sizing.PageSizingError, SceneError) as exc:
# Never fall back silently: keeping the shared page is what produced a 23%
# inverted aspect on finger-xynova.
print(
f"ERROR: cannot auto-size the page for view {pair_id!r}: {exc}",
file=sys.stderr,
)
return 1
texture_width = derived.raster_width_px
texture_height = derived.raster_height_px
page_width_mm = derived.page_width_mm
page_height_mm = derived.page_height_mm
page_info = {
"page_source": "auto",
"page_mm": [round(page_width_mm, 6), round(page_height_mm, 6)],
"raster_px": [texture_width, texture_height],
"page_residual": [derived.residual_u, derived.residual_v],
"page_slope": [derived.slope_u, derived.slope_v],
}
detail(f"[{pair_id}] {derived.describe()} (derived from {pair['camera']})")
else:
texture_width = config["width"]
texture_height = config["height"]
page_width_mm = config["vector_width_mm"]
page_height_mm = config["vector_height_mm"]
page_info = {
"page_source": "fixed",
"page_mm": [round(page_width_mm, 6), round(page_height_mm, 6)],
"raster_px": [texture_width, texture_height],
}
patches = pattern_geometry[uv_map]["patches"]
mesh_layer = pattern_geometry[uv_map]["mesh"]
fragment_seam = pattern_meta.get("fragment_seam_distance_mm")
@@ -855,6 +947,7 @@ def main() -> int:
"uv_map": uv_map,
"region_camera": pair["camera"],
"pattern_camera": pair["pattern_camera"],
**page_info,
"owned_triangles": len(owned_triangles),
"owned_mm2": sum(meta["triangle_area_mm2"][t] for t in owned_triangles),
"inked_triangles": len(kept_triangles),
@@ -901,13 +994,13 @@ def main() -> int:
nx=config["nx"],
ny=config["ny"],
r=config["color_r"],
texture_width=config["width"],
texture_height=config["height"],
texture_width=texture_width,
texture_height=texture_height,
output_path=pair["artifacts"]["png"],
svg_path=pair["artifacts"]["svg"],
pdf_path=pair["artifacts"]["pdf"],
vector_width_mm=config["vector_width_mm"],
vector_height_mm=config["vector_height_mm"],
vector_width_mm=page_width_mm,
vector_height_mm=page_height_mm,
uv_centre=uv_centre,
cross_length=config["cross_length"],
cross_thickness=config["cross_thickness"],
@@ -968,8 +1061,11 @@ def main() -> int:
"settings": {
"nx": config["nx"],
"ny": config["ny"],
"raster": [config["width"], config["height"]],
"page_mm": [config["vector_width_mm"], config["vector_height_mm"]],
# The fallback page/raster for pairs with `page: fixed`; with `page: auto` the
# per-view values below are authoritative.
"default_raster": [config["width"], config["height"]],
"default_page_mm": [config["vector_width_mm"], config["vector_height_mm"]],
"page": dict(config["page"]),
"seed": config["seed"],
},
"ownership": {