new grayscale output for testing laser, not working, will change to new design
@@ -681,9 +681,21 @@ def _clipped_shapes(shapes, width, height):
|
|||||||
|
|
||||||
def write_svg_vector(
|
def write_svg_vector(
|
||||||
output_path, texture_width, texture_height, width_mm, height_mm,
|
output_path, texture_width, texture_height, width_mm, height_mm,
|
||||||
mesh_polygons, patch_shapes, cross_rects, cross_color, uv_map
|
mesh_polygons, patch_shapes, cross_rects, cross_color, uv_map,
|
||||||
|
background=None,
|
||||||
):
|
):
|
||||||
"""Write editable grouped SVG geometry; no raster data is embedded."""
|
"""Write editable grouped SVG geometry; no raster data is embedded.
|
||||||
|
|
||||||
|
``background`` is the page fill. Left as ``None`` it is the black production ground; the
|
||||||
|
laser-engraving variant passes white, because a laser cuts what is dark and an
|
||||||
|
unengraved sheet must be light.
|
||||||
|
"""
|
||||||
|
if background is None:
|
||||||
|
background_fill = "#000000"
|
||||||
|
else:
|
||||||
|
background_fill = "#" + "".join(
|
||||||
|
f"{round(_clamp_channel(channel) * 255):02x}" for channel in background[:3]
|
||||||
|
)
|
||||||
lines = [
|
lines = [
|
||||||
'<?xml version="1.0" encoding="UTF-8"?>',
|
'<?xml version="1.0" encoding="UTF-8"?>',
|
||||||
(
|
(
|
||||||
@@ -705,7 +717,7 @@ def write_svg_vector(
|
|||||||
(
|
(
|
||||||
f' <rect id="background-fill" x="0" y="0" '
|
f' <rect id="background-fill" x="0" y="0" '
|
||||||
f'width="{texture_width}" height="{texture_height}" '
|
f'width="{texture_width}" height="{texture_height}" '
|
||||||
f'fill="#000000" stroke="none"/>'
|
f'fill="{background_fill}" stroke="none"/>'
|
||||||
),
|
),
|
||||||
" </g>",
|
" </g>",
|
||||||
' <g id="mesh">',
|
' <g id="mesh">',
|
||||||
@@ -785,15 +797,26 @@ def _build_pdf(objects):
|
|||||||
|
|
||||||
def write_pdf_vector(
|
def write_pdf_vector(
|
||||||
output_path, texture_width, texture_height, width_mm, height_mm,
|
output_path, texture_width, texture_height, width_mm, height_mm,
|
||||||
mesh_polygons, patch_shapes, cross_rects, cross_color
|
mesh_polygons, patch_shapes, cross_rects, cross_color,
|
||||||
|
background=None,
|
||||||
):
|
):
|
||||||
"""Write native vector PDF paths at the configured physical page size."""
|
"""Write native vector PDF paths at the configured physical page size.
|
||||||
|
|
||||||
|
``background`` as for `write_svg_vector`. The default emits the original literal command
|
||||||
|
so existing documents stay byte-identical.
|
||||||
|
"""
|
||||||
page_width_pt = float(width_mm) * 72.0 / 25.4
|
page_width_pt = float(width_mm) * 72.0 / 25.4
|
||||||
page_height_pt = float(height_mm) * 72.0 / 25.4
|
page_height_pt = float(height_mm) * 72.0 / 25.4
|
||||||
scale_x = page_width_pt / float(texture_width)
|
scale_x = page_width_pt / float(texture_width)
|
||||||
scale_y = page_height_pt / float(texture_height)
|
scale_y = page_height_pt / float(texture_height)
|
||||||
|
|
||||||
content = ["q", "% background", "0 0 0 rg"]
|
if background is None:
|
||||||
|
background_command = "0 0 0 rg"
|
||||||
|
else:
|
||||||
|
background_command = _pdf_set_fill(
|
||||||
|
(background[0], background[1], background[2], 1.0)
|
||||||
|
)
|
||||||
|
content = ["q", "% background", background_command]
|
||||||
content.append(
|
content.append(
|
||||||
f"0 0 {_format_number(page_width_pt)} {_format_number(page_height_pt)} re f"
|
f"0 0 {_format_number(page_width_pt)} {_format_number(page_height_pt)} re f"
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -46,9 +46,13 @@ from random_patch_dd02c import ( # noqa: E402
|
|||||||
_require_uv_layer,
|
_require_uv_layer,
|
||||||
_uv_centre,
|
_uv_centre,
|
||||||
build_patch_geometry,
|
build_patch_geometry,
|
||||||
|
fill_polygon_value,
|
||||||
generate_patch_texture,
|
generate_patch_texture,
|
||||||
precompute_cell_colors,
|
precompute_cell_colors,
|
||||||
|
write_pdf_vector,
|
||||||
|
write_svg_vector,
|
||||||
)
|
)
|
||||||
|
from patch_geometry import clip_rect # noqa: E402
|
||||||
from texture_merge import ( # noqa: E402
|
from texture_merge import ( # noqa: E402
|
||||||
MergeConfigError,
|
MergeConfigError,
|
||||||
MergePolicy,
|
MergePolicy,
|
||||||
@@ -183,8 +187,12 @@ def _resolve_pairs(job: dict[str, Any]) -> list[dict[str, Any]]:
|
|||||||
if not isinstance(artifacts, dict):
|
if not isinstance(artifacts, dict):
|
||||||
raise ConfigError(f"{where}.artifacts: must be a mapping")
|
raise ConfigError(f"{where}.artifacts: must be a mapping")
|
||||||
paths: dict[str, str] = {}
|
paths: dict[str, str] = {}
|
||||||
for format_name in ARTIFACT_FORMATS:
|
# The three print formats are required. The `gray_*` laser-engraving set is optional,
|
||||||
|
# so it is validated only when the job asks for it.
|
||||||
|
for format_name in (*ARTIFACT_FORMATS, *(f"gray_{f}" for f in ARTIFACT_FORMATS)):
|
||||||
value = artifacts.get(format_name)
|
value = artifacts.get(format_name)
|
||||||
|
if value is None and format_name.startswith("gray_"):
|
||||||
|
continue
|
||||||
if not isinstance(value, str) or not value.strip():
|
if not isinstance(value, str) or not value.strip():
|
||||||
raise ConfigError(
|
raise ConfigError(
|
||||||
f"{where}.artifacts.{format_name}: must be a non-empty path"
|
f"{where}.artifacts.{format_name}: must be a non-empty path"
|
||||||
@@ -207,6 +215,9 @@ def _resolve_pairs(job: dict[str, Any]) -> list[dict[str, Any]]:
|
|||||||
"pattern_camera": pattern_camera,
|
"pattern_camera": pattern_camera,
|
||||||
"uv_map": uv_map,
|
"uv_map": uv_map,
|
||||||
"seed": None if seed is None else int(seed),
|
"seed": None if seed is None else int(seed),
|
||||||
|
# Carried through, not defaulted: dropping this silently ignored every
|
||||||
|
# per-pair `page:` override because the global default masked it.
|
||||||
|
"page": entry.get("page"),
|
||||||
"artifacts": paths,
|
"artifacts": paths,
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
@@ -537,6 +548,100 @@ def _triangle_seam_edges(
|
|||||||
return out
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _gray_pixel_polygon(uv_polygon, width, height):
|
||||||
|
"""The colour path's UV -> pixel mapping, reproduced exactly.
|
||||||
|
|
||||||
|
`generate_patch_texture` builds this as a nested closure over `texture_width`/`height`, so
|
||||||
|
it cannot be imported. Duplicating these two lines is the cost of keeping the gray image
|
||||||
|
registered with the colour image: **if the colour mapping ever changes, this must too.**
|
||||||
|
"""
|
||||||
|
return [
|
||||||
|
(u * (width - 1), v * (height - 1))
|
||||||
|
for u, v in clip_rect(uv_polygon, 0.0, 1.0, 0.0, 1.0)
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _gray_shapes(patches, patch_colors, width, height):
|
||||||
|
"""The laser-engraving artwork as (pixel_polygon, rgba) shapes.
|
||||||
|
|
||||||
|
**Polarity is inverted relative to the print texture, deliberately.** Most laser software
|
||||||
|
engraves what is DARK, and the print texture's background is black -- so a straight
|
||||||
|
luminance conversion would engrave the entire sheet and leave the marker cells almost
|
||||||
|
untouched. Here the page and the mesh ground are white (untouched) and only the pattern
|
||||||
|
cells carry tone, so the laser cuts the marker and nothing else.
|
||||||
|
|
||||||
|
Tone is Rec.709 luminance of the colour the cell would have been printed in, so engraving
|
||||||
|
depth varies cell to cell the way printed colour does. The seam margin is inherited from
|
||||||
|
``patches``, which is the already-clipped emitted set, so every gray format carries the
|
||||||
|
same alignment band as the colour file.
|
||||||
|
"""
|
||||||
|
shapes = []
|
||||||
|
for i, j, uv_poly in patches:
|
||||||
|
pixel_poly = _gray_pixel_polygon(uv_poly, width, height)
|
||||||
|
if len(pixel_poly) < 3:
|
||||||
|
continue
|
||||||
|
red, green, blue = patch_colors[j, i]
|
||||||
|
tone = 0.2126 * float(red) + 0.7152 * float(green) + 0.0722 * float(blue)
|
||||||
|
shapes.append((pixel_poly, (tone, tone, tone, 1.0)))
|
||||||
|
return shapes
|
||||||
|
|
||||||
|
|
||||||
|
def _write_grayscale_png(path, width, height, shapes):
|
||||||
|
"""Rasterise the laser-engraving artwork: pattern dark on white."""
|
||||||
|
grid = np.ones((height, width), dtype=np.float32)
|
||||||
|
for pixel_poly, (tone, _g, _b, _a) in shapes:
|
||||||
|
fill_polygon_value(grid, width, height, pixel_poly, tone)
|
||||||
|
|
||||||
|
rgba = np.ones((height, width, 4), dtype=np.float32)
|
||||||
|
rgba[:, :, 0] = grid
|
||||||
|
rgba[:, :, 1] = grid
|
||||||
|
rgba[:, :, 2] = grid
|
||||||
|
|
||||||
|
# Same row convention as the colour path: fill_polygon_value indexes grid[y, x] and the
|
||||||
|
# colour writer indexes pixels[(y * width + x) * 4], so the two images register exactly.
|
||||||
|
out = Path(path)
|
||||||
|
out.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
name = "LaserGray"
|
||||||
|
if name in bpy.data.images:
|
||||||
|
bpy.data.images.remove(bpy.data.images[name])
|
||||||
|
image = bpy.data.images.new(name, width=width, height=height, alpha=True)
|
||||||
|
try:
|
||||||
|
image.pixels.foreach_set(rgba.reshape(-1))
|
||||||
|
image.update()
|
||||||
|
image.filepath_raw = str(out)
|
||||||
|
image.file_format = "PNG"
|
||||||
|
image.save()
|
||||||
|
finally:
|
||||||
|
bpy.data.images.remove(image)
|
||||||
|
|
||||||
|
|
||||||
|
def _write_grayscale_vector(
|
||||||
|
svg_path, pdf_path, width, height, page_width_mm, page_height_mm, shapes, uv_name
|
||||||
|
):
|
||||||
|
"""Write the laser-engraving artwork as vector geometry.
|
||||||
|
|
||||||
|
White page, grey pattern cells, no mesh layer and no reference cross -- the same artwork
|
||||||
|
`_write_grayscale_png` rasterises, so the three formats cannot disagree about what is cut.
|
||||||
|
|
||||||
|
The mesh ground is omitted rather than drawn white: the page background is already white,
|
||||||
|
so emitting thousands of coincident white polygons would bloat the file for nothing.
|
||||||
|
"""
|
||||||
|
# White because a laser cuts what is dark and an unengraved sheet must be light; see
|
||||||
|
# `_gray_shapes` for why the polarity is inverted relative to the print texture.
|
||||||
|
white = (1.0, 1.0, 1.0)
|
||||||
|
no_cross: list = []
|
||||||
|
if svg_path:
|
||||||
|
write_svg_vector(
|
||||||
|
svg_path, width, height, page_width_mm, page_height_mm,
|
||||||
|
[], shapes, no_cross, (1.0, 0.0, 0.0, 1.0), uv_name, background=white,
|
||||||
|
)
|
||||||
|
if pdf_path:
|
||||||
|
write_pdf_vector(
|
||||||
|
pdf_path, width, height, page_width_mm, page_height_mm,
|
||||||
|
[], shapes, no_cross, (1.0, 0.0, 0.0, 1.0), background=white,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def _derive_pair_page(obj, camera, uv_name, depsgraph, px_per_mm):
|
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.
|
"""Derive and verify one pair's page from the geometry its layout projects.
|
||||||
|
|
||||||
@@ -1018,6 +1123,39 @@ def main() -> int:
|
|||||||
)
|
)
|
||||||
return 1
|
return 1
|
||||||
|
|
||||||
|
# The laser-engraving set is written before the artifact check below, so it is verified
|
||||||
|
# by the same existence test as the print set rather than trusted.
|
||||||
|
gray_paths = {
|
||||||
|
name: pair["artifacts"].get(f"gray_{name}") for name in ARTIFACT_FORMATS
|
||||||
|
}
|
||||||
|
if any(gray_paths.values()):
|
||||||
|
try:
|
||||||
|
gray_shapes = _gray_shapes(
|
||||||
|
emitted_patches, patch_colors, texture_width, texture_height
|
||||||
|
)
|
||||||
|
if gray_paths["png"]:
|
||||||
|
_write_grayscale_png(
|
||||||
|
gray_paths["png"], texture_width, texture_height, gray_shapes
|
||||||
|
)
|
||||||
|
_write_grayscale_vector(
|
||||||
|
gray_paths["svg"], gray_paths["pdf"],
|
||||||
|
texture_width, texture_height, page_width_mm, page_height_mm,
|
||||||
|
gray_shapes, uv_map,
|
||||||
|
)
|
||||||
|
except Exception as exc: # noqa: BLE001
|
||||||
|
print(
|
||||||
|
f"ERROR: laser-engraving texture failed for view {pair_id!r}: {exc}",
|
||||||
|
file=sys.stderr,
|
||||||
|
)
|
||||||
|
return 1
|
||||||
|
detail(
|
||||||
|
f"[{pair_id}] laser gray {texture_width}x{texture_height} px, "
|
||||||
|
f"{len(gray_shapes)} cells -> "
|
||||||
|
+ ", ".join(
|
||||||
|
Path(path).name for path in gray_paths.values() if path
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
for format_name, output in pair["artifacts"].items():
|
for format_name, output in pair["artifacts"].items():
|
||||||
path = Path(output)
|
path = Path(output)
|
||||||
if not path.is_file() or path.stat().st_size == 0:
|
if not path.is_file() or path.stat().st_size == 0:
|
||||||
|
|||||||
@@ -184,7 +184,10 @@
|
|||||||
"artifacts": {
|
"artifacts": {
|
||||||
"png": "D:\\orisys_ws\\orisys-sensor-design\\build\\textures\\finger-xynova\\r01\\patch_texture_uv_1.png",
|
"png": "D:\\orisys_ws\\orisys-sensor-design\\build\\textures\\finger-xynova\\r01\\patch_texture_uv_1.png",
|
||||||
"svg": "D:\\orisys_ws\\orisys-sensor-design\\build\\textures\\finger-xynova\\r01\\patch_texture_uv_1.svg",
|
"svg": "D:\\orisys_ws\\orisys-sensor-design\\build\\textures\\finger-xynova\\r01\\patch_texture_uv_1.svg",
|
||||||
"pdf": "D:\\orisys_ws\\orisys-sensor-design\\build\\textures\\finger-xynova\\r01\\patch_texture_uv_1.pdf"
|
"pdf": "D:\\orisys_ws\\orisys-sensor-design\\build\\textures\\finger-xynova\\r01\\patch_texture_uv_1.pdf",
|
||||||
|
"gray_png": "D:\\orisys_ws\\orisys-sensor-design\\build\\textures\\finger-xynova\\r01\\patch_texture_uv_1_gray.png",
|
||||||
|
"gray_svg": "D:\\orisys_ws\\orisys-sensor-design\\build\\textures\\finger-xynova\\r01\\patch_texture_uv_1_gray.svg",
|
||||||
|
"gray_pdf": "D:\\orisys_ws\\orisys-sensor-design\\build\\textures\\finger-xynova\\r01\\patch_texture_uv_1_gray.pdf"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -306,7 +309,10 @@
|
|||||||
"artifacts": {
|
"artifacts": {
|
||||||
"png": "D:\\orisys_ws\\orisys-sensor-design\\build\\textures\\finger-xynova\\r01\\patch_texture_uv_2.png",
|
"png": "D:\\orisys_ws\\orisys-sensor-design\\build\\textures\\finger-xynova\\r01\\patch_texture_uv_2.png",
|
||||||
"svg": "D:\\orisys_ws\\orisys-sensor-design\\build\\textures\\finger-xynova\\r01\\patch_texture_uv_2.svg",
|
"svg": "D:\\orisys_ws\\orisys-sensor-design\\build\\textures\\finger-xynova\\r01\\patch_texture_uv_2.svg",
|
||||||
"pdf": "D:\\orisys_ws\\orisys-sensor-design\\build\\textures\\finger-xynova\\r01\\patch_texture_uv_2.pdf"
|
"pdf": "D:\\orisys_ws\\orisys-sensor-design\\build\\textures\\finger-xynova\\r01\\patch_texture_uv_2.pdf",
|
||||||
|
"gray_png": "D:\\orisys_ws\\orisys-sensor-design\\build\\textures\\finger-xynova\\r01\\patch_texture_uv_2_gray.png",
|
||||||
|
"gray_svg": "D:\\orisys_ws\\orisys-sensor-design\\build\\textures\\finger-xynova\\r01\\patch_texture_uv_2_gray.svg",
|
||||||
|
"gray_pdf": "D:\\orisys_ws\\orisys-sensor-design\\build\\textures\\finger-xynova\\r01\\patch_texture_uv_2_gray.pdf"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"schema": "orisys-sensor-design/texture-job-multiview-v1",
|
"schema": "orisys-sensor-design/texture-job-multiview-v1",
|
||||||
"mode": "patches-multiview",
|
"mode": "patches-multiview",
|
||||||
|
"grayscale": true,
|
||||||
"revision_dir": "D:\\orisys_ws\\orisys-sensor-design\\sensors\\finger-xynova\\r01",
|
"revision_dir": "D:\\orisys_ws\\orisys-sensor-design\\sensors\\finger-xynova\\r01",
|
||||||
"source_blend": "D:\\orisys_ws\\orisys-sensor-design\\sensors\\finger-xynova\\r01\\source\\geometry.blend",
|
"source_blend": "D:\\orisys_ws\\orisys-sensor-design\\sensors\\finger-xynova\\r01\\source\\geometry.blend",
|
||||||
"objects": {
|
"objects": {
|
||||||
@@ -70,7 +71,10 @@
|
|||||||
"artifacts": {
|
"artifacts": {
|
||||||
"png": "D:\\orisys_ws\\orisys-sensor-design\\build\\textures\\finger-xynova\\r01\\patch_texture_uv_1.png",
|
"png": "D:\\orisys_ws\\orisys-sensor-design\\build\\textures\\finger-xynova\\r01\\patch_texture_uv_1.png",
|
||||||
"svg": "D:\\orisys_ws\\orisys-sensor-design\\build\\textures\\finger-xynova\\r01\\patch_texture_uv_1.svg",
|
"svg": "D:\\orisys_ws\\orisys-sensor-design\\build\\textures\\finger-xynova\\r01\\patch_texture_uv_1.svg",
|
||||||
"pdf": "D:\\orisys_ws\\orisys-sensor-design\\build\\textures\\finger-xynova\\r01\\patch_texture_uv_1.pdf"
|
"pdf": "D:\\orisys_ws\\orisys-sensor-design\\build\\textures\\finger-xynova\\r01\\patch_texture_uv_1.pdf",
|
||||||
|
"gray_png": "D:\\orisys_ws\\orisys-sensor-design\\build\\textures\\finger-xynova\\r01\\patch_texture_uv_1_gray.png",
|
||||||
|
"gray_svg": "D:\\orisys_ws\\orisys-sensor-design\\build\\textures\\finger-xynova\\r01\\patch_texture_uv_1_gray.svg",
|
||||||
|
"gray_pdf": "D:\\orisys_ws\\orisys-sensor-design\\build\\textures\\finger-xynova\\r01\\patch_texture_uv_1_gray.pdf"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -83,7 +87,10 @@
|
|||||||
"artifacts": {
|
"artifacts": {
|
||||||
"png": "D:\\orisys_ws\\orisys-sensor-design\\build\\textures\\finger-xynova\\r01\\patch_texture_uv_2.png",
|
"png": "D:\\orisys_ws\\orisys-sensor-design\\build\\textures\\finger-xynova\\r01\\patch_texture_uv_2.png",
|
||||||
"svg": "D:\\orisys_ws\\orisys-sensor-design\\build\\textures\\finger-xynova\\r01\\patch_texture_uv_2.svg",
|
"svg": "D:\\orisys_ws\\orisys-sensor-design\\build\\textures\\finger-xynova\\r01\\patch_texture_uv_2.svg",
|
||||||
"pdf": "D:\\orisys_ws\\orisys-sensor-design\\build\\textures\\finger-xynova\\r01\\patch_texture_uv_2.pdf"
|
"pdf": "D:\\orisys_ws\\orisys-sensor-design\\build\\textures\\finger-xynova\\r01\\patch_texture_uv_2.pdf",
|
||||||
|
"gray_png": "D:\\orisys_ws\\orisys-sensor-design\\build\\textures\\finger-xynova\\r01\\patch_texture_uv_2_gray.png",
|
||||||
|
"gray_svg": "D:\\orisys_ws\\orisys-sensor-design\\build\\textures\\finger-xynova\\r01\\patch_texture_uv_2_gray.svg",
|
||||||
|
"gray_pdf": "D:\\orisys_ws\\orisys-sensor-design\\build\\textures\\finger-xynova\\r01\\patch_texture_uv_2_gray.pdf"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
|
After Width: | Height: | Size: 781 KiB |
|
After Width: | Height: | Size: 2.0 MiB |
|
After Width: | Height: | Size: 805 KiB |
|
After Width: | Height: | Size: 2.1 MiB |
@@ -184,7 +184,10 @@
|
|||||||
"artifacts": {
|
"artifacts": {
|
||||||
"png": "D:\\orisys_ws\\orisys-sensor-design\\build\\textures\\finger\\r01\\patch_texture_uv_1.png",
|
"png": "D:\\orisys_ws\\orisys-sensor-design\\build\\textures\\finger\\r01\\patch_texture_uv_1.png",
|
||||||
"svg": "D:\\orisys_ws\\orisys-sensor-design\\build\\textures\\finger\\r01\\patch_texture_uv_1.svg",
|
"svg": "D:\\orisys_ws\\orisys-sensor-design\\build\\textures\\finger\\r01\\patch_texture_uv_1.svg",
|
||||||
"pdf": "D:\\orisys_ws\\orisys-sensor-design\\build\\textures\\finger\\r01\\patch_texture_uv_1.pdf"
|
"pdf": "D:\\orisys_ws\\orisys-sensor-design\\build\\textures\\finger\\r01\\patch_texture_uv_1.pdf",
|
||||||
|
"gray_png": "D:\\orisys_ws\\orisys-sensor-design\\build\\textures\\finger\\r01\\patch_texture_uv_1_gray.png",
|
||||||
|
"gray_svg": "D:\\orisys_ws\\orisys-sensor-design\\build\\textures\\finger\\r01\\patch_texture_uv_1_gray.svg",
|
||||||
|
"gray_pdf": "D:\\orisys_ws\\orisys-sensor-design\\build\\textures\\finger\\r01\\patch_texture_uv_1_gray.pdf"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -306,7 +309,10 @@
|
|||||||
"artifacts": {
|
"artifacts": {
|
||||||
"png": "D:\\orisys_ws\\orisys-sensor-design\\build\\textures\\finger\\r01\\patch_texture_uv_2.png",
|
"png": "D:\\orisys_ws\\orisys-sensor-design\\build\\textures\\finger\\r01\\patch_texture_uv_2.png",
|
||||||
"svg": "D:\\orisys_ws\\orisys-sensor-design\\build\\textures\\finger\\r01\\patch_texture_uv_2.svg",
|
"svg": "D:\\orisys_ws\\orisys-sensor-design\\build\\textures\\finger\\r01\\patch_texture_uv_2.svg",
|
||||||
"pdf": "D:\\orisys_ws\\orisys-sensor-design\\build\\textures\\finger\\r01\\patch_texture_uv_2.pdf"
|
"pdf": "D:\\orisys_ws\\orisys-sensor-design\\build\\textures\\finger\\r01\\patch_texture_uv_2.pdf",
|
||||||
|
"gray_png": "D:\\orisys_ws\\orisys-sensor-design\\build\\textures\\finger\\r01\\patch_texture_uv_2_gray.png",
|
||||||
|
"gray_svg": "D:\\orisys_ws\\orisys-sensor-design\\build\\textures\\finger\\r01\\patch_texture_uv_2_gray.svg",
|
||||||
|
"gray_pdf": "D:\\orisys_ws\\orisys-sensor-design\\build\\textures\\finger\\r01\\patch_texture_uv_2_gray.pdf"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
{
|
{
|
||||||
"schema": "orisys-sensor-design/texture-job-multiview-v1",
|
"schema": "orisys-sensor-design/texture-job-multiview-v1",
|
||||||
"mode": "patches-multiview",
|
"mode": "patches-multiview",
|
||||||
|
"grayscale": true,
|
||||||
"revision_dir": "D:\\orisys_ws\\orisys-sensor-design\\sensors\\finger\\r01",
|
"revision_dir": "D:\\orisys_ws\\orisys-sensor-design\\sensors\\finger\\r01",
|
||||||
"source_blend": "D:\\orisys_ws\\orisys-sensor-design\\sensors\\finger\\r01\\source\\geometry.blend",
|
"source_blend": "D:\\orisys_ws\\orisys-sensor-design\\sensors\\finger\\r01\\source\\geometry.blend",
|
||||||
"objects": {
|
"objects": {
|
||||||
@@ -70,7 +71,10 @@
|
|||||||
"artifacts": {
|
"artifacts": {
|
||||||
"png": "D:\\orisys_ws\\orisys-sensor-design\\build\\textures\\finger\\r01\\patch_texture_uv_1.png",
|
"png": "D:\\orisys_ws\\orisys-sensor-design\\build\\textures\\finger\\r01\\patch_texture_uv_1.png",
|
||||||
"svg": "D:\\orisys_ws\\orisys-sensor-design\\build\\textures\\finger\\r01\\patch_texture_uv_1.svg",
|
"svg": "D:\\orisys_ws\\orisys-sensor-design\\build\\textures\\finger\\r01\\patch_texture_uv_1.svg",
|
||||||
"pdf": "D:\\orisys_ws\\orisys-sensor-design\\build\\textures\\finger\\r01\\patch_texture_uv_1.pdf"
|
"pdf": "D:\\orisys_ws\\orisys-sensor-design\\build\\textures\\finger\\r01\\patch_texture_uv_1.pdf",
|
||||||
|
"gray_png": "D:\\orisys_ws\\orisys-sensor-design\\build\\textures\\finger\\r01\\patch_texture_uv_1_gray.png",
|
||||||
|
"gray_svg": "D:\\orisys_ws\\orisys-sensor-design\\build\\textures\\finger\\r01\\patch_texture_uv_1_gray.svg",
|
||||||
|
"gray_pdf": "D:\\orisys_ws\\orisys-sensor-design\\build\\textures\\finger\\r01\\patch_texture_uv_1_gray.pdf"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -83,7 +87,10 @@
|
|||||||
"artifacts": {
|
"artifacts": {
|
||||||
"png": "D:\\orisys_ws\\orisys-sensor-design\\build\\textures\\finger\\r01\\patch_texture_uv_2.png",
|
"png": "D:\\orisys_ws\\orisys-sensor-design\\build\\textures\\finger\\r01\\patch_texture_uv_2.png",
|
||||||
"svg": "D:\\orisys_ws\\orisys-sensor-design\\build\\textures\\finger\\r01\\patch_texture_uv_2.svg",
|
"svg": "D:\\orisys_ws\\orisys-sensor-design\\build\\textures\\finger\\r01\\patch_texture_uv_2.svg",
|
||||||
"pdf": "D:\\orisys_ws\\orisys-sensor-design\\build\\textures\\finger\\r01\\patch_texture_uv_2.pdf"
|
"pdf": "D:\\orisys_ws\\orisys-sensor-design\\build\\textures\\finger\\r01\\patch_texture_uv_2.pdf",
|
||||||
|
"gray_png": "D:\\orisys_ws\\orisys-sensor-design\\build\\textures\\finger\\r01\\patch_texture_uv_2_gray.png",
|
||||||
|
"gray_svg": "D:\\orisys_ws\\orisys-sensor-design\\build\\textures\\finger\\r01\\patch_texture_uv_2_gray.svg",
|
||||||
|
"gray_pdf": "D:\\orisys_ws\\orisys-sensor-design\\build\\textures\\finger\\r01\\patch_texture_uv_2_gray.pdf"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
|
After Width: | Height: | Size: 469 KiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 491 KiB |
|
After Width: | Height: | Size: 1.3 MiB |
@@ -0,0 +1,12 @@
|
|||||||
|
# orisys-sensor-design documentation
|
||||||
|
|
||||||
|
| document | answers | status |
|
||||||
|
|---|---|---|
|
||||||
|
| [design-parameters.md](design-parameters.md) | What does each `design.yaml` key do, what were the measured values, and what invalidates them? | current |
|
||||||
|
| [probe-pipeline.md](probe-pipeline.md) | How does the probe sensitivity bench work, and what is its contract? | current |
|
||||||
|
| [probe-method.md](probe-method.md) | Why is the bench measured this way? Contains explicit retractions. | current |
|
||||||
|
| [probe-findings.md](probe-findings.md) | What did the probe bench find? | current |
|
||||||
|
| [xynova-texture-investigation.md](xynova-texture-investigation.md) | How the xynova texture question was investigated. | current |
|
||||||
|
| [code-review.md](code-review.md) | Code review notes and process. | current |
|
||||||
|
|
||||||
|
The workflow-level index lives outside this repository at `D:\orisys_ws\docs\README.md`.
|
||||||
@@ -0,0 +1,449 @@
|
|||||||
|
# `design.yaml` 参数说明
|
||||||
|
|
||||||
|
本文是 `sensors/<sensor>/r01/design.yaml` 的详细说明,主要面向传感器研发工程师。
|
||||||
|
|
||||||
|
|
||||||
|
本文重点解释两个命令:
|
||||||
|
|
||||||
|
- `texture_patch`:在普通 UV 展开图上生成一整张标记图案;
|
||||||
|
- `texture_patch_views`:把同一套图案拆成多个相机投影视图,并决定每个视图负责印刷哪一部分。
|
||||||
|
|
||||||
|
例子:`orisys-design texture-views .\sensors\finger\r01 --grayscale`
|
||||||
|
|
||||||
|
## 1. 总览
|
||||||
|
|
||||||
|
传感器设计工具依赖开源3维绘图软件blender以及自带的Python API 开发,需要传感器3维CAD模型、镜头FOV参数、cmos的尺寸。目前主要用于摄像头位置和标记点的设计,后续会加入光源优化。
|
||||||
|
|
||||||
|
摄像头位置的设计主要是确保在镜头FOV限制下,可以完全覆盖传感的表面,分为感知面积最大化和死区清除两部分。
|
||||||
|
|
||||||
|
标记点设计目的是:
|
||||||
|
* 算法上要导出unwarp conformal的平面图,把3维变形映射到2维计算同时降低畸变。
|
||||||
|
* 生产上要在不可展曲面上印刷/雕刻标记点,程序需要导出曲面在加工平面上投影的平面图,格式包括 PNG、SVG 或 PDF。
|
||||||
|
|
||||||
|
导出的图片类型一览:
|
||||||
|
```text
|
||||||
|
Blender 三维传感表面 (sensing surface)
|
||||||
|
├─ `export` conformal UV 展开 → 计算用
|
||||||
|
│
|
||||||
|
├─ `texture` 单相机投影(projected from view) UV 展开 → 生产用,一张图覆盖整个表面
|
||||||
|
│
|
||||||
|
└─ `texture-views` 多个相机投影视图 + 分割 → 生产用,每张图只印自己负责的区域
|
||||||
|
```
|
||||||
|
设计参数放置在`sensors/<sensor>/r01/design.yaml`里面,具体设计资源参考现有传感器的文件树。
|
||||||
|
|
||||||
|
### 1.1 基础原理
|
||||||
|
|
||||||
|
叠动视触觉传感器基于单色光技术,标记图案要具备高密度纹理信息,用于光流追踪,目前采用随机彩色方格矩阵纹理设计和UV打印生产,对于平面传感器可以简单密铺,对于Finger曲面传感器需要考虑曲面畸变、镜头透视误差、可视区域、可生产性等等,要在3维层面进行设计,与三维绘图的呈现方式类似。考虑到代码兼容性、价格、功能等因素,采用开源的三维绘图软件Blender和配套的python进行程序开发,可以从相机、模型、图案、光源4个部分独立和耦合设计。
|
||||||
|
|
||||||
|
设计流程是:1. 结构传感器曲面设计;2.模型导入Blender摄像头位置摆放;3.曲面不可视区域去除;4.结构曲面修改;5.模型导入Blender和标记图案设计;6.生产摄像头位置摆放和图案生产文件导出。
|
||||||
|
|
||||||
|
曲面结构除了标品外,要根据客户灵巧手可释放的空间来设计,但一般有一个非触觉版本的模型,可以作为基准来修改,考虑塑料壳、硅胶层的厚度,得出一个初步的感知曲面形状和位置。然后导入Blender进入详细设计,在结构和摄像头FOV的约束下,选择合适的摄像头摆放位置来达到可视区域的最大化,再根据摄像头FOV去除曲面的不可视区域,然后反馈结构进行修改。最后根据修改后的摄像头位置、曲面结构,生成计算用和生产用标记点图案。
|
||||||
|
|
||||||
|
计算用标记点图案作用是把在三维空间的曲面投影到二维的平面上,方便通过过去用于平面的算法计算,投影方法目前采用conformal投影,未来可以尝试其他方法。三维到二维的投影通过Blender里面在实际摄像头位置的透视投影相机,和感知曲面的模型获取,并通过`export`指令导出图案和投影矩阵。生产用图案是通过Blender的正交投影相机模拟在传感器治具正上方,由加工设备(UV打印机或激光雕刻机)看到的形状/投影,从而把我们在曲面上的目标图案,从三维空间投影到生产的二维平面上,支持多角度生产和重复部分去除功能。
|
||||||
|
|
||||||
|
通过Bleder Python API,在具备视触觉传感器结构和材料特性下可以进行物理准确的仿真,是本设计和模拟工具的核心支撑。
|
||||||
|
|
||||||
|
|
||||||
|
### 1.2 Blender 使用
|
||||||
|
以下文档基于Blender 5.0 版本 (5.0.1)编写,如非必要不进行版本更新。
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
Blender的资源树在右上方,不同集合(Collection)分别代表不同的资源:
|
||||||
|
|
||||||
|
| 集合 | 种类 | 作用 | 导出 |
|
||||||
|
| --- | --- | --- |--- |
|
||||||
|
| ASSEMBLY| mesh、摄像头 | 外壳模型、生产视角摄像头 |所有网格物体,cam_uv_**x** |
|
||||||
|
| EXCLUDE | 非导出物体 | 文件暂存 |无|
|
||||||
|
| EXPORT_COMPUTE| mesh | 感知曲面 |sensing_surface|
|
||||||
|
| FRAMES| empty | 原点定义 |origin_sensor |
|
||||||
|
| DESIGN_SIM| 摄像头、光源 | 视场设计仿真 |cam_sensor |
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
---
|
||||||
|
# 下方文档是AI生成,待整理
|
||||||
|
---
|
||||||
|
|
||||||
|
### 1.1 初学者需要知道的几个词
|
||||||
|
|
||||||
|
|术语|简单解释|
|
||||||
|
|---|---|
|
||||||
|
|UV|把三维表面的点映射到二维平面的坐标。`U`、`V` 类似图像中的 `x`、`y`|
|
||||||
|
|UV 展开(unwrap)|像展开纸盒一样,把弯曲表面摊到二维平面;允许人为安排接缝和布局|
|
||||||
|
|Project from View (Bounds)|从指定相机看向物体,把画面投影到 UV,并缩放到 `[0,1]²`|
|
||||||
|
|pattern camera|定义标记图案从哪个方向投到表面,即“图案来源”|
|
||||||
|
|region camera|判断一个区域由哪个印刷视图负责,即“区域分工”|
|
||||||
|
|ownership|某个三角形最终归哪一个视图打印|
|
||||||
|
|hole|没有任何视图负责的表面区域,会形成漏印或“秃斑”|
|
||||||
|
|seam|Blender 中标记的接缝,用于把表面分区|
|
||||||
|
|page|输出 SVG/PDF 对应的实际物理尺寸,单位为毫米|
|
||||||
|
|raster|PNG 的像素尺寸,例如 `3700 × 3280 px`|
|
||||||
|
|
||||||
|
### 1.2 阅读参数时遵守三条规则
|
||||||
|
|
||||||
|
1. **每一个测量值都必须说明失效条件。** 例如相机移动后,以旧相机位置测出的覆盖率就不能
|
||||||
|
继续使用。
|
||||||
|
2. **测量条件是结果的一部分。** 涉及区域相机时,必须同时记录相机位姿,因为位姿就是算法
|
||||||
|
输入。
|
||||||
|
3. **文件哈希只代表测量当时的文件。** `geometry.blend` 会持续修改。引用旧哈希前必须重新
|
||||||
|
计算,不能把历史哈希当成当前状态。
|
||||||
|
|
||||||
|
本文最重要的目的,是防止一个过期数值继续显得“很合理”。只把说明写在 YAML 注释里,没有
|
||||||
|
自动失败路径;参数失效后程序仍可能正常运行,却生成错误结果。因此本文会明确写出每个关键
|
||||||
|
数值的来源、失效条件和重测方法。
|
||||||
|
|
||||||
|
## 2. 两种纹理命令的区别
|
||||||
|
|
||||||
|
|项目|`texture`|`texture-views`|
|
||||||
|
|---|---|---|
|
||||||
|
|YAML 配置块|`texture_patch`|`texture_patch_views`|
|
||||||
|
|输出定义|`uv_outputs`:名称到文件名的映射|`pairs[]`:视图 ID、相机、UV map|
|
||||||
|
|二维布局|普通 UV 展开,不是相机投影|由相机拟合到边界的投影视图|
|
||||||
|
|物理页面|`fixed`:读取 `texture_patch.vector` 中声明的毫米尺寸|`auto`:从相机图像平面推导,并验证它确实是投影视图|
|
||||||
|
|区域归属|不需要,一张图覆盖整个表面|必须,每个 pair 都包含完整表面,但只打印归自己负责的部分|
|
||||||
|
|任务格式|`texture-job-v3`|`texture-job-multiview-v1`|
|
||||||
|
|Blender 驱动|`blender/scripts/random_patch_dd02c.py`|`blender/scripts/random_patch_multiview.py`|
|
||||||
|
|
||||||
|
`texture_patch_views` 会从 `texture_patch` 继承以下“图案本身”的设置:
|
||||||
|
|
||||||
|
- 栅格尺寸 `width`、`height`;
|
||||||
|
- 网格数量 `nx`、`ny`;
|
||||||
|
- 随机种子 `seed`;
|
||||||
|
- 固定页面备用值 `vector`;
|
||||||
|
- 颜色配置;
|
||||||
|
- `edge_sample_grid`。
|
||||||
|
|
||||||
|
代码入口是 `texture_views.resolve_views_settings(...)`,内部调用
|
||||||
|
`resolve_texture_settings(...)`。它不会继承 `uv_outputs`、`merge`、`seam` 或 `print`,
|
||||||
|
因为这些字段分别属于单图输出或多视图分区。
|
||||||
|
|
||||||
|
继承方向只有一个:`texture_patch` 是页面、颜色和图案随机性的基础配置;
|
||||||
|
`texture_patch_views` 在此基础上增加投影视图和区域分工。
|
||||||
|
|
||||||
|
> 为什么不合并两个配置块?在 `texture_patch` 中,页面是人工声明的输入,例如
|
||||||
|
> `18.5 × 16.4 mm`;在 `texture_patch_views` 中,页面是经过验证后从相机投影推导出的
|
||||||
|
> 结果。合并后,`page.mode` 的含义会依赖旁边是否出现某个键,还会削弱当前严格的未知键
|
||||||
|
> 检查,使拼错的配置可能被静默忽略。如果未来出现第三个消费者,或者 `uv_1`、`uv_2`
|
||||||
|
> 不再是边界投影,再重新评估公共配置结构。
|
||||||
|
|
||||||
|
## 3. `texture_patch`:在普通 UV 展开图上生成图案
|
||||||
|
|
||||||
|
命令会为 `uv_outputs` 中的每个 UV map 各生成一套 PNG、SVG 和 PDF。所有输出共用相同的
|
||||||
|
随机颜色和页面设置,仅 UV 布局不同。
|
||||||
|
|
||||||
|
UV 展开必须预先在 Blender GUI 中完成并保存在 `source_blend`。无界面运行只读取已有 UV
|
||||||
|
map,不会自动创建或重新展开 UV。
|
||||||
|
|
||||||
|
### 3.1 `uv_outputs` 的顺序不能随意调整
|
||||||
|
|
||||||
|
程序通过下面的方式构造输出列表:
|
||||||
|
|
||||||
|
```python
|
||||||
|
block["uv_outputs"].items()
|
||||||
|
```
|
||||||
|
|
||||||
|
因此 YAML mapping 的顺序会进入 `texture-job-v3` 的 `outputs[]`。目前四个测试固定要求顺序
|
||||||
|
为:
|
||||||
|
|
||||||
|
```text
|
||||||
|
["produce", "compute"]
|
||||||
|
```
|
||||||
|
|
||||||
|
仅从生成像素来看,调换顺序不会改变颜色,因为 `patch_colors` 在遍历各 UV 前只计算一次。
|
||||||
|
但任务文件数组顺序会改变并导致测试失败,所以不要为了“整理”而重排。
|
||||||
|
|
||||||
|
### 3.2 为什么有的配置是 `nx == ny`,有的是 `80 × 45`
|
||||||
|
|
||||||
|
`nx`、`ny` 表示相机空间中的图案网格数量。`finger-xynova/r01` 的 `cam_sensor` 是正方形
|
||||||
|
画幅,所以 `nx == ny` 可以让相机空间中的单元接近正方形。
|
||||||
|
|
||||||
|
`finger/r01` 使用 `80 × 45`,来自 16:9 相机。这个比例对它自己的相机是合理的,不能因为
|
||||||
|
另一个型号采用方形网格就统一修改。
|
||||||
|
|
||||||
|
### 3.3 `width`、`height` 与 `vector` 的关系
|
||||||
|
|
||||||
|
- `width`、`height`:PNG 栅格尺寸,单位是像素;
|
||||||
|
- `vector.width_mm`、`vector.height_mm`:SVG/PDF 对应的物理页面尺寸,单位是毫米。
|
||||||
|
|
||||||
|
当前两个 revision 都使用 200 px/mm:
|
||||||
|
|
||||||
|
```text
|
||||||
|
3280 / 16.4 = 200
|
||||||
|
3200 / 16.0 = 200
|
||||||
|
3700 / 18.5 = 200
|
||||||
|
```
|
||||||
|
|
||||||
|
维护时应满足:
|
||||||
|
|
||||||
|
```text
|
||||||
|
raster pixels = px_per_mm × physical millimetres
|
||||||
|
```
|
||||||
|
|
||||||
|
`_warn_if_page_aspect_differs` 会比较栅格长宽比与物理页面长宽比。如果不一致,SVG/PDF
|
||||||
|
填满页面时会沿某一方向被拉伸。保持上述关系,警告就不会出现。
|
||||||
|
|
||||||
|
### 3.4 `seed` 是整个物理图案的随机种子
|
||||||
|
|
||||||
|
固定 `seed` 是为了让同一配置反复运行得到相同的图案。同一次生成中的 PNG、SVG、PDF 和
|
||||||
|
所有 UV map 共用一套颜色。
|
||||||
|
|
||||||
|
`texture-views` 里的每个 pair 也必须使用相同 seed,因为它们打印的是同一个物理表面的
|
||||||
|
不同部分。如果每个视图使用 `seed + i`,两个印刷文件会在同一个表面上产生不同图案。
|
||||||
|
|
||||||
|
> 已知文档缺陷:`orisys-design texture-views --seed` 的帮助文字声称“第 i 个 pair 使用
|
||||||
|
> `seed + i`”,但实际代码让所有 pair 使用 `settings.seed`。实际代码行为是正确的,CLI
|
||||||
|
> 帮助文字需要另行修正。
|
||||||
|
|
||||||
|
## 4. `texture_patch_views`:多个投影视图和区域分工
|
||||||
|
|
||||||
|
这个流程较容易混淆,因为它同时使用三类信息。
|
||||||
|
|
||||||
|
### 4.1 三种角色
|
||||||
|
|
||||||
|
1. **图案相机 `pattern_camera`**:决定标记图案从哪个方向投到传感表面。所有视图共用一个
|
||||||
|
图案来源,默认是 `cam_sensor`。
|
||||||
|
2. **区域相机 `pairs[].camera`**:决定该视图适合负责哪些三角形。它只参与区域分工,不是
|
||||||
|
图案来源。
|
||||||
|
3. **`pairs[].uv_map`**:决定该视图最终写入哪个二维布局。
|
||||||
|
|
||||||
|
可以把它们理解成:pattern camera 决定“印什么”,region camera 决定“谁来印”,UV map
|
||||||
|
决定“印刷文件中的坐标”。区分这三者,也能解释为什么片段数量和三角形数量不是同一个概念。
|
||||||
|
|
||||||
|
### 4.2 `merge.incidence_limit_deg`
|
||||||
|
|
||||||
|
区域归属以 loop triangle 为单位。一个三角形只有同时满足以下条件,才可能归某个视图负责:
|
||||||
|
|
||||||
|
- 区域相机能看到它;
|
||||||
|
- 相机视线与表面法向的夹角不超过 `incidence_limit_deg`;
|
||||||
|
- 投影面积达到要求。
|
||||||
|
|
||||||
|
没有任何视图能够负责的三角形就是 hole,会造成漏印。
|
||||||
|
|
||||||
|
在 `finger-xynova/r01` 上,使用真正的 `assign_ownership` 和本次运行生成的 ground truth,
|
||||||
|
测得:
|
||||||
|
|
||||||
|
|`incidence_limit_deg`|hole fraction|相对 `max_hole_fraction: 0.02`|
|
||||||
|
|---|---|---|
|
||||||
|
|62°|**0.0480**|**失败**|
|
||||||
|
|65°|0.0130|通过|
|
||||||
|
|**70°**|**0.0001**|通过|
|
||||||
|
|
||||||
|
这里 `min_triangle_coverage` 不是限制因素:把它从 0.60 放宽到 0,在 62° 和 70° 下都不改变
|
||||||
|
结果。
|
||||||
|
|
||||||
|
这组测量对应以下区域相机位姿:
|
||||||
|
|
||||||
|
|相机|方位角|仰角|距离|
|
||||||
|
|---|---:|---:|---:|
|
||||||
|
|`cam_uv_1`|308.26°|−24.46°|32.62 mm|
|
||||||
|
|`cam_uv_2`|231.74°|−24.46°|32.62 mm|
|
||||||
|
|
||||||
|
**只要移动区域相机,这张表就立即失效。** `incidence_limit_deg` 描述的是“特定相机位姿与
|
||||||
|
特定表面”的组合,不是传感器型号自身永远不变的属性。重新调整相机后必须重跑扫描。
|
||||||
|
|
||||||
|
> 已撤回的旧结论:早期 `design.yaml` 曾写过“默认 62° 时 hole 面积为 0.100 mm²、比例
|
||||||
|
> 0.0002”,以及“4960/5007 个三角形有归属,输出 20454 + 21483 个片段”。这些数据在
|
||||||
|
> 相机移动前成立,现在已经失效。保留这段记录,是为了说明为什么相机相关数据必须写明
|
||||||
|
> 重测触发条件。
|
||||||
|
|
||||||
|
不要删除 YAML 中显式设置的 `incidence_limit_deg: 70.0`。代码中
|
||||||
|
`texture_views.MERGE_DEFAULTS` 和 `blender/scripts/texture_merge.py` 的默认值都是 62.0,
|
||||||
|
并有测试要求两处一致。但对当前 Xynova 表面,62° 会产生 4.8% hole;删除覆盖值会静默恢复
|
||||||
|
错误默认值。
|
||||||
|
|
||||||
|
### 4.3 `max_hole_fraction` 是阻断门槛
|
||||||
|
|
||||||
|
当 hole fraction 超过 `max_hole_fraction` 时,命令会失败并报告:
|
||||||
|
|
||||||
|
- 实际 hole fraction;
|
||||||
|
- 未覆盖的三角形数量;
|
||||||
|
- 可能原因。
|
||||||
|
|
||||||
|
它不是普通警告。这项检查是在一次 4.8% 漏印被静默输出之后增加的,目的是让“表面有秃斑”
|
||||||
|
从肉眼发现的问题变成自动失败。
|
||||||
|
|
||||||
|
### 4.4 `page.mode: auto` 与 `fixed`
|
||||||
|
|
||||||
|
`auto` 只适合使用 Blender **Project from View (Bounds)** 生成的布局。这种布局把相机图像
|
||||||
|
平面映射到 `[0,1]²`,程序可通过最小二乘仿射拟合反推出页面的实际尺寸。
|
||||||
|
|
||||||
|
程序不会因为写了 `auto` 就盲目信任 UV。它会验证:
|
||||||
|
|
||||||
|
- 最大拟合残差不超过 `1e-5`;
|
||||||
|
- UV 边界位于 `[0,1]²`。
|
||||||
|
|
||||||
|
真实投影视图的拟合误差大约为 `1.2e-7`;普通 UV 展开的误差约为 0.17–0.70,因此会被拒绝。
|
||||||
|
`auto` 不会在失败时悄悄退回固定页面。
|
||||||
|
|
||||||
|
`fixed` 使用 `texture_patch.vector` 中的页面尺寸。`produce` 和 `compute` 是真正的普通 UV
|
||||||
|
展开,必须使用 `fixed`;从相机反推它们的页面没有物理意义。
|
||||||
|
|
||||||
|
自动页面最终按整数像素确定:
|
||||||
|
|
||||||
|
```text
|
||||||
|
page size = rounded raster pixels / px_per_mm
|
||||||
|
```
|
||||||
|
|
||||||
|
这样页面和栅格的长宽比天然一致。200 px/mm 下,舍入导致的尺寸误差最多为 0.0025 mm,
|
||||||
|
远低于当前 0.1 mm 对位公差。
|
||||||
|
|
||||||
|
已经测得的页面尺寸:
|
||||||
|
|
||||||
|
|revision|view|对应测量位姿下的页面|
|
||||||
|
|---|---|---|
|
||||||
|
|`finger/r01`|`uv_1`|17.510 × 15.975 mm|
|
||||||
|
|`finger/r01`|`uv_2`|17.510 × 15.980 mm|
|
||||||
|
|`finger-xynova/r01`|`uv_1` / `uv_2`|17.670 × 23.565 mm|
|
||||||
|
|
||||||
|
这些数值会随相机位置改变,正是使用 `auto` 的原因。Xynova 相机重新定位后,页面从
|
||||||
|
21.200 × 23.200 mm 自动更新为 17.670 × 23.565 mm。引入自动推导前,固定共享页面在
|
||||||
|
`finger/r01` 上误差为 6.9%,在 `finger-xynova/r01` 上误差达到 23%,而且横竖方向相反。
|
||||||
|
|
||||||
|
### 4.5 `seam.boundary`、`pins` 与 `gap`
|
||||||
|
|
||||||
|
`boundary: use_seam` 表示使用 Blender 中人工标记的 seam 作为区域边界。图案生成阶段不会
|
||||||
|
读取 seam;`patch_projection.py` 只读取 `loop_triangles` 和 `uv_layers`。seam 由后续
|
||||||
|
ownership 步骤使用。
|
||||||
|
|
||||||
|
`pins` 用于在自动分配不符合设计意图时,强制某个连通区域归指定视图。默认留空,让覆盖率
|
||||||
|
指标决定归属。每次增加 pin 都应写清原因,避免把几何或相机错误隐藏成“人工设置”。
|
||||||
|
|
||||||
|
如果启用 gap,总间隙宽度是 `alignment_tolerance_mm` 的两倍。原因是两次独立印刷都可能
|
||||||
|
产生最大对位误差;每一侧退让一个公差,最坏情况下仍不会重叠。
|
||||||
|
|
||||||
|
- `apply: post_process`:将 UV 多边形裁剪到 seam 偏移后的半平面,生成物理宽度准确的直边;
|
||||||
|
- `apply: geometry_trim`:丢弃整个片段,会产生锯齿边,仅保留作比较。
|
||||||
|
|
||||||
|
> 当前仍可看到的阶梯形状来自按三角形分配的 ownership 边界,而不是 gap 自身。gap 只是
|
||||||
|
> 从这条阶梯边界向内退让。要得到平滑边界,需要支持三角形内部的更细粒度分区。
|
||||||
|
|
||||||
|
### 4.6 `print.base_fill`
|
||||||
|
|
||||||
|
`base_fill: owned` 会把本视图负责的三角形填成白色,其余区域填黑。这样白色底层也已经沿
|
||||||
|
seam 分开,两张印刷文件不会在同一位置重复打印白底。
|
||||||
|
|
||||||
|
## 5. 常用参数速查
|
||||||
|
|
||||||
|
|参数|当前值|来源/测量条件|什么时候失效|怎样重测|
|
||||||
|
|---|---:|---|---|---|
|
||||||
|
|Xynova `incidence_limit_deg`|70.0|第 4.2 节记录的相机位姿|移动区域相机或修改表面|对新 ground truth 运行 `assign_ownership`,扫描 62/65/70/75°|
|
||||||
|
|自动页面|见第 4.4 节|对应区域相机位姿|移动相机或修改网格|重跑 `texture-views`,读取 `_print_views.json`|
|
||||||
|
|`px_per_mm`|200|设计选择|修改栅格或页面尺寸|计算 `width / vector.width_mm` 和 `height / vector.height_mm`|
|
||||||
|
|`alignment_tolerance_mm`|0.1 mm|当前印刷与对位能力假设|更换打印、夹具或对位工艺|通过实际计量重新评估,不由本仓库推断|
|
||||||
|
|
||||||
|
### 内置默认值不适合当前表面的键
|
||||||
|
|
||||||
|
|键|内置默认值|为什么必须保留在 YAML 中|
|
||||||
|
|---|---:|---|
|
||||||
|
|Xynova `incidence_limit_deg`|62.0|当前表面会产生 4.8% hole;显式 70.0 是必要配置|
|
||||||
|
|
||||||
|
## 6. `expected.*`:目前还没有真正起到保护作用
|
||||||
|
|
||||||
|
`expected.sensing_vertices` 和 `expected.sensing_triangles` 看起来像拓扑变化门槛,但当前
|
||||||
|
`src/`、`blender/`、`tools/`、`tests/` 中没有代码读取它们。
|
||||||
|
|
||||||
|
Xynova YAML 当前写的是 2595 vertices / 5005 triangles,实际测得为 2584 vertices /
|
||||||
|
5007 triangles。早期注释曾声称 `expected.*` 能发现这类变化,这个说法不成立,因为字段尚未
|
||||||
|
接入导出 manifest 或测试。
|
||||||
|
|
||||||
|
在它们真正接入检查之前:
|
||||||
|
|
||||||
|
- 把这些数值视为“未验证”;
|
||||||
|
- 不要在报告中引用它们作为自动验收结果;
|
||||||
|
- 不要因为字段存在,就假设拓扑变化已经受到保护。
|
||||||
|
|
||||||
|
理想的后续实现是:导出时测量实际拓扑,与 `expected.*` 比较;不一致时明确失败并打印差异。
|
||||||
|
|
||||||
|
## 7. 已知几何漂移
|
||||||
|
|
||||||
|
仓库中的历史资料对 Xynova sensing surface 的描述互不一致:
|
||||||
|
|
||||||
|
|来源|记录值|
|
||||||
|
|---|---|
|
||||||
|
|`probe-pipeline.md`|2508 vertices / 4834 polygons|
|
||||||
|
|`xynova-texture-investigation.md`|4818 triangles + 16 quads|
|
||||||
|
|当前实际测量|2584 vertices / 5007 triangles|
|
||||||
|
|
||||||
|
`build/probe/finger-xynova/r01/probe_frames.npz` 来自 2508 顶点的
|
||||||
|
`sensing_surface_918_s`,而当前带 seam、`uv_1` 和 `uv_2` 的表面有 2584 顶点。因此
|
||||||
|
`tests/test_render.py::test_end_to_end_render_manifest` 会在顶点数量检查处失败。
|
||||||
|
|
||||||
|
这是过期 fixture,不是纹理生成错误。修复方式需要先决定哪个表面才是该 revision 的唯一权威
|
||||||
|
表面,然后选择其中一种方案:
|
||||||
|
|
||||||
|
1. 为当前权威表面重新生成 probe frames;或
|
||||||
|
2. 把 seam 和 UV 布局制作到已经生成 probe frames 的那个表面上。
|
||||||
|
|
||||||
|
当前 revision 包含六个 sensing-surface 变体:
|
||||||
|
|
||||||
|
```text
|
||||||
|
sensing_surface
|
||||||
|
sensing_surface_0
|
||||||
|
sensing_surface_0918
|
||||||
|
sensing_surface_0928
|
||||||
|
sensing_surface_45
|
||||||
|
sensing_surface_918_s
|
||||||
|
```
|
||||||
|
|
||||||
|
其中 `_0928` 与 `sensing_surface` 位于相同位置,是重合副本。`EXCLUDE` collection 的
|
||||||
|
`exclude=False` 表明它只是名称叫 EXCLUDE,并没有在 view layer 中真正排除。射线检测会先
|
||||||
|
击中这个副本,随后把所有片段判为不可用。
|
||||||
|
|
||||||
|
目前 `_0928` 已隐藏;`texture-views` 现在也会在发生遮挡时失败,并指出拦截射线的对象,
|
||||||
|
不会继续生成空白纹理。
|
||||||
|
|
||||||
|
## 8. 两个 revision 的差异
|
||||||
|
|
||||||
|
|项目|`finger/r01`|`finger-xynova/r01`|
|
||||||
|
|---|---|---|
|
||||||
|
|栅格尺寸|3280 × 3200|3700 × 3280|
|
||||||
|
|固定页面|16.4 × 16.0 mm|18.5 × 16.4 mm|
|
||||||
|
|`nx`, `ny`|80, 45(16:9 相机)|80, 80(方形相机)|
|
||||||
|
|seam 边数|32|45|
|
||||||
|
|自身 hole|0.049 mm²(0.0001)|0.092 mm²(0.00015)|
|
||||||
|
|`incidence_limit_deg`|62.0;对应旧相机位姿,移动后必须重测|70.0|
|
||||||
|
|`optics` 配置|无|有|
|
||||||
|
|
||||||
|
不能为了让两个 revision 的 YAML 看起来一致,就统一这些值。分辨率、页面、网格比例、相机
|
||||||
|
位姿和入射角门槛都是各自设计与测量条件的一部分。
|
||||||
|
|
||||||
|
## 9. 新同事修改参数时的建议步骤
|
||||||
|
|
||||||
|
1. 先确认正在修改哪个 sensor 和 revision,不要把 `finger` 的参数复制到
|
||||||
|
`finger-xynova` 后直接使用。
|
||||||
|
2. 判断改动属于“图案”“UV 布局”“相机位姿”“印刷工艺”还是“几何”。
|
||||||
|
3. 根据第 5 节查找被改动失效的测量值。
|
||||||
|
4. 如果移动区域相机,重测 ownership、hole fraction 和自动页面。
|
||||||
|
5. 如果修改网格,核对拓扑、UV、seam、probe fixture 和 field-domain 相关输出。
|
||||||
|
6. 如果修改栅格或物理页面,重新计算两个方向的 `px_per_mm`,不能只检查宽度。
|
||||||
|
7. 运行对应测试并查看生成任务中的 manifest,不要只凭最终图片看起来正常。
|
||||||
|
8. 将新数值、测量条件、结果和下次失效条件一起更新到本文。
|
||||||
|
|
||||||
|
遇到结果异常时,可以按以下顺序排查:
|
||||||
|
|
||||||
|
```text
|
||||||
|
输出为空
|
||||||
|
→ 是否有重合/遮挡对象
|
||||||
|
→ region camera 是否能看到表面
|
||||||
|
→ UV map 名称和对象是否正确
|
||||||
|
|
||||||
|
表面有漏印
|
||||||
|
→ 查看 hole fraction
|
||||||
|
→ 检查 incidence_limit_deg
|
||||||
|
→ 检查相机位姿和 footprint 条件
|
||||||
|
|
||||||
|
页面比例不对
|
||||||
|
→ 确认 UV 是 unwrap 还是 Project from View (Bounds)
|
||||||
|
→ unwrap 使用 fixed;投影视图才使用 auto
|
||||||
|
→ 检查 raster / px_per_mm / page 的关系
|
||||||
|
|
||||||
|
接缝呈阶梯形
|
||||||
|
→ 先区分 ownership 边界与 gap
|
||||||
|
→ gap 使用 post_process 只能让退让边变直
|
||||||
|
→ 平滑 ownership 需要更细的分区算法
|
||||||
|
```
|
||||||
|
|
||||||
|
最后,不要删除“已知缺陷”“撤回结论”和失效条件。这些内容不是历史包袱,而是防止同类问题
|
||||||
|
再次以合理外观进入生产输出的工程约束。
|
||||||
|
After Width: | Height: | Size: 197 KiB |
@@ -8,12 +8,6 @@ meters_per_source_unit: 0.001
|
|||||||
|
|
||||||
source_blend: source/geometry.blend
|
source_blend: source/geometry.blend
|
||||||
|
|
||||||
# Expected sensing surface topology for r01 (from shipped 30fps-0703 runtime).
|
|
||||||
# First exports from geometry.blend may differ until topology is locked.
|
|
||||||
expected:
|
|
||||||
sensing_vertices: 2595
|
|
||||||
sensing_triangles: 5005
|
|
||||||
|
|
||||||
frames:
|
frames:
|
||||||
sensor: origin_sensor
|
sensor: origin_sensor
|
||||||
|
|
||||||
@@ -22,115 +16,79 @@ objects:
|
|||||||
cam_sensor: cam_sensor
|
cam_sensor: cam_sensor
|
||||||
origin_sensor: origin_sensor
|
origin_sensor: origin_sensor
|
||||||
|
|
||||||
# Headless colored-patch texture generation (`orisys-design texture`).
|
# Read on every export; changing `assembly` silently changes what ships.
|
||||||
# UV unwrapping is still done in the Blender GUI: the headless run consumes the
|
collections:
|
||||||
# UV maps already stored in source_blend and never creates or unwraps one.
|
frames: FRAMES
|
||||||
# One PNG/SVG/PDF set is written per UV map below. All maps and formats share
|
export_compute: EXPORT_COMPUTE
|
||||||
# the settings and patch colours, so they differ only by UV layout.
|
assembly: ASSEMBLY
|
||||||
# These are the revision defaults; CLI flags override every value.
|
design_sim: DESIGN_SIM
|
||||||
|
exclude: EXCLUDE
|
||||||
|
|
||||||
|
# Topology guard. NOT YET WIRED - no code reads these, and the measured topology is
|
||||||
|
# 2584/5007. Unverified until connected to the export manifest; see
|
||||||
|
# docs/design-parameters.md part 5.
|
||||||
|
expected:
|
||||||
|
sensing_vertices: 2595
|
||||||
|
sensing_triangles: 5005
|
||||||
|
|
||||||
|
# Camera-space artwork on UV unwraps (`orisys-design texture`).
|
||||||
|
# See docs/design-parameters.md part 1.
|
||||||
texture_patch:
|
texture_patch:
|
||||||
mesh: sensing_surface
|
mesh: sensing_surface
|
||||||
camera: cam_sensor
|
camera: cam_sensor
|
||||||
uv_outputs:
|
uv_outputs: # ORDER IS OBSERVABLE: becomes the job's outputs[] array
|
||||||
produce: patch_texture_produce.png # hardware production layout
|
produce: patch_texture_produce.png
|
||||||
compute: patch_texture_compute.png # compute/runtime layout
|
compute: patch_texture_compute.png
|
||||||
width: 3700 # 1850*2
|
width: 3700 # raster px; keep == px_per_mm * vector below
|
||||||
height: 3280 # 1640*2
|
height: 3280
|
||||||
# Camera frame is square (1000x1000), so equal counts make camera-space
|
nx: 80 # square camera frame, so nx == ny
|
||||||
# elements square. The original 80x45 grid was copied from a 16:9 camera.
|
|
||||||
nx: 80
|
|
||||||
ny: 80
|
ny: 80
|
||||||
color_r: 0.05
|
color_r: 0.05
|
||||||
edge_sample_grid: 5
|
edge_sample_grid: 5
|
||||||
seed: 7 # fixed so PNG/SVG/PDF share one reproducible colour set
|
seed: 7 # pinned: one reproducible colour set across PNG/SVG/PDF
|
||||||
vector:
|
vector: # physical page; the `page: fixed` fallback for texture_patch_views
|
||||||
width_mm: 18.5 # editable SVG/PDF production page width
|
width_mm: 18.5
|
||||||
height_mm: 16.4 # editable SVG/PDF production page height
|
height_mm: 16.4
|
||||||
cross:
|
cross:
|
||||||
enabled: true
|
enabled: true
|
||||||
length_px: 40
|
length_px: 40
|
||||||
thickness_px: 3
|
thickness_px: 3
|
||||||
color: [1.0, 0.0, 0.0, 1.0] # red reference cross
|
color: [1.0, 0.0, 0.0, 1.0]
|
||||||
|
|
||||||
# Multi-view production textures (`orisys-design texture-views`).
|
# Projected views with per-triangle ownership (`orisys-design texture-views`).
|
||||||
#
|
# See docs/design-parameters.md part 2.
|
||||||
# `cam_uv_1` / `cam_uv_2` already existed in this blend; `uv_1` / `uv_2` and the seam were
|
|
||||||
# added to `sensing_surface`. Verified: the marked seam is a valid cut — 45 edges splitting
|
|
||||||
# the surface into 2 components of 308.314 / 308.311 mm^2 (50.0 / 50.0) — and at the default
|
|
||||||
# 62 degree limit the partition leaves 0.100 mm^2 of hole (fraction 0.0002), well inside
|
|
||||||
# max_hole_fraction. Coverage: 4960/5007 triangles owned, 20454 + 21483 fragments emitted.
|
|
||||||
#
|
|
||||||
# NOTE: this revision contains SIX sensing-surface variants (`sensing_surface`, `_0`, `_0918`,
|
|
||||||
# `_0928`, `_45`, `_918_s`). `_0928` is the only one carrying the seam and `uv_1`/`uv_2`, so
|
|
||||||
# `sensing_surface` is a copy of it. Two consequences to be aware of:
|
|
||||||
# 1. `_0928` is a COINCIDENT duplicate of `sensing_surface` at identical coordinates. The
|
|
||||||
# `EXCLUDE` collection has `exclude=False`, so it is not actually excluded from the view
|
|
||||||
# layer, and the raycast hits the duplicate instead of the target. That rejects every
|
|
||||||
# fragment and the region cameras return 0 — `texture-views` now fails loudly and names
|
|
||||||
# the intercepting object rather than writing blank textures. `_0928` is hidden.
|
|
||||||
# 2. The probe/render baseline (`build/probe/finger-xynova/r01/probe_frames.npz`) was built
|
|
||||||
# from the 2508-vertex `sensing_surface_918_s`, while `sensing_surface` has 2584 vertices.
|
|
||||||
# `tests/test_render.py::test_end_to_end_render_manifest` therefore fails on the vertex
|
|
||||||
# count. That is a fixture/stale-baseline mismatch, not a texture bug: either regenerate
|
|
||||||
# the probe frames for the current surface, or author the seam and the extra UV layouts
|
|
||||||
# on whichever surface is canonical. `expected.*` below would have caught this and is
|
|
||||||
# still read by nothing.
|
|
||||||
texture_patch_views:
|
texture_patch_views:
|
||||||
enabled: true
|
enabled: true
|
||||||
pattern_camera: cam_sensor # the pattern every layout carries, same source as `compute`
|
pattern_camera: cam_sensor # the ink source; a pair's `camera` only decides ownership
|
||||||
# Per-view page sizing. Measured here: the uv_1/uv_2 layouts are 21.1989 x 23.2003 mm
|
|
||||||
# (aspect 0.9137) and 21.2001 x 23.2003 mm (aspect 0.9138) — PORTRAIT. `vector:` above says
|
|
||||||
# 18.5 x 16.4 (aspect 1.1280), which is LANDSCAPE, so the artwork was stretched 23% in the
|
|
||||||
# wrong direction. `auto` derives the page per view and proves the layout really is a
|
|
||||||
# bounds-projection from the named camera before using the result.
|
|
||||||
#
|
|
||||||
# `px_per_mm: 200` reproduces the existing density exactly (3700/18.5 = 200.0).
|
|
||||||
#
|
|
||||||
# `fixed` keeps `texture_patch.vector`, which unwrapped layouts (`produce`, `compute`)
|
|
||||||
# require — they are not projections.
|
|
||||||
#
|
|
||||||
# Overlay note: the scene render is 250x250 while these layouts are 0.9138 aspect, so set the
|
|
||||||
# render resolution to the emitted raster size and use "Stretch to fill".
|
|
||||||
page:
|
page:
|
||||||
mode: auto
|
mode: auto # auto = derived per view (projections only); fixed = use vector above
|
||||||
px_per_mm: 200
|
px_per_mm: 200
|
||||||
pairs:
|
pairs:
|
||||||
- id: uv_1
|
- id: uv_1
|
||||||
camera: cam_uv_1 # REGION camera
|
camera: cam_uv_1 # REGION camera, not the pattern source
|
||||||
uv_map: uv_1
|
uv_map: uv_1
|
||||||
- id: uv_2
|
- id: uv_2
|
||||||
camera: cam_uv_2
|
camera: cam_uv_2
|
||||||
uv_map: uv_2
|
uv_map: uv_2
|
||||||
merge:
|
merge:
|
||||||
mode: authored # honour the 45-edge seam marked on the mesh
|
mode: authored # honour the seam marked on the mesh
|
||||||
# RE-MEASURED 2026-09-29 after the region cameras moved (cam_uv_1 az 318.9 -> 308.3,
|
incidence_limit_deg: 70.0 # DO NOT DELETE: built-in default 62.0 FAILS here (4.8% hole)
|
||||||
# elevation -43.3 -> -24.5, distance 37.6 -> 32.6 mm). Sweep at min_triangle_coverage 0.60:
|
max_hole_fraction: 0.02 # enforced: the run fails above this rather than shipping a gap
|
||||||
# 62deg -> hole 0.0480 FAILS max_hole_fraction (a visible bald patch of 29.6 mm^2)
|
|
||||||
# 65deg -> hole 0.0130 passes, but barely
|
|
||||||
# 70deg -> hole 0.0001 passes with margin <-- current
|
|
||||||
# The limit is a property of the camera poses and the surface, so MOVING THE CAMERAS
|
|
||||||
# INVALIDATES IT. Re-run the sweep after any re-pose. min_triangle_coverage is not binding
|
|
||||||
# here: relaxing it to 0 changes nothing at 62 or 70 degrees.
|
|
||||||
incidence_limit_deg: 70.0
|
|
||||||
min_footprint_mm2: 2.0e-3
|
min_footprint_mm2: 2.0e-3
|
||||||
min_triangle_coverage: 0.60
|
min_triangle_coverage: 0.60
|
||||||
max_hole_fraction: 0.02 # enforced: the run fails above this rather than shipping a gap
|
|
||||||
component_policy: best_coverage
|
component_policy: best_coverage
|
||||||
seam:
|
seam:
|
||||||
boundary: use_seam
|
boundary: use_seam
|
||||||
pins: []
|
pins: []
|
||||||
# Empty band along the seam, to absorb two-pass registration error. Default total width is
|
|
||||||
# 2 x alignment_tolerance_mm = 0.2 mm, half per file; `total_mm` overrides the derivation.
|
|
||||||
gap:
|
gap:
|
||||||
enabled: false # false = hard butt at the seam, no band at all
|
enabled: false # false = hard butt at the seam, no band at all
|
||||||
apply: post_process # post_process = straight edge | geometry_trim = sawtooth
|
apply: post_process # post_process = straight edge | geometry_trim = sawtooth
|
||||||
treatment: no_print # the band carries nothing
|
treatment: no_print
|
||||||
alignment_tolerance_mm: 0.1 # per-pass placement accuracy, mm
|
alignment_tolerance_mm: 0.1
|
||||||
print:
|
print:
|
||||||
base_fill: owned # white ground, clipped by the seam
|
base_fill: owned # white ground, already split by the seam
|
||||||
|
|
||||||
# Post-generation printability inspection (`orisys-design inspect`). Advisory
|
# Advisory only: reports a verdict, never blocks generation or changes exit codes.
|
||||||
# only: it reports a verdict but never blocks generation or changes exit codes.
|
|
||||||
# Millimetres are measured on the vector page declared above.
|
# Millimetres are measured on the vector page declared above.
|
||||||
printability:
|
printability:
|
||||||
printer_dpi: 600 # target UV printer resolution
|
printer_dpi: 600 # target UV printer resolution
|
||||||
@@ -140,21 +98,14 @@ printability:
|
|||||||
max_sliver_ratio: 4.0 # longest edge / minimum width
|
max_sliver_ratio: 4.0 # longest edge / minimum width
|
||||||
min_patch_area_mm2: 1.0e-4 # area above which a polygon is a material feature
|
min_patch_area_mm2: 1.0e-4 # area above which a polygon is a material feature
|
||||||
|
|
||||||
# Optical stack above the markers, ordered from the camera inward.
|
# Optical stack above the markers, ordered camera-inward. ior values are LITERATURE
|
||||||
# The markers sit on the sensing surface underneath these layers, so their image
|
# ASSUMPTIONS, not measurements - that is what ior_source records.
|
||||||
# is displaced relative to the straight-line pinhole model.
|
|
||||||
#
|
|
||||||
# ior values are LITERATURE ASSUMPTIONS, not measurements - that is what
|
|
||||||
# ior_source records, so an assumed index can never pass for a measured one.
|
|
||||||
# Replace them with measured values when available; nothing else needs to change.
|
|
||||||
optics:
|
optics:
|
||||||
marker_surface: sensing_surface
|
marker_surface: sensing_surface
|
||||||
layers:
|
layers:
|
||||||
# Solids, not surface pairs: `shell` and `rubber-1` are each one closed mesh,
|
# Solids, not surface pairs: `shell` and `rubber-1` are each one closed mesh, so the
|
||||||
# so the ray enters and leaves the same object.
|
# ray enters and leaves the same object. rubber-2 is BEHIND the markers, so it does
|
||||||
# Stack: camera > shell > rubber-1 > sensing_surface > rubber-2.
|
# not refract camera light and is deliberately not a layer.
|
||||||
# rubber-2 is BEHIND the markers, so it does not refract camera light and is
|
|
||||||
# deliberately not a layer.
|
|
||||||
- name: plastic_shell
|
- name: plastic_shell
|
||||||
solid: shell
|
solid: shell
|
||||||
ior: 1.59
|
ior: 1.59
|
||||||
@@ -165,48 +116,33 @@ optics:
|
|||||||
ior_source: assumed_silicone_literature
|
ior_source: assumed_silicone_literature
|
||||||
|
|
||||||
# Probe sensitivity bench (`orisys-design deform|render|track|sensitivity|optimise`).
|
# Probe sensitivity bench (`orisys-design deform|render|track|sensitivity|optimise`).
|
||||||
# Deforms the vertices of `group` through known x/y/z displacements expressed in a
|
# Contract: docs/probe-pipeline.md. The .blend is never written by the pipeline.
|
||||||
# local probe-average basis (normal force along z, shear along x/y), renders what
|
|
||||||
# `camera` sees, and measures DIS optical flow against the zero-displacement frame.
|
|
||||||
#
|
|
||||||
# Contract: docs/probe-pipeline.md. The `.blend` is never written by the pipeline.
|
|
||||||
probe:
|
probe:
|
||||||
mesh: sensing_surface
|
mesh: sensing_surface
|
||||||
camera: cam_sensor
|
camera: cam_sensor
|
||||||
group: probe
|
group: probe
|
||||||
uv_map: compute
|
uv_map: compute
|
||||||
texture: patch_texture_compute.png
|
texture: patch_texture_compute.png
|
||||||
# Proportional falloff radius for the outer rubber. Note this mesh has a median
|
# Median mesh edge here is 0.63 mm, so 0.5 mm reaches only immediate neighbours; the
|
||||||
# edge of 0.63 mm, so 0.5 mm reaches only the probe's immediate neighbours; the
|
|
||||||
# deform report prints how many vertices actually move rather than hiding it.
|
# deform report prints how many vertices actually move rather than hiding it.
|
||||||
radius_mm: 0.5
|
radius_mm: 0.5
|
||||||
falloff: smooth
|
falloff: smooth
|
||||||
step_mm: 0.01
|
step_mm: 0.01
|
||||||
max_mm: 0.10
|
max_mm: 0.10
|
||||||
axes: [x, y, z]
|
axes: [x, y, z]
|
||||||
# Frames are rendered at the revision's real camera resolution. Keep this in step with
|
# The pipeline overrides the scene resolution from here, so a blend edited to a
|
||||||
# the camera in source/geometry.blend: the pipeline overrides the scene resolution from
|
# different size is silently ignored. Keep in step with source/geometry.blend.
|
||||||
# here, so a blend edited to a different size is silently ignored otherwise.
|
|
||||||
#
|
|
||||||
# Single scale on purpose. The ROI radius is radius_mm x the probe-local px/mm, so at
|
|
||||||
# 125x125 it would be 1.66 px against an 8 px DIS window: the estimator measures its own
|
|
||||||
# regulariser, not the displacement. See docs/probe-findings.md section 2.
|
|
||||||
resolution: [250, 250]
|
resolution: [250, 250]
|
||||||
process_resolution: [250, 250]
|
process_resolution: [250, 250]
|
||||||
# Mirrors the SDK sensor tracker keys (orisys-sdk config/finger.json), EXCEPT
|
# Mirrors the SDK tracker keys EXCEPT dis_mode: the 8*2**dis_mode window against a
|
||||||
# dis_mode. The estimator window is 8 * 2**dis_mode input px (16 at dis_mode=1, 8 at 0)
|
# ~6.6 px moving region makes the shipped dis_mode=1 collapse the estimate ~3x.
|
||||||
# and the moving region is ~6.6 px across at 250x250, so the shipped dis_mode=1 gives
|
|
||||||
# window/moving = 2.42: DIS's smoothness pulls the estimate toward zero and the measured
|
|
||||||
# response drops ~3x (1.42 -> 4.30 px/mm at the matched cell). dis_mode=0 gives 1.21,
|
|
||||||
# the same ratio as 500x500 with dis_mode=1, which recovers to ~73%.
|
|
||||||
# See docs/probe-findings.md section 2.
|
# See docs/probe-findings.md section 2.
|
||||||
# If the deployed tracker is fixed at dis_mode=1, run the bench at 500x500 instead.
|
|
||||||
tracker:
|
tracker:
|
||||||
algorithm: dis
|
algorithm: dis
|
||||||
dis_preset: medium
|
dis_preset: medium
|
||||||
dis_mode: 0
|
dis_mode: 0
|
||||||
# Standard (not AgX) and denoise off: a filmic view transform silently distorts
|
# Standard (not AgX) and denoise off: a filmic view transform silently distorts the
|
||||||
# the marker values the tracker is supposed to measure.
|
# marker values the tracker is supposed to measure.
|
||||||
render:
|
render:
|
||||||
engine: CYCLES
|
engine: CYCLES
|
||||||
samples: 16
|
samples: 16
|
||||||
@@ -214,39 +150,20 @@ probe:
|
|||||||
view_transform: Standard
|
view_transform: Standard
|
||||||
interpolate: Closest
|
interpolate: Closest
|
||||||
seed: 0
|
seed: 0
|
||||||
# Noise calibration and the injected noise model.
|
# `floor_px` from a re-render at seed+1 is Cycles antialiasing jitter, NOT sensor
|
||||||
#
|
# noise, so it is kept only as a reproducibility diagnostic; the score is anchored to
|
||||||
# The estimator's `floor_px` is measured as reference vs a re-render at seed+1, which
|
# the injected model plus the idle-footage calibration below.
|
||||||
# is Cycles antialiasing jitter, NOT sensor noise: on this data it differs by ~5 grey
|
# See docs/probe-findings.md section 2.
|
||||||
# levels RMS on half the pixels and scales with edge contrast. Ranking patterns by
|
|
||||||
# gain/that-floor is unsound - the ordering reverses under additive noise, and it
|
|
||||||
# understates the real idle floor measured from the footage below, coarse patterns
|
|
||||||
# most. So the render-seed floor is kept only as a reproducibility diagnostic and the
|
|
||||||
# score is anchored to the injected model plus this measured calibration.
|
|
||||||
noise:
|
noise:
|
||||||
# Real idle (no-contact) footage, relative to the repo root. Measured: floor
|
|
||||||
# ~0.23 px at 1280x720 (short baseline == long baseline, so it is white noise and
|
|
||||||
# the clip is genuinely idle), dominant marker period ~25 px, i.e. ~0.0093 floor
|
|
||||||
# per period. That is 2-6x the simulated render-seed floor per cell.
|
|
||||||
idle_footage: ../data/dd02-0703/190.mp4
|
idle_footage: ../data/dd02-0703/190.mp4
|
||||||
roi_fraction: 0.18
|
roi_fraction: 0.18
|
||||||
max_frames: 120
|
max_frames: 120
|
||||||
# Additive white Gaussian in grey levels. Defaults are deliberately small: each
|
|
||||||
# sigma costs a full DIS pass, and the process scale is a resample of the render
|
|
||||||
# one, so only the render scale is evaluated unless asked.
|
|
||||||
sigmas: [4.0, 16.0]
|
sigmas: [4.0, 16.0]
|
||||||
draws: 1
|
draws: 1
|
||||||
seed: 0
|
seed: 0
|
||||||
models: [render_seed, injected]
|
models: [render_seed, injected]
|
||||||
scales: [render]
|
scales: [render]
|
||||||
|
|
||||||
collections:
|
|
||||||
frames: FRAMES
|
|
||||||
export_compute: EXPORT_COMPUTE
|
|
||||||
assembly: ASSEMBLY
|
|
||||||
design_sim: DESIGN_SIM
|
|
||||||
exclude: EXCLUDE
|
|
||||||
|
|
||||||
notes: |
|
notes: |
|
||||||
Canonical source is geometry.blend with named collections and objects.
|
Canonical source is geometry.blend with named collections and objects.
|
||||||
Compute export uses sensing_surface + cam_sensor relative to origin_sensor.
|
Compute export uses sensing_surface + cam_sensor relative to origin_sensor.
|
||||||
|
|||||||
@@ -8,12 +8,6 @@ meters_per_source_unit: 0.001
|
|||||||
|
|
||||||
source_blend: source/geometry.blend
|
source_blend: source/geometry.blend
|
||||||
|
|
||||||
# Expected sensing surface topology for r01 (from shipped 30fps-0703 runtime).
|
|
||||||
# First exports from geometry.blend may differ until topology is locked.
|
|
||||||
expected:
|
|
||||||
sensing_vertices: 1032
|
|
||||||
sensing_triangles: 1974
|
|
||||||
|
|
||||||
frames:
|
frames:
|
||||||
sensor: origin_sensor
|
sensor: origin_sensor
|
||||||
|
|
||||||
@@ -22,124 +16,85 @@ objects:
|
|||||||
cam_sensor: cam_sensor
|
cam_sensor: cam_sensor
|
||||||
origin_sensor: origin_sensor
|
origin_sensor: origin_sensor
|
||||||
|
|
||||||
# Headless colored-patch texture generation (`orisys-design texture`).
|
# Read on every export; changing `assembly` silently changes what ships.
|
||||||
# UV unwrapping is still done in the Blender GUI: the headless run consumes the
|
collections:
|
||||||
# UV maps already stored in source_blend and never creates or unwraps one.
|
frames: FRAMES
|
||||||
# One PNG/SVG/PDF set is written per UV map below. All maps and formats share
|
export_compute: EXPORT_COMPUTE
|
||||||
# the settings and patch colours, so they differ only by UV layout.
|
assembly: ASSEMBLY
|
||||||
# These are the revision defaults; CLI flags override every value.
|
design_sim: DESIGN_SIM
|
||||||
|
exclude: EXCLUDE
|
||||||
|
|
||||||
|
# Topology guard. NOT YET WIRED - no code reads these, and the measured topology is
|
||||||
|
# 1034/1977. Unverified until connected to the export manifest; see
|
||||||
|
# docs/design-parameters.md part 5.
|
||||||
|
expected:
|
||||||
|
sensing_vertices: 1032
|
||||||
|
sensing_triangles: 1974
|
||||||
|
|
||||||
|
# Camera-space artwork on UV unwraps (`orisys-design texture`).
|
||||||
|
# See docs/design-parameters.md part 1.
|
||||||
texture_patch:
|
texture_patch:
|
||||||
mesh: sensing_surface
|
mesh: sensing_surface
|
||||||
camera: cam_sensor
|
camera: cam_sensor
|
||||||
uv_outputs:
|
uv_outputs: # ORDER IS OBSERVABLE: becomes the job's outputs[] array
|
||||||
produce: patch_texture_produce.png # hardware production layout
|
produce: patch_texture_produce.png
|
||||||
compute: patch_texture_compute.png # compute/runtime layout
|
compute: patch_texture_compute.png
|
||||||
width: 3280 # 1640*2
|
width: 3280 # raster px; keep == px_per_mm * vector below
|
||||||
height: 3200 # 1600*2
|
height: 3200
|
||||||
nx: 80
|
nx: 80 # 16:9 camera frame, left as-is from the original grid
|
||||||
ny: 45
|
ny: 45
|
||||||
color_r: 0.05
|
color_r: 0.05
|
||||||
edge_sample_grid: 5
|
edge_sample_grid: 5
|
||||||
seed: 7 # fixed so PNG/SVG/PDF share one reproducible colour set
|
seed: 7 # pinned: one reproducible colour set across PNG/SVG/PDF
|
||||||
vector:
|
vector: # physical page; the `page: fixed` fallback for texture_patch_views
|
||||||
width_mm: 16.4 # editable SVG/PDF production page width
|
width_mm: 16.4
|
||||||
height_mm: 16.0 # editable SVG/PDF production page height
|
height_mm: 16.0
|
||||||
cross:
|
cross:
|
||||||
enabled: true
|
enabled: true
|
||||||
length_px: 40
|
length_px: 40
|
||||||
thickness_px: 3
|
thickness_px: 3
|
||||||
color: [1.0, 0.0, 0.0, 1.0] # red reference cross
|
color: [1.0, 0.0, 0.0, 1.0]
|
||||||
|
|
||||||
# Multi-view production textures (`orisys-design texture-views`).
|
# Projected views with per-triangle ownership (`orisys-design texture-views`).
|
||||||
#
|
# The PATTERN is grounded on cam_sensor, not on the production views: uv_1 and uv_2 are two
|
||||||
# The PATTERN is grounded on the sensing camera, not on the production views. `uv_1` and
|
# parameterisations of the one surface pattern `compute` carries, so the printed marker stays
|
||||||
# `uv_2` are two different *parameterisations* of the one surface pattern that
|
# a single pattern. `cam_uv_1`/`cam_uv_2` authored those layouts (Project from View (Bounds))
|
||||||
# `texture_compute` carries, so the printed marker stays a single pattern across the whole
|
# and now serve as each pair's REGION camera, deciding what that pair may own.
|
||||||
# surface. `cam_uv_1` / `cam_uv_2` authored those layouts in Blender (Project from View
|
# Ownership closes the overlap: both layouts are full-surface, so without a partition printing
|
||||||
# (Bounds)) and now serve as each pair's REGION camera: they decide which surface that pair
|
# both would double-ink essentially the whole part.
|
||||||
# may own. Neither is a pattern source.
|
# See docs/design-parameters.md part 2.
|
||||||
#
|
|
||||||
# OWNERSHIP closes the overlap. `uv_1` and `uv_2` are both full-surface layouts, so printing
|
|
||||||
# both would double-ink essentially the whole part. Every loop triangle is assigned to
|
|
||||||
# exactly one pair and each file emits only its own triangles, which makes zero double ink a
|
|
||||||
# property of the partition rather than something to measure after printing.
|
|
||||||
#
|
|
||||||
# Rendering settings (page size, grid, palette, seed) come from `texture_patch` above,
|
|
||||||
# because they are decisions about the printed page rather than about a particular layout.
|
|
||||||
# `produce`/`compute` and `orisys-design texture` are untouched.
|
|
||||||
texture_patch_views:
|
texture_patch_views:
|
||||||
enabled: true
|
enabled: true
|
||||||
pattern_camera: cam_sensor # the pattern every layout carries, same source as `compute`
|
pattern_camera: cam_sensor # the ink source; a pair's `camera` only decides ownership
|
||||||
# Per-view page sizing. A layout authored by Project from View (Bounds) maps [0,1]^2 to the
|
|
||||||
# image-plane extent of the mesh through ITS camera, so the page is measurable and differs
|
|
||||||
# per view: measured 17.5116 x 15.9772 mm for uv_1 (aspect 1.0960) and 17.5114 x 15.9820 mm
|
|
||||||
# for uv_2 (aspect 1.0957). `vector:` above says 16.4 x 16.0 (aspect 1.0250), which stretched
|
|
||||||
# the artwork by 6.9% along u. `auto` derives it and proves the layout really is a
|
|
||||||
# bounds-projection from the named camera before using the result.
|
|
||||||
#
|
|
||||||
# `px_per_mm: 200` reproduces the existing density exactly (3280/16.4 = 3700/18.5 = 200.0),
|
|
||||||
# and the page is set from the rounded raster so page and raster always share an aspect.
|
|
||||||
#
|
|
||||||
# `fixed` keeps `texture_patch.vector`. Unwrapped layouts (`produce`, `compute`) are not
|
|
||||||
# projections and must use it — a camera-derived page would be meaningless there.
|
|
||||||
#
|
|
||||||
# To overlay a texture on a render from its camera: set the render resolution to the emitted
|
|
||||||
# raster size and the background image to "Stretch to fill". At any other resolution the
|
|
||||||
# scene aspect (1280x720 here) will distort the overlay regardless of the page.
|
|
||||||
page:
|
page:
|
||||||
mode: auto
|
mode: auto # auto = derived per view (projections only); fixed = use vector above
|
||||||
px_per_mm: 200
|
px_per_mm: 200
|
||||||
pairs:
|
pairs:
|
||||||
- id: uv_1 # layout authored from cam_uv_1
|
- id: uv_1
|
||||||
camera: cam_uv_1 # REGION camera: what this pair may own
|
camera: cam_uv_1 # REGION camera, not the pattern source
|
||||||
uv_map: uv_1
|
uv_map: uv_1
|
||||||
- id: uv_2
|
- id: uv_2
|
||||||
camera: cam_uv_2
|
camera: cam_uv_2
|
||||||
uv_map: uv_2
|
uv_map: uv_2
|
||||||
merge:
|
merge:
|
||||||
# `authored` honours the seam marked on the mesh: 32 edges on y = 0, which cuts the
|
mode: authored # honour the seam marked on the mesh
|
||||||
# surface into 2 components of 177.882 / 177.885 mm^2 (50.0 / 50.0). Because the two
|
incidence_limit_deg: 62.0 # calibrated to THIS revision's cameras; re-measure if they move
|
||||||
# cameras are mirrored about XZ that plane is also where incidence_uv1 == incidence_uv2,
|
max_hole_fraction: 0.02 # enforced: the run fails above this rather than shipping a gap
|
||||||
# so the hand-drawn seam coincides with the metric-optimal boundary: measured coverage
|
min_footprint_mm2: 2.0e-3
|
||||||
# differs from `computed` by 0.0014 at 60 deg and by nothing from 62 deg up.
|
|
||||||
mode: authored
|
|
||||||
# Calibrated on this mesh (355.767 mm^2, 1977 tris): 60 deg leaves 0.0218 holes and FAILS
|
|
||||||
# max_hole_fraction, 62 deg leaves 0.0000. 65 deg was an earlier estimate from the
|
|
||||||
# pre-edit mesh; 62 is the measured threshold.
|
|
||||||
incidence_limit_deg: 62.0
|
|
||||||
min_footprint_mm2: 2.0e-3 # ~1 printer dot at 600 dpi, squared x 1.1
|
|
||||||
min_triangle_coverage: 0.60
|
min_triangle_coverage: 0.60
|
||||||
max_hole_fraction: 0.02
|
|
||||||
component_policy: best_coverage # not `pinned`: both views qualify, so let the metric rank
|
component_policy: best_coverage # not `pinned`: both views qualify, so let the metric rank
|
||||||
seam:
|
seam:
|
||||||
boundary: use_seam # read mesh.edges[].use_seam as a hard graph cut
|
boundary: use_seam # read mesh.edges[].use_seam as a hard graph cut
|
||||||
pins: [] # {component, view, reason} to override a component's owner
|
pins: [] # {component, view, reason} to override a component's owner
|
||||||
# Empty band along the seam, to absorb two-pass registration error. Without it the two
|
|
||||||
# files butt exactly at the seam and any placement error makes them overlap there, which
|
|
||||||
# is the delamination case.
|
|
||||||
#
|
|
||||||
# The width is derived, not guessed. Each pass is placed to within
|
|
||||||
# `alignment_tolerance_mm`, so the two can close on each other by twice that; the band
|
|
||||||
# must be at least that wide. Default total = 2 x tolerance = 0.2 mm, half per file, and
|
|
||||||
# `total_mm` overrides the derivation if a different margin is wanted.
|
|
||||||
gap:
|
gap:
|
||||||
enabled: true # false = hard butt at the seam, no band at all
|
enabled: true # false = hard butt at the seam, no band at all
|
||||||
apply: post_process # post_process = straight edge | geometry_trim = sawtooth
|
apply: post_process # post_process = straight edge | geometry_trim = sawtooth
|
||||||
treatment: no_print # the band carries nothing
|
treatment: no_print
|
||||||
alignment_tolerance_mm: 0.1 # per-pass placement accuracy, mm
|
alignment_tolerance_mm: 0.1 # per-pass placement accuracy; band total = 2x this
|
||||||
# total_mm: 0.2 # default: 2 x alignment_tolerance_mm
|
|
||||||
print:
|
print:
|
||||||
# Every artifact carries a full-page black `<rect id="background-fill">`. That background
|
base_fill: owned # white ground, already split by the seam
|
||||||
# is itself a print element, so it cannot be used as the "no print" marker: the split is
|
|
||||||
# expressed by WHERE the white mesh layer is drawn and where the colour patches are.
|
|
||||||
#
|
|
||||||
# `owned` keeps the same white ground the `compute` texture carries, but clipped to the
|
|
||||||
# triangles this view owns. So white is split by the seam exactly like the pattern is,
|
|
||||||
# and neither file paints white over the other's territory.
|
|
||||||
base_fill: owned
|
|
||||||
|
|
||||||
# Post-generation printability inspection (`orisys-design inspect`). Advisory
|
# Advisory only: reports a verdict, never blocks generation or changes exit codes.
|
||||||
# only: it reports a verdict but never blocks generation or changes exit codes.
|
|
||||||
# Millimetres are measured on the vector page declared above.
|
# Millimetres are measured on the vector page declared above.
|
||||||
printability:
|
printability:
|
||||||
printer_dpi: 600 # target UV printer resolution
|
printer_dpi: 600 # target UV printer resolution
|
||||||
@@ -149,13 +104,6 @@ printability:
|
|||||||
max_sliver_ratio: 4.0 # longest edge / minimum width
|
max_sliver_ratio: 4.0 # longest edge / minimum width
|
||||||
min_patch_area_mm2: 1.0e-4 # area above which a polygon is a material feature
|
min_patch_area_mm2: 1.0e-4 # area above which a polygon is a material feature
|
||||||
|
|
||||||
collections:
|
|
||||||
frames: FRAMES
|
|
||||||
export_compute: EXPORT_COMPUTE
|
|
||||||
assembly: ASSEMBLY
|
|
||||||
design_sim: DESIGN_SIM
|
|
||||||
exclude: EXCLUDE
|
|
||||||
|
|
||||||
notes: |
|
notes: |
|
||||||
Canonical source is geometry.blend with named collections and objects.
|
Canonical source is geometry.blend with named collections and objects.
|
||||||
Compute export uses sensing_surface + cam_sensor relative to origin_sensor.
|
Compute export uses sensing_surface + cam_sensor relative to origin_sensor.
|
||||||
|
|||||||
@@ -488,7 +488,9 @@ def cmd_texture_views(revision_dir: Path, args: argparse.Namespace) -> int:
|
|||||||
overrides = texture_overrides_from_args(args)
|
overrides = texture_overrides_from_args(args)
|
||||||
try:
|
try:
|
||||||
settings = texture_views.resolve_views_settings(paths, overrides)
|
settings = texture_views.resolve_views_settings(paths, overrides)
|
||||||
pairs = texture_views.resolve_view_pairs(paths, settings)
|
pairs = texture_views.resolve_view_pairs(
|
||||||
|
paths, settings, grayscale=bool(getattr(args, "grayscale", False))
|
||||||
|
)
|
||||||
output_dir = resolve_texture_output_dir(root, paths, args.output_dir)
|
output_dir = resolve_texture_output_dir(root, paths, args.output_dir)
|
||||||
except TextureConfigError as exc:
|
except TextureConfigError as exc:
|
||||||
for message in str(exc).splitlines():
|
for message in str(exc).splitlines():
|
||||||
@@ -496,7 +498,9 @@ def cmd_texture_views(revision_dir: Path, args: argparse.Namespace) -> int:
|
|||||||
return 2
|
return 2
|
||||||
|
|
||||||
targets = texture_views.multiview_output_paths(output_dir, pairs)
|
targets = texture_views.multiview_output_paths(output_dir, pairs)
|
||||||
job = texture_views.build_multiview_job(paths, settings, pairs, output_dir)
|
job = texture_views.build_multiview_job(
|
||||||
|
paths, settings, pairs, output_dir, grayscale=bool(getattr(args, "grayscale", False))
|
||||||
|
)
|
||||||
job_path = texture_views.write_multiview_job(job, output_dir)
|
job_path = texture_views.write_multiview_job(job, output_dir)
|
||||||
# The job path is a fact about an intermediate file, not a deliverable: verbose only.
|
# The job path is a fact about an intermediate file, not a deliverable: verbose only.
|
||||||
console.detail(f"Wrote multi-view texture job: {job_path}")
|
console.detail(f"Wrote multi-view texture job: {job_path}")
|
||||||
@@ -2096,7 +2100,16 @@ def main(argv: list[str] | None = None) -> None:
|
|||||||
help="Edge sampling grid for partially covered patches",
|
help="Edge sampling grid for partially covered patches",
|
||||||
)
|
)
|
||||||
texture_views_p.add_argument(
|
texture_views_p.add_argument(
|
||||||
"--seed", type=int, default=None, help="Base seed; pair i uses seed + i"
|
"--seed", type=int, default=None, help="Base seed; every pair shares it deliberately"
|
||||||
|
)
|
||||||
|
texture_views_p.add_argument(
|
||||||
|
"--grayscale",
|
||||||
|
action="store_true",
|
||||||
|
help=(
|
||||||
|
"Also write a laser-engraving set per view (<stem>_gray.png/.svg/.pdf): pattern "
|
||||||
|
"dark on a white page, tone = Rec.709 luminance of the colour it replaces. "
|
||||||
|
"Additive - the colour PNG/SVG/PDF set is unchanged"
|
||||||
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
manifest_p = sub.add_parser("manifest", help="Write release manifest with file hashes")
|
manifest_p = sub.add_parser("manifest", help="Write release manifest with file hashes")
|
||||||
|
|||||||
@@ -111,13 +111,33 @@ class TextureOutputTarget:
|
|||||||
"""Backward-compatible convenience for the PNG artifact path."""
|
"""Backward-compatible convenience for the PNG artifact path."""
|
||||||
return Path(output_dir) / self.artifact_filename("png")
|
return Path(output_dir) / self.artifact_filename("png")
|
||||||
|
|
||||||
def artifact_paths(self, output_dir: Path) -> dict[str, Path]:
|
def gray_filename(self, format_name: str) -> str:
|
||||||
"""Return absolute artifact paths in stable PNG/SVG/PDF order."""
|
"""Return the laser-engraving companion's name for one format.
|
||||||
|
|
||||||
|
A separate stem (``<stem>_gray.<ext>``) rather than a replacement, so the colour
|
||||||
|
production set stays byte-identical and a print run cannot accidentally consume
|
||||||
|
engraving artwork.
|
||||||
|
"""
|
||||||
|
return f"{Path(self.filename).stem}_gray.{format_name}"
|
||||||
|
|
||||||
|
def artifact_paths(self, output_dir: Path, grayscale: bool = False) -> dict[str, Path]:
|
||||||
|
"""Return absolute artifact paths in stable PNG/SVG/PDF order.
|
||||||
|
|
||||||
|
``grayscale`` appends the laser-engraving set (``gray_png``/``gray_svg``/``gray_pdf``)
|
||||||
|
after the print set. Off by default, so the single-view path and every existing caller
|
||||||
|
are unaffected.
|
||||||
|
"""
|
||||||
resolved_dir = Path(output_dir).resolve()
|
resolved_dir = Path(output_dir).resolve()
|
||||||
return {
|
paths = {
|
||||||
format_name: resolved_dir / self.artifact_filename(format_name)
|
format_name: resolved_dir / self.artifact_filename(format_name)
|
||||||
for format_name in TEXTURE_ARTIFACT_FORMATS
|
for format_name in TEXTURE_ARTIFACT_FORMATS
|
||||||
}
|
}
|
||||||
|
if grayscale:
|
||||||
|
for format_name in TEXTURE_ARTIFACT_FORMATS:
|
||||||
|
paths[f"gray_{format_name}"] = (
|
||||||
|
resolved_dir / self.gray_filename(format_name)
|
||||||
|
)
|
||||||
|
return paths
|
||||||
|
|
||||||
def to_job_entry(self, output_dir: Path) -> dict[str, Any]:
|
def to_job_entry(self, output_dir: Path) -> dict[str, Any]:
|
||||||
"""Return the v3 job entry Blender consumes (absolute paths, ordered)."""
|
"""Return the v3 job entry Blender consumes (absolute paths, ordered)."""
|
||||||
|
|||||||
@@ -166,6 +166,8 @@ class ViewPair:
|
|||||||
target: TextureOutputTarget
|
target: TextureOutputTarget
|
||||||
seed: int | None
|
seed: int | None
|
||||||
page_mode: str = PAGE_DEFAULTS["mode"]
|
page_mode: str = PAGE_DEFAULTS["mode"]
|
||||||
|
#: Also emit the laser-engraving PNG for this pair (`--grayscale`).
|
||||||
|
grayscale: bool = False
|
||||||
|
|
||||||
def to_job_entry(self, output_dir: Path) -> dict[str, Any]:
|
def to_job_entry(self, output_dir: Path) -> dict[str, Any]:
|
||||||
return {
|
return {
|
||||||
@@ -177,7 +179,9 @@ class ViewPair:
|
|||||||
"page": self.page_mode,
|
"page": self.page_mode,
|
||||||
"artifacts": {
|
"artifacts": {
|
||||||
format_name: str(path)
|
format_name: str(path)
|
||||||
for format_name, path in self.target.artifact_paths(output_dir).items()
|
for format_name, path in self.target.artifact_paths(
|
||||||
|
output_dir, grayscale=self.grayscale
|
||||||
|
).items()
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -478,6 +482,7 @@ def resolve_view_pairs(
|
|||||||
paths: RevisionPaths,
|
paths: RevisionPaths,
|
||||||
settings: TexturePatchSettings,
|
settings: TexturePatchSettings,
|
||||||
block: Mapping[str, Any] | None = None,
|
block: Mapping[str, Any] | None = None,
|
||||||
|
grayscale: bool = False,
|
||||||
) -> tuple[ViewPair, ...]:
|
) -> tuple[ViewPair, ...]:
|
||||||
"""Resolve the views block into concrete pairs, or raise ViewPairConfigError."""
|
"""Resolve the views block into concrete pairs, or raise ViewPairConfigError."""
|
||||||
if block is None:
|
if block is None:
|
||||||
@@ -520,6 +525,7 @@ def resolve_view_pairs(
|
|||||||
),
|
),
|
||||||
seed=None if seed is None else int(seed),
|
seed=None if seed is None else int(seed),
|
||||||
page_mode=str(raw.get("page") or default_page_mode),
|
page_mode=str(raw.get("page") or default_page_mode),
|
||||||
|
grayscale=grayscale,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
return tuple(pairs)
|
return tuple(pairs)
|
||||||
@@ -623,6 +629,7 @@ def build_multiview_job(
|
|||||||
pairs: Sequence[ViewPair],
|
pairs: Sequence[ViewPair],
|
||||||
output_dir: Path,
|
output_dir: Path,
|
||||||
block: Mapping[str, Any] | None = None,
|
block: Mapping[str, Any] | None = None,
|
||||||
|
grayscale: bool = False,
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""Build the JSON job ``random_patch_multiview.py`` consumes."""
|
"""Build the JSON job ``random_patch_multiview.py`` consumes."""
|
||||||
if not pairs:
|
if not pairs:
|
||||||
@@ -633,6 +640,9 @@ def build_multiview_job(
|
|||||||
return {
|
return {
|
||||||
"schema": MULTIVIEW_JOB_SCHEMA,
|
"schema": MULTIVIEW_JOB_SCHEMA,
|
||||||
"mode": "patches-multiview",
|
"mode": "patches-multiview",
|
||||||
|
# Also emit the laser-engraving PNG per view. Recorded in the job so the run's
|
||||||
|
# inputs are fully described by the job rather than by how it was invoked.
|
||||||
|
"grayscale": bool(grayscale),
|
||||||
"revision_dir": str(paths.root.resolve()),
|
"revision_dir": str(paths.root.resolve()),
|
||||||
"source_blend": str(paths.source_blend.resolve()),
|
"source_blend": str(paths.source_blend.resolve()),
|
||||||
"objects": {"mesh": settings.mesh},
|
"objects": {"mesh": settings.mesh},
|
||||||
@@ -661,10 +671,10 @@ def multiview_output_paths(
|
|||||||
output_dir: Path,
|
output_dir: Path,
|
||||||
pairs: Sequence[ViewPair],
|
pairs: Sequence[ViewPair],
|
||||||
) -> list[tuple[ViewPair, dict[str, Path]]]:
|
) -> list[tuple[ViewPair, dict[str, Path]]]:
|
||||||
"""Pair each view with its ordered PNG/SVG/PDF artifact paths."""
|
"""Pair each view with its ordered PNG/SVG/PDF (and optional laser-gray) artifact paths."""
|
||||||
resolved_dir = Path(output_dir).resolve()
|
resolved_dir = Path(output_dir).resolve()
|
||||||
return [
|
return [
|
||||||
(pair, pair.target.artifact_paths(resolved_dir))
|
(pair, pair.target.artifact_paths(resolved_dir, grayscale=pair.grayscale))
|
||||||
for pair in pairs
|
for pair in pairs
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||