fix algining xynova texture to uv printing fixture.
This commit is contained in:
@@ -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": {
|
||||
|
||||
Reference in New Issue
Block a user