repose_angle_silhouette — DRIVE granular op

• Data kinds: image2d → table

• Call: import fullseye as fs; fs.ledger.repose_angle_silhouette(side, *, threshold: 'float' = 0.5, toe_frac: 'float' = 0.15, apex_frac: 'float' = 0.15, min_points: 'int' = 8, correct_ground: 'bool' = False, method: 'str' = 'edge') -> 'dict' (to call the implementation directly, import granular; granular.repose_angle_silhouette(side, *, threshold: 'float' = 0.5, toe_frac: 'float' = 0.15, apex_frac: 'float' = 0.15, min_points: 'int' = 8, correct_ground: 'bool' = False, method: 'str' = 'edge') -> 'dict'; from the registry, opsdrive.get("repose_angle_silhouette"))

Usage

Reads the angle of repose φ from a side image (powder coverage) — fits a line separately to the left and right slopes, excluding the toe and the apex.

Procedure (formalising the practice of Sunday et al. 2020 §5.3): (1) per-column powder-surface height = position of the edge pixel where the coverage drops below threshold + its coverage (sub-pixel; method="edge", default) (2) ground = lowest crossing in each column (same interpolation) → ground slope β via measure.fit_line. method="column_sum" is the column sum of coverage (exact sub-pixel if the edge is anti-aliased, but if the powder's pixel value deviates from 1, tan φ is off by that ratio: merely clipping uniform noise ±0.1 to [0, 1] gave 0.62 degrees too low, measured)

(3) apex = column of maximum height (4) only points whose height above ground is at least toe_frac·H and at most (1 − apex_frac)·H are

fitted with fit_line (total least squares) separately for left and right (5) φ = mean of left and right.

• correct_ground: removes the reference-plane slope β by subtracting tangents (tan φ' = tan θ ∓ tan β; exact for a shear-type datum tilt). For a camera roll, subtracting angles is correct, and the two differ by more than 1 degree at β = 5 degrees, so by default it is not removed. Suspect the reference plane only when the left-right difference asymmetry_deg is large.

• Returns: phi_deg, phi_left_deg, phi_right_deg, asymmetry_deg, ground_deg, height_px, base_px, apex_col, left_cols / right_cols (range of fitted columns), rms_left / rms_right (residual from the line [px]), n_left / n_right.

• Raises ValueError: not an image / coverage outside [0, 1] / no powder visible / the pile touches the top, left or right edge of the image (cut off) / fewer fit points than min_points (pile too small) / toe_frac + apex_frac ≥ 1 / misspelled method.

Resolution guide (measured with uniform noise ±0.1 on the coverage, 4 angles × 6 seeds): 0.007 degrees at a pile width of 400 px, 0.2 degrees at 50 px, 0.6 degrees at 25 px.

References (sample data, literature)

• Sample-data catalog (download URLs / licences) — 2-D uses skimage.data (BSD/public domain) plus synthetic images; 3-D lists download URLs for real data sources (Stanford, PDS, …).

• Operator provenance and references — the sources of the research/methods this op family came from.

• The canonical algorithm (author, year) and its uses are named in the family usage guide above.

Runnable examples (verified samples that actually call this op)

• poc_granular_heap_repose — py -3.11 examples/poc_granular_heap_repose.py

Ops the type connects to (they accept table as input)

course_layout · course_occupancy · course_contains · world_build · world_camera · world_move · lidar_scan · rss_longitudinal_same

Same category (granular)

heap_volume_cone · heap_mass · heap_volume_heightmap · beverloo_rate · beverloo_fit · discharge_synth · dispense_mass_from_video · hopper_discharge_rate


*Provenance: granular.py — DRIVE operator registry. This per-op note is generated by tools/opdocs.py md (do not hand-edit).*

© 2026 Kazufumi Furuse — Fullseye operator documentation. Licensed under Apache-2.0.