为相机网格笔画增加双视图导出
沿用彩色纹理的视图归属、接缝留白和自动页面尺寸。 已验证双视图与单视图 Blender 导出、相关测试及文档门禁。
This commit is contained in:
@@ -233,12 +233,19 @@ blender -b sensors/finger/r01/source/geometry.blend -P blender/scripts/random_pa
|
|||||||
```
|
```
|
||||||
|
|
||||||
`stroke-texture` exports black capsule strokes using the same camera grid,
|
`stroke-texture` exports black capsule strokes using the same camera grid,
|
||||||
projection and UV output size as `texture_patch`. First generate the single-view
|
projection and UV output size as `texture_patch`. First generate the texture job,
|
||||||
texture job, then run `orisys-design stroke-texture sensors/finger-xynova/r01`.
|
then run `orisys-design stroke-texture sensors/finger-xynova/r01` for the
|
||||||
|
single-view UV maps. For two-view production, first run `texture-views`, then use:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
orisys-design stroke-texture sensors/finger-xynova/r01 --views --output-dir build/textures/finger-xynova/r01/strokes-views
|
||||||
|
```
|
||||||
|
|
||||||
The stroke output is independent of the color artwork pixels. The revision's
|
The stroke output is independent of the color artwork pixels. The revision's
|
||||||
`stroke_pattern` block controls the seed, width and length ratios, and orientation
|
`stroke_pattern` block controls the seed, width and length ratios, and orientation
|
||||||
mode; `--output-dir` selects a fresh destination. The command writes PNG/SVG/PDF,
|
mode; `--output-dir` selects a fresh destination. `--views` uses the existing
|
||||||
resolved settings and a hash manifest. Two-view `--views` jobs are not supported.
|
`texture-views` job's triangle ownership, seam margin and
|
||||||
|
per-view page sizes. The command writes PNG/SVG/PDF, resolved settings and a hash manifest.
|
||||||
The projected-cell ratios do not guarantee a 0.2 mm physical stroke width;
|
The projected-cell ratios do not guarantee a 0.2 mm physical stroke width;
|
||||||
measure the production SVG before manufacturing.
|
measure the production SVG before manufacturing.
|
||||||
|
|
||||||
|
|||||||
@@ -63,11 +63,9 @@ def _pixel_polygon(uv_polygon, width, height):
|
|||||||
for u, v in clip_rect(uv_polygon, 0.0, 1.0, 0.0, 1.0)]
|
for u, v in clip_rect(uv_polygon, 0.0, 1.0, 0.0, 1.0)]
|
||||||
|
|
||||||
|
|
||||||
def _render(uv_name, geometry, settings, output_dir, width_ratio, length_ratio,
|
def render_stroke_polygons(uv_name, mesh, strokes, width, height, vector,
|
||||||
angle_map=None, artifact_prefix="camera_grid_stroke"):
|
base):
|
||||||
width, height = settings["width"], settings["height"]
|
"""Write the shared raster and vector artwork from UV polygons."""
|
||||||
nx, ny = settings["nx"], settings["ny"]
|
|
||||||
seed = settings.get("seed") or 0
|
|
||||||
image_name = f"CameraGridStroke_{uv_name}"
|
image_name = f"CameraGridStroke_{uv_name}"
|
||||||
if image_name in bpy.data.images:
|
if image_name in bpy.data.images:
|
||||||
bpy.data.images.remove(bpy.data.images[image_name])
|
bpy.data.images.remove(bpy.data.images[image_name])
|
||||||
@@ -75,21 +73,14 @@ def _render(uv_name, geometry, settings, output_dir, width_ratio, length_ratio,
|
|||||||
pixels = list(image.pixels)
|
pixels = list(image.pixels)
|
||||||
fill_background(pixels, width, height, (0, 0, 0, 1))
|
fill_background(pixels, width, height, (0, 0, 0, 1))
|
||||||
|
|
||||||
mesh_polygons = [_pixel_polygon(poly, width, height) for poly in geometry["mesh"]]
|
mesh_polygons = [_pixel_polygon(poly, width, height) for poly in mesh]
|
||||||
mesh_polygons = [poly for poly in mesh_polygons if len(poly) >= 3]
|
mesh_polygons = [poly for poly in mesh_polygons if len(poly) >= 3]
|
||||||
for poly in mesh_polygons:
|
for poly in mesh_polygons:
|
||||||
fill_polygon(None, pixels, width, height, poly, (1, 1, 1, 1))
|
fill_polygon(None, pixels, width, height, poly, (1, 1, 1, 1))
|
||||||
|
|
||||||
shapes = []
|
shapes = []
|
||||||
outline_cache = {}
|
for polygon in strokes:
|
||||||
for i, j, fragment, loop_uvs in geometry["camera_fragments"]:
|
pixel_poly = _pixel_polygon(polygon, width, height)
|
||||||
key = (i, j)
|
|
||||||
if key not in outline_cache:
|
|
||||||
outline_cache[key] = capsule_in_cell(
|
|
||||||
i, j, nx, ny, seed, width_ratio, length_ratio,
|
|
||||||
angle=None if angle_map is None else float(angle_map[j, i]))
|
|
||||||
uv_poly = projected_stroke_uv(fragment, loop_uvs, outline_cache[key])
|
|
||||||
pixel_poly = _pixel_polygon(uv_poly, width, height)
|
|
||||||
if len(pixel_poly) < 3:
|
if len(pixel_poly) < 3:
|
||||||
continue
|
continue
|
||||||
shapes.append((pixel_poly, (0, 0, 0, 1)))
|
shapes.append((pixel_poly, (0, 0, 0, 1)))
|
||||||
@@ -97,14 +88,9 @@ def _render(uv_name, geometry, settings, output_dir, width_ratio, length_ratio,
|
|||||||
|
|
||||||
image.pixels[:] = pixels
|
image.pixels[:] = pixels
|
||||||
image.update()
|
image.update()
|
||||||
label = artifact_prefix
|
|
||||||
if angle_map is not None and artifact_prefix == "camera_grid_stroke":
|
|
||||||
label += "_optimized"
|
|
||||||
base = output_dir / f"{label}_{uv_name}"
|
|
||||||
save_png_image(image, base.with_suffix(".png"))
|
save_png_image(image, base.with_suffix(".png"))
|
||||||
page_mesh = _clipped_polygons(mesh_polygons, width, height)
|
page_mesh = _clipped_polygons(mesh_polygons, width, height)
|
||||||
page_shapes = _clipped_shapes(shapes, width, height)
|
page_shapes = _clipped_shapes(shapes, width, height)
|
||||||
vector = settings["vector"]
|
|
||||||
vector_args = (width, height, vector["width_mm"], vector["height_mm"])
|
vector_args = (width, height, vector["width_mm"], vector["height_mm"])
|
||||||
write_svg_vector(base.with_suffix(".svg"), *vector_args,
|
write_svg_vector(base.with_suffix(".svg"), *vector_args,
|
||||||
page_mesh, page_shapes, [], (0, 0, 0, 1), uv_name,
|
page_mesh, page_shapes, [], (0, 0, 0, 1), uv_name,
|
||||||
@@ -112,7 +98,28 @@ def _render(uv_name, geometry, settings, output_dir, width_ratio, length_ratio,
|
|||||||
write_pdf_vector(base.with_suffix(".pdf"), *vector_args,
|
write_pdf_vector(base.with_suffix(".pdf"), *vector_args,
|
||||||
page_mesh, page_shapes, [], (0, 0, 0, 1),
|
page_mesh, page_shapes, [], (0, 0, 0, 1),
|
||||||
artwork_label="stroke")
|
artwork_label="stroke")
|
||||||
print(f"{uv_name}: {len(outline_cache)} cells, {len(shapes)} projected fragments")
|
print(f"{uv_name}: {len(shapes)} projected stroke fragments")
|
||||||
|
|
||||||
|
|
||||||
|
def _render(uv_name, geometry, settings, output_dir, width_ratio, length_ratio,
|
||||||
|
angle_map=None, artifact_prefix="camera_grid_stroke"):
|
||||||
|
nx, ny = settings["nx"], settings["ny"]
|
||||||
|
seed = settings.get("seed") or 0
|
||||||
|
outline_cache = {}
|
||||||
|
strokes = []
|
||||||
|
for i, j, fragment, loop_uvs in geometry["camera_fragments"]:
|
||||||
|
key = (i, j)
|
||||||
|
if key not in outline_cache:
|
||||||
|
outline_cache[key] = capsule_in_cell(
|
||||||
|
i, j, nx, ny, seed, width_ratio, length_ratio,
|
||||||
|
angle=None if angle_map is None else float(angle_map[j, i]))
|
||||||
|
strokes.append(projected_stroke_uv(fragment, loop_uvs, outline_cache[key]))
|
||||||
|
label = artifact_prefix
|
||||||
|
if angle_map is not None and artifact_prefix == "camera_grid_stroke":
|
||||||
|
label += "_optimized"
|
||||||
|
base = output_dir / f"{label}_{uv_name}"
|
||||||
|
render_stroke_polygons(uv_name, geometry["mesh"], strokes,
|
||||||
|
settings["width"], settings["height"], settings["vector"], base)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
|
|||||||
@@ -53,6 +53,10 @@ from random_patch_dd02c import ( # noqa: E402
|
|||||||
write_svg_vector,
|
write_svg_vector,
|
||||||
)
|
)
|
||||||
from patch_geometry import clip_rect # noqa: E402
|
from patch_geometry import clip_rect # noqa: E402
|
||||||
|
from camera_grid_stroke import DEFAULT_PARAMS as STROKE_DEFAULT_PARAMS # noqa: E402
|
||||||
|
from camera_grid_stroke import render_stroke_polygons # noqa: E402
|
||||||
|
from camera_grid_stroke_geometry import capsule_in_cell, projected_stroke_uv # noqa: E402
|
||||||
|
from camera_grid_stroke_optimizer import optimize_angles # noqa: E402
|
||||||
from texture_merge import ( # noqa: E402
|
from texture_merge import ( # noqa: E402
|
||||||
MergeConfigError,
|
MergeConfigError,
|
||||||
MergePolicy,
|
MergePolicy,
|
||||||
@@ -109,9 +113,39 @@ def parse_args(argv: list[str]) -> argparse.Namespace:
|
|||||||
description="Multi-view colored-patch texture generator (job JSON only)"
|
description="Multi-view colored-patch texture generator (job JSON only)"
|
||||||
)
|
)
|
||||||
parser.add_argument("--job", required=True, type=Path, help="path to the job JSON")
|
parser.add_argument("--job", required=True, type=Path, help="path to the job JSON")
|
||||||
|
parser.add_argument("--stroke-params", type=Path, default=None)
|
||||||
|
parser.add_argument("--stroke-output-dir", type=Path, default=None)
|
||||||
return parser.parse_args(argv)
|
return parser.parse_args(argv)
|
||||||
|
|
||||||
|
|
||||||
|
def _stroke_job(job: dict[str, Any], params_path: Path, output_dir: Path):
|
||||||
|
"""Point a multiview geometry job at stroke artifacts without changing the source job."""
|
||||||
|
if job.get("schema") != SCHEMA:
|
||||||
|
raise ConfigError(f"stroke views require {SCHEMA}")
|
||||||
|
try:
|
||||||
|
params = json.loads(params_path.read_text(encoding="utf-8"))
|
||||||
|
except (OSError, ValueError) as exc:
|
||||||
|
raise ConfigError(f"cannot read stroke params: {exc}") from exc
|
||||||
|
if not isinstance(params, dict) or set(params) != set(STROKE_DEFAULT_PARAMS):
|
||||||
|
raise ConfigError("stroke params must contain the complete camera-grid settings")
|
||||||
|
if not 0 < params["width_ratio"] <= params["length_ratio"] < 1:
|
||||||
|
raise ConfigError("stroke ratios require 0 < width_ratio <= length_ratio < 1")
|
||||||
|
if params["orientation_mode"] not in ("random", "optimized") or params["proposals"] < 1:
|
||||||
|
raise ConfigError("invalid stroke orientation settings")
|
||||||
|
copied = dict(job)
|
||||||
|
copied["pairs"] = []
|
||||||
|
for pair in job["pairs"]:
|
||||||
|
pair_id = pair["id"]
|
||||||
|
copied["pairs"].append({
|
||||||
|
**pair,
|
||||||
|
"artifacts": {
|
||||||
|
fmt: str(output_dir / f"stroke_texture_{pair_id}.{fmt}")
|
||||||
|
for fmt in ARTIFACT_FORMATS
|
||||||
|
},
|
||||||
|
})
|
||||||
|
return copied, params
|
||||||
|
|
||||||
|
|
||||||
def _load_job(path: Path) -> dict[str, Any]:
|
def _load_job(path: Path) -> dict[str, Any]:
|
||||||
try:
|
try:
|
||||||
with path.open(encoding="utf-8") as handle:
|
with path.open(encoding="utf-8") as handle:
|
||||||
@@ -707,7 +741,17 @@ def _write_ground_truth(path: Path, payload: dict[str, Any]) -> None:
|
|||||||
def main() -> int:
|
def main() -> int:
|
||||||
args = parse_args(_argv_after_double_dash())
|
args = parse_args(_argv_after_double_dash())
|
||||||
try:
|
try:
|
||||||
config = _resolve_config(_load_job(args.job.resolve()))
|
job = _load_job(args.job.resolve())
|
||||||
|
stroke_params = None
|
||||||
|
if args.stroke_params is not None:
|
||||||
|
if args.stroke_output_dir is None:
|
||||||
|
raise ConfigError("--stroke-params requires --stroke-output-dir")
|
||||||
|
job, stroke_params = _stroke_job(
|
||||||
|
job, args.stroke_params.resolve(), args.stroke_output_dir.resolve()
|
||||||
|
)
|
||||||
|
elif args.stroke_output_dir is not None:
|
||||||
|
raise ConfigError("--stroke-output-dir requires --stroke-params")
|
||||||
|
config = _resolve_config(job)
|
||||||
except (ConfigError, OSError) as exc:
|
except (ConfigError, OSError) as exc:
|
||||||
print(f"ERROR: {exc}", file=sys.stderr)
|
print(f"ERROR: {exc}", file=sys.stderr)
|
||||||
return 2
|
return 2
|
||||||
@@ -747,6 +791,15 @@ def main() -> int:
|
|||||||
start_time = time.time()
|
start_time = time.time()
|
||||||
policy: MergePolicy = config["policy"]
|
policy: MergePolicy = config["policy"]
|
||||||
priority = [pair["id"] for pair, _a, _b, _c in resolved]
|
priority = [pair["id"] for pair, _a, _b, _c in resolved]
|
||||||
|
angle_map = None
|
||||||
|
if stroke_params is not None and stroke_params["orientation_mode"] == "optimized":
|
||||||
|
angle_map, report_data = optimize_angles(
|
||||||
|
config["nx"], config["ny"], stroke_params["seed"],
|
||||||
|
stroke_params["width_ratio"], stroke_params["length_ratio"],
|
||||||
|
stroke_params["proposals"],
|
||||||
|
)
|
||||||
|
report_path = args.stroke_output_dir / "stroke_texture_optimization.json"
|
||||||
|
report_path.write_text(json.dumps(report_data, indent=2) + "\n", encoding="utf-8")
|
||||||
|
|
||||||
detail(
|
detail(
|
||||||
f"Multi-view: {len(resolved)} pair(s), {config['nx']}x{config['ny']} grid, "
|
f"Multi-view: {len(resolved)} pair(s), {config['nx']}x{config['ny']} grid, "
|
||||||
@@ -878,6 +931,9 @@ def main() -> int:
|
|||||||
pattern_cameras: dict[str, Any] = {}
|
pattern_cameras: dict[str, Any] = {}
|
||||||
for pair, _rc, pattern_cam, _layer in resolved:
|
for pair, _rc, pattern_cam, _layer in resolved:
|
||||||
pattern_cameras[pair["pattern_camera"]] = pattern_cam
|
pattern_cameras[pair["pattern_camera"]] = pattern_cam
|
||||||
|
if stroke_params is not None and len(pattern_cameras) != 1:
|
||||||
|
print("ERROR: stroke views require one shared pattern camera", file=sys.stderr)
|
||||||
|
return 2
|
||||||
uv_maps = [pair["uv_map"] for pair, _a, _b, _c in resolved]
|
uv_maps = [pair["uv_map"] for pair, _a, _b, _c in resolved]
|
||||||
pattern_geometry: dict[str, Any] | None = None
|
pattern_geometry: dict[str, Any] | None = None
|
||||||
pattern_meta: dict[str, Any] | None = None
|
pattern_meta: dict[str, Any] | None = None
|
||||||
@@ -889,6 +945,7 @@ def main() -> int:
|
|||||||
fill_unresolved_leaves=True,
|
fill_unresolved_leaves=True,
|
||||||
seam_segments=seam_segments,
|
seam_segments=seam_segments,
|
||||||
seam_search_mm=gap["inset_mm"],
|
seam_search_mm=gap["inset_mm"],
|
||||||
|
capture_camera_fragments=stroke_params is not None,
|
||||||
)
|
)
|
||||||
except Exception as exc: # noqa: BLE001
|
except Exception as exc: # noqa: BLE001
|
||||||
print(
|
print(
|
||||||
@@ -905,6 +962,7 @@ def main() -> int:
|
|||||||
assert pattern_geometry is not None and pattern_meta is not None
|
assert pattern_geometry is not None and pattern_meta is not None
|
||||||
|
|
||||||
pattern_triangles = list(pattern_meta["fragment_triangle"])
|
pattern_triangles = list(pattern_meta["fragment_triangle"])
|
||||||
|
stroke_outline_cache = {}
|
||||||
|
|
||||||
# ---- 5. Emit: mask each layout to its owned triangles ---------------------------
|
# ---- 5. Emit: mask each layout to its owned triangles ---------------------------
|
||||||
emitted_stats: dict[str, dict[str, Any]] = {}
|
emitted_stats: dict[str, dict[str, Any]] = {}
|
||||||
@@ -912,10 +970,11 @@ def main() -> int:
|
|||||||
pair_id = pair["id"]
|
pair_id = pair["id"]
|
||||||
uv_map = pair["uv_map"]
|
uv_map = pair["uv_map"]
|
||||||
detail(f"--- view {pair_id!r}: region={pair['camera']!r} pattern={pair['pattern_camera']!r} uv_map={uv_map!r}")
|
detail(f"--- view {pair_id!r}: region={pair['camera']!r} pattern={pair['pattern_camera']!r} uv_map={uv_map!r}")
|
||||||
if pair["seed"] is not None:
|
if stroke_params is None and pair["seed"] is not None:
|
||||||
np.random.seed(pair["seed"])
|
np.random.seed(pair["seed"])
|
||||||
detail(f" colour seed {pair['seed']}")
|
detail(f" colour seed {pair['seed']}")
|
||||||
patch_colors = precompute_cell_colors(config["nx"], config["ny"], config["color_r"])
|
patch_colors = (precompute_cell_colors(config["nx"], config["ny"], config["color_r"])
|
||||||
|
if stroke_params is None else None)
|
||||||
|
|
||||||
# ---- page: derived per view, or inherited from texture_patch ----------------
|
# ---- page: derived per view, or inherited from texture_patch ----------------
|
||||||
pair_page_mode = str(pair.get("page") or config["page"]["mode"])
|
pair_page_mode = str(pair.get("page") or config["page"]["mode"])
|
||||||
@@ -956,18 +1015,35 @@ def main() -> int:
|
|||||||
}
|
}
|
||||||
|
|
||||||
patches = pattern_geometry[uv_map]["patches"]
|
patches = pattern_geometry[uv_map]["patches"]
|
||||||
|
camera_fragments = pattern_geometry[uv_map].get("camera_fragments")
|
||||||
mesh_layer = pattern_geometry[uv_map]["mesh"]
|
mesh_layer = pattern_geometry[uv_map]["mesh"]
|
||||||
|
|
||||||
|
def fragment_polygon(k: int, patches=patches, camera_fragments=camera_fragments):
|
||||||
|
if stroke_params is None:
|
||||||
|
return patches[k][2]
|
||||||
|
i, j, fragment, loop_uvs = camera_fragments[k]
|
||||||
|
key = (i, j)
|
||||||
|
if key not in stroke_outline_cache:
|
||||||
|
stroke_outline_cache[key] = capsule_in_cell(
|
||||||
|
i, j, config["nx"], config["ny"], stroke_params["seed"],
|
||||||
|
stroke_params["width_ratio"], stroke_params["length_ratio"],
|
||||||
|
angle=None if angle_map is None else float(angle_map[j, i]),
|
||||||
|
)
|
||||||
|
return projected_stroke_uv(fragment, loop_uvs, stroke_outline_cache[key])
|
||||||
fragment_seam = pattern_meta.get("fragment_seam_distance_mm")
|
fragment_seam = pattern_meta.get("fragment_seam_distance_mm")
|
||||||
triangle_seam = pattern_meta.get("triangle_seam_distance_mm")
|
triangle_seam = pattern_meta.get("triangle_seam_distance_mm")
|
||||||
gap_cfg = config["gap"]
|
gap_cfg = config["gap"]
|
||||||
inset = gap_cfg["inset_mm"] if gap_cfg["enabled"] else 0.0
|
inset = gap_cfg["inset_mm"] if gap_cfg["enabled"] else 0.0
|
||||||
is_post = gap_cfg["enabled"] and gap_cfg["apply"] == "post_process" and triangle_seam_edges
|
is_post = gap_cfg["enabled"] and gap_cfg["apply"] == "post_process" and triangle_seam_edges
|
||||||
|
|
||||||
def outside_margin(distance: float | None) -> bool:
|
def outside_margin(distance: float | None, inset=inset) -> bool:
|
||||||
"""True when this fragment/triangle is clear of the seam margin."""
|
"""True when this fragment/triangle is clear of the seam margin."""
|
||||||
return distance is None or distance >= inset
|
return distance is None or distance >= inset
|
||||||
|
|
||||||
def gap_planes(triangle_index: int):
|
plane_cache: dict[int, list] = {}
|
||||||
|
|
||||||
|
def gap_planes(triangle_index: int, plane_cache=plane_cache,
|
||||||
|
mesh_layer=mesh_layer, inset=inset):
|
||||||
"""Cached UV half-planes cutting this triangle back from its seam edges."""
|
"""Cached UV half-planes cutting this triangle back from its seam edges."""
|
||||||
cached = plane_cache.get(triangle_index)
|
cached = plane_cache.get(triangle_index)
|
||||||
if cached is None:
|
if cached is None:
|
||||||
@@ -984,7 +1060,6 @@ def main() -> int:
|
|||||||
plane_cache[triangle_index] = cached
|
plane_cache[triangle_index] = cached
|
||||||
return cached
|
return cached
|
||||||
|
|
||||||
plane_cache: dict[int, list] = {}
|
|
||||||
owned_here = [k for k, t in enumerate(pattern_triangles) if owner.get(t) == pair_id]
|
owned_here = [k for k, t in enumerate(pattern_triangles) if owner.get(t) == pair_id]
|
||||||
if is_post:
|
if is_post:
|
||||||
# Post-process: every owned fragment is kept, then clipped. A fragment is a
|
# Post-process: every owned fragment is kept, then clipped. A fragment is a
|
||||||
@@ -994,7 +1069,10 @@ def main() -> int:
|
|||||||
emitted_patches = []
|
emitted_patches = []
|
||||||
trimmed = 0
|
trimmed = 0
|
||||||
for k in keep:
|
for k in keep:
|
||||||
i, j, polygon = patches[k]
|
i, j, _ = patches[k]
|
||||||
|
polygon = fragment_polygon(k)
|
||||||
|
if len(polygon) < 3:
|
||||||
|
continue
|
||||||
planes = gap_planes(pattern_triangles[k])
|
planes = gap_planes(pattern_triangles[k])
|
||||||
if planes:
|
if planes:
|
||||||
before_area = seam_gap.polygon_area(polygon)
|
before_area = seam_gap.polygon_area(polygon)
|
||||||
@@ -1016,7 +1094,11 @@ def main() -> int:
|
|||||||
k for k in owned_here
|
k for k in owned_here
|
||||||
if outside_margin(None if fragment_seam is None else fragment_seam[k])
|
if outside_margin(None if fragment_seam is None else fragment_seam[k])
|
||||||
]
|
]
|
||||||
emitted_patches = [patches[k] for k in keep]
|
emitted_patches = [
|
||||||
|
(patches[k][0], patches[k][1], polygon)
|
||||||
|
for k in keep
|
||||||
|
if len(polygon := fragment_polygon(k)) >= 3
|
||||||
|
]
|
||||||
|
|
||||||
owned_triangles = sorted(t for t, v in owner.items() if v == pair_id)
|
owned_triangles = sorted(t for t, v in owner.items() if v == pair_id)
|
||||||
kept_triangles = sorted({pattern_triangles[k] for k in keep})
|
kept_triangles = sorted({pattern_triangles[k] for k in keep})
|
||||||
@@ -1056,7 +1138,8 @@ def main() -> int:
|
|||||||
"owned_triangles": len(owned_triangles),
|
"owned_triangles": len(owned_triangles),
|
||||||
"owned_mm2": sum(meta["triangle_area_mm2"][t] for t in owned_triangles),
|
"owned_mm2": sum(meta["triangle_area_mm2"][t] for t in owned_triangles),
|
||||||
"inked_triangles": len(kept_triangles),
|
"inked_triangles": len(kept_triangles),
|
||||||
"inked_mm2": sum(meta["triangle_area_mm2"][t] for t in kept_triangles),
|
"inked_mm2": (sum(meta["triangle_area_mm2"][t] for t in kept_triangles)
|
||||||
|
if stroke_params is None else None),
|
||||||
"emitted_fragments": len(emitted_patches),
|
"emitted_fragments": len(emitted_patches),
|
||||||
"total_fragments": len(patches),
|
"total_fragments": len(patches),
|
||||||
"gap_enabled": gap_cfg["enabled"],
|
"gap_enabled": gap_cfg["enabled"],
|
||||||
@@ -1067,7 +1150,7 @@ def main() -> int:
|
|||||||
"gap_clipped_polygons": len(keep) - len(emitted_patches) if is_post else 0,
|
"gap_clipped_polygons": len(keep) - len(emitted_patches) if is_post else 0,
|
||||||
"gap_trimmed_polygons": trimmed_polygons if is_post else 0,
|
"gap_trimmed_polygons": trimmed_polygons if is_post else 0,
|
||||||
"base_triangles": len(base_layer),
|
"base_triangles": len(base_layer),
|
||||||
"seed": pair["seed"],
|
"seed": stroke_params["seed"] if stroke_params is not None else pair["seed"],
|
||||||
"artifacts": pair["artifacts"],
|
"artifacts": pair["artifacts"],
|
||||||
}
|
}
|
||||||
summary = (
|
summary = (
|
||||||
@@ -1089,33 +1172,31 @@ def main() -> int:
|
|||||||
)
|
)
|
||||||
report(summary)
|
report(summary)
|
||||||
|
|
||||||
uv_centre = _uv_centre(uv_layer) if config["cross_enabled"] else None
|
|
||||||
try:
|
try:
|
||||||
|
if stroke_params is None:
|
||||||
|
uv_centre = _uv_centre(uv_layer) if config["cross_enabled"] else None
|
||||||
generate_patch_texture(
|
generate_patch_texture(
|
||||||
obj,
|
obj, pattern_cameras[pair["pattern_camera"]], scene, depsgraph,
|
||||||
pattern_cameras[pair["pattern_camera"]],
|
nx=config["nx"], ny=config["ny"], r=config["color_r"],
|
||||||
scene,
|
texture_width=texture_width, texture_height=texture_height,
|
||||||
depsgraph,
|
|
||||||
nx=config["nx"],
|
|
||||||
ny=config["ny"],
|
|
||||||
r=config["color_r"],
|
|
||||||
texture_width=texture_width,
|
|
||||||
texture_height=texture_height,
|
|
||||||
output_path=pair["artifacts"]["png"],
|
output_path=pair["artifacts"]["png"],
|
||||||
svg_path=pair["artifacts"]["svg"],
|
svg_path=pair["artifacts"]["svg"],
|
||||||
pdf_path=pair["artifacts"]["pdf"],
|
pdf_path=pair["artifacts"]["pdf"],
|
||||||
vector_width_mm=page_width_mm,
|
vector_width_mm=page_width_mm, vector_height_mm=page_height_mm,
|
||||||
vector_height_mm=page_height_mm,
|
uv_centre=uv_centre, cross_length=config["cross_length"],
|
||||||
uv_centre=uv_centre,
|
|
||||||
cross_length=config["cross_length"],
|
|
||||||
cross_thickness=config["cross_thickness"],
|
cross_thickness=config["cross_thickness"],
|
||||||
cross_color=config["cross_color"],
|
cross_color=config["cross_color"],
|
||||||
edge_sample_grid=config["edge_sample_grid"],
|
edge_sample_grid=config["edge_sample_grid"], uv_layer=uv_layer,
|
||||||
uv_layer=uv_layer,
|
image_name=f"PatchTexture_{pair_id}", patch_colors=patch_colors,
|
||||||
image_name=f"PatchTexture_{pair_id}",
|
|
||||||
patch_colors=patch_colors,
|
|
||||||
geometry=filtered,
|
geometry=filtered,
|
||||||
)
|
)
|
||||||
|
else:
|
||||||
|
render_stroke_polygons(
|
||||||
|
uv_map, base_layer, [poly for _, _, poly in emitted_patches],
|
||||||
|
texture_width, texture_height,
|
||||||
|
{"width_mm": page_width_mm, "height_mm": page_height_mm},
|
||||||
|
Path(pair["artifacts"]["png"]).with_suffix(""),
|
||||||
|
)
|
||||||
except Exception as exc: # noqa: BLE001
|
except Exception as exc: # noqa: BLE001
|
||||||
print(
|
print(
|
||||||
f"ERROR: texture generation failed for view {pair_id!r}: {exc}",
|
f"ERROR: texture generation failed for view {pair_id!r}: {exc}",
|
||||||
@@ -1217,10 +1298,11 @@ def main() -> int:
|
|||||||
"hole_mm2": round(result["hole_mm2"], 6),
|
"hole_mm2": round(result["hole_mm2"], 6),
|
||||||
"total_mm2": round(result["total_mm2"], 6),
|
"total_mm2": round(result["total_mm2"], 6),
|
||||||
"hole_fraction": round(result["hole_fraction"], 8),
|
"hole_fraction": round(result["hole_fraction"], 8),
|
||||||
"inked_mm2": round(sum(triangle_area[t] for t in inked_triangles), 6),
|
"inked_mm2": (round(sum(triangle_area[t] for t in inked_triangles), 6)
|
||||||
"inked_fraction": round(
|
if stroke_params is None else None),
|
||||||
|
"inked_fraction": (round(
|
||||||
sum(triangle_area[t] for t in inked_triangles) / result["total_mm2"], 8
|
sum(triangle_area[t] for t in inked_triangles) / result["total_mm2"], 8
|
||||||
) if result["total_mm2"] else 0.0,
|
) if result["total_mm2"] else 0.0) if stroke_params is None else None,
|
||||||
},
|
},
|
||||||
"seam_margin": dict(config["gap"]),
|
"seam_margin": dict(config["gap"]),
|
||||||
"views": [],
|
"views": [],
|
||||||
@@ -1255,7 +1337,9 @@ def main() -> int:
|
|||||||
f"polar={world['polar_from_z_deg']:7.2f} d={world['distance_to_centroid_mm']:.2f}mm\n"
|
f"polar={world['polar_from_z_deg']:7.2f} d={world['distance_to_centroid_mm']:.2f}mm\n"
|
||||||
f" object-local az={local['azimuth_deg']:8.2f} el={local['elevation_deg']:+8.2f} "
|
f" object-local az={local['azimuth_deg']:8.2f} el={local['elevation_deg']:+8.2f} "
|
||||||
f"polar={local['polar_from_z_deg']:7.2f}\n"
|
f"polar={local['polar_from_z_deg']:7.2f}\n"
|
||||||
f" owned={entry['owned_mm2']:.3f}mm^2 inked={entry['inked_mm2']:.3f}mm^2"
|
f" owned={entry['owned_mm2']:.3f}mm^2 "
|
||||||
|
+ (f"inked={entry['inked_mm2']:.3f}mm^2" if stroke_params is None
|
||||||
|
else "stroke ink area not measured")
|
||||||
)
|
)
|
||||||
pc = angle_report["pattern_camera"]
|
pc = angle_report["pattern_camera"]
|
||||||
detail(
|
detail(
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1,403 @@
|
|||||||
|
{
|
||||||
|
"schema": "orisys-sensor-design/print-views-v1",
|
||||||
|
"mesh": "sensing_surface",
|
||||||
|
"units": "mm",
|
||||||
|
"angle_convention": "TWO frames are reported per camera, because they answer different questions. `world` is measured against the scene axes: that is the frame Blender's Transform panel shows for the camera's location and rotation, so check the numbers here against Blender. `object_local` is measured against the mesh object's own axes, which is the physically meaningful frame when the part is a body of revolution. They differ whenever the mesh object carries a rotation. In both: azimuth = atan2(y, x) in the frame's XY plane from +X, wrapped to [0, 360); elevation = signed angle from that plane; polar_from_z_deg = angle from that frame's +Z. The flat azimuth_deg/elevation_deg keys are the WORLD frame.",
|
||||||
|
"centroid_world_mm": [
|
||||||
|
0.000793,
|
||||||
|
-17.867939,
|
||||||
|
-5.954863
|
||||||
|
],
|
||||||
|
"centroid_object_mm": [
|
||||||
|
0.000793,
|
||||||
|
0.515454,
|
||||||
|
18.827053
|
||||||
|
],
|
||||||
|
"part_transform": {
|
||||||
|
"rotation_euler_deg": [
|
||||||
|
110.0,
|
||||||
|
-0.0,
|
||||||
|
0.0
|
||||||
|
],
|
||||||
|
"scale": [
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"nx": 80,
|
||||||
|
"ny": 80,
|
||||||
|
"default_raster": [
|
||||||
|
3700,
|
||||||
|
3280
|
||||||
|
],
|
||||||
|
"default_page_mm": [
|
||||||
|
18.5,
|
||||||
|
16.4
|
||||||
|
],
|
||||||
|
"page": {
|
||||||
|
"mode": "auto",
|
||||||
|
"px_per_mm": 200.0
|
||||||
|
},
|
||||||
|
"seed": 7
|
||||||
|
},
|
||||||
|
"ownership": {
|
||||||
|
"mode": "authored",
|
||||||
|
"incidence_limit_deg": 70.0,
|
||||||
|
"min_footprint_mm2": 0.002,
|
||||||
|
"min_triangle_coverage": 0.6,
|
||||||
|
"max_hole_fraction": 0.02,
|
||||||
|
"triangles": 5007,
|
||||||
|
"owned_mm2": 616.533224,
|
||||||
|
"hole_mm2": 0.091787,
|
||||||
|
"total_mm2": 616.625011,
|
||||||
|
"hole_fraction": 0.00014885,
|
||||||
|
"inked_mm2": null,
|
||||||
|
"inked_fraction": null
|
||||||
|
},
|
||||||
|
"seam_margin": {
|
||||||
|
"enabled": false,
|
||||||
|
"apply": "post_process",
|
||||||
|
"treatment": "no_print",
|
||||||
|
"inset_mm": 0.1,
|
||||||
|
"total_mm": 0.2,
|
||||||
|
"alignment_tolerance_mm": 0.1
|
||||||
|
},
|
||||||
|
"views": [
|
||||||
|
{
|
||||||
|
"name": "cam_uv_1",
|
||||||
|
"type": "ORTHO",
|
||||||
|
"world": {
|
||||||
|
"position_mm": [
|
||||||
|
18.384392,
|
||||||
|
-41.181198,
|
||||||
|
-19.463104
|
||||||
|
],
|
||||||
|
"view_direction": [
|
||||||
|
0.563601971,
|
||||||
|
-0.714734852,
|
||||||
|
-0.414133906
|
||||||
|
],
|
||||||
|
"distance_to_centroid_mm": 32.618052,
|
||||||
|
"azimuth_deg": 308.2574,
|
||||||
|
"elevation_deg": -24.4648,
|
||||||
|
"polar_from_z_deg": 114.4648
|
||||||
|
},
|
||||||
|
"object_local": {
|
||||||
|
"position_mm": [
|
||||||
|
18.384392,
|
||||||
|
-4.204537,
|
||||||
|
45.354443
|
||||||
|
],
|
||||||
|
"view_direction": [
|
||||||
|
0.563601971,
|
||||||
|
-0.144704893,
|
||||||
|
0.813273251
|
||||||
|
],
|
||||||
|
"distance_to_centroid_mm": 32.618053,
|
||||||
|
"azimuth_deg": 345.6003,
|
||||||
|
"elevation_deg": 54.417,
|
||||||
|
"polar_from_z_deg": 35.583
|
||||||
|
},
|
||||||
|
"camera_transform": {
|
||||||
|
"rotation_euler_deg": [
|
||||||
|
45.0,
|
||||||
|
135.0,
|
||||||
|
-0.0
|
||||||
|
],
|
||||||
|
"scale": [
|
||||||
|
0.014988,
|
||||||
|
0.014988,
|
||||||
|
0.014988
|
||||||
|
],
|
||||||
|
"parent": null
|
||||||
|
},
|
||||||
|
"part_transform": {
|
||||||
|
"rotation_euler_deg": [
|
||||||
|
110.0,
|
||||||
|
-0.0,
|
||||||
|
0.0
|
||||||
|
],
|
||||||
|
"scale": [
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"ortho_scale_mm": 27.0,
|
||||||
|
"lens_mm": null,
|
||||||
|
"sensor_width_mm": 36.0,
|
||||||
|
"clip_start_mm": 0.1,
|
||||||
|
"clip_end_mm": 1000.0,
|
||||||
|
"position_mm": [
|
||||||
|
18.384392,
|
||||||
|
-41.181198,
|
||||||
|
-19.463104
|
||||||
|
],
|
||||||
|
"view_direction": [
|
||||||
|
0.563601971,
|
||||||
|
-0.714734852,
|
||||||
|
-0.414133906
|
||||||
|
],
|
||||||
|
"distance_to_centroid_mm": 32.618052,
|
||||||
|
"azimuth_deg": 308.2574,
|
||||||
|
"elevation_deg": -24.4648,
|
||||||
|
"polar_from_z_deg": 114.4648,
|
||||||
|
"azimuth_deg_frame": "world",
|
||||||
|
"id": "uv_1",
|
||||||
|
"uv_map": "uv_1",
|
||||||
|
"region_camera": "cam_uv_1",
|
||||||
|
"pattern_camera": "cam_sensor",
|
||||||
|
"page_source": "auto",
|
||||||
|
"page_mm": [
|
||||||
|
17.67,
|
||||||
|
23.565
|
||||||
|
],
|
||||||
|
"raster_px": [
|
||||||
|
3534,
|
||||||
|
4713
|
||||||
|
],
|
||||||
|
"page_residual": [
|
||||||
|
1.2885921574223858e-07,
|
||||||
|
1.0443071318810837e-07
|
||||||
|
],
|
||||||
|
"page_slope": [
|
||||||
|
0.05659285670700212,
|
||||||
|
0.04243635765680492
|
||||||
|
],
|
||||||
|
"owned_triangles": 2471,
|
||||||
|
"owned_mm2": 308.2663995595292,
|
||||||
|
"inked_triangles": 2471,
|
||||||
|
"inked_mm2": null,
|
||||||
|
"emitted_fragments": 8292,
|
||||||
|
"total_fragments": 32593,
|
||||||
|
"gap_enabled": false,
|
||||||
|
"gap_apply": null,
|
||||||
|
"gap_requested_total_mm": 0.0,
|
||||||
|
"gap_achieved_total_mm": null,
|
||||||
|
"gap_removed_fragments": 0,
|
||||||
|
"gap_clipped_polygons": 0,
|
||||||
|
"gap_trimmed_polygons": 0,
|
||||||
|
"base_triangles": 2471,
|
||||||
|
"seed": 7,
|
||||||
|
"artifacts": {
|
||||||
|
"png": "D:\\orisys_ws\\orisys-sensor-design\\build\\textures\\finger-xynova\\r01\\strokes-views\\stroke_texture_uv_1.png",
|
||||||
|
"svg": "D:\\orisys_ws\\orisys-sensor-design\\build\\textures\\finger-xynova\\r01\\strokes-views\\stroke_texture_uv_1.svg",
|
||||||
|
"pdf": "D:\\orisys_ws\\orisys-sensor-design\\build\\textures\\finger-xynova\\r01\\strokes-views\\stroke_texture_uv_1.pdf"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "cam_uv_2",
|
||||||
|
"type": "ORTHO",
|
||||||
|
"world": {
|
||||||
|
"position_mm": [
|
||||||
|
-18.384399,
|
||||||
|
-41.181198,
|
||||||
|
-19.463104
|
||||||
|
],
|
||||||
|
"view_direction": [
|
||||||
|
-0.563635349,
|
||||||
|
-0.714715183,
|
||||||
|
-0.414122522
|
||||||
|
],
|
||||||
|
"distance_to_centroid_mm": 32.618951,
|
||||||
|
"azimuth_deg": 231.7401,
|
||||||
|
"elevation_deg": -24.4641,
|
||||||
|
"polar_from_z_deg": 114.4641
|
||||||
|
},
|
||||||
|
"object_local": {
|
||||||
|
"position_mm": [
|
||||||
|
-18.384399,
|
||||||
|
-4.204537,
|
||||||
|
45.354443
|
||||||
|
],
|
||||||
|
"view_direction": [
|
||||||
|
-0.56363529,
|
||||||
|
-0.1447009,
|
||||||
|
0.81325078
|
||||||
|
],
|
||||||
|
"distance_to_centroid_mm": 32.618952,
|
||||||
|
"azimuth_deg": 194.3985,
|
||||||
|
"elevation_deg": 54.4148,
|
||||||
|
"polar_from_z_deg": 35.5852
|
||||||
|
},
|
||||||
|
"camera_transform": {
|
||||||
|
"rotation_euler_deg": [
|
||||||
|
45.0,
|
||||||
|
-135.0,
|
||||||
|
-0.0
|
||||||
|
],
|
||||||
|
"scale": [
|
||||||
|
0.014988,
|
||||||
|
0.014988,
|
||||||
|
0.014988
|
||||||
|
],
|
||||||
|
"parent": null
|
||||||
|
},
|
||||||
|
"part_transform": {
|
||||||
|
"rotation_euler_deg": [
|
||||||
|
110.0,
|
||||||
|
-0.0,
|
||||||
|
0.0
|
||||||
|
],
|
||||||
|
"scale": [
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"ortho_scale_mm": 27.0,
|
||||||
|
"lens_mm": null,
|
||||||
|
"sensor_width_mm": 36.0,
|
||||||
|
"clip_start_mm": 0.1,
|
||||||
|
"clip_end_mm": 1000.0,
|
||||||
|
"position_mm": [
|
||||||
|
-18.384399,
|
||||||
|
-41.181198,
|
||||||
|
-19.463104
|
||||||
|
],
|
||||||
|
"view_direction": [
|
||||||
|
-0.563635349,
|
||||||
|
-0.714715183,
|
||||||
|
-0.414122522
|
||||||
|
],
|
||||||
|
"distance_to_centroid_mm": 32.618951,
|
||||||
|
"azimuth_deg": 231.7401,
|
||||||
|
"elevation_deg": -24.4641,
|
||||||
|
"polar_from_z_deg": 114.4641,
|
||||||
|
"azimuth_deg_frame": "world",
|
||||||
|
"id": "uv_2",
|
||||||
|
"uv_map": "uv_2",
|
||||||
|
"region_camera": "cam_uv_2",
|
||||||
|
"pattern_camera": "cam_sensor",
|
||||||
|
"page_source": "auto",
|
||||||
|
"page_mm": [
|
||||||
|
17.67,
|
||||||
|
23.565
|
||||||
|
],
|
||||||
|
"raster_px": [
|
||||||
|
3534,
|
||||||
|
4713
|
||||||
|
],
|
||||||
|
"page_residual": [
|
||||||
|
1.0079875600865762e-07,
|
||||||
|
9.84046944485506e-08
|
||||||
|
],
|
||||||
|
"page_slope": [
|
||||||
|
0.056592803427866885,
|
||||||
|
0.04243927082965147
|
||||||
|
],
|
||||||
|
"owned_triangles": 2488,
|
||||||
|
"owned_mm2": 308.2668241740881,
|
||||||
|
"inked_triangles": 2488,
|
||||||
|
"inked_mm2": null,
|
||||||
|
"emitted_fragments": 8334,
|
||||||
|
"total_fragments": 32593,
|
||||||
|
"gap_enabled": false,
|
||||||
|
"gap_apply": null,
|
||||||
|
"gap_requested_total_mm": 0.0,
|
||||||
|
"gap_achieved_total_mm": null,
|
||||||
|
"gap_removed_fragments": 0,
|
||||||
|
"gap_clipped_polygons": 0,
|
||||||
|
"gap_trimmed_polygons": 0,
|
||||||
|
"base_triangles": 2488,
|
||||||
|
"seed": 7,
|
||||||
|
"artifacts": {
|
||||||
|
"png": "D:\\orisys_ws\\orisys-sensor-design\\build\\textures\\finger-xynova\\r01\\strokes-views\\stroke_texture_uv_2.png",
|
||||||
|
"svg": "D:\\orisys_ws\\orisys-sensor-design\\build\\textures\\finger-xynova\\r01\\strokes-views\\stroke_texture_uv_2.svg",
|
||||||
|
"pdf": "D:\\orisys_ws\\orisys-sensor-design\\build\\textures\\finger-xynova\\r01\\strokes-views\\stroke_texture_uv_2.pdf"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"seam": {
|
||||||
|
"boundary": "use_seam",
|
||||||
|
"edges": 45,
|
||||||
|
"components": 2,
|
||||||
|
"component_owner": {
|
||||||
|
"0": "uv_2",
|
||||||
|
"1": "uv_1"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"pattern_camera": {
|
||||||
|
"name": "cam_sensor",
|
||||||
|
"type": "PERSP",
|
||||||
|
"world": {
|
||||||
|
"position_mm": [
|
||||||
|
0.0,
|
||||||
|
-9.29984,
|
||||||
|
4.357839
|
||||||
|
],
|
||||||
|
"view_direction": [
|
||||||
|
-5.912e-05,
|
||||||
|
0.639047205,
|
||||||
|
0.769167483
|
||||||
|
],
|
||||||
|
"distance_to_centroid_mm": 13.407615,
|
||||||
|
"azimuth_deg": 90.0053,
|
||||||
|
"elevation_deg": 50.2792,
|
||||||
|
"polar_from_z_deg": 39.7208
|
||||||
|
},
|
||||||
|
"object_local": {
|
||||||
|
"position_mm": [
|
||||||
|
0.0,
|
||||||
|
7.275762,
|
||||||
|
7.248523
|
||||||
|
],
|
||||||
|
"view_direction": [
|
||||||
|
-5.912e-05,
|
||||||
|
0.504214048,
|
||||||
|
-0.863578737
|
||||||
|
],
|
||||||
|
"distance_to_centroid_mm": 13.407614,
|
||||||
|
"azimuth_deg": 90.0067,
|
||||||
|
"elevation_deg": -59.7208,
|
||||||
|
"polar_from_z_deg": 149.7208
|
||||||
|
},
|
||||||
|
"camera_transform": {
|
||||||
|
"rotation_euler_deg": [
|
||||||
|
30.0,
|
||||||
|
-0.0,
|
||||||
|
-179.9999
|
||||||
|
],
|
||||||
|
"scale": [
|
||||||
|
4.671241,
|
||||||
|
4.671243,
|
||||||
|
4.671243
|
||||||
|
],
|
||||||
|
"parent": null
|
||||||
|
},
|
||||||
|
"part_transform": {
|
||||||
|
"rotation_euler_deg": [
|
||||||
|
110.0,
|
||||||
|
-0.0,
|
||||||
|
0.0
|
||||||
|
],
|
||||||
|
"scale": [
|
||||||
|
1.0,
|
||||||
|
1.0,
|
||||||
|
1.0
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"ortho_scale_mm": null,
|
||||||
|
"lens_mm": 0.396041,
|
||||||
|
"sensor_width_mm": 1.4,
|
||||||
|
"clip_start_mm": 0.01,
|
||||||
|
"clip_end_mm": 100.0,
|
||||||
|
"position_mm": [
|
||||||
|
0.0,
|
||||||
|
-9.29984,
|
||||||
|
4.357839
|
||||||
|
],
|
||||||
|
"view_direction": [
|
||||||
|
-5.912e-05,
|
||||||
|
0.639047205,
|
||||||
|
0.769167483
|
||||||
|
],
|
||||||
|
"distance_to_centroid_mm": 13.407615,
|
||||||
|
"azimuth_deg": 90.0053,
|
||||||
|
"elevation_deg": 50.2792,
|
||||||
|
"polar_from_z_deg": 39.7208,
|
||||||
|
"azimuth_deg_frame": "world"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
{
|
||||||
|
"length_ratio": 0.84,
|
||||||
|
"orientation_mode": "random",
|
||||||
|
"proposals": 100000,
|
||||||
|
"seed": 7,
|
||||||
|
"width_ratio": 0.42
|
||||||
|
}
|
||||||
@@ -0,0 +1,77 @@
|
|||||||
|
{
|
||||||
|
"schema": "orisys-sensor-design/stroke-texture-manifest-v2",
|
||||||
|
"revision_dir": "D:\\orisys_ws\\orisys-sensor-design\\sensors\\finger-xynova\\r01",
|
||||||
|
"views": true,
|
||||||
|
"source_job": {
|
||||||
|
"path": "D:\\orisys_ws\\orisys-sensor-design\\build\\textures\\finger-xynova\\r01\\_texture_views_job.json",
|
||||||
|
"sha256": "7259df462de85a86ec575a6296592bf78c008d44ae531d7efd233118686b194f"
|
||||||
|
},
|
||||||
|
"source_blend": {
|
||||||
|
"path": "D:\\orisys_ws\\orisys-sensor-design\\sensors\\finger-xynova\\r01\\source\\geometry.blend",
|
||||||
|
"sha256": "2a48957a88c760775582e4dc4780d98bf9fc94348918cff8efd82e840217530c"
|
||||||
|
},
|
||||||
|
"settings": {
|
||||||
|
"mode": "camera_grid_capsules",
|
||||||
|
"resolved": {
|
||||||
|
"seed": 7,
|
||||||
|
"width_ratio": 0.42,
|
||||||
|
"length_ratio": 0.84,
|
||||||
|
"orientation_mode": "random",
|
||||||
|
"proposals": 100000
|
||||||
|
},
|
||||||
|
"design_yaml": {
|
||||||
|
"path": "D:\\orisys_ws\\orisys-sensor-design\\sensors\\finger-xynova\\r01\\design.yaml",
|
||||||
|
"sha256": "711ad69649ef0d771e8f68295bea129af0f13c0e003d9aaf361e25ea5ab8c34e"
|
||||||
|
},
|
||||||
|
"params": {
|
||||||
|
"path": "D:\\orisys_ws\\orisys-sensor-design\\build\\textures\\finger-xynova\\r01\\strokes-views\\_stroke_pattern_params.json",
|
||||||
|
"sha256": "823639aa72706b1d47d0c531e704cb99f36517e87fcad7de707417cb474e6993"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"id": "uv_1",
|
||||||
|
"artifacts": {
|
||||||
|
"png": {
|
||||||
|
"path": "D:\\orisys_ws\\orisys-sensor-design\\build\\textures\\finger-xynova\\r01\\strokes-views\\stroke_texture_uv_1.png",
|
||||||
|
"sha256": "53a8af45eecedd370a152223d32fa0ff3b42cbf6d5729953ac04bcb20db7a393"
|
||||||
|
},
|
||||||
|
"svg": {
|
||||||
|
"path": "D:\\orisys_ws\\orisys-sensor-design\\build\\textures\\finger-xynova\\r01\\strokes-views\\stroke_texture_uv_1.svg",
|
||||||
|
"sha256": "392363d18ff980e3f698ebb2e745a0f0e5efa3870c9af966671b19d39831420e"
|
||||||
|
},
|
||||||
|
"pdf": {
|
||||||
|
"path": "D:\\orisys_ws\\orisys-sensor-design\\build\\textures\\finger-xynova\\r01\\strokes-views\\stroke_texture_uv_1.pdf",
|
||||||
|
"sha256": "11956e5dc65963334b9277797270cb28b1a0d6d5b80d8b927122d04dce3ff6cb"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "uv_2",
|
||||||
|
"artifacts": {
|
||||||
|
"png": {
|
||||||
|
"path": "D:\\orisys_ws\\orisys-sensor-design\\build\\textures\\finger-xynova\\r01\\strokes-views\\stroke_texture_uv_2.png",
|
||||||
|
"sha256": "25ca871f39de3d7380289b825021e5f555182be63b1809857de38952121a0fec"
|
||||||
|
},
|
||||||
|
"svg": {
|
||||||
|
"path": "D:\\orisys_ws\\orisys-sensor-design\\build\\textures\\finger-xynova\\r01\\strokes-views\\stroke_texture_uv_2.svg",
|
||||||
|
"sha256": "76a72e09a2d7ff22972b99b14b5849d500be9060208f875719360d1cb45321c1"
|
||||||
|
},
|
||||||
|
"pdf": {
|
||||||
|
"path": "D:\\orisys_ws\\orisys-sensor-design\\build\\textures\\finger-xynova\\r01\\strokes-views\\stroke_texture_uv_2.pdf",
|
||||||
|
"sha256": "09e7cee75d459aaae234362350046af1bdbc6557d5ef950c87462e48e2b907fa"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"auxiliary": {
|
||||||
|
"_multiview_ground_truth.json": {
|
||||||
|
"path": "D:\\orisys_ws\\orisys-sensor-design\\build\\textures\\finger-xynova\\r01\\strokes-views\\_multiview_ground_truth.json",
|
||||||
|
"sha256": "a54e3ecc696471bf2e64b8e9693aa006538292954bc7dd8d2309b4f31447a160"
|
||||||
|
},
|
||||||
|
"_print_views.json": {
|
||||||
|
"path": "D:\\orisys_ws\\orisys-sensor-design\\build\\textures\\finger-xynova\\r01\\strokes-views\\_print_views.json",
|
||||||
|
"sha256": "abd5938d35fc8863b91753b2915fb5b39d261f0920286ef3eca2cc4e6a6c89bf"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
After Width: | Height: | Size: 705 KiB |
File diff suppressed because it is too large
Load Diff
|
After Width: | Height: | Size: 1.8 MiB |
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
After Width: | Height: | Size: 704 KiB |
File diff suppressed because it is too large
Load Diff
|
After Width: | Height: | Size: 1.8 MiB |
@@ -95,7 +95,7 @@ texture_patch_views:
|
|||||||
base_fill: owned # white ground, already split by the seam
|
base_fill: owned # white ground, already split by the seam
|
||||||
|
|
||||||
# Camera-grid capsule strokes (`orisys-design stroke-texture`). Geometry and output size
|
# Camera-grid capsule strokes (`orisys-design stroke-texture`). Geometry and output size
|
||||||
# follow texture_patch. This mode currently accepts single-view jobs only.
|
# follow texture_patch; --views follows texture_patch_views ownership and page sizing.
|
||||||
stroke_pattern:
|
stroke_pattern:
|
||||||
seed: 7
|
seed: 7
|
||||||
width_ratio: 0.42
|
width_ratio: 0.42
|
||||||
|
|||||||
@@ -478,15 +478,15 @@ def cmd_texture(revision_dir: Path, args: argparse.Namespace) -> int:
|
|||||||
|
|
||||||
|
|
||||||
def _stroke_source_job(paths: RevisionPaths, root: Path, args: argparse.Namespace):
|
def _stroke_source_job(paths: RevisionPaths, root: Path, args: argparse.Namespace):
|
||||||
"""Validate the single-view patch geometry job used by the stroke generator."""
|
"""Validate the patch geometry job used by the stroke generator."""
|
||||||
if args.views:
|
|
||||||
raise ValueError("camera-grid strokes require a single-view job; --views is unsupported")
|
|
||||||
texture_dir = resolve_texture_output_dir(root, paths, None)
|
texture_dir = resolve_texture_output_dir(root, paths, None)
|
||||||
job_path = (args.source_job or texture_dir / "_texture_job.json").resolve()
|
job_name = "_texture_views_job.json" if args.views else "_texture_job.json"
|
||||||
|
job_path = (args.source_job or texture_dir / job_name).resolve()
|
||||||
if not job_path.is_file():
|
if not job_path.is_file():
|
||||||
raise FileNotFoundError(f"texture job missing: {job_path}")
|
raise FileNotFoundError(f"texture job missing: {job_path}")
|
||||||
job = json.loads(job_path.read_text(encoding="utf-8"))
|
job = json.loads(job_path.read_text(encoding="utf-8"))
|
||||||
expected_schema = "orisys-sensor-design/texture-job-v3"
|
expected_schema = ("orisys-sensor-design/texture-job-multiview-v1" if args.views
|
||||||
|
else "orisys-sensor-design/texture-job-v3")
|
||||||
if job.get("schema") != expected_schema:
|
if job.get("schema") != expected_schema:
|
||||||
raise ValueError(f"expected {expected_schema}, got {job.get('schema')!r}")
|
raise ValueError(f"expected {expected_schema}, got {job.get('schema')!r}")
|
||||||
if Path(job["revision_dir"]).resolve() != paths.root.resolve():
|
if Path(job["revision_dir"]).resolve() != paths.root.resolve():
|
||||||
@@ -495,8 +495,8 @@ def _stroke_source_job(paths: RevisionPaths, root: Path, args: argparse.Namespac
|
|||||||
raise ValueError("source job was built from a different Blender scene")
|
raise ValueError("source job was built from a different Blender scene")
|
||||||
if not isinstance(job["settings"], dict) or not isinstance(job["objects"], dict):
|
if not isinstance(job["settings"], dict) or not isinstance(job["objects"], dict):
|
||||||
raise ValueError("texture job needs settings and objects")
|
raise ValueError("texture job needs settings and objects")
|
||||||
entries = job["outputs"]
|
entries = job["pairs"] if args.views else job["outputs"]
|
||||||
source_ids = [str(entry["uv_map"]) for entry in entries]
|
source_ids = [str(entry["id"] if args.views else entry["uv_map"]) for entry in entries]
|
||||||
if (not source_ids or len(source_ids) != len(set(source_ids))
|
if (not source_ids or len(source_ids) != len(set(source_ids))
|
||||||
or any(not source_id or not all(
|
or any(not source_id or not all(
|
||||||
char.isalnum() or char in "_-" for char in source_id
|
char.isalnum() or char in "_-" for char in source_id
|
||||||
@@ -557,20 +557,29 @@ def cmd_stroke_texture(revision_dir: Path, args: argparse.Namespace) -> int:
|
|||||||
}
|
}
|
||||||
manifest_path = output_dir / "stroke_texture_manifest.json"
|
manifest_path = output_dir / "stroke_texture_manifest.json"
|
||||||
temporary = manifest_path.with_suffix(".json.tmp")
|
temporary = manifest_path.with_suffix(".json.tmp")
|
||||||
if manifest_path.exists() or temporary.exists() or (output_dir / "_stroke_pattern_params.json").exists() or any(path.exists() for artifacts in outputs.values()
|
auxiliary = ([output_dir / "_multiview_ground_truth.json",
|
||||||
for path in artifacts.values()):
|
output_dir / "_print_views.json"] if args.views else [])
|
||||||
|
if settings["orientation_mode"] == "optimized":
|
||||||
|
auxiliary.append(output_dir / "stroke_texture_optimization.json")
|
||||||
|
if (manifest_path.exists() or temporary.exists()
|
||||||
|
or (output_dir / "_stroke_pattern_params.json").exists()
|
||||||
|
or any(path.exists() for path in auxiliary)
|
||||||
|
or any(path.exists() for artifacts in outputs.values()
|
||||||
|
for path in artifacts.values())):
|
||||||
print("ERROR: stroke artifacts already exist; choose a fresh --output-dir",
|
print("ERROR: stroke artifacts already exist; choose a fresh --output-dir",
|
||||||
file=sys.stderr)
|
file=sys.stderr)
|
||||||
return 2
|
return 2
|
||||||
params_path = write_stroke_params(settings, output_dir)
|
params_path = write_stroke_params(settings, output_dir)
|
||||||
code = _run_blender_script(
|
script = "random_patch_multiview.py" if args.views else "camera_grid_stroke.py"
|
||||||
"camera_grid_stroke.py", paths.source_blend,
|
script_args = (["--job", str(job_path), "--stroke-params", str(params_path),
|
||||||
|
"--stroke-output-dir", str(output_dir)] if args.views else
|
||||||
["--job", str(job_path), "--output-dir", str(output_dir),
|
["--job", str(job_path), "--output-dir", str(output_dir),
|
||||||
"--artifact-prefix", "stroke_texture", "--params", str(params_path)],
|
"--artifact-prefix", "stroke_texture", "--params", str(params_path)])
|
||||||
)
|
code = _run_blender_script(script, paths.source_blend, script_args)
|
||||||
if code != 0:
|
if code != 0:
|
||||||
return code
|
return code
|
||||||
missing = [path for artifacts in outputs.values() for path in artifacts.values()
|
missing = [path for path in auxiliary if not path.is_file() or path.stat().st_size == 0]
|
||||||
|
missing += [path for artifacts in outputs.values() for path in artifacts.values()
|
||||||
if not path.is_file() or path.stat().st_size == 0]
|
if not path.is_file() or path.stat().st_size == 0]
|
||||||
if missing:
|
if missing:
|
||||||
for path in missing:
|
for path in missing:
|
||||||
@@ -579,6 +588,7 @@ def cmd_stroke_texture(revision_dir: Path, args: argparse.Namespace) -> int:
|
|||||||
manifest = {
|
manifest = {
|
||||||
"schema": "orisys-sensor-design/stroke-texture-manifest-v2",
|
"schema": "orisys-sensor-design/stroke-texture-manifest-v2",
|
||||||
"revision_dir": str(paths.root),
|
"revision_dir": str(paths.root),
|
||||||
|
"views": bool(args.views),
|
||||||
"source_job": {"path": str(job_path), "sha256": sha256_file(job_path)},
|
"source_job": {"path": str(job_path), "sha256": sha256_file(job_path)},
|
||||||
"source_blend": {"path": str(paths.source_blend),
|
"source_blend": {"path": str(paths.source_blend),
|
||||||
"sha256": sha256_file(paths.source_blend)},
|
"sha256": sha256_file(paths.source_blend)},
|
||||||
@@ -596,6 +606,8 @@ def cmd_stroke_texture(revision_dir: Path, args: argparse.Namespace) -> int:
|
|||||||
for fmt in ("png", "svg", "pdf")}}
|
for fmt in ("png", "svg", "pdf")}}
|
||||||
for source_id in source_ids
|
for source_id in source_ids
|
||||||
],
|
],
|
||||||
|
"auxiliary": {path.name: {"path": str(path), "sha256": sha256_file(path)}
|
||||||
|
for path in auxiliary},
|
||||||
}
|
}
|
||||||
temporary.write_text(json.dumps(manifest, indent=2) + "\n", encoding="utf-8")
|
temporary.write_text(json.dumps(manifest, indent=2) + "\n", encoding="utf-8")
|
||||||
temporary.replace(manifest_path)
|
temporary.replace(manifest_path)
|
||||||
@@ -2256,7 +2268,7 @@ def main(argv: list[str] | None = None) -> None:
|
|||||||
stroke_p.add_argument("revision_dir", type=Path)
|
stroke_p.add_argument("revision_dir", type=Path)
|
||||||
stroke_p.add_argument(
|
stroke_p.add_argument(
|
||||||
"--views", action="store_true",
|
"--views", action="store_true",
|
||||||
help="Unsupported for camera-grid strokes; single-view jobs only",
|
help="Use the multiview job and preserve ownership, seam margin and per-view page size",
|
||||||
)
|
)
|
||||||
stroke_p.add_argument(
|
stroke_p.add_argument(
|
||||||
"--source-job", type=Path, default=None,
|
"--source-job", type=Path, default=None,
|
||||||
|
|||||||
@@ -59,7 +59,33 @@ def test_exports_from_geometry_job(tmp_path, monkeypatch):
|
|||||||
assert len(calls) == 1
|
assert len(calls) == 1
|
||||||
|
|
||||||
|
|
||||||
def test_rejects_multiview(tmp_path, monkeypatch):
|
def test_multiview_exports_owned_pairs_without_colour_artwork(tmp_path, monkeypatch):
|
||||||
job = _job(tmp_path)
|
paths = RevisionPaths.from_revision_dir(REVISION)
|
||||||
monkeypatch.setattr(cli, "_run_blender_script", lambda *_: 99)
|
job = tmp_path / "_texture_views_job.json"
|
||||||
assert cli.cmd_stroke_texture(REVISION, _args(job, tmp_path / "out", views=True)) == 2
|
job.write_text(json.dumps({
|
||||||
|
"schema": "orisys-sensor-design/texture-job-multiview-v1",
|
||||||
|
"revision_dir": str(paths.root), "source_blend": str(paths.source_blend),
|
||||||
|
"settings": {"nx": 80, "ny": 80}, "objects": {"mesh": "sensing_surface"},
|
||||||
|
"pairs": [{"id": "uv_1"}, {"id": "uv_2"}],
|
||||||
|
}), encoding="utf-8")
|
||||||
|
output_dir = tmp_path / "strokes"
|
||||||
|
calls = []
|
||||||
|
|
||||||
|
def run_blender(script, blend, args):
|
||||||
|
calls.append((script, blend, args))
|
||||||
|
for pair in ("uv_1", "uv_2"):
|
||||||
|
for fmt in ("png", "svg", "pdf"):
|
||||||
|
(output_dir / f"stroke_texture_{pair}.{fmt}").write_bytes(pair.encode())
|
||||||
|
for name in ("_multiview_ground_truth.json", "_print_views.json"):
|
||||||
|
(output_dir / name).write_text("{}", encoding="utf-8")
|
||||||
|
return 0
|
||||||
|
|
||||||
|
monkeypatch.setattr(cli, "_run_blender_script", run_blender)
|
||||||
|
assert cli.cmd_stroke_texture(REVISION, _args(job, output_dir, views=True)) == 0
|
||||||
|
assert calls[0][0] == "random_patch_multiview.py"
|
||||||
|
assert "--stroke-params" in calls[0][2]
|
||||||
|
manifest = json.loads((output_dir / "stroke_texture_manifest.json").read_text())
|
||||||
|
assert manifest["views"] is True
|
||||||
|
assert [entry["id"] for entry in manifest["outputs"]] == ["uv_1", "uv_2"]
|
||||||
|
assert set(manifest["auxiliary"]) == {"_multiview_ground_truth.json", "_print_views.json"}
|
||||||
|
assert len(calls) == 1
|
||||||
|
|||||||
Reference in New Issue
Block a user