b5d7533e24
The two canonical examples taught the advanced path: they sequenced get_img() -> compute_deformation() -> compute_contact() -> read_info() by hand and unpacked a 13-element tuple positionally, which is what led developers to reach for the compute backend, the capture queue and the stitch mode. The managed alternative already existed and was documented nowhere. Library (add-only, nothing removed): - FrameSnapshot.image_size_raw carries the capture resolution, so a caller can compare it with the processed frame instead of reaching into Sensor. - StreamPipeline.reset() resets the tracker and force filters *safely*. It is queued and applied by the flow stage before its next frame, never inline: Sensor.reset rebases initial_img on the current Sensor.img, which the flow stage is reading concurrently, so a caller-thread reset would capture a half-written reference frame. Idempotent. - TactileProcessor.pipeline(produce) and FingerRuntime.pipeline() build a pipeline without the caller naming a backend concept; the latter is `with runtime.pipeline(contact=True) as pipe`. Examples: - examples/basic_pipeline.py and examples/finger_pipeline.py rewritten onto StreamPipeline + FrameSnapshot: one object, one call per frame, no tuple, and no compute backend, queue or stitch setting in sight. - examples/archive/basic_pipeline_manual.py and _finger_pipeline_manual.py keep the previous versions byte-for-byte apart from a supersede header, so code copied from them keeps working. The finger header also records the archived --show defect (store_true with default=True, so it could never be disabled). - examples/_display.py holds the shared display helpers, so an example no longer hand-rolls depth normalisation and colormap application. - examples/README.md leads with the recommended pair and documents the manual form as still-supported, and states that examples/gui/ is moving to orisys-studio. Tests: three added to tests/test_pipeline.py, covering the reset queue (it must not run on the caller's thread and must land before the first compute_flow), image_size_raw, and the None case when a producer reports no size. Verified the reset test fails when reset() is made inline, so it discriminates. Verified end-to-end against real media: basic_pipeline on data/sample/s1_480p.mp4 (box) reaches ~113 FPS and exits 0; finger_pipeline on data/dd02-0703/1.mp4 (finger) reaches ~140 FPS with mask coverage 0.41 and exits 0. Both archived manual versions still run. tests/test_pipeline.py: 20 passed. Full suite: 342 passed, 3 failed - all three failures reproduce on the pristine tree (import precedence, longrun timing, and a stale ruff version literal in docs/code-review.md) and are unrelated to this change.
120 lines
5.3 KiB
Python
120 lines
5.3 KiB
Python
"""ORY-BOX / ORY-MINI: read a sensor and get tactile information, in one loop.
|
|
|
|
The whole recommended flow:
|
|
|
|
sensor -> StreamPipeline -> FrameSnapshot per frame
|
|
|
|
One object reads the camera, pre-processes (stitch, grey, resize), computes the deformation field
|
|
and the forces, and hands back one immutable snapshot per frame. There is no stage sequence to
|
|
remember and no tuple to unpack by position.
|
|
|
|
What you do NOT need to touch: the compute backend, the capture queue, the stitch mode, the
|
|
low-pass filter, or the order in which the stages run. Those are the SDK's concern and
|
|
``StreamPipeline`` sequences them on three concurrent threads. If you later need that control, the
|
|
manual form is preserved in ``examples/archive/basic_pipeline_manual.py``.
|
|
|
|
Run:
|
|
|
|
python examples/basic_pipeline.py # first camera, box config
|
|
python examples/basic_pipeline.py -v 0 -c mini
|
|
python examples/basic_pipeline.py -v recording.mp4 -c box
|
|
python examples/basic_pipeline.py --no-show # headless: print only
|
|
|
|
Keys: 'q' quit, 'r' reset the tracker.
|
|
"""
|
|
|
|
import argparse
|
|
import os
|
|
import sys
|
|
|
|
# Set before OpenCV is imported: stops the capture backend applying its own transform on top of
|
|
# the SDK's preprocessing. This is a capture detail, not a tuning knob you need to own - it is
|
|
# here so the file can be copied as a whole.
|
|
os.environ.setdefault("OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS", "0")
|
|
|
|
import cv2
|
|
|
|
import orisys
|
|
from _display import depth_to_color, draw_contact, draw_flow_arrows
|
|
|
|
|
|
def main() -> int:
|
|
parser = argparse.ArgumentParser(description="Read a sensor and print its tactile information.")
|
|
parser.add_argument("-v", "--video", default="0",
|
|
help="camera index or video file path (default: 0)")
|
|
parser.add_argument("-c", "--config", default="box",
|
|
help="product config: box, mini, finger, or a config path")
|
|
parser.add_argument("--cal", default=None,
|
|
help="force calibration file; omit to use the sensor bundle's own")
|
|
parser.add_argument("--no-show", dest="show", action="store_false",
|
|
help="do not open the display windows")
|
|
parser.add_argument("-verbose", action="store_true", help="SDK verbose logs")
|
|
args = parser.parse_args()
|
|
|
|
# ------------------------------------------------------------------ create the object
|
|
# That is the whole setup. The source may be a camera index or a video path.
|
|
source = int(args.video) if args.video.isdigit() else args.video
|
|
sensor = orisys.Sensor(source, config_name=args.config, cal_path=args.cal,
|
|
verbose=args.verbose)
|
|
|
|
# contact=True runs contact analysis. include=... selects which arrays each snapshot carries;
|
|
# arrays that are not included are None, so you pay only for what you use.
|
|
include = ("image", "flow", "depth", "centroid", "contour") if args.show else ("image",)
|
|
|
|
if args.show:
|
|
print("Keys: 'q' quit, 'r' reset tracker.")
|
|
|
|
try:
|
|
with orisys.StreamPipeline(sensor, sensor.get_img, contact=True,
|
|
include=include) as pipe:
|
|
while True:
|
|
# ------------------------------------------------------ read one frame
|
|
# Blocks until a frame newer than the last one is ready. Everything in the
|
|
# snapshot belongs to the same frame.
|
|
snap = pipe.wait()
|
|
if snap is None:
|
|
print("\nInput ended.")
|
|
break
|
|
|
|
# ------------------------------------------------------ use the results
|
|
# Scalars are calibrated Newtons when the bundle ships a calibration; the *_raw
|
|
# pair is always the unfiltered integrator output.
|
|
touching = "yes" if snap.centroid is not None else "no"
|
|
print(
|
|
f"FPS={snap.fps:5.1f} "
|
|
f"normal={snap.normal_force:8.4f} N (raw {snap.normal_force_raw:10.2f}) "
|
|
f"shear=({snap.shear_x:7.4f}, {snap.shear_y:7.4f}) "
|
|
f"contact={touching} age={snap.age_ms:4.1f} ms",
|
|
end="\r",
|
|
)
|
|
|
|
if args.show and snap.image is not None:
|
|
if snap.flow is not None:
|
|
cv2.imshow("deformation", draw_flow_arrows(
|
|
snap.image, snap.flow, threshold=2, grid_spacing=20))
|
|
if snap.contour is not None and snap.centroid is not None:
|
|
cv2.imshow("contact", draw_contact(
|
|
snap.image, snap.contour, snap.centroid))
|
|
colour = depth_to_color(snap.depth)
|
|
if colour is not None:
|
|
cv2.imshow("depth", colour)
|
|
|
|
key = cv2.waitKey(1) & 0xFF
|
|
if key == ord("q"):
|
|
print("\nExiting.")
|
|
break
|
|
if key == ord("r"):
|
|
# Queued onto the flow stage, so this is safe from the consumer loop.
|
|
pipe.reset()
|
|
print("\nTracker reset.")
|
|
except KeyboardInterrupt:
|
|
print("\nInterrupted.")
|
|
finally:
|
|
sensor.disconnect()
|
|
if args.show:
|
|
cv2.destroyAllWindows()
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main()) |