Files
2026-06-08 17:52:48 +08:00

256 lines
10 KiB
Python
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""
示例 3:实时曲线与幅值图显示
本示例演示如何在读取传感器结果的同时:
1. 显示形变矢量场
2. 显示流场幅值热力图
3. 使用 PyQtGraph 实时绘制法向力与切向力曲线
4. 按键控制录像开始与结束
SDK version: 0.3.1
"""
import os
import sys
os.environ["OPENCV_VIDEOIO_MSMF_ENABLE_HW_TRANSFORMS"] = "0"
import cv2
import orisys
import numpy as np
import time
import argparse
from datetime import datetime
try:
import pyqtgraph as pg
except:
print("PyQtGraph is not installed. Please install it using 'pip install pyqt5 pyqtgraph'.")
exit()
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):
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, colormap=cv2.COLORMAP_JET) -> np.ndarray:
"""
绘制流场幅值伪彩色图。
Args:
vfield: 需要可视化的二维矢量场
threshold: 最小显示阈值;低于阈值的区域显示为低值颜色
colormap: OpenCV 颜色映射类型(默认 COLORMAP_JET
Returns:
np.ndarray: 400x400 的 BGR 伪彩图像
"""
flow_field = vfield
# 计算每个像素点的幅值
magnitude = np.sqrt(flow_field[:, :, 0]**2 + flow_field[:, :, 1]**2)
# 先降采样再插值回原尺寸,使显示更平滑
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)
# 应用阈值,将低于阈值的区域置零
magnitude_thresholded = magnitude_smoothed.copy()
magnitude_thresholded[magnitude_smoothed < threshold] = 0
# 归一化到 0-255
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)
# 应用伪彩色映射
magnitude_colored = cv2.applyColorMap(magnitude_norm, colormap)
# 缩放到固定输出尺寸
out_img = cv2.resize(magnitude_colored, (400, 400), interpolation=cv2.INTER_CUBIC)
# 添加边距,使图像在窗口中居中显示
# margin = 30
# out_img = cv2.copyMakeBorder(out_img, margin, margin, margin, margin,
# cv2.BORDER_CONSTANT, value=(22, 16, 11))
return out_img
# 实时图表
npoints = 100 # 曲线缓冲区长度
pg.setConfigOption('background', 'w')
pg.setConfigOption('foreground', 'k')
app = pg.mkQApp("Orisys Plot Example")
win = pg.GraphicsLayoutWidget(show=True, title="Orisys 实时力曲线")
win.setWindowTitle('Orisys 实时力曲线')
win.resize(640, 400)
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
# 将所有输出重定向到日志文件
script_dir = os.path.dirname(os.path.abspath(__file__))
log_dir = os.path.join(script_dir, "logs")
os.makedirs(log_dir, exist_ok=True)
log_path = os.path.join(log_dir, f"plot_{datetime.now().strftime('%Y%m%d_%H%M%S')}.log")
sys.stdout = open(log_path, 'w', encoding='utf-8')
sys.stderr = sys.stdout
parser = argparse.ArgumentParser()
parser.add_argument("--video","-v", type=str, default="0", help="视频源:摄像头编号或视频文件路径")
parser.add_argument("--config","-c", type=str, default="./config/ddjx01.json", help="配置文件名称或路径")
parser.add_argument("--cal","-cal", type=str, default="./config/ddjx01.npy", help="标定文件路径")
parser.add_argument("--verbose","-verbose", type=bool, default=True, help="是否输出详细日志")
args = parser.parse_args()
# 步骤 1:创建传感器对象(必需)
# config_name 可以是内置配置名称,也可以是自定义配置文件路径
# cal_path 为标定文件路径;每台设备建议使用独立的标定文件
# 输入既可以是摄像头编号,也可以是视频文件路径
try:
input = int(args.video)
is_camera = True
sensor = orisys.Sensor(input, config_name=args.config, cal_path= args.cal, verbose=args.verbose)
except:
is_camera = False
sensor = orisys.Sensor(args.video, config_name=args.config, cal_path= args.cal, verbose=args.verbose)
# 下面保留了历史示例写法,便于参考
# sensor1 = orisys.Sensor("./s1.mp4", 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
save_path = None
print("\n按键说明:'q' 退出程序,'s' 开始录像,'e' 结束录像。")
while True:
sensor.get_img() # 获取并拼接复眼图像
if is_save and writer is not None and sensor.frame is not None:
frame_count += 1
save_img = sensor.frame
# 若为灰度图,则先转换为 BGR 再写入视频
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)
sensor.compute_deformation(check_motion=True, threshold=0) # 计算形变与分解结果
# 读取信息:
# 向量场:VRAW、VNORMAL、VSHEAR -> 原始 / 法向 / 切向
# 标量:FNORMAL、FSHEARX、FSHEARY -> 法向力 / X 向切向力 / Y 向切向力
fps, fn, fx, fy, flow, vnormal, vshear = sensor.read_info(sensor.info.FPS, sensor.info.FNORMAL, sensor.info.FSHEARX, sensor.info.FSHEARY, sensor.info.VRAW, sensor.info.VNORMAL,sensor.info.VSHEAR)
print(f"FPS={fps:.2f}, 法向力={fn:.4f}, 切向力X={fx:.4f}, 切向力Y={fy:.4f}")
# 显示形变矢量场
# arrows = orisys.util.draw_arrows(sensor.img, flow, threshold=5, grid_spacing=10, arrow_scale=1.0)
_bg_bgr = (22, 16, 11)
_img = sensor.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]
arrows = orisys.util.draw_arrows(
black_img,
flow,
threshold=2,
grid_spacing=20,
arrow_scale=0.5,
below_threshold_color=(160, 160, 160),
# target_size=int(side),
)
arrows = cv2.resize(arrows, (400, 400), interpolation=cv2.INTER_CUBIC)
arrows = cv2.rotate(arrows, cv2.ROTATE_90_COUNTERCLOCKWISE)
cv2.imshow("形变矢量场".encode("gbk"), arrows)
# 显示流场幅值图
mag = draw_magnitude_map(flow)
mag = cv2.rotate(mag, cv2.ROTATE_90_COUNTERCLOCKWISE)
cv2.imshow("流场幅值图".encode("gbk"), mag)
# 更新实时曲线
current_time = time.time() - start_time
for i in range(3):
data[:-1, 0, i] = data[1:, 0, i] # 时间轴左移
data[:-1, 1, i] = data[1:, 1, i] # 力数据左移
data[-1, 0, :] = current_time # 写入当前时间
data[-1, 1, 0] = fn
data[-1, 1, 1] = fx
data[-1, 1, 2] = fy
update(plot_af, plot_tfx, plot_tfy, data, p1, p2, p3) # 更新曲线
key = cv2.waitKey(1)
if key & 0xFF == ord("q"):
print("\n正在退出示例程序...")
break
if key == ord("s"):
save_path = f"./data/pulse_test.mp4"
# 确保输出目录存在
os.makedirs(os.path.dirname(save_path), exist_ok=True)
# 从当前帧获取视频尺寸
save_img = sensor.frame
if save_img is not None:
# 如为灰度图,先转换为 BGR 以获取视频尺寸
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"开始录像:{save_path}")
else:
print(f"无法创建录像文件:{save_path}")
writer = None
else:
print("当前没有可用帧,无法确定录像尺寸。")
if key == ord("e"):
is_save = False
if writer is not None:
writer.release()
writer = None
if save_path is not None:
print(f"录像结束:{save_path},总帧数:{frame_count}")
else:
print(f"录像结束,总帧数:{frame_count}")
frame_count = 0
sensor.disconnect() # 断开传感器连接并释放资源
if __name__ == '__main__':
main()