clean up source folder
This commit is contained in:
+2
-1
@@ -14,4 +14,5 @@ orisys.egg-info/
|
|||||||
/models/
|
/models/
|
||||||
/doc/.quarto/
|
/doc/.quarto/
|
||||||
*.npy
|
*.npy
|
||||||
*.png
|
*.png
|
||||||
|
SDK/
|
||||||
+67
-25
@@ -5,6 +5,28 @@ import subprocess
|
|||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
from importlib.machinery import EXTENSION_SUFFIXES
|
from importlib.machinery import EXTENSION_SUFFIXES
|
||||||
|
|
||||||
|
INCLUDE_LAYOUT = {
|
||||||
|
"configs": {
|
||||||
|
"source_dir": Path("src/orisys/configs"),
|
||||||
|
"target_subdir": Path("configs"),
|
||||||
|
"patterns": ["ddjx01.json"],
|
||||||
|
},
|
||||||
|
"examples": {
|
||||||
|
"source_dir": Path("."),
|
||||||
|
"target_subdir": Path("examples"),
|
||||||
|
"files": [
|
||||||
|
Path("example1_basic_pipeline.py"),
|
||||||
|
Path("example2_two_sensor_pipeline.py"),
|
||||||
|
Path("example3_plot_data.py"),
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"docs": {
|
||||||
|
"source_dir": Path("docs"),
|
||||||
|
"target_subdir": Path("docs"),
|
||||||
|
"patterns": ["*.md", "*.pdf"],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def install_nuitka():
|
def install_nuitka():
|
||||||
print("Checking Nuitka installation...")
|
print("Checking Nuitka installation...")
|
||||||
@@ -49,11 +71,49 @@ def move_wheel_to_sdk(wheel_path: Path, version: str) -> Path:
|
|||||||
return dest_wheel
|
return dest_wheel
|
||||||
|
|
||||||
|
|
||||||
|
def copy_included_files(pkg_dir: Path) -> dict[str, int]:
|
||||||
|
copied_counts = {}
|
||||||
|
for group_name, group_cfg in INCLUDE_LAYOUT.items():
|
||||||
|
src_dir = group_cfg["source_dir"]
|
||||||
|
dst_dir = pkg_dir / group_cfg["target_subdir"]
|
||||||
|
dst_dir.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
copied = 0
|
||||||
|
if "files" in group_cfg:
|
||||||
|
for file_path in group_cfg["files"]:
|
||||||
|
if file_path.exists():
|
||||||
|
shutil.copy(file_path, dst_dir)
|
||||||
|
copied += 1
|
||||||
|
print(f"Copied {group_name}: {file_path.name}")
|
||||||
|
else:
|
||||||
|
for pattern in group_cfg.get("patterns", []):
|
||||||
|
for src_file in src_dir.glob(pattern):
|
||||||
|
shutil.copy(src_file, dst_dir)
|
||||||
|
copied += 1
|
||||||
|
print(f"Copied {group_name}: {src_file.name}")
|
||||||
|
|
||||||
|
if group_name == "examples":
|
||||||
|
(dst_dir / "__init__.py").write_text("# Examples for orisys package\n", encoding="utf-8")
|
||||||
|
|
||||||
|
copied_counts[group_name] = copied
|
||||||
|
return copied_counts
|
||||||
|
|
||||||
|
|
||||||
|
def get_package_data_entries(ext_suffix: str) -> dict[str, list[str]]:
|
||||||
|
package_data = {"orisys": [f"*{ext_suffix}"], "orisys.examples": ["*.py"]}
|
||||||
|
for group_name, group_cfg in INCLUDE_LAYOUT.items():
|
||||||
|
if group_name == "examples":
|
||||||
|
continue
|
||||||
|
target_subdir = str(group_cfg["target_subdir"]).replace("\\", "/")
|
||||||
|
for pattern in group_cfg.get("patterns", []):
|
||||||
|
package_data["orisys"].append(f"{target_subdir}/{pattern}")
|
||||||
|
return package_data
|
||||||
|
|
||||||
|
|
||||||
def build():
|
def build():
|
||||||
install_nuitka()
|
install_nuitka()
|
||||||
|
|
||||||
version = get_version()
|
version = get_version()
|
||||||
src_dir = Path("src/orisys")
|
|
||||||
output_dir = Path("build_nuitka")
|
output_dir = Path("build_nuitka")
|
||||||
dist_dir = Path("dist_nuitka")
|
dist_dir = Path("dist_nuitka")
|
||||||
|
|
||||||
@@ -93,26 +153,11 @@ def build():
|
|||||||
shutil.copy(ext_file, dest_ext)
|
shutil.copy(ext_file, dest_ext)
|
||||||
print(f"Copied compiled package to {dest_ext}")
|
print(f"Copied compiled package to {dest_ext}")
|
||||||
|
|
||||||
configs_src = src_dir / "configs"
|
copied_counts = copy_included_files(pkg_dir)
|
||||||
configs_dst = pkg_dir / "configs"
|
print(f"Copied {copied_counts['configs']} JSON config files")
|
||||||
configs_dst.mkdir()
|
print(f"Copied {copied_counts['examples']} example files to orisys/examples/")
|
||||||
json_count = 0
|
print(f"Copied {copied_counts['docs']} doc files to orisys/docs/")
|
||||||
for json_file in configs_src.glob("*.json"):
|
package_data = get_package_data_entries(ext_suffix)
|
||||||
shutil.copy(json_file, configs_dst)
|
|
||||||
json_count += 1
|
|
||||||
print(f"Copied {json_count} JSON config files")
|
|
||||||
|
|
||||||
examples_dst = pkg_dir / "examples"
|
|
||||||
examples_dst.mkdir(exist_ok=True)
|
|
||||||
(examples_dst / "__init__.py").write_text("# Examples for orisys package\n", encoding="utf-8")
|
|
||||||
example_files = [Path("example1_basic_pipeline.py"), Path("example2_two_sensor_pipeline.py")]
|
|
||||||
example_count = 0
|
|
||||||
for example_file in example_files:
|
|
||||||
if example_file.exists():
|
|
||||||
shutil.copy(example_file, examples_dst)
|
|
||||||
example_count += 1
|
|
||||||
print(f"Copied example: {example_file.name}")
|
|
||||||
print(f"Copied {example_count} example files to orisys/examples/")
|
|
||||||
|
|
||||||
setup_py_path = dist_dir / "setup.py"
|
setup_py_path = dist_dir / "setup.py"
|
||||||
setup_content = f"""
|
setup_content = f"""
|
||||||
@@ -129,10 +174,7 @@ setup(
|
|||||||
author="Orisys",
|
author="Orisys",
|
||||||
packages=['orisys', 'orisys.examples'],
|
packages=['orisys', 'orisys.examples'],
|
||||||
package_dir={{'': 'wheel_root'}},
|
package_dir={{'': 'wheel_root'}},
|
||||||
package_data={{
|
package_data={package_data},
|
||||||
'orisys': ['*{ext_suffix}', 'configs/*.json'],
|
|
||||||
'orisys.examples': ['*.py'],
|
|
||||||
}},
|
|
||||||
include_package_data=True,
|
include_package_data=True,
|
||||||
install_requires=[
|
install_requires=[
|
||||||
"numpy>=1.24.4",
|
"numpy>=1.24.4",
|
||||||
|
|||||||
Binary file not shown.
|
Before Width: | Height: | Size: 2.0 MiB |
@@ -0,0 +1,45 @@
|
|||||||
|
{
|
||||||
|
"name": "prototype",
|
||||||
|
"version": "1.0",
|
||||||
|
"camera": {
|
||||||
|
"resolution_raw": [1280, 720],
|
||||||
|
"fps": 15,
|
||||||
|
"units": 15,
|
||||||
|
"units_used": 9,
|
||||||
|
"resolution_stitch": [263, 263],
|
||||||
|
"stitch_res_list": [365,263,123,63]
|
||||||
|
},
|
||||||
|
"stitcher": {
|
||||||
|
"min_area_frac": 0.01,
|
||||||
|
"max_area_frac": 0.04,
|
||||||
|
"max_aspect_ratio": 25,
|
||||||
|
"index": [1,2,3,6,7,8,11,12,13],
|
||||||
|
"margin": 10,
|
||||||
|
"nominal_ov_pos": 67,
|
||||||
|
"n": [3,3],
|
||||||
|
"row_tol": 0.6,
|
||||||
|
"box_size": [180, 180]
|
||||||
|
},
|
||||||
|
"tracker": {
|
||||||
|
"adaptive": true,
|
||||||
|
"lower_threshold": 12.0,
|
||||||
|
"upper_threshold": 18.0,
|
||||||
|
"loop_closure_interval": 1,
|
||||||
|
"algorithm": "dis",
|
||||||
|
"dis_preset": "medium",
|
||||||
|
"dis_mode": 3,
|
||||||
|
"algorithm_list": ["raft", "dis"],
|
||||||
|
"hhd_downsample": 3,
|
||||||
|
"backend": "cpu"
|
||||||
|
},
|
||||||
|
"contact": {
|
||||||
|
"min_contour_area": 3000,
|
||||||
|
"flow_threshold":1,
|
||||||
|
"div_threshold":0.02,
|
||||||
|
"expansion_threshold": 0.01,
|
||||||
|
"kernel_size": [5,5],
|
||||||
|
"denoise_sigma": 10
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
-296
@@ -1,296 +0,0 @@
|
|||||||
"""
|
|
||||||
示例 4: 多传感器并行处理(多进程模式 - 不使用 MultiSensor)
|
|
||||||
|
|
||||||
本示例演示如何使用 multiprocessing 同时运行两个传感器:
|
|
||||||
- 每个传感器运行在独立的进程中
|
|
||||||
- 每个进程拥有独立的 CUDA 上下文
|
|
||||||
- 使用 multiprocessing.Process 和 Queue 进行进程间通信
|
|
||||||
|
|
||||||
架构说明:
|
|
||||||
进程 1 (传感器 0): 采集 → 光流 → HHD → 接触检测 → 队列
|
|
||||||
进程 2 (传感器 1): 采集 → 光流 → HHD → 接触检测 → 队列
|
|
||||||
↓
|
|
||||||
主进程: 显示 ← ───────────────┘
|
|
||||||
|
|
||||||
适用场景:多指触觉传感器、多视角采集、需要 GPU 加速的多路处理
|
|
||||||
|
|
||||||
注意事项:
|
|
||||||
- Windows 下必须在 if __name__ == '__main__' 中调用 mp.freeze_support()
|
|
||||||
- 每个传感器进程会占用独立的 GPU 显存
|
|
||||||
"""
|
|
||||||
import os
|
|
||||||
os.environ["OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS"] = "0"
|
|
||||||
import cv2
|
|
||||||
import orisys
|
|
||||||
import numpy as np
|
|
||||||
import multiprocessing as mp
|
|
||||||
from multiprocessing import Queue, Process
|
|
||||||
import time
|
|
||||||
|
|
||||||
|
|
||||||
def sensor_worker(sensor_id, vid_src, config_name, cal_path, result_queue, stop_event, target_fps=None):
|
|
||||||
"""
|
|
||||||
传感器工作进程函数
|
|
||||||
|
|
||||||
Args:
|
|
||||||
sensor_id: 传感器编号
|
|
||||||
vid_src: 视频源(文件路径或摄像头索引)
|
|
||||||
config_name: 配置名称
|
|
||||||
cal_path: 标定路径
|
|
||||||
result_queue: 结果队列,用于向主进程发送数据
|
|
||||||
stop_event: 停止事件,用于通知工作进程退出
|
|
||||||
target_fps: 目标帧率;若设置则用定时器限速,使多传感器 FPS 接近一致
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
# 在每个进程中创建独立的传感器实例
|
|
||||||
sensor = orisys.Sensor(
|
|
||||||
vid_src,
|
|
||||||
isstitch=True,
|
|
||||||
cuda=True,
|
|
||||||
verbose=False, # 减少子进程输出
|
|
||||||
config_name=config_name,
|
|
||||||
cal_path=cal_path
|
|
||||||
)
|
|
||||||
|
|
||||||
print(f"传感器 {sensor_id} 已启动" + (f" (target_fps={target_fps})" if target_fps else ""))
|
|
||||||
|
|
||||||
frame_interval = 1.0 / target_fps if target_fps and target_fps > 0 else 0.0
|
|
||||||
|
|
||||||
while not stop_event.is_set():
|
|
||||||
t_start = time.perf_counter()
|
|
||||||
try:
|
|
||||||
# 步骤1: 获取图像
|
|
||||||
sensor.get_img()
|
|
||||||
|
|
||||||
# 步骤2: 计算形变场
|
|
||||||
sensor.compute_deformation()
|
|
||||||
|
|
||||||
# 步骤3: 计算触碰点
|
|
||||||
sensor.compute_contact()
|
|
||||||
|
|
||||||
# 步骤4: 读取信息
|
|
||||||
fps, flow, vnormal, img, contour, centroid, depth_map = sensor.read_info(
|
|
||||||
sensor.info.FPS,
|
|
||||||
sensor.info.VRAW,
|
|
||||||
sensor.info.VNORMAL,
|
|
||||||
sensor.info.IMG,
|
|
||||||
sensor.info.CONTOUR,
|
|
||||||
sensor.info.CENTROID,
|
|
||||||
sensor.info.DEPTH
|
|
||||||
)
|
|
||||||
|
|
||||||
# 步骤5: 准备数据字典(确保所有数据都是可序列化的)
|
|
||||||
data = {
|
|
||||||
'sensor_id': sensor_id,
|
|
||||||
'fps': fps,
|
|
||||||
'flow': flow.copy() if flow is not None else None,
|
|
||||||
'vnormal': vnormal.copy() if vnormal is not None else None,
|
|
||||||
'img': img.copy() if img is not None else None,
|
|
||||||
'contour': contour,
|
|
||||||
'centroid': centroid,
|
|
||||||
'depth_map': depth_map.copy() if depth_map is not None else None,
|
|
||||||
'timestamp': time.time()
|
|
||||||
}
|
|
||||||
|
|
||||||
# 步骤6: 发送数据到主进程(非阻塞)
|
|
||||||
try:
|
|
||||||
result_queue.put_nowait(data)
|
|
||||||
except Exception:
|
|
||||||
# 如果队列已满,跳过这一帧
|
|
||||||
pass
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"传感器 {sensor_id} 处理错误: {e}")
|
|
||||||
time.sleep(0.1)
|
|
||||||
|
|
||||||
# 步骤7: 精确限速(time.sleep 在 Windows 上精度仅 ~15.6ms,需留足余量后 spin-wait)
|
|
||||||
if frame_interval > 0:
|
|
||||||
deadline = t_start + frame_interval
|
|
||||||
remaining = deadline - time.perf_counter()
|
|
||||||
if remaining > 0.018:
|
|
||||||
time.sleep(remaining - 0.018) # coarse sleep; leave 18ms margin for Windows granularity
|
|
||||||
while time.perf_counter() < deadline:
|
|
||||||
pass # spin-wait for the last few ms
|
|
||||||
|
|
||||||
# 清理资源
|
|
||||||
sensor.disconnect()
|
|
||||||
print(f"传感器 {sensor_id} 已停止")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"传感器 {sensor_id} 初始化失败: {e}")
|
|
||||||
result_queue.put({'sensor_id': sensor_id, 'error': str(e)})
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
# =========================================================================
|
|
||||||
# 1. 配置多个传感器
|
|
||||||
# =========================================================================
|
|
||||||
# 目标帧率:设为 None 不限速(可跑满 100+ FPS);设为正数则 sleep 限速,使 FPS ≤ target_fps
|
|
||||||
target_fps = 30 # 不限速;设为 30 等可 cap 帧率
|
|
||||||
|
|
||||||
num_sensors = 5
|
|
||||||
sensor_configs = [
|
|
||||||
{
|
|
||||||
'sensor_id': 0,
|
|
||||||
'vid_src': './s1_480p.mp4',
|
|
||||||
'config_name': './config/prototype_of3_480p.json',
|
|
||||||
'cal_path': './config/dd01a_0_480p.npy'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'sensor_id': 1,
|
|
||||||
'vid_src': './s1_480p.mp4', # 可以改为不同的视频源或摄像头索引
|
|
||||||
'config_name': './config/prototype_of3_480p.json',
|
|
||||||
'cal_path': './config/dd01a_0_480p.npy'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'sensor_id': 2,
|
|
||||||
'vid_src': './s1_480p.mp4', # 可以改为不同的视频源或摄像头索引
|
|
||||||
'config_name': './config/prototype_of3_480p.json',
|
|
||||||
'cal_path': './config/dd01a_0_480p.npy'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'sensor_id': 3,
|
|
||||||
'vid_src': './s1_480p.mp4', # 可以改为不同的视频源或摄像头索引
|
|
||||||
'config_name': './config/prototype_of3_480p.json',
|
|
||||||
'cal_path': './config/dd01a_0_480p.npy'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'sensor_id': 4,
|
|
||||||
'vid_src': './s1_480p.mp4', # 可以改为不同的视频源或摄像头索引
|
|
||||||
'config_name': './config/prototype_of3_480p.json',
|
|
||||||
'cal_path': './config/dd01a_0_480p.npy'
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
# =========================================================================
|
|
||||||
# 2. 创建进程间通信对象
|
|
||||||
# =========================================================================
|
|
||||||
# 增大队列大小以缓冲更多帧,避免因主进程处理延迟导致的丢帧
|
|
||||||
result_queues = [Queue(maxsize=30) for _ in sensor_configs]
|
|
||||||
stop_events = [mp.Event() for _ in sensor_configs]
|
|
||||||
|
|
||||||
# =========================================================================
|
|
||||||
# 3. 启动传感器工作进程
|
|
||||||
# =========================================================================
|
|
||||||
processes = []
|
|
||||||
for i, config in enumerate(sensor_configs):
|
|
||||||
p = Process(
|
|
||||||
target=sensor_worker,
|
|
||||||
args=(i, config['vid_src'], config['config_name'], config['cal_path'],
|
|
||||||
result_queues[i], stop_events[i], target_fps)
|
|
||||||
)
|
|
||||||
p.start()
|
|
||||||
processes.append(p)
|
|
||||||
|
|
||||||
print(f"\n已启动 {len(processes)} 个传感器进程")
|
|
||||||
print("按 'q' 退出程序")
|
|
||||||
|
|
||||||
# 用于存储每个传感器的最新数据
|
|
||||||
latest_data = [None] * len(sensor_configs)
|
|
||||||
|
|
||||||
# 用于控制打印频率,减少I/O开销
|
|
||||||
print_counter = 0
|
|
||||||
print_interval = 30 # 每30帧打印一次,进一步减少I/O
|
|
||||||
|
|
||||||
# 用于控制按键检查频率
|
|
||||||
key_check_counter = 0
|
|
||||||
key_check_interval = 5 # 每5帧检查一次按键
|
|
||||||
|
|
||||||
try:
|
|
||||||
# =====================================================================
|
|
||||||
# 4. 主循环:从队列获取数据并显示
|
|
||||||
# =====================================================================
|
|
||||||
while True:
|
|
||||||
# 从所有队列中快速获取最新数据(高效清空队列,只保留最新帧)
|
|
||||||
for i, queue in enumerate(result_queues):
|
|
||||||
# 快速清空队列,只保留最后一帧
|
|
||||||
data = None
|
|
||||||
# 一次性获取所有可用数据,只保留最后一个
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
data = queue.get_nowait()
|
|
||||||
except:
|
|
||||||
break
|
|
||||||
|
|
||||||
if data is not None:
|
|
||||||
latest_data[i] = data
|
|
||||||
|
|
||||||
# 显示所有传感器的数据
|
|
||||||
for data in latest_data:
|
|
||||||
if data is None or 'error' in data:
|
|
||||||
continue
|
|
||||||
|
|
||||||
sid = data['sensor_id']
|
|
||||||
|
|
||||||
# 显示形变场
|
|
||||||
if data['img'] is not None and data['flow'] is not None:
|
|
||||||
arrows = orisys.util.draw_arrows(
|
|
||||||
data['img'],
|
|
||||||
data['flow'],
|
|
||||||
threshold=5,
|
|
||||||
grid_spacing=10,
|
|
||||||
arrow_scale=1.0
|
|
||||||
)
|
|
||||||
cv2.imshow(f"Deformation {sid}", arrows)
|
|
||||||
|
|
||||||
# # 显示触碰点和区域
|
|
||||||
# if data['img'] is not None:
|
|
||||||
# image_with_foe = orisys.util.draw_contact(
|
|
||||||
# data['img'],
|
|
||||||
# data['contour'],
|
|
||||||
# data['centroid']
|
|
||||||
# )
|
|
||||||
# cv2.imshow(f"Contact Centroid {sid}", image_with_foe)
|
|
||||||
|
|
||||||
# # 显示深度图
|
|
||||||
# if data['depth_map'] is not None and data['depth_map'].size > 0:
|
|
||||||
# div_abs = data['depth_map']
|
|
||||||
# if div_abs.max() > div_abs.min():
|
|
||||||
# div_normalized = ((div_abs - div_abs.min()) /
|
|
||||||
# (div_abs.max() - div_abs.min()) * 255).astype(np.uint8)
|
|
||||||
# else:
|
|
||||||
# div_normalized = np.zeros_like(div_abs, dtype=np.uint8)
|
|
||||||
# cv2.imshow(f"Depth Map {sid}",
|
|
||||||
# cv2.applyColorMap(div_normalized, cv2.COLORMAP_JET))
|
|
||||||
|
|
||||||
# 降低打印频率以减少I/O开销(在主循环级别控制)
|
|
||||||
print_counter += 1
|
|
||||||
if print_counter >= print_interval:
|
|
||||||
for data in latest_data:
|
|
||||||
if data is not None and 'error' not in data:
|
|
||||||
sid = data['sensor_id']
|
|
||||||
print(f"传感器 {sid}: FPS={data['fps']:5.1f}")
|
|
||||||
print_counter = 0
|
|
||||||
|
|
||||||
# 降低按键检查频率以减少开销
|
|
||||||
key_check_counter += 1
|
|
||||||
if key_check_counter >= key_check_interval:
|
|
||||||
key = cv2.waitKey(1) & 0xFF
|
|
||||||
if key == ord('q'):
|
|
||||||
print("\n退出程序...")
|
|
||||||
break
|
|
||||||
key_check_counter = 0
|
|
||||||
|
|
||||||
finally:
|
|
||||||
# =====================================================================
|
|
||||||
# 5. 清理资源
|
|
||||||
# =====================================================================
|
|
||||||
# 通知所有工作进程停止
|
|
||||||
for event in stop_events:
|
|
||||||
event.set()
|
|
||||||
|
|
||||||
# 等待所有进程结束
|
|
||||||
for p in processes:
|
|
||||||
p.join(timeout=2)
|
|
||||||
if p.is_alive():
|
|
||||||
p.terminate()
|
|
||||||
p.join()
|
|
||||||
|
|
||||||
cv2.destroyAllWindows()
|
|
||||||
print("所有传感器进程已停止")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
# Windows 下多进程必需的初始化
|
|
||||||
mp.freeze_support()
|
|
||||||
main()
|
|
||||||
-354
@@ -1,354 +0,0 @@
|
|||||||
"""
|
|
||||||
示例 4: 多传感器并行处理(多进程模式 - 不使用 MultiSensor)
|
|
||||||
|
|
||||||
本示例演示如何使用 multiprocessing 同时运行两个传感器:
|
|
||||||
- 每个传感器运行在独立的进程中
|
|
||||||
- 每个进程拥有独立的 CUDA 上下文
|
|
||||||
- 使用 multiprocessing.Process 和 Queue 进行进程间通信
|
|
||||||
|
|
||||||
架构说明:
|
|
||||||
进程 1 (传感器 0): 采集 → 光流 → HHD → 接触检测 → 队列
|
|
||||||
进程 2 (传感器 1): 采集 → 光流 → HHD → 接触检测 → 队列
|
|
||||||
↓
|
|
||||||
主进程: 显示 ← ───────────────┘
|
|
||||||
|
|
||||||
适用场景:多指触觉传感器、多视角采集、需要 GPU 加速的多路处理
|
|
||||||
|
|
||||||
注意事项:
|
|
||||||
- Windows 下必须在 if __name__ == '__main__' 中调用 mp.freeze_support()
|
|
||||||
- 每个传感器进程会占用独立的 GPU 显存
|
|
||||||
"""
|
|
||||||
import os
|
|
||||||
os.environ["OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS"] = "0"
|
|
||||||
import cv2
|
|
||||||
import orisys
|
|
||||||
import numpy as np
|
|
||||||
import multiprocessing as mp
|
|
||||||
from multiprocessing import Queue, Process
|
|
||||||
import time
|
|
||||||
import ctypes
|
|
||||||
import platform
|
|
||||||
import ctypes
|
|
||||||
|
|
||||||
# Multi-sensor FPS stabilization options
|
|
||||||
STABILITY_MODE = True # True: prioritize similar FPS across sensors
|
|
||||||
TARGET_FPS = 30.0 # Used only when STABILITY_MODE=True
|
|
||||||
SEND_VISUALIZATION_DATA = False # True sends img/flow/depth to main process (higher IPC cost)
|
|
||||||
|
|
||||||
def _enable_high_res_sleep():
|
|
||||||
"""
|
|
||||||
Enable high-resolution sleep for accurate FPS pacing.
|
|
||||||
Note: time.perf_counter() is already high-resolution on all platforms (Windows, macOS, Linux).
|
|
||||||
This function only improves time.sleep() accuracy on Windows.
|
|
||||||
"""
|
|
||||||
if os.name == "nt": # Windows (x64 and ARM64)
|
|
||||||
try:
|
|
||||||
# timeBeginPeriod improves time.sleep() accuracy (default ~15ms -> 1ms)
|
|
||||||
# time.perf_counter() is already high-res on Windows (uses QueryPerformanceCounter)
|
|
||||||
winmm = ctypes.WinDLL("winmm")
|
|
||||||
winmm.timeBeginPeriod(1) # Request 1ms timer resolution for sleep()
|
|
||||||
return winmm
|
|
||||||
except Exception:
|
|
||||||
return None
|
|
||||||
# macOS and Linux: time.perf_counter() and time.sleep() are already high-resolution
|
|
||||||
# No special setup needed
|
|
||||||
return None
|
|
||||||
|
|
||||||
|
|
||||||
def _disable_high_res_sleep(handle):
|
|
||||||
"""Restore default scheduler timer resolution (Windows only)."""
|
|
||||||
if handle is None:
|
|
||||||
return
|
|
||||||
if os.name == "nt" and isinstance(handle, ctypes.WinDLL):
|
|
||||||
try:
|
|
||||||
handle.timeEndPeriod(1)
|
|
||||||
except Exception:
|
|
||||||
pass
|
|
||||||
# macOS and Linux don't need cleanup
|
|
||||||
|
|
||||||
|
|
||||||
def sensor_worker(sensor_id, vid_src, config_name, cal_path, result_queue, stop_event):
|
|
||||||
"""
|
|
||||||
传感器工作进程函数
|
|
||||||
|
|
||||||
Args:
|
|
||||||
sensor_id: 传感器编号
|
|
||||||
vid_src: 视频源(文件路径或摄像头索引)
|
|
||||||
config_name: 配置名称
|
|
||||||
result_queue: 结果队列,用于向主进程发送数据
|
|
||||||
stop_event: 停止事件,用于通知工作进程退出
|
|
||||||
"""
|
|
||||||
timer_handle = None
|
|
||||||
try:
|
|
||||||
# Limit per-process CPU thread fan-out to reduce contention jitter.
|
|
||||||
os.environ["OMP_NUM_THREADS"] = "1"
|
|
||||||
os.environ["OPENBLAS_NUM_THREADS"] = "1"
|
|
||||||
os.environ["MKL_NUM_THREADS"] = "1"
|
|
||||||
cv2.setNumThreads(1)
|
|
||||||
if STABILITY_MODE and TARGET_FPS > 0:
|
|
||||||
timer_handle = _enable_high_res_sleep()
|
|
||||||
|
|
||||||
# 在每个进程中创建独立的传感器实例
|
|
||||||
sensor = orisys.Sensor(
|
|
||||||
vid_src,
|
|
||||||
isstitch=True,
|
|
||||||
cuda=True,
|
|
||||||
verbose=False, # 减少子进程输出
|
|
||||||
config_name=config_name,
|
|
||||||
cal_path=cal_path
|
|
||||||
)
|
|
||||||
|
|
||||||
print(f"传感器 {sensor_id} 已启动")
|
|
||||||
|
|
||||||
target_period = (1.0 / TARGET_FPS) if (STABILITY_MODE and TARGET_FPS > 0) else 0.0
|
|
||||||
next_tick = time.perf_counter()
|
|
||||||
while not stop_event.is_set():
|
|
||||||
try:
|
|
||||||
# 步骤1: 获取图像
|
|
||||||
sensor.get_img()
|
|
||||||
|
|
||||||
# 步骤2: 计算形变场
|
|
||||||
sensor.compute_deformation()
|
|
||||||
|
|
||||||
# 步骤3: 计算触碰点
|
|
||||||
sensor.compute_contact()
|
|
||||||
|
|
||||||
# 步骤4: 读取信息
|
|
||||||
if SEND_VISUALIZATION_DATA:
|
|
||||||
fps, flow, img, contour, centroid, depth_map = sensor.read_info(
|
|
||||||
sensor.info.FPS,
|
|
||||||
sensor.info.VRAW,
|
|
||||||
sensor.info.IMG,
|
|
||||||
sensor.info.CONTOUR,
|
|
||||||
sensor.info.CENTROID,
|
|
||||||
sensor.info.DEPTH
|
|
||||||
)
|
|
||||||
# Full payload (higher IPC cost)
|
|
||||||
data = {
|
|
||||||
'sensor_id': sensor_id,
|
|
||||||
'fps': fps,
|
|
||||||
'flow': flow.copy() if flow is not None else None,
|
|
||||||
'img': img.copy() if img is not None else None,
|
|
||||||
'contour': contour,
|
|
||||||
'centroid': centroid,
|
|
||||||
'depth_map': depth_map.copy() if depth_map is not None else None,
|
|
||||||
'timestamp': time.time()
|
|
||||||
}
|
|
||||||
else:
|
|
||||||
# Lightweight payload for stable multi-process throughput
|
|
||||||
fps, = sensor.read_info(sensor.info.FPS)
|
|
||||||
data = {
|
|
||||||
'sensor_id': sensor_id,
|
|
||||||
'fps': fps,
|
|
||||||
'timestamp': time.time()
|
|
||||||
}
|
|
||||||
|
|
||||||
# 步骤6: 发送数据到主进程(非阻塞)
|
|
||||||
try:
|
|
||||||
result_queue.put_nowait(data)
|
|
||||||
except:
|
|
||||||
# Queue full: keep latest data (drop oldest)
|
|
||||||
try:
|
|
||||||
result_queue.get_nowait()
|
|
||||||
result_queue.put_nowait(data)
|
|
||||||
except:
|
|
||||||
pass
|
|
||||||
|
|
||||||
# Pace workers to a common target so sensor FPS stays similar.
|
|
||||||
# Use drift-free scheduler: next_tick increments by fixed period.
|
|
||||||
if target_period > 0:
|
|
||||||
next_tick += target_period
|
|
||||||
now = time.perf_counter()
|
|
||||||
sleep_time = next_tick - now
|
|
||||||
if sleep_time > 0:
|
|
||||||
time.sleep(sleep_time)
|
|
||||||
else:
|
|
||||||
# If we are behind, reset schedule to current time.
|
|
||||||
next_tick = now
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
print(f"传感器 {sensor_id} 处理错误: {e}")
|
|
||||||
time.sleep(0.1)
|
|
||||||
|
|
||||||
# 清理资源
|
|
||||||
sensor.disconnect()
|
|
||||||
if timer_handle is not None:
|
|
||||||
_disable_high_res_sleep(timer_handle)
|
|
||||||
print(f"传感器 {sensor_id} 已停止")
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
if timer_handle is not None:
|
|
||||||
_disable_high_res_sleep(timer_handle)
|
|
||||||
print(f"传感器 {sensor_id} 初始化失败: {e}")
|
|
||||||
result_queue.put({'sensor_id': sensor_id, 'error': str(e)})
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
# =========================================================================
|
|
||||||
# 1. 配置多个传感器
|
|
||||||
# =========================================================================
|
|
||||||
num_sensors = 5
|
|
||||||
sensor_configs = [
|
|
||||||
{
|
|
||||||
'sensor_id': 0,
|
|
||||||
'vid_src': './s1_720p.mp4',
|
|
||||||
'config_name': './config/prototype_of3_720p.json',
|
|
||||||
'cal_path': './config/dd01a_0_720p.npy'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'sensor_id': 1,
|
|
||||||
'vid_src': './s1_720p.mp4',
|
|
||||||
'config_name': './config/prototype_of3_720p.json',
|
|
||||||
'cal_path': './config/dd01a_0_720p.npy'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'sensor_id': 2,
|
|
||||||
'vid_src': './s1_720p.mp4',
|
|
||||||
'config_name': './config/prototype_of3_720p.json',
|
|
||||||
'cal_path': './config/dd01a_0_720p.npy'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'sensor_id': 3,
|
|
||||||
'vid_src': './s1_720p.mp4',
|
|
||||||
'config_name': './config/prototype_of3_720p.json',
|
|
||||||
'cal_path': './config/dd01a_0_720p.npy'
|
|
||||||
},
|
|
||||||
{
|
|
||||||
'sensor_id': 4,
|
|
||||||
'vid_src': './s1_720p.mp4',
|
|
||||||
'config_name': './config/prototype_of3_720p.json',
|
|
||||||
'cal_path': './config/dd01a_0_720p.npy'
|
|
||||||
},
|
|
||||||
]
|
|
||||||
|
|
||||||
# =========================================================================
|
|
||||||
# 2. 创建进程间通信对象
|
|
||||||
# =========================================================================
|
|
||||||
# Keep queue short to prevent backlog-induced jitter (latest-data policy)
|
|
||||||
result_queues = [Queue(maxsize=2) for _ in sensor_configs]
|
|
||||||
stop_events = [mp.Event() for _ in sensor_configs] # 停止事件
|
|
||||||
|
|
||||||
# =========================================================================
|
|
||||||
# 3. 启动传感器工作进程
|
|
||||||
# =========================================================================
|
|
||||||
processes = []
|
|
||||||
for i, config in enumerate(sensor_configs):
|
|
||||||
p = Process(
|
|
||||||
target=sensor_worker,
|
|
||||||
args=(i, config['vid_src'], config['config_name'], config['cal_path'], result_queues[i], stop_events[i])
|
|
||||||
)
|
|
||||||
p.start()
|
|
||||||
processes.append(p)
|
|
||||||
|
|
||||||
print(f"\n已启动 {len(processes)} 个传感器进程")
|
|
||||||
print("按 'q' 退出程序")
|
|
||||||
|
|
||||||
# 用于存储每个传感器的最新数据
|
|
||||||
latest_data = [None] * len(sensor_configs)
|
|
||||||
|
|
||||||
# 用于控制打印频率,减少I/O开销
|
|
||||||
print_counter = 0
|
|
||||||
print_interval = 30 # 每30帧打印一次,进一步减少I/O
|
|
||||||
|
|
||||||
# 用于控制按键检查频率
|
|
||||||
key_check_counter = 0
|
|
||||||
key_check_interval = 5 # 每5帧检查一次按键
|
|
||||||
|
|
||||||
try:
|
|
||||||
# =====================================================================
|
|
||||||
# 4. 主循环:从队列获取数据并显示
|
|
||||||
# =====================================================================
|
|
||||||
while True:
|
|
||||||
# 从所有队列中快速获取最新数据(高效清空队列,只保留最新帧)
|
|
||||||
for i, queue in enumerate(result_queues):
|
|
||||||
# 快速清空队列,只保留最后一帧
|
|
||||||
data = None
|
|
||||||
# 一次性获取所有可用数据,只保留最后一个
|
|
||||||
while True:
|
|
||||||
try:
|
|
||||||
data = queue.get_nowait()
|
|
||||||
except:
|
|
||||||
break
|
|
||||||
|
|
||||||
if data is not None:
|
|
||||||
latest_data[i] = data
|
|
||||||
|
|
||||||
# 显示所有传感器的数据
|
|
||||||
for data in latest_data:
|
|
||||||
if data is None or 'error' in data:
|
|
||||||
continue
|
|
||||||
|
|
||||||
sid = data['sensor_id']
|
|
||||||
|
|
||||||
# 显示形变场
|
|
||||||
if data.get('img') is not None and data.get('flow') is not None:
|
|
||||||
arrows = orisys.util.draw_arrows(
|
|
||||||
data['img'],
|
|
||||||
data['flow'],
|
|
||||||
threshold=5,
|
|
||||||
grid_spacing=10,
|
|
||||||
arrow_scale=1.0
|
|
||||||
)
|
|
||||||
cv2.imshow(f"Deformation {sid}", arrows)
|
|
||||||
|
|
||||||
# # 显示触碰点和区域
|
|
||||||
# if data['img'] is not None:
|
|
||||||
# image_with_foe = orisys.util.draw_contact(
|
|
||||||
# data['img'],
|
|
||||||
# data['contour'],
|
|
||||||
# data['centroid']
|
|
||||||
# )
|
|
||||||
# cv2.imshow(f"Contact Centroid {sid}", image_with_foe)
|
|
||||||
|
|
||||||
# # 显示深度图
|
|
||||||
# if data['depth_map'] is not None and data['depth_map'].size > 0:
|
|
||||||
# div_abs = data['depth_map']
|
|
||||||
# if div_abs.max() > div_abs.min():
|
|
||||||
# div_normalized = ((div_abs - div_abs.min()) /
|
|
||||||
# (div_abs.max() - div_abs.min()) * 255).astype(np.uint8)
|
|
||||||
# else:
|
|
||||||
# div_normalized = np.zeros_like(div_abs, dtype=np.uint8)
|
|
||||||
# cv2.imshow(f"Depth Map {sid}",
|
|
||||||
# cv2.applyColorMap(div_normalized, cv2.COLORMAP_JET))
|
|
||||||
|
|
||||||
# 降低打印频率以减少I/O开销(在主循环级别控制)
|
|
||||||
print_counter += 1
|
|
||||||
if print_counter >= print_interval:
|
|
||||||
for data in latest_data:
|
|
||||||
if data is not None and 'error' not in data:
|
|
||||||
sid = data['sensor_id']
|
|
||||||
print(f"传感器 {sid}: FPS={data['fps']:5.1f}")
|
|
||||||
print_counter = 0
|
|
||||||
|
|
||||||
# 降低按键检查频率以减少开销
|
|
||||||
key_check_counter += 1
|
|
||||||
if key_check_counter >= key_check_interval:
|
|
||||||
key = cv2.waitKey(1) & 0xFF
|
|
||||||
if key == ord('q'):
|
|
||||||
print("\n退出程序...")
|
|
||||||
break
|
|
||||||
key_check_counter = 0
|
|
||||||
|
|
||||||
finally:
|
|
||||||
# =====================================================================
|
|
||||||
# 5. 清理资源
|
|
||||||
# =====================================================================
|
|
||||||
# 通知所有工作进程停止
|
|
||||||
for event in stop_events:
|
|
||||||
event.set()
|
|
||||||
|
|
||||||
# 等待所有进程结束
|
|
||||||
for p in processes:
|
|
||||||
p.join(timeout=2)
|
|
||||||
if p.is_alive():
|
|
||||||
p.terminate()
|
|
||||||
p.join()
|
|
||||||
|
|
||||||
cv2.destroyAllWindows()
|
|
||||||
print("所有传感器进程已停止")
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
|
||||||
# Windows 下多进程必需的初始化
|
|
||||||
mp.freeze_support()
|
|
||||||
main()
|
|
||||||
-75
@@ -1,75 +0,0 @@
|
|||||||
import cv2
|
|
||||||
import time
|
|
||||||
|
|
||||||
# Toggle these to test whether exposure is limiting delivered FPS.
|
|
||||||
USE_MANUAL_EXPOSURE = False
|
|
||||||
MANUAL_EXPOSURE = -6 # Typical V4L2/OpenCV manual range is negative values.
|
|
||||||
|
|
||||||
cap = cv2.VideoCapture(4, cv2.CAP_V4L2)
|
|
||||||
cap.set(cv2.CAP_PROP_FOURCC, cv2.VideoWriter.fourcc('M', 'J', 'P', 'G'))
|
|
||||||
cap.set(cv2.CAP_PROP_FPS, 30)
|
|
||||||
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 1920)
|
|
||||||
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 1080)
|
|
||||||
cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
|
|
||||||
|
|
||||||
# For V4L2 in OpenCV: auto-exposure is usually 3, manual is usually 1.
|
|
||||||
if USE_MANUAL_EXPOSURE:
|
|
||||||
ok_auto = cap.set(cv2.CAP_PROP_AUTO_EXPOSURE, 1)
|
|
||||||
ok_exp = cap.set(cv2.CAP_PROP_EXPOSURE, MANUAL_EXPOSURE)
|
|
||||||
else:
|
|
||||||
ok_auto = cap.set(cv2.CAP_PROP_AUTO_EXPOSURE, 3)
|
|
||||||
ok_exp = True
|
|
||||||
|
|
||||||
actual_fourcc = int(cap.get(cv2.CAP_PROP_FOURCC))
|
|
||||||
print(f"Backend: {cap.getBackendName()}")
|
|
||||||
print(
|
|
||||||
f"FOURCC: {chr(actual_fourcc & 0xFF)}"
|
|
||||||
f"{chr((actual_fourcc >> 8) & 0xFF)}"
|
|
||||||
f"{chr((actual_fourcc >> 16) & 0xFF)}"
|
|
||||||
f"{chr((actual_fourcc >> 24) & 0xFF)}"
|
|
||||||
)
|
|
||||||
print(f"Resolution: {int(cap.get(cv2.CAP_PROP_FRAME_WIDTH))}x{int(cap.get(cv2.CAP_PROP_FRAME_HEIGHT))}")
|
|
||||||
print(f"FPS: {cap.get(cv2.CAP_PROP_FPS)}")
|
|
||||||
print(
|
|
||||||
f"Exposure mode: {'manual' if USE_MANUAL_EXPOSURE else 'auto'} "
|
|
||||||
f"(set_auto_ok={ok_auto}, set_exp_ok={ok_exp})"
|
|
||||||
)
|
|
||||||
print(f"AutoExposure(readback): {cap.get(cv2.CAP_PROP_AUTO_EXPOSURE)}")
|
|
||||||
print(f"Exposure(readback): {cap.get(cv2.CAP_PROP_EXPOSURE)}")
|
|
||||||
|
|
||||||
frame_count = 0
|
|
||||||
start = time.perf_counter()
|
|
||||||
while cap.isOpened():
|
|
||||||
t0 = time.perf_counter()
|
|
||||||
ok = cap.grab()
|
|
||||||
t1 = time.perf_counter()
|
|
||||||
if not ok:
|
|
||||||
break
|
|
||||||
|
|
||||||
ret, frame = cap.retrieve()
|
|
||||||
t2 = time.perf_counter()
|
|
||||||
if not ret:
|
|
||||||
continue
|
|
||||||
|
|
||||||
cv2.imshow("webcam", frame)
|
|
||||||
t3 = time.perf_counter()
|
|
||||||
key = cv2.waitKey(1) & 0xFF
|
|
||||||
t4 = time.perf_counter()
|
|
||||||
|
|
||||||
frame_count += 1
|
|
||||||
if frame_count % 30 == 0:
|
|
||||||
elapsed = t4 - start
|
|
||||||
print(
|
|
||||||
f"Frame {frame_count}: "
|
|
||||||
f"grab={1000*(t1-t0):.1f}ms, "
|
|
||||||
f"retrieve={1000*(t2-t1):.1f}ms, "
|
|
||||||
f"imshow={1000*(t3-t2):.1f}ms, "
|
|
||||||
f"waitKey={1000*(t4-t3):.1f}ms, "
|
|
||||||
f"avg_fps={frame_count/elapsed:.1f}"
|
|
||||||
)
|
|
||||||
|
|
||||||
if key == ord('q'):
|
|
||||||
break
|
|
||||||
|
|
||||||
cap.release()
|
|
||||||
cv2.destroyAllWindows()
|
|
||||||
Reference in New Issue
Block a user