sensor: finger-xynova revision: r01 status: draft # Authoring unit in Blender (mm). Export converts using meters_per_source_unit. authoring_unit: mm meters_per_source_unit: 0.001 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: sensor: origin_sensor objects: sensing_surface: sensing_surface cam_sensor: cam_sensor origin_sensor: origin_sensor # Headless colored-patch texture generation (`orisys-design texture`). # UV unwrapping is still done in the Blender GUI: the headless run consumes the # UV maps already stored in source_blend and never creates or unwraps one. # One PNG/SVG/PDF set is written per UV map below. All maps and formats share # the settings and patch colours, so they differ only by UV layout. # These are the revision defaults; CLI flags override every value. texture_patch: mesh: sensing_surface camera: cam_sensor uv_outputs: produce: patch_texture_produce.png # hardware production layout compute: patch_texture_compute.png # compute/runtime layout width: 3700 # 1850*2 height: 3280 # 1640*2 # Camera frame is square (1000x1000), so equal counts make camera-space # elements square. The original 80x45 grid was copied from a 16:9 camera. nx: 80 ny: 80 color_r: 0.05 edge_sample_grid: 5 seed: 7 # fixed so PNG/SVG/PDF share one reproducible colour set vector: width_mm: 18.5 # editable SVG/PDF production page width height_mm: 16.4 # editable SVG/PDF production page height cross: enabled: true length_px: 40 thickness_px: 3 color: [1.0, 0.0, 0.0, 1.0] # red reference cross # Multi-view production textures (`orisys-design texture-views`). # # `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: enabled: true pattern_camera: cam_sensor # the pattern every layout carries, same source as `compute` pairs: - id: uv_1 camera: cam_uv_1 # REGION camera uv_map: uv_1 - id: uv_2 camera: cam_uv_2 uv_map: uv_2 merge: mode: authored # honour the 45-edge seam marked on the mesh incidence_limit_deg: 62.0 # measured on this part: hole fraction 0.0002 at 62 deg min_footprint_mm2: 2.0e-3 min_triangle_coverage: 0.60 max_hole_fraction: 0.02 component_policy: best_coverage seam: boundary: use_seam 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: enabled: false # false = hard butt at the seam, no band at all apply: post_process # post_process = straight edge | geometry_trim = sawtooth treatment: no_print # the band carries nothing alignment_tolerance_mm: 0.1 # per-pass placement accuracy, mm print: base_fill: owned # white ground, clipped by the seam # Post-generation printability inspection (`orisys-design inspect`). Advisory # only: it reports a verdict but never blocks generation or changes exit codes. # Millimetres are measured on the vector page declared above. printability: printer_dpi: 600 # target UV printer resolution min_dots: 2 # printer dots required across the smallest feature min_feature_mm: 0.10 # smallest acceptable feature width min_edge_mm: 0.05 # shortest acceptable polygon edge 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 # Optical stack above the markers, ordered from the camera inward. # The markers sit on the sensing surface underneath these layers, so their image # 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: marker_surface: sensing_surface layers: # Solids, not surface pairs: `shell` and `rubber-1` are each one closed mesh, # so the ray enters and leaves the same object. # Stack: camera > shell > rubber-1 > sensing_surface > rubber-2. # rubber-2 is BEHIND the markers, so it does not refract camera light and is # deliberately not a layer. - name: plastic_shell solid: shell ior: 1.59 ior_source: assumed_pc_literature - name: clear_rubber solid: rubber-1 ior: 1.43 ior_source: assumed_silicone_literature # Probe sensitivity bench (`orisys-design deform|render|track|sensitivity|optimise`). # Deforms the vertices of `group` through known x/y/z displacements expressed in a # 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: mesh: sensing_surface camera: cam_sensor group: probe uv_map: compute texture: patch_texture_compute.png # Proportional falloff radius for the outer rubber. Note this mesh has a median # 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. radius_mm: 0.5 falloff: smooth step_mm: 0.01 max_mm: 0.10 axes: [x, y, z] # Frames are rendered at the revision's real camera resolution. Keep this in step with # the camera in source/geometry.blend: the pipeline overrides the scene resolution from # 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] process_resolution: [250, 250] # Mirrors the SDK sensor tracker keys (orisys-sdk config/finger.json), EXCEPT # dis_mode. The estimator window is 8 * 2**dis_mode input px (16 at dis_mode=1, 8 at 0) # 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. # If the deployed tracker is fixed at dis_mode=1, run the bench at 500x500 instead. tracker: algorithm: dis dis_preset: medium dis_mode: 0 # Standard (not AgX) and denoise off: a filmic view transform silently distorts # the marker values the tracker is supposed to measure. render: engine: CYCLES samples: 16 denoise: false view_transform: Standard interpolate: Closest seed: 0 # Noise calibration and the injected noise model. # # The estimator's `floor_px` is measured as reference vs a re-render at seed+1, which # is Cycles antialiasing jitter, NOT sensor noise: on this data it differs by ~5 grey # 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: # 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 roi_fraction: 0.18 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] draws: 1 seed: 0 models: [render_seed, injected] scales: [render] collections: frames: FRAMES export_compute: EXPORT_COMPUTE assembly: ASSEMBLY design_sim: DESIGN_SIM exclude: EXCLUDE notes: | Canonical source is geometry.blend with named collections and objects. Compute export uses sensing_surface + cam_sensor relative to origin_sensor. Studio observation cameras are derived from mesh bounds; no cam_viz required.