Merge branch 'stable'
This commit is contained in:
@@ -0,0 +1,144 @@
|
||||
|
||||
import os
|
||||
import sys
|
||||
os.environ["OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS"] = "0"
|
||||
import cv2
|
||||
|
||||
# Ensure benchmark uses local workspace code, not site-packages installation.
|
||||
_local_src = os.path.join(os.path.dirname(__file__), "src")
|
||||
if _local_src not in sys.path:
|
||||
sys.path.insert(0, _local_src)
|
||||
|
||||
import orisys
|
||||
import numpy as np
|
||||
import time
|
||||
import argparse
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--video","-v", type=str, default="0", help="Camera ID / Video file path")
|
||||
parser.add_argument("--config","-c", type=str, default="./config/ddjx01.json", help="Configuration file path")
|
||||
parser.add_argument("--cal","-cal", type=str, default="./config/ddjx01.npy", help="Calibration file path")
|
||||
parser.add_argument("--verbose","-verbose", type=bool, default=True, help="Print detailed information")
|
||||
parser.add_argument("--debug","-debug", type=bool, default=True, help="Print detailed information")
|
||||
args = parser.parse_args()
|
||||
print(f"Using orisys module: {orisys.__file__}")
|
||||
# 步骤1:创建传感器对象 (必要步骤),
|
||||
# config_name为配置文件名(内置配置文件名)或路径(自定义配置文件路径)
|
||||
# cal_path为校准文件路径, 每一个设备需要创建/使用一个校准文件
|
||||
|
||||
# 方式1: 从视频文件读取
|
||||
try:
|
||||
input = int(args.video)
|
||||
is_camera = True
|
||||
sensor = orisys.Sensor(input, config_name=args.config, cal_path= args.cal, verbose=args.verbose, debug=args.debug)
|
||||
except:
|
||||
is_camera = False
|
||||
sensor = orisys.Sensor(args.video, config_name=args.config, cal_path= args.cal, verbose=args.verbose, debug=args.debug)
|
||||
# sensor = orisys.Sensor(2, config_name="./config/ddjx01_q6a.json", cal_path= "./config/ddjx01.npy", isstitch=True, cuda=False, verbose=True, backend='cpu')
|
||||
|
||||
# 方式2: 从摄像头读取(取消注释使用)
|
||||
# sensor = orisys.Sensor(0, config_name="./config/prototype_of3.json", cal_path= "./config/cal.npy", isstitch=True, cuda=True, verbose=True)
|
||||
|
||||
print("\n按 'q' 退出程序,按 'r' 重置光流追踪器")
|
||||
|
||||
|
||||
while True:
|
||||
# 步骤2:信息提取
|
||||
# 步骤 2.1: 获取并拼接图像 (必要步骤)
|
||||
start_time = time.perf_counter()
|
||||
sensor.get_img()
|
||||
end_time = time.perf_counter()
|
||||
get_img_time = end_time - start_time
|
||||
# if sensor.detect_motion(threshold=2):
|
||||
# print("motion detected")
|
||||
# 步骤 2.2: 计算形变场 (必要步骤)
|
||||
start_time = time.perf_counter()
|
||||
sensor.compute_flow()
|
||||
|
||||
end_time = time.perf_counter()
|
||||
compute_flow_time = end_time - start_time
|
||||
|
||||
start_time = time.perf_counter()
|
||||
sensor.compute_hhd()
|
||||
|
||||
end_time = time.perf_counter()
|
||||
compute_hhd_time = end_time - start_time
|
||||
|
||||
# 步骤 2.3: 计算触碰点
|
||||
start_time = time.perf_counter()
|
||||
# sensor.compute_contact()
|
||||
end_time = time.perf_counter()
|
||||
compute_contact_time = end_time - start_time
|
||||
|
||||
|
||||
#步骤 2.4: 读取信息
|
||||
start_time = time.perf_counter()
|
||||
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
|
||||
)
|
||||
end_time = time.perf_counter()
|
||||
read_info_time = end_time - start_time
|
||||
#步骤 3: 可视化显示
|
||||
# 步骤 3.1: 绘制形变场
|
||||
start_time = time.perf_counter()
|
||||
# arrows = orisys.util.draw_arrows(
|
||||
# img,
|
||||
# flow,
|
||||
# threshold=4,
|
||||
# grid_spacing=40,
|
||||
# arrow_scale=1.0
|
||||
# )
|
||||
# cv2.imshow("Deformation", arrows)
|
||||
|
||||
# # 步骤 3.2: 在图像上触碰点和区域
|
||||
# image_with_foe = orisys.util.draw_contact(img, contour, centroid)
|
||||
# cv2.imshow("Contact Centroid", image_with_foe)
|
||||
|
||||
# # 步骤 3.3: 绘制深度图
|
||||
# # 归一化散度值到0-255范围(使用绝对值,然后归一化)
|
||||
# div_abs = 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)
|
||||
|
||||
# # 应用颜色映射(使用JET:蓝色=低散度,红色=高散度)
|
||||
# cv2.imshow("Depth Map", cv2.applyColorMap(div_normalized, cv2.COLORMAP_JET))
|
||||
|
||||
#步骤 3.4: 打印资料
|
||||
# print(f"fps: {fps}")
|
||||
end_time = time.perf_counter()
|
||||
display_time = end_time - start_time
|
||||
|
||||
# Get detailed timing from sensor (capture vs processing)
|
||||
capture_time = sensor._last_capture_time
|
||||
processing_time = sensor._last_processing_time
|
||||
|
||||
total_time = get_img_time + compute_flow_time + compute_hhd_time + compute_contact_time + read_info_time + display_time
|
||||
print(f"get_img: {get_img_time*1000:.2f}ms (capture: {capture_time*1000:.2f}ms, processing: {processing_time*1000:.2f}ms), flow: {compute_flow_time*1000:.2f}ms,hhd: {compute_hhd_time*1000:.2f}ms, contact: {compute_contact_time*1000:.2f}ms, read: {read_info_time*1000:.2f}ms, display: {display_time*1000:.2f}ms, total: {total_time*1000:.2f}ms")
|
||||
|
||||
#步骤 3.5: 键盘控制
|
||||
key = cv2.waitKey(1) & 0xFF
|
||||
if key == ord("q"):
|
||||
print("\n退出程序...")
|
||||
break
|
||||
if key == ord("r"):
|
||||
# 重置光流追踪器(当追踪漂移时使用)
|
||||
sensor.reset()
|
||||
print("传感器已重置")
|
||||
|
||||
# =========================================================================
|
||||
# 4. 清理资源
|
||||
# =========================================================================
|
||||
sensor.disconnect() # 释放视频源和相关资源
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,228 @@
|
||||
import os
|
||||
import sys
|
||||
import shutil
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
from importlib.machinery import EXTENSION_SUFFIXES
|
||||
|
||||
INCLUDE_LAYOUT = {
|
||||
"configs": {
|
||||
"source_dir": Path("config"),
|
||||
"target_subdir": Path("configs"),
|
||||
"patterns": ["*.json"],
|
||||
},
|
||||
"examples": {
|
||||
"source_dir": Path("examples"),
|
||||
"target_subdir": Path("examples"),
|
||||
"files": [
|
||||
Path("examples/basic_pipeline.py"),
|
||||
Path("examples/two_sensor_pipeline.py"),
|
||||
Path("examples/plot_data.py"),
|
||||
],
|
||||
},
|
||||
"docs": {
|
||||
"source_dir": Path("docs"),
|
||||
"target_subdir": Path("docs"),
|
||||
"patterns": ["*.md", "*.pdf"],
|
||||
},
|
||||
}
|
||||
|
||||
def install_nuitka():
|
||||
print("Checking Nuitka installation...")
|
||||
try:
|
||||
import nuitka # noqa: F401
|
||||
print("Nuitka is installed.")
|
||||
except ImportError:
|
||||
print("Installing Nuitka...")
|
||||
subprocess.check_call(
|
||||
[sys.executable, "-m", "pip", "install", "nuitka", "ordered-set", "zstandard"]
|
||||
)
|
||||
|
||||
|
||||
def get_version() -> str:
|
||||
init_file = Path("src/orisys/__init__.py")
|
||||
if not init_file.exists():
|
||||
return "0.3.1"
|
||||
for line in init_file.read_text(encoding="utf-8").splitlines():
|
||||
if line.startswith("__version__"):
|
||||
return line.split("=", 1)[1].strip().strip('"').strip("'")
|
||||
return "0.3.1"
|
||||
|
||||
|
||||
def find_compiled_extension(output_dir: Path, module_name: str) -> Path:
|
||||
candidates = []
|
||||
for suffix in EXTENSION_SUFFIXES:
|
||||
candidates.extend(output_dir.glob(f"{module_name}*{suffix}"))
|
||||
if not candidates:
|
||||
all_files = ", ".join(p.name for p in output_dir.iterdir()) if output_dir.exists() else "(none)"
|
||||
raise RuntimeError(
|
||||
f"Nuitka did not produce a compiled extension for {module_name}. Found: {all_files}"
|
||||
)
|
||||
return sorted(candidates, key=lambda p: len(p.name))[0]
|
||||
|
||||
|
||||
def move_wheel_to_sdk(wheel_path: Path, version: str) -> Path:
|
||||
sdk_version_dir = Path("SDK") / version
|
||||
sdk_version_dir.mkdir(parents=True, exist_ok=True)
|
||||
dest_wheel = sdk_version_dir / wheel_path.name
|
||||
if dest_wheel.exists():
|
||||
dest_wheel.unlink()
|
||||
shutil.move(str(wheel_path), str(dest_wheel))
|
||||
return dest_wheel
|
||||
|
||||
|
||||
def compile_orisys(build_root: Path, env: dict[str, str]) -> Path:
|
||||
module_output_dir = build_root / "orisys"
|
||||
if module_output_dir.exists():
|
||||
shutil.rmtree(module_output_dir)
|
||||
module_output_dir.mkdir(parents=True)
|
||||
|
||||
cmd = [
|
||||
sys.executable,
|
||||
"-m",
|
||||
"nuitka",
|
||||
"--module",
|
||||
"--include-package=orisys",
|
||||
"--output-dir=" + str(module_output_dir),
|
||||
"--remove-output",
|
||||
"--no-pyi-file",
|
||||
"src/orisys",
|
||||
]
|
||||
|
||||
print(f"Running: {' '.join(cmd)}")
|
||||
subprocess.check_call(cmd, env=env)
|
||||
|
||||
ext_file = find_compiled_extension(module_output_dir, "orisys")
|
||||
print(f"Generated compiled extension for orisys: {ext_file}")
|
||||
return ext_file
|
||||
|
||||
|
||||
def copy_compiled_orisys(pkg_dir: Path, ext_file: Path) -> str:
|
||||
ext_suffix = "".join(ext_file.suffixes)
|
||||
dest_ext = pkg_dir / f"__init__{ext_suffix}"
|
||||
shutil.copy(ext_file, dest_ext)
|
||||
print(f"Copied compiled module orisys to {dest_ext}")
|
||||
return ext_suffix
|
||||
|
||||
|
||||
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():
|
||||
install_nuitka()
|
||||
|
||||
version = get_version()
|
||||
output_dir = Path("build_nuitka")
|
||||
dist_dir = Path("dist_nuitka")
|
||||
|
||||
if output_dir.exists():
|
||||
shutil.rmtree(output_dir)
|
||||
if dist_dir.exists():
|
||||
shutil.rmtree(dist_dir)
|
||||
output_dir.mkdir()
|
||||
dist_dir.mkdir()
|
||||
|
||||
print("\n=== Compiling with Nuitka ===")
|
||||
env = os.environ.copy()
|
||||
env["PYTHONPATH"] = "src" + os.pathsep + env.get("PYTHONPATH", "")
|
||||
ext_file = compile_orisys(output_dir, env)
|
||||
|
||||
print("\n=== Preparing Wheel Structure ===")
|
||||
wheel_root = dist_dir / "wheel_root"
|
||||
pkg_dir = wheel_root / "orisys"
|
||||
pkg_dir.mkdir(parents=True)
|
||||
|
||||
ext_suffix = copy_compiled_orisys(pkg_dir, ext_file)
|
||||
|
||||
copied_counts = copy_included_files(pkg_dir)
|
||||
print(f"Copied {copied_counts['configs']} JSON config files")
|
||||
print(f"Copied {copied_counts['examples']} example files to orisys/examples/")
|
||||
print(f"Copied {copied_counts['docs']} doc files to orisys/docs/")
|
||||
package_data = get_package_data_entries(ext_suffix)
|
||||
|
||||
setup_py_path = dist_dir / "setup.py"
|
||||
setup_content = f"""
|
||||
from setuptools import setup, Distribution
|
||||
|
||||
class BinaryDistribution(Distribution):
|
||||
def has_ext_modules(self):
|
||||
return True
|
||||
|
||||
setup(
|
||||
name="orisys",
|
||||
version="{version}",
|
||||
description="Orisys - Optical Tactile Sensor SDK (Nuitka Compiled)",
|
||||
author="Orisys",
|
||||
packages=['orisys', 'orisys.examples'],
|
||||
package_dir={{'': 'wheel_root'}},
|
||||
package_data={package_data},
|
||||
include_package_data=True,
|
||||
install_requires=[
|
||||
"numpy>=1.24.4",
|
||||
"opencv-python>=4.10.0.84",
|
||||
"scipy>=1.10.1",
|
||||
],
|
||||
extras_require={{
|
||||
"cuda12": ["cupy-cuda12x>=12.3.0"],
|
||||
"cuda13": ["cupy-cuda13x>=13.0.0"],
|
||||
"cuda": ["cupy-cuda12x>=12.3.0"],
|
||||
}},
|
||||
distclass=BinaryDistribution,
|
||||
zip_safe=False,
|
||||
)
|
||||
"""
|
||||
setup_py_path.write_text(setup_content, encoding="utf-8")
|
||||
|
||||
print("\n=== Building Wheel ===")
|
||||
subprocess.check_call([sys.executable, "setup.py", "bdist_wheel"], cwd=dist_dir)
|
||||
|
||||
wheels = sorted((dist_dir / "dist").glob("*.whl"))
|
||||
if wheels:
|
||||
final_wheel = move_wheel_to_sdk(wheels[0], version)
|
||||
print(f"\n[SUCCESS] Wheel created and moved to: {final_wheel}")
|
||||
else:
|
||||
print("\n[ERROR] Wheel not found.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
build()
|
||||
|
||||
@@ -0,0 +1,192 @@
|
||||
import os
|
||||
os.environ["OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS"] = "0"
|
||||
import cv2
|
||||
import orisys
|
||||
import numpy as np
|
||||
import time
|
||||
|
||||
import pyqtgraph as pg
|
||||
|
||||
def create_plot():
|
||||
p1 = win.addPlot(title="法相力")
|
||||
p1.setLabel('bottom', '时间', units='s')
|
||||
win.nextRow()
|
||||
p2 = win.addPlot(title="切向力 X")
|
||||
p2.setLabel('bottom', '时间', units='s')
|
||||
win.nextRow()
|
||||
p3 = win.addPlot(title="切向力 Y")
|
||||
p3.setLabel('bottom', '时间', units='s')
|
||||
plot_af = p1.plot(pen='k')
|
||||
plot_tfx = p2.plot(pen='k')
|
||||
plot_tfy = p3.plot(pen='k')
|
||||
return p1, p2, p3, plot_af, plot_tfx, plot_tfy
|
||||
|
||||
def update(plot_af, plot_tfx, plot_tfy, data, p1, p2, p3):
|
||||
# global plot_af, plot_tfx, plot_tfy, data, p1, p2, p3
|
||||
plot_af.setData(data[:,:,0])
|
||||
p1.setYRange(min(data[:,1,0]),max(data[:,1,0]))
|
||||
plot_tfx.setData(data[:,:,1])
|
||||
p2.setYRange(min(data[:,1,1]),max(data[:,1,1]))
|
||||
plot_tfy.setData(data[:,:,2])
|
||||
p3.setYRange(min(data[:,1,2]),max(data[:,1,2]))
|
||||
|
||||
# 深度图
|
||||
def draw_magnitude_map(vfield, threshold=3.0, colormap=cv2.COLORMAP_JET) -> np.ndarray:
|
||||
"""
|
||||
Draw color map of pointwise magnitude of velocity field.
|
||||
|
||||
Args:
|
||||
vfield: Velocity field to visualize (2D vector field)
|
||||
threshold: Minimum magnitude to display (below threshold shows as zero/blue)
|
||||
colormap: OpenCV colormap to use (default: COLORMAP_JET)
|
||||
|
||||
Returns:
|
||||
np.ndarray: 400x400 BGR image with colored magnitude map
|
||||
"""
|
||||
flow_field = vfield
|
||||
|
||||
# Compute magnitude at each point
|
||||
magnitude = np.sqrt(flow_field[:, :, 0]**2 + flow_field[:, :, 1]**2)
|
||||
|
||||
# Smooth by downsampling and interpolating back
|
||||
downsample_factor = 10
|
||||
h, w = magnitude.shape
|
||||
magnitude_downsampled = magnitude[::downsample_factor, ::downsample_factor]
|
||||
magnitude_smoothed = cv2.resize(magnitude_downsampled, (w, h), interpolation=cv2.INTER_CUBIC)
|
||||
|
||||
# Apply threshold - set below-threshold values to zero
|
||||
magnitude_thresholded = magnitude_smoothed.copy()
|
||||
magnitude_thresholded[magnitude_smoothed < threshold] = 0
|
||||
|
||||
# Normalize to 0-255 range
|
||||
mag_min, mag_max = magnitude_thresholded.min(), magnitude_thresholded.max()
|
||||
if mag_max > mag_min:
|
||||
magnitude_norm = ((magnitude_thresholded - mag_min) / (mag_max - mag_min) * 255).astype(np.uint8)
|
||||
else:
|
||||
magnitude_norm = np.zeros_like(magnitude_thresholded, dtype=np.uint8)
|
||||
|
||||
# Apply colormap
|
||||
magnitude_colored = cv2.applyColorMap(magnitude_norm, colormap)
|
||||
|
||||
# Resize to output size
|
||||
out_img = cv2.resize(magnitude_colored, (400, 400), interpolation=cv2.INTER_CUBIC)
|
||||
|
||||
return out_img
|
||||
|
||||
# 实时图表
|
||||
npoints = 100 # 图表数据长度
|
||||
pg.setConfigOption('background', 'w')
|
||||
pg.setConfigOption('foreground', 'k')
|
||||
app = pg.mkQApp("Plotting Example")
|
||||
win = pg.GraphicsLayoutWidget(show=True, title="Basic plotting examples")
|
||||
win.setWindowTitle('pyqtgraph example: Plotting')
|
||||
p1, p2, p3, plot_af, plot_tfx, plot_tfy = create_plot()
|
||||
data = np.zeros([npoints,2,3])
|
||||
start_time = None # Will be set when sensor starts
|
||||
for i in range(3):
|
||||
data[:,0,i] = np.linspace(-npoints,0,npoints)
|
||||
|
||||
|
||||
|
||||
def main():
|
||||
global start_time
|
||||
|
||||
# 打开视频
|
||||
# sensor1 = Sensor("./s1.mp4",isstitch=True,cuda = True,verbose = True)
|
||||
# 方式1: 从视频文件读取
|
||||
sensor1 = orisys.Sensor(8, config_name="./config/ddjx01.json", cal_path= "./config/ddjx01.npy", cuda=False, verbose=True)
|
||||
|
||||
start_time = time.time()
|
||||
is_save = False
|
||||
frame_count = 0
|
||||
writer = None
|
||||
while True:
|
||||
sensor1.get_img() # 提取并拼接复眼图像
|
||||
|
||||
if is_save and writer is not None and sensor1.frame is not None:
|
||||
frame_count += 1
|
||||
save_img = sensor1.frame
|
||||
|
||||
# Convert grayscale (1 channel) to BGR (3 channels) if needed
|
||||
if len(save_img.shape) == 2 or (len(save_img.shape) == 3 and save_img.shape[2] == 1):
|
||||
if len(save_img.shape) == 3:
|
||||
save_img = save_img.squeeze(2)
|
||||
save_img = cv2.cvtColor(save_img, cv2.COLOR_GRAY2BGR)
|
||||
|
||||
writer.write(save_img)
|
||||
|
||||
sensor1.compute_deformation(check_motion=False) # 计算形变和分解
|
||||
# 提取信息:
|
||||
# 向量场: VRAW, VNORMAL, VSHEAR: 原始,法向,切向
|
||||
# 帧率,时间戳: FPS, TIMESTAMP
|
||||
# 变形量总和: FNORMAL , FSHEARX, FSHEARY : 法向,切向(X),切向(Y)
|
||||
fps, fn, fx, fy, flow = sensor1.read_info(sensor1.info.FPS, sensor1.info.FNORMAL, sensor1.info.FSHEARX, sensor1.info.FSHEARY, sensor1.info.VRAW)
|
||||
# print(fps,fn,fx,fy)
|
||||
# 显示向量场
|
||||
arrows = orisys.util.draw_arrows(sensor1.img, flow, threshold=5, grid_spacing=10, arrow_scale=1.0)
|
||||
cv2.imshow("Deformation", arrows)
|
||||
# 显示深度图
|
||||
mag = draw_magnitude_map(flow)
|
||||
cv2.imshow("Magnitude", mag)
|
||||
|
||||
# 显示图表 - 更新时间轴
|
||||
current_time = time.time() - start_time
|
||||
for i in range(3):
|
||||
data[:-1, 0, i] = data[1:, 0, i] # shift time data left
|
||||
data[:-1, 1, i] = data[1:, 1, i] # shift force data left
|
||||
data[-1, 0, :] = current_time # set current time for all plots
|
||||
data[-1, 1, 0] = fn
|
||||
data[-1, 1, 1] = fx
|
||||
data[-1, 1, 2] = fy
|
||||
fn_mean = np.mean(data[:,1,0])
|
||||
fx_mean = np.mean(data[:,1,1])
|
||||
fy_mean = np.mean(data[:,1,2])
|
||||
print(f"fps: {fps}, fn_mean: {fn_mean:.2f}, fx_mean: {fx_mean:.2f}, fy_mean: {fy_mean:.2f}")
|
||||
update(plot_af, plot_tfx, plot_tfy, data, p1, p2, p3) # Update plot
|
||||
|
||||
key = cv2.waitKey(1)
|
||||
if key & 0xFF == ord("q"):
|
||||
print("\n 停止程序")
|
||||
break
|
||||
if key == ord("s"):
|
||||
save_path = f"./data/pulse_test.mp4"
|
||||
# Ensure directory exists
|
||||
os.makedirs(os.path.dirname(save_path), exist_ok=True)
|
||||
# Get frame dimensions from the actual frame
|
||||
save_img = sensor1.frame
|
||||
|
||||
if save_img is not None:
|
||||
# Convert grayscale (1 channel) to BGR (3 channels) if needed for dimension check
|
||||
temp_img = save_img.copy()
|
||||
if len(temp_img.shape) == 2 or (len(temp_img.shape) == 3 and temp_img.shape[2] == 1):
|
||||
if len(temp_img.shape) == 3:
|
||||
temp_img = temp_img.squeeze(2)
|
||||
temp_img = cv2.cvtColor(temp_img, cv2.COLOR_GRAY2BGR)
|
||||
|
||||
frame_height, frame_width = temp_img.shape[:2]
|
||||
writer = cv2.VideoWriter(save_path, cv2.VideoWriter_fourcc(*'mp4v'), 30, (frame_width, frame_height))
|
||||
if writer.isOpened():
|
||||
is_save = True
|
||||
print(f"Start writing to {save_path}")
|
||||
else:
|
||||
print(f"Failed to open video writer for {save_path}")
|
||||
writer = None
|
||||
else:
|
||||
print("No frame available to determine video dimensions")
|
||||
if key == ord("e"):
|
||||
is_save = False
|
||||
if writer is not None:
|
||||
writer.release()
|
||||
writer = None
|
||||
if save_path is not None:
|
||||
print(f"Stop writing to {save_path}, total frames: {frame_count}")
|
||||
else:
|
||||
print(f"Stop writing, total frames: {frame_count}")
|
||||
frame_count = 0
|
||||
|
||||
sensor1.disconnect() # 断开传感器连接
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,961 @@
|
||||
import os
|
||||
os.environ["OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS"] = "0"
|
||||
# Force pyqtgraph to bind to PyQt5 when multiple Qt bindings are installed.
|
||||
os.environ.setdefault("PYQTGRAPH_QT_LIB", "PyQt5")
|
||||
import cv2
|
||||
import orisys
|
||||
import numpy as np
|
||||
import time
|
||||
import argparse
|
||||
from datetime import datetime
|
||||
import pyqtgraph as pg
|
||||
from PyQt5.QtCore import Qt, QSize, QEvent
|
||||
from PyQt5.QtGui import QImage, QFont, QPixmap, QKeySequence, QCursor
|
||||
from PyQt5.QtWidgets import QLabel, QHBoxLayout, QVBoxLayout, QWidget, QSizePolicy, QShortcut, QApplication
|
||||
from pyqtgraph.Qt import QtGui
|
||||
|
||||
|
||||
# Final desired appearance on the mirrored 1872x1560 (6:5) display.
|
||||
# The desktop is first shown on a 1920x1080 (16:9) screen, then stretched by
|
||||
# the OS onto the 6:5 mirror. We therefore pre-distort the source layout so
|
||||
# the mirrored output looks correct.
|
||||
TARGET_SINGLE_PANEL_W = 936
|
||||
TARGET_ASPECT_W = TARGET_SINGLE_PANEL_W * 2
|
||||
TARGET_VIDEO_FRAME_H = 1053
|
||||
TARGET_BOTTOM_PANEL_H = 507
|
||||
TARGET_ASPECT_H_TOTAL = TARGET_VIDEO_FRAME_H + TARGET_BOTTOM_PANEL_H
|
||||
TARGET_MIRROR_W = 1872
|
||||
TARGET_MIRROR_H = 1560
|
||||
|
||||
SINGLE_PANEL_W = TARGET_SINGLE_PANEL_W
|
||||
ASPECT_W = TARGET_ASPECT_W
|
||||
BOTTOM_PANEL_H = TARGET_BOTTOM_PANEL_H
|
||||
VIDEO_FRAME_W = TARGET_ASPECT_W
|
||||
VIDEO_FRAME_H = TARGET_VIDEO_FRAME_H
|
||||
ASPECT_H_TOTAL = TARGET_ASPECT_H_TOTAL
|
||||
UI_SCALE = 1.0
|
||||
ARROW_STRETCH = 507
|
||||
PLOT_STRETCH = 429
|
||||
VIDEO_LOOP_PATH = "./叠动机械爪_成片.m4v"
|
||||
npoints = 120 # 图表数据长度
|
||||
dispaly_angle = -90 # 箭头图显示角度
|
||||
touch_threshold = 5 # 触碰反应阈值
|
||||
frame_to_recover = 10 # 触碰后等待回复帧数
|
||||
|
||||
# Opneing Hour for keep reading image
|
||||
open_hour = 9
|
||||
close_hour = 17
|
||||
manual_open_enable = True # 控制是否可以手动切换营业状态 (按键G)
|
||||
is_open = True
|
||||
|
||||
# Match arrow panel / Qt style (not pure black)
|
||||
PLOT_THEME_BG = "#0B1016"
|
||||
PLOT_THEME_FG = "#A0A0A0"
|
||||
|
||||
# OS title bar cannot be tinted to #0B1016; frameless removes it (use Alt+drag to move).
|
||||
USE_FRAMELESS_WINDOW = False
|
||||
|
||||
|
||||
def scale_ui_px(px, floor=1):
|
||||
return max(floor, int(round(px * UI_SCALE)))
|
||||
|
||||
|
||||
def compute_source_layout(screen_w, screen_h):
|
||||
"""Pre-distort the app on the source screen so the mirrored monitor is correct."""
|
||||
target_video_ratio = TARGET_VIDEO_FRAME_H / float(TARGET_ASPECT_H_TOTAL)
|
||||
target_bottom_ratio = TARGET_BOTTOM_PANEL_H / float(TARGET_ASPECT_H_TOTAL)
|
||||
|
||||
return {
|
||||
"window_w": max(320, int(screen_w)),
|
||||
"window_h": max(240, int(screen_h)),
|
||||
"video_w": max(320, int(screen_w)),
|
||||
"video_h": max(1, int(round(screen_h * target_video_ratio))),
|
||||
"bottom_h": max(1, int(round(screen_h * target_bottom_ratio))),
|
||||
"single_panel_w": max(1, int(round(screen_w / 2.0))),
|
||||
}
|
||||
|
||||
|
||||
class CombinedTactileWindow(QWidget):
|
||||
"""
|
||||
Resizable window that keeps width:height = ASPECT_W : ASPECT_H_TOTAL.
|
||||
Dragging the width or height edge updates the other dimension accordingly.
|
||||
"""
|
||||
|
||||
RESIZE_MARGIN = 8
|
||||
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self._aspect_hw = ASPECT_H_TOTAL / float(ASPECT_W)
|
||||
self._resize_guard = False
|
||||
self.user_closed = False
|
||||
self._drag_anchor = None
|
||||
self._frameless_resize_edge = None
|
||||
self._frameless_resize_start_global = None
|
||||
self._frameless_resize_start_geom = None
|
||||
|
||||
def _min_client_size(self):
|
||||
w = max(320, self.minimumSizeHint().width())
|
||||
h = int(round(w * self._aspect_hw))
|
||||
return w, h
|
||||
|
||||
def _hit_test_resize(self, global_pos):
|
||||
if not USE_FRAMELESS_WINDOW:
|
||||
return None
|
||||
p = self.mapFromGlobal(global_pos)
|
||||
w, h = self.width(), self.height()
|
||||
m = self.RESIZE_MARGIN
|
||||
left = p.x() <= m
|
||||
right = p.x() >= w - m
|
||||
top = p.y() <= m
|
||||
bottom = p.y() >= h - m
|
||||
if top and left:
|
||||
return "tl"
|
||||
if top and right:
|
||||
return "tr"
|
||||
if bottom and left:
|
||||
return "bl"
|
||||
if bottom and right:
|
||||
return "br"
|
||||
if top:
|
||||
return "t"
|
||||
if bottom:
|
||||
return "b"
|
||||
if left:
|
||||
return "l"
|
||||
if right:
|
||||
return "r"
|
||||
return None
|
||||
|
||||
def _cursor_for_edge(self, edge):
|
||||
return {
|
||||
"tl": Qt.SizeFDiagCursor,
|
||||
"br": Qt.SizeFDiagCursor,
|
||||
"tr": Qt.SizeBDiagCursor,
|
||||
"bl": Qt.SizeBDiagCursor,
|
||||
"l": Qt.SizeHorCursor,
|
||||
"r": Qt.SizeHorCursor,
|
||||
"t": Qt.SizeVerCursor,
|
||||
"b": Qt.SizeVerCursor,
|
||||
}.get(edge, Qt.ArrowCursor)
|
||||
|
||||
def _apply_frameless_resize(self, dg):
|
||||
edge = self._frameless_resize_edge
|
||||
g0 = self._frameless_resize_start_geom
|
||||
min_w, min_h = self._min_client_size()
|
||||
dx, dy = dg.x(), dg.y()
|
||||
x0, y0, w0, h0 = g0.x(), g0.y(), g0.width(), g0.height()
|
||||
|
||||
if edge == "br":
|
||||
nw = max(min_w, w0 + dx)
|
||||
nh = int(round(nw * self._aspect_hw))
|
||||
self.setGeometry(x0, y0, nw, nh)
|
||||
elif edge == "r":
|
||||
nw = max(min_w, w0 + dx)
|
||||
nh = int(round(nw * self._aspect_hw))
|
||||
self.setGeometry(x0, y0, nw, nh)
|
||||
elif edge == "b":
|
||||
nh = max(min_h, h0 + dy)
|
||||
nw = int(round(nh / self._aspect_hw))
|
||||
self.setGeometry(x0, y0, nw, nh)
|
||||
elif edge == "l":
|
||||
nw = max(min_w, w0 - dx)
|
||||
nh = int(round(nw * self._aspect_hw))
|
||||
nx = g0.right() - nw
|
||||
self.setGeometry(nx, y0, nw, nh)
|
||||
elif edge == "t":
|
||||
nh = max(min_h, h0 - dy)
|
||||
nw = int(round(nh / self._aspect_hw))
|
||||
ny = g0.bottom() - nh
|
||||
self.setGeometry(x0, ny, nw, nh)
|
||||
elif edge == "tr":
|
||||
nw = max(min_w, w0 + dx)
|
||||
nh = int(round(nw * self._aspect_hw))
|
||||
ny = g0.bottom() - nh
|
||||
self.setGeometry(x0, ny, nw, nh)
|
||||
elif edge == "bl":
|
||||
nw = max(min_w, w0 - dx)
|
||||
nh = int(round(nw * self._aspect_hw))
|
||||
nx = g0.right() - nw
|
||||
self.setGeometry(nx, y0, nw, nh)
|
||||
elif edge == "tl":
|
||||
nw = max(min_w, w0 - dx)
|
||||
nh = int(round(nw * self._aspect_hw))
|
||||
nx = g0.right() - nw
|
||||
ny = g0.bottom() - nh
|
||||
self.setGeometry(nx, ny, nw, nh)
|
||||
|
||||
def install_drag_filters(self):
|
||||
"""Alt+left-drag to move when frameless; grabs go to this widget."""
|
||||
for w in self.findChildren(QWidget):
|
||||
if w is not self:
|
||||
w.installEventFilter(self)
|
||||
|
||||
def eventFilter(self, watched, event):
|
||||
if not USE_FRAMELESS_WINDOW:
|
||||
return super().eventFilter(watched, event)
|
||||
et = event.type()
|
||||
if et == QEvent.MouseButtonPress and event.button() == Qt.LeftButton:
|
||||
edge = self._hit_test_resize(event.globalPos())
|
||||
if edge is not None:
|
||||
self._frameless_resize_edge = edge
|
||||
self._frameless_resize_start_global = event.globalPos()
|
||||
self._frameless_resize_start_geom = self.geometry()
|
||||
self.grabMouse()
|
||||
return True
|
||||
if QApplication.keyboardModifiers() & Qt.AltModifier:
|
||||
self._drag_anchor = event.globalPos() - self.frameGeometry().topLeft()
|
||||
self.grabMouse()
|
||||
return True
|
||||
if et == QEvent.MouseMove and not event.buttons():
|
||||
edge = self._hit_test_resize(event.globalPos())
|
||||
watched.setCursor(QCursor(self._cursor_for_edge(edge)))
|
||||
return super().eventFilter(watched, event)
|
||||
|
||||
def mouseMoveEvent(self, event):
|
||||
if USE_FRAMELESS_WINDOW and self._frameless_resize_edge and event.buttons() & Qt.LeftButton:
|
||||
dg = event.globalPos() - self._frameless_resize_start_global
|
||||
self._apply_frameless_resize(dg)
|
||||
event.accept()
|
||||
return
|
||||
if (
|
||||
USE_FRAMELESS_WINDOW
|
||||
and self._drag_anchor is not None
|
||||
and event.buttons() & Qt.LeftButton
|
||||
):
|
||||
self.move(event.globalPos() - self._drag_anchor)
|
||||
event.accept()
|
||||
return
|
||||
super().mouseMoveEvent(event)
|
||||
|
||||
def mouseReleaseEvent(self, event):
|
||||
if USE_FRAMELESS_WINDOW and self._frameless_resize_edge and event.button() == Qt.LeftButton:
|
||||
self.releaseMouse()
|
||||
self._frameless_resize_edge = None
|
||||
self._frameless_resize_start_global = None
|
||||
self._frameless_resize_start_geom = None
|
||||
event.accept()
|
||||
return
|
||||
if USE_FRAMELESS_WINDOW and self._drag_anchor is not None and event.button() == Qt.LeftButton:
|
||||
self.releaseMouse()
|
||||
self._drag_anchor = None
|
||||
event.accept()
|
||||
return
|
||||
super().mouseReleaseEvent(event)
|
||||
|
||||
def closeEvent(self, event):
|
||||
self.user_closed = True
|
||||
event.accept()
|
||||
|
||||
def minimumSizeHint(self):
|
||||
w = 320
|
||||
return QSize(w, int(round(w * self._aspect_hw)))
|
||||
|
||||
def resizeEvent(self, event):
|
||||
if self._resize_guard:
|
||||
super().resizeEvent(event)
|
||||
return
|
||||
super().resizeEvent(event)
|
||||
w, h = self.width(), self.height()
|
||||
if w < 8 or h < 8:
|
||||
return
|
||||
old = event.oldSize()
|
||||
self._resize_guard = True
|
||||
try:
|
||||
if old.isValid() and old.width() > 0 and old.height() > 0:
|
||||
dw = abs(w - old.width())
|
||||
dh = abs(h - old.height())
|
||||
if dw >= dh:
|
||||
new_w, new_h = w, int(round(w * self._aspect_hw))
|
||||
else:
|
||||
new_w, new_h = int(round(h / self._aspect_hw)), h
|
||||
else:
|
||||
new_w, new_h = w, int(round(w * self._aspect_hw))
|
||||
if new_w != w or new_h != h:
|
||||
self.resize(new_w, new_h)
|
||||
finally:
|
||||
self._resize_guard = False
|
||||
|
||||
|
||||
def compute_arrow_canvas_side(panel_widget):
|
||||
"""
|
||||
Pixel size of the square arrow canvas from the window client area and the
|
||||
fixed horizontal split ARROW_STRETCH : PLOT_STRETCH (matches QHBoxLayout stretch).
|
||||
"""
|
||||
w = panel_widget.width()
|
||||
h = panel_widget.height()
|
||||
if w < 16 or h < 16:
|
||||
return max(32, ASPECT_H_TOTAL)
|
||||
w_arrow = int(round(w * ARROW_STRETCH / float(ARROW_STRETCH + PLOT_STRETCH)))
|
||||
# Keep the rendered arrow image square inside the left panel.
|
||||
return max(32, min(h, w_arrow))
|
||||
|
||||
|
||||
def draw_arrows_wuxi(
|
||||
frame,
|
||||
optical_flow,
|
||||
fps=True,
|
||||
threshold=5,
|
||||
grid_spacing=40,
|
||||
arrow_scale=1.0,
|
||||
margin=0.8,
|
||||
colormap=cv2.COLORMAP_PARULA,
|
||||
below_threshold_color=(0, 0, 255),
|
||||
edge_margin=15,
|
||||
target_size=936,
|
||||
):
|
||||
"""
|
||||
Draw colored optical flow arrows on the image by magnitude (JET colormap).
|
||||
Resizes frame and flow to target_size x target_size; scales flow vectors and
|
||||
grid_spacing / margins / threshold so arrow density matches the original resolution.
|
||||
"""
|
||||
image = frame.copy()
|
||||
if len(image.shape) == 2 or image.shape[2] == 1:
|
||||
image = cv2.cvtColor(image, cv2.COLOR_GRAY2BGR)
|
||||
|
||||
h0, w0 = image.shape[:2]
|
||||
if h0 != target_size or w0 != target_size:
|
||||
sx = target_size / float(w0)
|
||||
sy = target_size / float(h0)
|
||||
image = cv2.resize(image, (target_size, target_size), interpolation=cv2.INTER_LINEAR)
|
||||
u = cv2.resize(optical_flow[:, :, 0], (target_size, target_size), interpolation=cv2.INTER_LINEAR) * sx
|
||||
v = cv2.resize(optical_flow[:, :, 1], (target_size, target_size), interpolation=cv2.INTER_LINEAR) * sy
|
||||
optical_flow = np.stack([u, v], axis=2)
|
||||
scale = max(sx, sy)
|
||||
grid_spacing = max(4, int(round(grid_spacing * scale)))
|
||||
edge_margin = max(1, int(round(edge_margin * scale)))
|
||||
threshold = float(threshold) * scale
|
||||
|
||||
h, w = image.shape[:2]
|
||||
# Arrow grid in pixel coordinates (assume frame and flow same size)
|
||||
rr = np.arange(grid_spacing // 2, h, grid_spacing)
|
||||
cc = np.arange(grid_spacing // 2, w, grid_spacing)
|
||||
rr, cc = np.meshgrid(rr, cc, indexing="ij")
|
||||
rr, cc = rr.reshape(-1), cc.reshape(-1)
|
||||
|
||||
# Displacement at each grid point (flow stored as half-displacement)
|
||||
disp = 2.0 * optical_flow[rr, cc, :]
|
||||
mag = np.linalg.norm(disp, axis=1)
|
||||
dx = disp[:, 0] / arrow_scale
|
||||
dy = disp[:, 1] / arrow_scale
|
||||
length = np.sqrt(dx * dx + dy * dy)
|
||||
# Cap arrow length at grid_spacing
|
||||
scale = np.where(length > grid_spacing*margin, grid_spacing / np.maximum(length, 1e-9), 1.0)*margin
|
||||
dx, dy = dx * scale, dy * scale
|
||||
end_x = (cc + dx).astype(np.int32)
|
||||
end_y = (rr + dy).astype(np.int32)
|
||||
|
||||
valid = (
|
||||
(cc >= edge_margin) & (cc < w - edge_margin) & (rr >= edge_margin) & (rr < h - edge_margin)
|
||||
& (end_x >= 0) & (end_x < w) & (end_y >= 0) & (end_y < h)
|
||||
)
|
||||
mag_range = mag.max() - mag.min()
|
||||
norm_mag = ((mag - mag.min()) / mag_range * 255).astype(np.uint8) if mag_range > 0 else np.zeros_like(mag, dtype=np.uint8)
|
||||
norm_mag[mag < threshold] = 0
|
||||
colors = cv2.applyColorMap(norm_mag.reshape(-1, 1), colormap)
|
||||
|
||||
for i in np.where(valid)[0]:
|
||||
pt1, pt2 = (int(cc[i]), int(rr[i])), (int(end_x[i]), int(end_y[i]))
|
||||
color = tuple(int(c) for c in colors[i, 0])
|
||||
if mag[i] < threshold:
|
||||
cv2.circle(image, pt1, radius=1, color=below_threshold_color, thickness=-1)
|
||||
else:
|
||||
# thickness = max(1, int(2 * mag[i] / mag.max())) if mag.max() > 0 else 2
|
||||
thickness = 1
|
||||
cv2.arrowedLine(image, pt1, pt2, color, thickness=thickness, tipLength=0.2)
|
||||
return image
|
||||
|
||||
|
||||
def set_arrow_label_bgr(label, bgr_image):
|
||||
"""Show a BGR OpenCV image in a QLabel using source-screen pre-distortion."""
|
||||
rgb = cv2.cvtColor(bgr_image, cv2.COLOR_BGR2RGB)
|
||||
rgb = np.ascontiguousarray(rgb)
|
||||
h, w, ch = rgb.shape
|
||||
qimg = QImage(rgb.data, w, h, ch * w, QImage.Format_RGB888).copy()
|
||||
pm = QPixmap.fromImage(qimg)
|
||||
lw, lh = label.width(), label.height()
|
||||
if lw > 0 and lh > 0 and (pm.width() != lw or pm.height() != lh):
|
||||
pm = pm.scaled(lw, lh, Qt.IgnoreAspectRatio, Qt.SmoothTransformation)
|
||||
label.setPixmap(pm)
|
||||
|
||||
def set_video_label_bgr(label, bgr_image):
|
||||
"""Show loop video in the top frame using the fitted display size."""
|
||||
rgb = cv2.cvtColor(bgr_image, cv2.COLOR_BGR2RGB)
|
||||
rgb = np.ascontiguousarray(rgb)
|
||||
h, w, ch = rgb.shape
|
||||
qimg = QImage(rgb.data, w, h, ch * w, QImage.Format_RGB888).copy()
|
||||
pm = QPixmap.fromImage(qimg)
|
||||
lw, lh = label.width(), label.height()
|
||||
if lw > 0 and lh > 0 and (pm.width() != lw or pm.height() != lh):
|
||||
pm = pm.scaled(lw, lh, Qt.IgnoreAspectRatio, Qt.FastTransformation)
|
||||
label.setPixmap(pm)
|
||||
|
||||
|
||||
def read_loop_video_frame_realtime(video_cap, video_clock):
|
||||
"""Read loop-video frame paced to wall clock; skip frames when main loop is slow."""
|
||||
if video_cap is None or not video_cap.isOpened():
|
||||
return False, None
|
||||
|
||||
now = time.perf_counter()
|
||||
fps = max(1.0, float(video_clock.get("fps", 30.0)))
|
||||
frame_dt = 1.0 / fps
|
||||
next_ts = video_clock.get("next_ts")
|
||||
if next_ts is None:
|
||||
next_ts = now
|
||||
|
||||
frames_due = 1
|
||||
if now > next_ts + frame_dt:
|
||||
frames_due += int((now - next_ts) / frame_dt)
|
||||
# Cap skip burst to avoid long catch-up loops after pauses.
|
||||
frames_due = min(frames_due, int(max(2.0, fps)))
|
||||
|
||||
for _ in range(frames_due - 1):
|
||||
if not video_cap.grab():
|
||||
video_cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
|
||||
if not video_cap.grab():
|
||||
return False, None
|
||||
|
||||
vret, vframe = video_cap.read()
|
||||
if not vret:
|
||||
video_cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
|
||||
vret, vframe = video_cap.read()
|
||||
if not vret:
|
||||
return False, None
|
||||
|
||||
next_ts = next_ts + frames_due * frame_dt
|
||||
if next_ts < now - 2.0 * frame_dt:
|
||||
next_ts = now
|
||||
video_clock["next_ts"] = next_ts
|
||||
return True, vframe
|
||||
|
||||
|
||||
def make_gradient_brush(color_hex, max_alpha=150):
|
||||
"""
|
||||
创建一个从上到下渐变的笔刷 (兼容 PyQt5 和 PyQt6)
|
||||
"""
|
||||
c_top = QtGui.QColor(color_hex)
|
||||
c_top.setAlpha(max_alpha) # 顶部半透明
|
||||
|
||||
c_bot = QtGui.QColor(color_hex)
|
||||
c_bot.setAlpha(0) # 底部全透明
|
||||
|
||||
# 创建线性渐变:(0,0)到(0,1) 代表从控件顶部到底部
|
||||
grad = QtGui.QLinearGradient(0, 0, 0, 1)
|
||||
|
||||
if hasattr(QtGui.QGradient, 'CoordinateMode'):
|
||||
grad.setCoordinateMode(QtGui.QGradient.CoordinateMode.ObjectBoundingMode)
|
||||
else:
|
||||
grad.setCoordinateMode(QtGui.QGradient.ObjectBoundingMode)
|
||||
# ==============================================
|
||||
|
||||
grad.setColorAt(0, c_top) # 0.0 (顶端) 颜色
|
||||
grad.setColorAt(1, c_bot) # 1.0 (底端) 颜色
|
||||
|
||||
return QtGui.QBrush(grad)
|
||||
|
||||
def create_plot(plot_container):
|
||||
pg.setConfigOption("background", PLOT_THEME_BG)
|
||||
pg.setConfigOption("foreground", PLOT_THEME_FG)
|
||||
pg.setConfigOptions(antialias=True) # 抗锯齿
|
||||
theme_color = '#00E5FF' # '#00E5FF'
|
||||
line_pen = pg.mkPen(color=theme_color, width=2)
|
||||
grad_brush = make_gradient_brush(theme_color, max_alpha=120)
|
||||
|
||||
axis_font = QFont("Segoe UI", scale_ui_px(11, floor=8))
|
||||
p1 = plot_container.addPlot()
|
||||
p1.setLabel('left', 'N')
|
||||
p1.getAxis('left').label.setFont(axis_font)
|
||||
p1.getAxis('left').label.setRotation(dispaly_angle)
|
||||
p1.showGrid(x=True, y=True, alpha=0.3)
|
||||
plot_container.nextRow()
|
||||
p2 = plot_container.addPlot()
|
||||
p2.setLabel('left', 'X')
|
||||
p2.getAxis('left').label.setFont(axis_font)
|
||||
p2.getAxis('left').label.setRotation(dispaly_angle)
|
||||
p2.showGrid(x=True, y=True, alpha=0.3)
|
||||
plot_container.nextRow()
|
||||
p3 = plot_container.addPlot()
|
||||
p3.setLabel('left', 'Y')
|
||||
|
||||
# p3.setLabel('bottom', 'Time', units='s')
|
||||
p3.getAxis('left').label.setFont(axis_font)
|
||||
p3.getAxis('left').label.setRotation(dispaly_angle)
|
||||
p3.getAxis('bottom').label.setFont(axis_font)
|
||||
p3.showGrid(x=True, y=True, alpha=0.3)
|
||||
_plot_bg = pg.mkColor(PLOT_THEME_BG)
|
||||
for p in (p1, p2, p3):
|
||||
p.getAxis("bottom").setStyle(showValues=False)
|
||||
p.getAxis("left").setStyle(showValues=False)
|
||||
# Keep title as "N" / "X" / "Y" — no PyQtGraph "(x0.001)" suffix on small ranges.
|
||||
p.getAxis("left").enableAutoSIPrefix(False)
|
||||
# PlotItem.setBackground exists only in newer pyqtgraph; ViewBox works on older installs.
|
||||
p.vb.setBackgroundColor(_plot_bg)
|
||||
plot_af = p1.plot(pen=line_pen, fillLevel=0, fillBrush=grad_brush)
|
||||
plot_tfx = p2.plot(pen=line_pen, fillLevel=0, fillBrush=grad_brush)
|
||||
plot_tfy = p3.plot(pen=line_pen, fillLevel=0, fillBrush=grad_brush)
|
||||
return p1, p2, p3, plot_af, plot_tfx, plot_tfy
|
||||
|
||||
def _plot_y_range(y):
|
||||
"""Avoid degenerate Y span; explicit x/y in setData so older PyQtGraph never plots time as y."""
|
||||
mn = float(np.min(y))
|
||||
mx = float(np.max(y))
|
||||
if not (np.isfinite(mn) and np.isfinite(mx)):
|
||||
return 0.0, 1.0
|
||||
if mx - mn <= 1e-15:
|
||||
mid = 0.5 * (mn + mx)
|
||||
return mid - 0.5, mid + 0.5
|
||||
pad = (mx - mn) * 0.05
|
||||
return mn - pad, mx + pad
|
||||
|
||||
|
||||
def update(plot_af, plot_tfx, plot_tfy, data, p1, p2, p3):
|
||||
# global plot_af, plot_tfx, plot_tfy, data, p1, p2, p3
|
||||
plot_af.setData(data[:, 0, 0], data[:, 1, 0])
|
||||
p1.setYRange(*_plot_y_range(data[:, 1, 0]))
|
||||
plot_tfx.setData(data[:, 0, 1], data[:, 1, 1])
|
||||
p2.setYRange(*_plot_y_range(data[:, 1, 1]))
|
||||
plot_tfy.setData(data[:, 0, 2], data[:, 1, 2])
|
||||
p3.setYRange(*_plot_y_range(data[:, 1, 2]))
|
||||
|
||||
|
||||
def build_sensor_panel(parent_layout):
|
||||
panel = QWidget()
|
||||
panel_layout = QHBoxLayout(panel)
|
||||
panel_layout.setContentsMargins(0, 0, 0, 0)
|
||||
panel_layout.setSpacing(0)
|
||||
|
||||
# Ignored: do not let pixmap/content drive QLabel size — layout stretch sets arrow vs plot widths.
|
||||
_policy_layout = QSizePolicy(QSizePolicy.Ignored, QSizePolicy.Ignored)
|
||||
|
||||
arrow_label = QLabel()
|
||||
arrow_label.setAlignment(Qt.AlignHCenter | Qt.AlignVCenter)
|
||||
arrow_label.setStyleSheet(
|
||||
"background-color: #0B1016; margin: 0; padding: 0; border: none;"
|
||||
)
|
||||
arrow_label.setSizePolicy(_policy_layout)
|
||||
arrow_label.setMinimumSize(0, 0)
|
||||
arrow_label.setScaledContents(False)
|
||||
panel_layout.addWidget(arrow_label, stretch=ARROW_STRETCH)
|
||||
|
||||
plot_widget = pg.GraphicsLayoutWidget()
|
||||
plot_widget.setBackground(PLOT_THEME_BG)
|
||||
plot_widget.setSizePolicy(_policy_layout)
|
||||
plot_widget.setMinimumSize(0, 0)
|
||||
panel_layout.addWidget(plot_widget, stretch=PLOT_STRETCH)
|
||||
|
||||
disconnected_label = QLabel("已断开")
|
||||
disconnected_label.setAlignment(Qt.AlignCenter)
|
||||
disconnected_label.setStyleSheet(
|
||||
f"background-color: #0B1016; color: #FF4D4D; font-size: {scale_ui_px(54, floor=24)}px; "
|
||||
"font-weight: bold; letter-spacing: 2px; margin: 0; padding: 0; border: none;"
|
||||
)
|
||||
disconnected_label.hide()
|
||||
panel_layout.addWidget(disconnected_label, stretch=SINGLE_PANEL_W)
|
||||
|
||||
parent_layout.addWidget(panel, stretch=1)
|
||||
|
||||
p1, p2, p3, plot_af, plot_tfx, plot_tfy = create_plot(plot_widget)
|
||||
data = np.zeros([npoints, 2, 3])
|
||||
for i in range(3):
|
||||
data[:, 0, i] = np.linspace(0.0, npoints / 30.0, npoints)
|
||||
|
||||
return {
|
||||
"panel": panel,
|
||||
"arrow_label": arrow_label,
|
||||
"plot_widget": plot_widget,
|
||||
"disconnected_label": disconnected_label,
|
||||
"p1": p1,
|
||||
"p2": p2,
|
||||
"p3": p3,
|
||||
"plot_af": plot_af,
|
||||
"plot_tfx": plot_tfx,
|
||||
"plot_tfy": plot_tfy,
|
||||
"data": data,
|
||||
}
|
||||
|
||||
|
||||
# 实时图表:单窗口,两套并排(左 sensor1,右 sensor2)
|
||||
pg.setConfigOption("background", PLOT_THEME_BG)
|
||||
pg.setConfigOption("foreground", PLOT_THEME_FG)
|
||||
app = pg.mkQApp("Wuxi Demo")
|
||||
screen = app.primaryScreen()
|
||||
screen_geometry = screen.availableGeometry() if screen is not None else None
|
||||
screen_w = screen_geometry.width() if screen_geometry is not None else 1920
|
||||
screen_h = screen_geometry.height() if screen_geometry is not None else 1080
|
||||
source_layout = compute_source_layout(screen_w, screen_h)
|
||||
ASPECT_W = source_layout["window_w"]
|
||||
ASPECT_H_TOTAL = source_layout["window_h"]
|
||||
VIDEO_FRAME_W = source_layout["video_w"]
|
||||
VIDEO_FRAME_H = source_layout["video_h"]
|
||||
BOTTOM_PANEL_H = source_layout["bottom_h"]
|
||||
SINGLE_PANEL_W = source_layout["single_panel_w"]
|
||||
UI_SCALE = max(0.1, screen_h / float(TARGET_ASPECT_H_TOTAL))
|
||||
|
||||
combined_win = CombinedTactileWindow()
|
||||
combined_win.setWindowTitle("Wuxi tactile — dual flow + forces")
|
||||
main_layout = QVBoxLayout(combined_win)
|
||||
main_layout.setContentsMargins(0, 0, 0, 0)
|
||||
main_layout.setSpacing(0)
|
||||
|
||||
video_label = QLabel()
|
||||
video_label.setAlignment(Qt.AlignCenter)
|
||||
video_label.setStyleSheet("background-color: #000000; margin: 0; padding: 0; border: none;")
|
||||
video_label.setSizePolicy(QSizePolicy.Expanding, QSizePolicy.Expanding)
|
||||
video_label.setMinimumSize(0, 0)
|
||||
main_layout.addWidget(video_label, stretch=VIDEO_FRAME_H)
|
||||
|
||||
bottom_container = QWidget()
|
||||
bottom_layout = QHBoxLayout(bottom_container)
|
||||
bottom_layout.setContentsMargins(0, 0, 0, 0)
|
||||
bottom_layout.setSpacing(0)
|
||||
main_layout.addWidget(bottom_container, stretch=BOTTOM_PANEL_H)
|
||||
|
||||
panel1_ui = build_sensor_panel(bottom_layout)
|
||||
panel2_ui = build_sensor_panel(bottom_layout)
|
||||
|
||||
if USE_FRAMELESS_WINDOW:
|
||||
combined_win.setWindowFlags(Qt.Window | Qt.FramelessWindowHint)
|
||||
# Client area matches theme (no bright border flash on frameless)
|
||||
combined_win.setStyleSheet(f"background-color: {PLOT_THEME_BG};")
|
||||
combined_win.resize(ASPECT_W, ASPECT_H_TOTAL)
|
||||
combined_win.install_drag_filters()
|
||||
combined_win.showFullScreen()
|
||||
closed_hour_overlay = QLabel(combined_win)
|
||||
closed_hour_overlay.setAlignment(Qt.AlignCenter)
|
||||
closed_hour_overlay.setStyleSheet(
|
||||
f"background-color: {PLOT_THEME_BG}; color: #A0A0A0; "
|
||||
f"font-size: {scale_ui_px(72, floor=32)}px; font-weight: bold; "
|
||||
"margin: 0; padding: 0; border: none;"
|
||||
)
|
||||
closed_hour_overlay.setText("Closed hour\n非营业时间")
|
||||
closed_hour_overlay.hide()
|
||||
start_time = None # Will be set when sensor starts
|
||||
fps = 30.0
|
||||
|
||||
|
||||
def outside_opening_hours(now):
|
||||
return now.hour < open_hour or now.hour >= close_hour
|
||||
|
||||
|
||||
|
||||
def main():
|
||||
global start_time
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--video1","-v1", type=str, default="0", help="视频文件路径")
|
||||
parser.add_argument("--video2","-v2", type=str, default="0", help="视频文件路径")
|
||||
args = parser.parse_args()
|
||||
|
||||
session_start = datetime.now()
|
||||
print(
|
||||
f"[session] START {session_start:%Y-%m-%d %H:%M:%S} (long-run / crash log marker)",
|
||||
flush=True,
|
||||
)
|
||||
if USE_FRAMELESS_WINDOW:
|
||||
print(
|
||||
"[window] Frameless: resize = drag window edges/corners; move = Alt+left-drag; quit = Q.",
|
||||
flush=True,
|
||||
)
|
||||
|
||||
sensor1 = None
|
||||
sensor2 = None
|
||||
video_cap = None
|
||||
try:
|
||||
input1 = int(args.video1)
|
||||
input2 = int(args.video2)
|
||||
is_camera = True
|
||||
except:
|
||||
is_camera = False
|
||||
try:
|
||||
if is_camera:
|
||||
sensor1 = orisys.Sensor(
|
||||
input1,
|
||||
config_name="./config/ddjx01_wuxi.json",
|
||||
cal_path="./config/dd01a_0306.npy",
|
||||
isstitch=True,
|
||||
cuda=False,
|
||||
verbose=False,
|
||||
)
|
||||
|
||||
sensor2 = orisys.Sensor(
|
||||
input2,
|
||||
config_name="./config/ddjx01_wuxi.json",
|
||||
cal_path="./config/dd01a_0306.npy",
|
||||
isstitch=True,
|
||||
cuda=False,
|
||||
verbose=False,
|
||||
)
|
||||
|
||||
else:
|
||||
sensor1 = orisys.Sensor(
|
||||
args.video1,
|
||||
config_name="./config/ddjx01_wuxi.json",
|
||||
cal_path="./config/dd01a_0306.npy",
|
||||
isstitch=True,
|
||||
cuda=False,
|
||||
verbose=False,
|
||||
)
|
||||
sensor2 = orisys.Sensor(
|
||||
args.video2,
|
||||
config_name="./config/ddjx01_wuxi.json",
|
||||
cal_path="./config/dd01a_0306.npy",
|
||||
isstitch=True,
|
||||
cuda=False,
|
||||
verbose=False,
|
||||
)
|
||||
|
||||
def _shortcut_reset_tracker():
|
||||
sensor1.reset()
|
||||
sensor2.reset()
|
||||
print("传感器已重置 (快捷键 R)", flush=True)
|
||||
|
||||
def _shortcut_quit():
|
||||
combined_win.close()
|
||||
def _shortcut_toggle_open():
|
||||
global is_open
|
||||
is_open = not is_open
|
||||
print(f"营业状态: {'营业' if is_open else '非营业'}", flush=True)
|
||||
|
||||
_sc_r = QShortcut(QKeySequence(Qt.Key_R), combined_win)
|
||||
_sc_r.activated.connect(_shortcut_reset_tracker)
|
||||
if manual_open_enable:
|
||||
_sc_g = QShortcut(QKeySequence(Qt.Key_G), combined_win)
|
||||
_sc_g.activated.connect(_shortcut_toggle_open)
|
||||
_sc_q = QShortcut(QKeySequence(Qt.Key_Q), combined_win)
|
||||
_sc_q.activated.connect(_shortcut_quit)
|
||||
|
||||
video_cap = cv2.VideoCapture(VIDEO_LOOP_PATH)
|
||||
video_clock = {"fps": 30.0, "next_ts": None}
|
||||
if not video_cap.isOpened():
|
||||
print(f"[video] Failed to open loop video: {VIDEO_LOOP_PATH}", flush=True)
|
||||
else:
|
||||
src_fps = float(video_cap.get(cv2.CAP_PROP_FPS) or 0.0)
|
||||
if np.isfinite(src_fps) and src_fps >= 1.0:
|
||||
video_clock["fps"] = src_fps
|
||||
|
||||
start_time = time.time()
|
||||
is_save = False
|
||||
frame_count = 0
|
||||
writer = None
|
||||
sensor1_active = True
|
||||
sensor2_active = True
|
||||
# Local to main(): assignments inside the loop make this local; must exist
|
||||
# before the first iteration that skips the closed branch.
|
||||
start_open = False
|
||||
while True:
|
||||
now = datetime.now()
|
||||
if outside_opening_hours(now) or not is_open:
|
||||
start_open = False
|
||||
closed_hour_overlay.setGeometry(0, 0, combined_win.width(), combined_win.height())
|
||||
closed_hour_overlay.raise_()
|
||||
closed_hour_overlay.show()
|
||||
app.processEvents()
|
||||
if combined_win.user_closed or not combined_win.isVisible():
|
||||
print("\n窗口已关闭,退出程序")
|
||||
if writer is not None:
|
||||
writer.release()
|
||||
writer = None
|
||||
break
|
||||
time.sleep(1)
|
||||
continue
|
||||
|
||||
if not start_open:
|
||||
start_open = True
|
||||
sensor1.reset()
|
||||
sensor2.reset()
|
||||
print(f"传感器已重置: {datetime.now():%Y-%m-%d %H:%M:%S}", flush=True)
|
||||
closed_hour_overlay.hide()
|
||||
|
||||
if video_cap is not None and video_cap.isOpened():
|
||||
vret, vframe = read_loop_video_frame_realtime(video_cap, video_clock)
|
||||
if vret:
|
||||
if vframe.shape[1] != VIDEO_FRAME_W or vframe.shape[0] != VIDEO_FRAME_H:
|
||||
vframe = cv2.resize(vframe, (VIDEO_FRAME_W, VIDEO_FRAME_H), interpolation=cv2.INTER_LINEAR)
|
||||
set_video_label_bgr(video_label, vframe)
|
||||
|
||||
if sensor1_active:
|
||||
img1_valid = sensor1.get_img()
|
||||
if img1_valid is None:
|
||||
panel1_ui["arrow_label"].hide()
|
||||
panel1_ui["plot_widget"].hide()
|
||||
panel1_ui["disconnected_label"].show()
|
||||
sensor1_active = False
|
||||
print(f"Sensor1 disconnected. Showing DISCONNECTED screen. {datetime.now():%Y-%m-%d %H:%M:%S}", flush=True)
|
||||
else:
|
||||
sensor1.compute_deformation(check_motion=True, tail_frames=frame_to_recover, threshold=touch_threshold)
|
||||
_, fn, fx, fy, flow = sensor1.read_info(
|
||||
sensor1.info.FPS,
|
||||
sensor1.info.FNORMAL,
|
||||
sensor1.info.FSHEARX,
|
||||
sensor1.info.FSHEARY,
|
||||
sensor1.info.VRAW,
|
||||
)
|
||||
_bg_bgr = (22, 16, 11)
|
||||
_img = sensor1.img
|
||||
_h, _w = _img.shape[:2]
|
||||
black_img = np.empty((_h, _w, 3), dtype=_img.dtype)
|
||||
black_img[:, :, 0] = _bg_bgr[0]
|
||||
black_img[:, :, 1] = _bg_bgr[1]
|
||||
black_img[:, :, 2] = _bg_bgr[2]
|
||||
lw, lh = panel1_ui["arrow_label"].width(), panel1_ui["arrow_label"].height()
|
||||
if lw > 16 and lh > 16:
|
||||
side = max(32, min(lw, lh))
|
||||
else:
|
||||
side = compute_arrow_canvas_side(panel1_ui["panel"])
|
||||
arrows = draw_arrows_wuxi(
|
||||
black_img,
|
||||
flow,
|
||||
threshold=2,
|
||||
grid_spacing=20,
|
||||
arrow_scale=0.5,
|
||||
below_threshold_color=(160, 160, 160),
|
||||
target_size=int(side),
|
||||
)
|
||||
set_arrow_label_bgr(panel1_ui["arrow_label"], arrows)
|
||||
data1 = panel1_ui["data"]
|
||||
for i in range(3):
|
||||
data1[:-1, 0, i] = data1[1:, 0, i]
|
||||
data1[:-1, 1, i] = data1[1:, 1, i]
|
||||
data1[-1, 0, :] = frame_count / 15.0
|
||||
data1[-1, 1, 0] = fn
|
||||
data1[-1, 1, 1] = abs(fx)
|
||||
data1[-1, 1, 2] = abs(fy)
|
||||
update(
|
||||
panel1_ui["plot_af"],
|
||||
panel1_ui["plot_tfx"],
|
||||
panel1_ui["plot_tfy"],
|
||||
data1,
|
||||
panel1_ui["p1"],
|
||||
panel1_ui["p2"],
|
||||
panel1_ui["p3"],
|
||||
)
|
||||
|
||||
if sensor2_active:
|
||||
img2_valid = sensor2.get_img()
|
||||
if img2_valid is None:
|
||||
panel2_ui["arrow_label"].hide()
|
||||
panel2_ui["plot_widget"].hide()
|
||||
panel2_ui["disconnected_label"].show()
|
||||
sensor2_active = False
|
||||
print(f"Sensor2 disconnected. Showing DISCONNECTED screen. {datetime.now():%Y-%m-%d %H:%M:%S}", flush=True)
|
||||
else:
|
||||
sensor2.compute_deformation(check_motion=True, tail_frames=frame_to_recover, threshold=touch_threshold)
|
||||
_, fn2, fx2, fy2, flow2 = sensor2.read_info(
|
||||
sensor2.info.FPS,
|
||||
sensor2.info.FNORMAL,
|
||||
sensor2.info.FSHEARX,
|
||||
sensor2.info.FSHEARY,
|
||||
sensor2.info.VRAW,
|
||||
)
|
||||
_bg_bgr2 = (22, 16, 11)
|
||||
_img2 = sensor2.img
|
||||
_h2, _w2 = _img2.shape[:2]
|
||||
black_img2 = np.empty((_h2, _w2, 3), dtype=_img2.dtype)
|
||||
black_img2[:, :, 0] = _bg_bgr2[0]
|
||||
black_img2[:, :, 1] = _bg_bgr2[1]
|
||||
black_img2[:, :, 2] = _bg_bgr2[2]
|
||||
lw2, lh2 = panel2_ui["arrow_label"].width(), panel2_ui["arrow_label"].height()
|
||||
if lw2 > 16 and lh2 > 16:
|
||||
side2 = max(32, min(lw2, lh2))
|
||||
else:
|
||||
side2 = compute_arrow_canvas_side(panel2_ui["panel"])
|
||||
arrows2 = draw_arrows_wuxi(
|
||||
black_img2,
|
||||
flow2,
|
||||
threshold=2,
|
||||
grid_spacing=20,
|
||||
arrow_scale=0.5,
|
||||
below_threshold_color=(160, 160, 160),
|
||||
target_size=int(side2),
|
||||
)
|
||||
set_arrow_label_bgr(panel2_ui["arrow_label"], arrows2)
|
||||
data2 = panel2_ui["data"]
|
||||
for i in range(3):
|
||||
data2[:-1, 0, i] = data2[1:, 0, i]
|
||||
data2[:-1, 1, i] = data2[1:, 1, i]
|
||||
data2[-1, 0, :] = frame_count / 15.0
|
||||
data2[-1, 1, 0] = fn2
|
||||
data2[-1, 1, 1] = abs(fx2)
|
||||
data2[-1, 1, 2] = abs(fy2)
|
||||
update(
|
||||
panel2_ui["plot_af"],
|
||||
panel2_ui["plot_tfx"],
|
||||
panel2_ui["plot_tfy"],
|
||||
data2,
|
||||
panel2_ui["p1"],
|
||||
panel2_ui["p2"],
|
||||
panel2_ui["p3"],
|
||||
)
|
||||
|
||||
app.processEvents()
|
||||
now = datetime.now()
|
||||
if now.hour == 9 and now.minute == 0 and now.second == 0:
|
||||
if sensor1_active:
|
||||
sensor1.reset()
|
||||
if sensor2_active:
|
||||
sensor2.reset()
|
||||
print(
|
||||
f"[session] 传感器已重置: {now:%Y-%m-%d %H:%M:%S} ",
|
||||
flush=True,
|
||||
)
|
||||
|
||||
if not sensor1_active and not sensor2_active:
|
||||
while not combined_win.user_closed and combined_win.isVisible():
|
||||
app.processEvents()
|
||||
time.sleep(0.05)
|
||||
break
|
||||
|
||||
frame_count += 1
|
||||
app.processEvents()
|
||||
|
||||
if combined_win.user_closed or not combined_win.isVisible():
|
||||
print("\n窗口已关闭,退出程序")
|
||||
if writer is not None:
|
||||
writer.release()
|
||||
writer = None
|
||||
break
|
||||
|
||||
# Convert combined window to image (arrows + curves)
|
||||
pixmap = combined_win.grab()
|
||||
# Convert QPixmap to QImage
|
||||
qimg = pixmap.toImage()
|
||||
# Convert QImage to numpy array (RGB format)
|
||||
qimg = qimg.convertToFormat(QImage.Format_RGB888)
|
||||
width = qimg.width()
|
||||
height = qimg.height()
|
||||
# Convert QImage to numpy array
|
||||
ptr = qimg.bits()
|
||||
ptr.setsize(height * width * 3)
|
||||
arr = np.array(ptr, dtype=np.uint8).reshape((height, width, 3))
|
||||
|
||||
except BaseException as exc:
|
||||
session_abnormal = datetime.now()
|
||||
print(
|
||||
f"[session] ABNORMAL at {session_abnormal:%Y-%m-%d %H:%M:%S}: "
|
||||
f"{type(exc).__name__}: {exc}",
|
||||
flush=True,
|
||||
)
|
||||
raise
|
||||
finally:
|
||||
session_end = datetime.now()
|
||||
elapsed = session_end - session_start
|
||||
print(
|
||||
f"[session] END {session_end:%Y-%m-%d %H:%M:%S} "
|
||||
f"(elapsed {elapsed})",
|
||||
flush=True,
|
||||
)
|
||||
if sensor1 is not None:
|
||||
try:
|
||||
sensor1.disconnect()
|
||||
except Exception as disconnect_err:
|
||||
print(f"[session] disconnect error: {disconnect_err} {datetime.now():%Y-%m-%d %H:%M:%S}", flush=True)
|
||||
if 'sensor2' in locals() and sensor2 is not None:
|
||||
try:
|
||||
sensor2.disconnect()
|
||||
except Exception as disconnect_err:
|
||||
print(f"[session] disconnect error (sensor2): {disconnect_err} {datetime.now():%Y-%m-%d %H:%M:%S}", flush=True)
|
||||
if video_cap is not None:
|
||||
video_cap.release()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,248 @@
|
||||
import os
|
||||
os.environ["OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS"] = "0"
|
||||
import cv2
|
||||
import orisys
|
||||
import numpy as np
|
||||
import time
|
||||
import argparse
|
||||
import pyqtgraph as pg
|
||||
from PyQt5.QtGui import QImage, QFont
|
||||
from pyqtgraph.Qt import QtGui
|
||||
|
||||
def make_gradient_brush(color_hex, max_alpha=150):
|
||||
"""
|
||||
创建一个从上到下渐变的笔刷 (兼容 PyQt5 和 PyQt6)
|
||||
"""
|
||||
c_top = QtGui.QColor(color_hex)
|
||||
c_top.setAlpha(max_alpha) # 顶部半透明
|
||||
|
||||
c_bot = QtGui.QColor(color_hex)
|
||||
c_bot.setAlpha(0) # 底部全透明
|
||||
|
||||
# 创建线性渐变:(0,0)到(0,1) 代表从控件顶部到底部
|
||||
grad = QtGui.QLinearGradient(0, 0, 0, 1)
|
||||
|
||||
if hasattr(QtGui.QGradient, 'CoordinateMode'):
|
||||
grad.setCoordinateMode(QtGui.QGradient.CoordinateMode.ObjectBoundingMode)
|
||||
else:
|
||||
grad.setCoordinateMode(QtGui.QGradient.ObjectBoundingMode)
|
||||
# ==============================================
|
||||
|
||||
grad.setColorAt(0, c_top) # 0.0 (顶端) 颜色
|
||||
grad.setColorAt(1, c_bot) # 1.0 (底端) 颜色
|
||||
|
||||
return QtGui.QBrush(grad)
|
||||
|
||||
def create_plot():
|
||||
|
||||
pg.setConfigOption('background', '#0B1016') # 深蓝灰背景
|
||||
pg.setConfigOption('foreground', '#A0A0A0') # 浅灰文字
|
||||
pg.setConfigOptions(antialias=True) # 抗锯齿
|
||||
theme_color = '#00E5FF'
|
||||
line_pen = pg.mkPen(color=theme_color, width=2)
|
||||
grad_brush = make_gradient_brush(theme_color, max_alpha=120)
|
||||
|
||||
axis_font = QFont("Segoe UI", 11)
|
||||
p1 = win.addPlot()
|
||||
p1.setLabel('left', 'N')
|
||||
p1.getAxis('left').label.setFont(axis_font)
|
||||
p1.getAxis('left').label.setRotation(-90)
|
||||
p1.showGrid(x=True, y=True, alpha=0.3)
|
||||
win.nextRow()
|
||||
p2 = win.addPlot()
|
||||
p2.setLabel('left', 'X')
|
||||
p2.getAxis('left').label.setFont(axis_font)
|
||||
p2.getAxis('left').label.setRotation(-90)
|
||||
p2.showGrid(x=True, y=True, alpha=0.3)
|
||||
win.nextRow()
|
||||
p3 = win.addPlot()
|
||||
p3.setLabel('left', 'Y')
|
||||
p3.setLabel('bottom', 'Time', units='s')
|
||||
p3.getAxis('left').label.setFont(axis_font)
|
||||
p3.getAxis('left').label.setRotation(-90)
|
||||
p3.getAxis('bottom').label.setFont(axis_font)
|
||||
p3.showGrid(x=True, y=True, alpha=0.3)
|
||||
plot_af = p1.plot(pen=line_pen, fillLevel=0, fillBrush=grad_brush)
|
||||
plot_tfx = p2.plot(pen=line_pen, fillLevel=0, fillBrush=grad_brush)
|
||||
plot_tfy = p3.plot(pen=line_pen, fillLevel=0, fillBrush=grad_brush)
|
||||
return p1, p2, p3, plot_af, plot_tfx, plot_tfy
|
||||
|
||||
def update(plot_af, plot_tfx, plot_tfy, data, p1, p2, p3):
|
||||
# global plot_af, plot_tfx, plot_tfy, data, p1, p2, p3
|
||||
plot_af.setData(data[:,:,0])
|
||||
p1.setYRange(min(data[:,1,0]),max(data[:,1,0]))
|
||||
plot_tfx.setData(data[:,:,1])
|
||||
p2.setYRange(min(data[:,1,1]),max(data[:,1,1]))
|
||||
plot_tfy.setData(data[:,:,2])
|
||||
p3.setYRange(min(data[:,1,2]),max(data[:,1,2]))
|
||||
|
||||
# 深度图
|
||||
def draw_magnitude_map(vfield, threshold=3.0, colormap=cv2.COLORMAP_JET) -> np.ndarray:
|
||||
"""
|
||||
Draw color map of pointwise magnitude of velocity field.
|
||||
|
||||
Args:
|
||||
vfield: Velocity field to visualize (2D vector field)
|
||||
threshold: Minimum magnitude to display (below threshold shows as zero/blue)
|
||||
colormap: OpenCV colormap to use (default: COLORMAP_JET)
|
||||
|
||||
Returns:
|
||||
np.ndarray: 400x400 BGR image with colored magnitude map
|
||||
"""
|
||||
flow_field = vfield
|
||||
|
||||
# Compute magnitude at each point
|
||||
magnitude = np.sqrt(flow_field[:, :, 0]**2 + flow_field[:, :, 1]**2)
|
||||
|
||||
# Smooth by downsampling and interpolating back
|
||||
downsample_factor = 10
|
||||
h, w = magnitude.shape
|
||||
magnitude_downsampled = magnitude[::downsample_factor, ::downsample_factor]
|
||||
magnitude_smoothed = cv2.resize(magnitude_downsampled, (w, h), interpolation=cv2.INTER_CUBIC)
|
||||
|
||||
# Apply threshold - set below-threshold values to zero
|
||||
magnitude_thresholded = magnitude_smoothed.copy()
|
||||
magnitude_thresholded[magnitude_smoothed < threshold] = 0
|
||||
|
||||
# Normalize to 0-255 range
|
||||
mag_min, mag_max = magnitude_thresholded.min(), magnitude_thresholded.max()
|
||||
if mag_max > mag_min:
|
||||
magnitude_norm = ((magnitude_thresholded - mag_min) / (mag_max - mag_min) * 255).astype(np.uint8)
|
||||
else:
|
||||
magnitude_norm = np.zeros_like(magnitude_thresholded, dtype=np.uint8)
|
||||
|
||||
# Apply colormap
|
||||
magnitude_colored = cv2.applyColorMap(magnitude_norm, colormap)
|
||||
|
||||
# Resize to output size
|
||||
out_img = cv2.resize(magnitude_colored, (400, 400), interpolation=cv2.INTER_CUBIC)
|
||||
|
||||
return out_img
|
||||
|
||||
# 实时图表
|
||||
npoints = 100 # 图表数据长度
|
||||
pg.setConfigOption('background', 'k')
|
||||
pg.setConfigOption('foreground', 'w')
|
||||
app = pg.mkQApp("Plotting Example")
|
||||
win = pg.GraphicsLayoutWidget(show=True, title="Basic plotting examples")
|
||||
win.setWindowTitle('pyqtgraph example: Plotting')
|
||||
p1, p2, p3, plot_af, plot_tfx, plot_tfy = create_plot()
|
||||
data = np.zeros([npoints,2,3])
|
||||
start_time = None # Will be set when sensor starts
|
||||
fps = 30.0
|
||||
for i in range(3):
|
||||
data[:,0,i] = np.linspace(0.0, npoints / fps, npoints)
|
||||
|
||||
|
||||
|
||||
def main():
|
||||
global start_time
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--video","-v", type=str, default="s1.mp4", help="视频文件路径")
|
||||
args = parser.parse_args()
|
||||
|
||||
arrow_writer = cv2.VideoWriter(args.video.replace(".mp4", "_arrows.mp4"), cv2.VideoWriter_fourcc(*'mp4v'), 30, (365, 365))
|
||||
pyqt_writer = cv2.VideoWriter(args.video.replace(".mp4", "_pyqt.mp4"), cv2.VideoWriter_fourcc(*'mp4v'), 30, (1600, 1200))
|
||||
depth_writer = cv2.VideoWriter(args.video.replace(".mp4", "_depth.mp4"), cv2.VideoWriter_fourcc(*'mp4v'), 30, (365, 365))
|
||||
|
||||
sensor1 = orisys.Sensor(args.video, config_name="./config/ddjx01.json", cal_path= "./config/dd01a_0306.npy", isstitch=True, cuda=False, verbose=True)
|
||||
|
||||
start_time = time.time()
|
||||
is_save = False
|
||||
frame_count = 0
|
||||
writer = None
|
||||
while True:
|
||||
isimg = sensor1.get_img() # 提取并拼接复眼图像
|
||||
if isimg is None:
|
||||
break
|
||||
|
||||
|
||||
sensor1.compute_deformation() # 计算形变和分解
|
||||
sensor1.compute_contact()
|
||||
# 提取信息:
|
||||
# 向量场: VRAW, VNORMAL, VSHEAR: 原始,法向,切向
|
||||
# 帧率,时间戳: FPS, TIMESTAMP
|
||||
# 变形量总和: FNORMAL , FSHEARX, FSHEARY : 法向,切向(X),切向(Y)
|
||||
fps, fn, fx, fy, flow, depth_map = sensor1.read_info(sensor1.info.FPS, sensor1.info.FNORMAL, sensor1.info.FSHEARX, sensor1.info.FSHEARY, sensor1.info.VRAW, sensor1.info.DEPTH)
|
||||
print(fps,fn,fx,fy)
|
||||
# 显示向量场
|
||||
_bg_bgr = (22, 16, 11)
|
||||
black_img = np.empty_like(sensor1.img)
|
||||
black_img[:, :, 0] = _bg_bgr[0]
|
||||
black_img[:, :, 1] = _bg_bgr[1]
|
||||
black_img[:, :, 2] = _bg_bgr[2]
|
||||
|
||||
arrows = orisys.util.draw_arrows(black_img, flow, threshold=2, grid_spacing=20, arrow_scale=0.5,below_threshold_color=(160, 160, 160),edge_margin=15,colormap=cv2.COLORMAP_PARULA)
|
||||
cv2.imshow("Deformation", arrows)
|
||||
|
||||
# 显示深度图
|
||||
div_abs = 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)
|
||||
|
||||
# 应用颜色映射(使用JET:蓝色=低散度,红色=高散度)
|
||||
depth_colored = cv2.applyColorMap(div_normalized, cv2.COLORMAP_JET)
|
||||
cv2.imshow("Depth Map", depth_colored)
|
||||
# cv2.imshow("Depth Map", cv2.applyColorMap(div_normalized, cv2.COLORMAP_JET))
|
||||
|
||||
# 显示图表 - 更新时间轴
|
||||
current_time = time.time() - start_time
|
||||
for i in range(3):
|
||||
data[:-1, 0, i] = data[1:, 0, i] # shift time data left
|
||||
data[:-1, 1, i] = data[1:, 1, i] # shift force data left
|
||||
data[-1, 0, :] = frame_count/30.0 # set current time for all plots
|
||||
data[-1, 1, 0] = fn
|
||||
data[-1, 1, 1] = abs(fx)
|
||||
data[-1, 1, 2] = abs(fy)
|
||||
update(plot_af, plot_tfx, plot_tfy, data, p1, p2, p3) # Update plot
|
||||
frame_count += 1
|
||||
|
||||
# Convert pyqtgraph plot to image
|
||||
# Grab the GraphicsLayoutWidget as QPixmap
|
||||
pixmap = win.grab()
|
||||
# Convert QPixmap to QImage
|
||||
qimg = pixmap.toImage()
|
||||
# Convert QImage to numpy array (RGB format)
|
||||
qimg = qimg.convertToFormat(QImage.Format_RGB888)
|
||||
width = qimg.width()
|
||||
height = qimg.height()
|
||||
# Convert QImage to numpy array
|
||||
ptr = qimg.bits()
|
||||
ptr.setsize(height * width * 3)
|
||||
arr = np.array(ptr, dtype=np.uint8).reshape((height, width, 3))
|
||||
# Convert RGB to BGR for OpenCV
|
||||
pyqt_img_bgr = cv2.cvtColor(arr, cv2.COLOR_RGB2BGR)
|
||||
# Resize to match video writer dimensions (1920, 1080)
|
||||
pyqt_img_resized = cv2.resize(pyqt_img_bgr, (1600, 1200), interpolation=cv2.INTER_LINEAR)
|
||||
|
||||
arrow_writer.write(arrows)
|
||||
pyqt_writer.write(pyqt_img_resized)
|
||||
depth_frame = cv2.resize(depth_colored, (365, 365), interpolation=cv2.INTER_LINEAR)
|
||||
depth_writer.write(depth_frame)
|
||||
# depth_writer.write(depth_map)
|
||||
|
||||
key = cv2.waitKey(1)
|
||||
if key & 0xFF == ord("q"):
|
||||
if writer is not None:
|
||||
writer.release()
|
||||
writer = None
|
||||
print("\n 停止程序")
|
||||
break
|
||||
if key == ord("r"):
|
||||
# 重置光流追踪器(当追踪漂移时使用)
|
||||
sensor1.reset()
|
||||
print("传感器已重置")
|
||||
|
||||
# Release video writers
|
||||
arrow_writer.release()
|
||||
pyqt_writer.release()
|
||||
depth_writer.release()
|
||||
sensor1.disconnect() # 断开传感器连接
|
||||
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
main()
|
||||
@@ -0,0 +1,159 @@
|
||||
import argparse
|
||||
import av
|
||||
import cv2
|
||||
import platform
|
||||
import subprocess
|
||||
import sys
|
||||
import time
|
||||
|
||||
TARGET_FPS = 30
|
||||
WIDTH = 1920
|
||||
HEIGHT = 1080
|
||||
|
||||
|
||||
def list_windows_dshow_devices():
|
||||
"""
|
||||
Print DirectShow devices using ffmpeg, if available.
|
||||
"""
|
||||
cmd = ["ffmpeg", "-hide_banner", "-list_devices", "true", "-f", "dshow", "-i", "dummy"]
|
||||
print("Listing Windows DirectShow devices via ffmpeg...")
|
||||
try:
|
||||
result = subprocess.run(cmd, capture_output=True, text=True, check=False)
|
||||
except FileNotFoundError as exc:
|
||||
raise RuntimeError("ffmpeg was not found in PATH; cannot list DirectShow devices.") from exc
|
||||
|
||||
output = (result.stderr or "") + (result.stdout or "")
|
||||
print(output.strip())
|
||||
|
||||
|
||||
def open_pyav_capture(system_name, device, pixel_format):
|
||||
"""
|
||||
Open a camera device via PyAV on Linux or Windows.
|
||||
"""
|
||||
if system_name == "linux":
|
||||
options = {
|
||||
"video_size": f"{WIDTH}x{HEIGHT}",
|
||||
"framerate": str(TARGET_FPS),
|
||||
"input_format": pixel_format or "mjpeg",
|
||||
}
|
||||
container = av.open(device or "/dev/video4", format="v4l2", options=options)
|
||||
elif system_name == "windows":
|
||||
if not device:
|
||||
raise RuntimeError(
|
||||
"On Windows, PyAV/dshow requires a camera name, for example "
|
||||
'--device "USB Camera". Use --list-devices first.'
|
||||
)
|
||||
options = {
|
||||
"video_size": f"{WIDTH}x{HEIGHT}",
|
||||
"framerate": str(TARGET_FPS),
|
||||
}
|
||||
if pixel_format:
|
||||
options["pixel_format"] = pixel_format
|
||||
container = av.open(f"video={device}", format="dshow", options=options)
|
||||
else:
|
||||
raise RuntimeError(
|
||||
f"Unsupported OS: {platform.system()}. Only Linux and Windows are supported."
|
||||
)
|
||||
|
||||
stream = container.streams.video[0]
|
||||
return container, stream
|
||||
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(
|
||||
description="Capture from camera via PyAV on Linux or Windows, optionally save to MP4."
|
||||
)
|
||||
parser.add_argument(
|
||||
"-d",
|
||||
"--device",
|
||||
type=str,
|
||||
default="",
|
||||
help=(
|
||||
"Linux: V4L2 device path like /dev/video4. "
|
||||
"Windows: DirectShow camera name like 'USB Camera'. "
|
||||
"If empty, Linux uses /dev/video4; Windows requires --device."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--pixel-format",
|
||||
type=str,
|
||||
default="",
|
||||
help=(
|
||||
"Optional input pixel format / format hint. "
|
||||
"Examples: Linux 'mjpeg'; Windows may accept values such as 'mjpeg' if supported."
|
||||
),
|
||||
)
|
||||
parser.add_argument(
|
||||
"--list-devices",
|
||||
action="store_true",
|
||||
help="On Windows, list DirectShow device names via ffmpeg and exit.",
|
||||
)
|
||||
parser.add_argument(
|
||||
"-o",
|
||||
"--output",
|
||||
type=str,
|
||||
default="",
|
||||
help="Output MP4 path (e.g. capture.mp4). If empty, only preview, no file.",
|
||||
)
|
||||
args = parser.parse_args()
|
||||
|
||||
system_name = platform.system().lower()
|
||||
if args.list_devices:
|
||||
if system_name != "windows":
|
||||
raise RuntimeError("--list-devices is currently only implemented for Windows.")
|
||||
list_windows_dshow_devices()
|
||||
return
|
||||
|
||||
container, stream = open_pyav_capture(system_name, args.device, args.pixel_format)
|
||||
print(
|
||||
f"Using PyAV backend on {platform.system()} "
|
||||
f"with device={args.device or '/dev/video4'}"
|
||||
)
|
||||
|
||||
frame_count = 0
|
||||
t_start = time.perf_counter()
|
||||
|
||||
writer = None
|
||||
if args.output:
|
||||
fourcc = cv2.VideoWriter_fourcc(*"mp4v")
|
||||
writer = cv2.VideoWriter(
|
||||
args.output, fourcc, float(TARGET_FPS), (WIDTH, HEIGHT)
|
||||
)
|
||||
if not writer.isOpened():
|
||||
container.close()
|
||||
raise RuntimeError(f"Could not open VideoWriter for {args.output}")
|
||||
print(f"Recording to {args.output} @ {TARGET_FPS} fps, {WIDTH}x{HEIGHT}")
|
||||
|
||||
try:
|
||||
for frame in container.decode(stream):
|
||||
t0 = time.perf_counter()
|
||||
img = frame.to_ndarray(format="bgr24") # OpenCV-compatible
|
||||
t1 = time.perf_counter()
|
||||
|
||||
if writer is not None:
|
||||
writer.write(img)
|
||||
|
||||
cv2.imshow("pyav_cam", img)
|
||||
key = cv2.waitKey(1) & 0xFF
|
||||
t2 = time.perf_counter()
|
||||
|
||||
frame_count += 1
|
||||
if frame_count % 30 == 0:
|
||||
elapsed = t2 - t_start
|
||||
print(
|
||||
f"Frame {frame_count}: convert={1000*(t1-t0):.1f}ms, "
|
||||
f"imshow+waitKey={1000*(t2-t1):.1f}ms, avg_fps={frame_count/elapsed:.1f}"
|
||||
)
|
||||
|
||||
if key == ord("q"):
|
||||
break
|
||||
finally:
|
||||
container.close()
|
||||
if writer is not None:
|
||||
writer.release()
|
||||
print(f"Saved {frame_count} frames to {args.output}")
|
||||
cv2.destroyAllWindows()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user