Files
ykcheungab ceb06f5702 为基础示例接入按设备 ID 选择的力标定
让使用者从本地目录选标定并看见实际单位。短片回放及相关测试通过。
2026-10-05 14:02:40 +08:00

143 lines
6.6 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
python examples/basic_pipeline.py -v 0 -c box --calibration-dir data/calibrations --device-id BOX-001
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")
calibration = parser.add_mutually_exclusive_group()
calibration.add_argument("--cal", default=None,
help="force calibration file; omit to use the sensor bundle's own")
calibration.add_argument("--calibration-dir", default=None,
help="root containing <product>/<device-id>.json")
parser.add_argument("--device-id", default=None,
help="physical sensor ID; required with --calibration-dir")
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()
if args.calibration_dir is not None and args.device_id is None:
parser.error("--calibration-dir requires --device-id")
if args.device_id is not None and args.calibration_dir is None and args.cal is None:
parser.error("--device-id requires --calibration-dir or --cal")
# ------------------------------------------------------------------ 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,
calibration_dir=args.calibration_dir, device_id=args.device_id,
verbose=args.verbose,
)
fn_unit, fx_unit, fy_unit = sensor.force_units
selected = sensor.calibration.source_path if sensor.calibration else None
print(f"Force calibration: {selected or 'none'}")
print(f"Force units: FN={fn_unit}, FX={fx_unit}, FY={fy_unit}")
if sensor.device_calibration_missing:
print(
f"Warning: no calibration for {args.device_id!r} under "
f"{args.calibration_dir}; force is raw."
)
# 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
# The *_raw pair is always the unfiltered integrator output.
touching = "yes" if snap.centroid is not None else "no"
fn_unit, fx_unit, fy_unit = sensor.force_units
print(
f"FPS={snap.fps:5.1f} "
f"normal={snap.normal_force:8.4f} {fn_unit} (raw {snap.normal_force_raw:10.2f}) "
f"shear=({snap.shear_x:7.4f} {fx_unit}, {snap.shear_y:7.4f} {fy_unit}) "
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 args.show else -1
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())