import cv2
import numpy as np
import time
import logging
import argparse
# 配置日志
logging.basicConfig(level=logging.INFO,
format='%(asctime)s - %(name)s - %(levelname)s - %(message)s')
logger = logging.getLogger('OpticalTweezerVideoTester')
class OpticalTweezerVideoTester:
def __init__(self, video_path, output_path=None):
# 视频文件路径
self.video_path = video_path
# 输出设置
self.output_path = output_path
self.video_writer = None
# 处理参数
self.min_radius = 10
self.max_radius = 70
self.blur_size = (9, 9)
self.pixel_to_micron = 0.1
# 分析结果存储
self.trajectories = {} # {track_id: [(frame_idx, x, y, radius), ...]}
self.next_track_id = 1
self.current_frame_idx = 0
# 性能监控
self.frame_count = 0
self.start_time = time.time()
self.processing_times = []
# 创建调参窗口
cv2.namedWindow('Parameters')
cv2.createTrackbar('Min Radius', 'Parameters', self.min_radius, 100, self.on_trackbar)
cv2.createTrackbar('Max Radius', 'Parameters', self.max_radius, 200, self.on_trackbar)
cv2.createTrackbar('Blur Size', 'Parameters', self.blur_size[0], 30, self.on_trackbar)
def initialize_video(self):
"""初始化视频捕获"""
self.cap = cv2.VideoCapture(self.video_path)
if not self.cap.isOpened():
logger.error(f"无法打开视频文件: {self.video_path}")
raise RuntimeError("视频初始化失败")
# 获取视频属性
self.width = int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH))
self.height = int(self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
self.fps = self.cap.get(cv2.CAP_PROP_FPS)
self.frame_count = int(self.cap.get(cv2.CAP_PROP_FRAME_COUNT))
logger.info(f"视频属性: {self.width}x{self.height} @ {self.fps:.1f} FPS, 总帧数: {self.frame_count}")
# 初始化视频写入器
if self.output_path:
fourcc = cv2.VideoWriter_fourcc(*'mp4v') # 使用MP4V编解码器
self.video_writer = cv2.VideoWriter(
self.output_path, fourcc, self.fps, (self.width, self.height)
)
if not self.video_writer.isOpened():
logger.warning("无法创建输出视频文件")
return True
def on_trackbar(self, value):
"""实时参数调整的回调函数"""
self.min_radius = cv2.getTrackbarPos('Min Radius', 'Parameters')
self.max_radius = cv2.getTrackbarPos('Max Radius', 'Parameters')
blur_value = cv2.getTrackbarPos('Blur Size', 'Parameters')
self.blur_size = (blur_value if blur_value % 2 != 0 else blur_value + 1,
blur_value if blur_value % 2 != 0 else blur_value + 1)
def detect_spheres(self, frame):
"""优化的小球检测算法"""
# 转为灰度图并应用CLAHE增强对比度
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
clahe = cv2.createCLAHE(clipLimit=2.0, tileGridSize=(8, 8))
enhanced = clahe.apply(gray)
# 高斯模糊降噪
blurred = cv2.GaussianBlur(enhanced, self.blur_size, 0)
# 使用自适应阈值
thresh = cv2.adaptiveThreshold(
blurred, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY_INV, 11, 2
)
# 形态学操作优化
kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (3, 3))
cleaned = cv2.morphologyEx(thresh, cv2.MORPH_OPEN, kernel)
# 轮廓检测
contours, _ = cv2.findContours(cleaned, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
positions = []
for cnt in contours:
area = cv2.contourArea(cnt)
# 跳过过小的区域
if area < 30:
continue
# 最小包围圆检测
(x, y), radius = cv2.minEnclosingCircle(cnt)
radius = int(radius)
# 半径过滤
if self.min_radius <= radius <= self.max_radius:
positions.append((int(x), int(y), radius))
return positions
def track_spheres(self, positions):
"""简单的小球追踪"""
if not hasattr(self, 'previous_positions'):
# 第一帧,初始化所有轨迹
self.previous_positions = {}
for pos in positions:
track_id = self.next_track_id
self.next_track_id += 1
self.trajectories[track_id] = [(self.current_frame_idx, pos[0], pos[1], pos[2])]
self.previous_positions[track_id] = (pos[0], pos[1])
return
# 创建当前帧的跟踪列表
current_tracks = {}
# 为每个检测点寻找最近的已有轨迹
for pos in positions:
min_dist = float('inf')
matched_id = None
for track_id, prev_pos in self.previous_positions.items():
dist = np.sqrt((pos[0] - prev_pos[0])**2 + (pos[1] - prev_pos[1])**2)
# 最大移动距离阈值(像素)
if dist < 50 and dist < min_dist:
min_dist = dist
matched_id = track_id
if matched_id:
# 更新现有轨迹
self.trajectories[matched_id].append((self.current_frame_idx, pos[0], pos[1], pos[2]))
current_tracks[matched_id] = (pos[0], pos[1])
else:
# 创建新轨迹
track_id = self.next_track_id
self.next_track_id += 1
self.trajectories[track_id] = [(self.current_frame_idx, pos[0], pos[1], pos[2])]
current_tracks[track_id] = (pos[0], pos[1])
# 更新上一帧位置
self.previous_positions = current_tracks
def visualize(self, frame, positions):
"""增强可视化功能"""
display_frame = frame.copy()
# 绘制检测到的小球
for (x, y, r) in positions:
# 绘制小球和中心点
cv2.circle(display_frame, (x, y), r, (0, 255, 0), 2)
cv2.circle(display_frame, (x, y), 2, (0, 0, 255), 3)
# 显示物理尺寸
phys_x = x * self.pixel_to_micron
phys_y = y * self.pixel_to_micron
txt = f"({phys_x:.1f}μm, {phys_y:.1f}μm)"
cv2.putText(display_frame, txt, (x + 10, y),
cv2.FONT_HERSHEY_SIMPLEX, 0.5, (255, 255, 0), 1)
# 显示当前帧信息
elapsed_time = time.time() - self.start_time
fps = self.frame_count / elapsed_time if elapsed_time > 0 else 0
avg_time = np.mean(self.processing_times) * 1000 if self.processing_times else 0
info_y = 30
cv2.putText(display_frame, f"Frame: {self.current_frame_idx}/{self.frame_count}", (10, info_y),
cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 255), 2)
cv2.putText(display_frame, f"FPS: {fps:.1f}", (10, info_y+30),
cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 255), 2)
cv2.putText(display_frame, f"Spheres: {len(positions)}", (10, info_y+60),
cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 255), 2)
cv2.putText(display_frame, f"Proc Time: {avg_time:.1f}ms", (10, info_y+90),
cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 255), 2)
# 添加进度条
progress = int(self.current_frame_idx / self.frame_count * self.width)
cv2.rectangle(display_frame, (0, self.height-15), (progress, self.height), (0, 255, 0), -1)
return display_frame
def save_analysis_results(self):
"""保存分析结果到文件"""
if not self.trajectories:
logger.warning("没有检测到轨迹,跳过保存")
return
timestamp = time.strftime("%Y%m%d_%H%M%S")
filename = f"analysis_{timestamp}.csv"
with open(filename, 'w') as f:
# 写入CSV文件头
f.write("TrackID,Frame,X(px),Y(px),Radius(px),X(um),Y(um),Radius(um)\n")
for track_id, positions in self.trajectories.items():
for frame_idx, x, y, r in positions:
# 转换为物理单位
phys_x = x * self.pixel_to_micron
phys_y = y * self.pixel_to_micron
phys_r = r * self.pixel_to_micron
f.write(f"{track_id},{frame_idx},{x},{y},{r},{phys_x:.2f},{phys_y:.2f},{phys_r:.2f}\n")
logger.info(f"分析结果已保存到 {filename}")
def run(self):
"""视频处理主循环"""
try:
self.initialize_video()
logger.info("开始视频分析...")
pause = False
while self.cap.isOpened():
if not pause:
start_time = time.time()
# 读取帧
ret, frame = self.cap.read()
if not ret:
logger.info("视频处理完成")
break
# 处理当前帧
positions = self.detect_spheres(frame)
self.track_spheres(positions)
# 可视化
display_frame = self.visualize(frame, positions)
# 记录处理时间
proc_time = time.time() - start_time
self.processing_times.append(proc_time)
# 保存处理后的帧
if self.video_writer:
self.video_writer.write(display_frame)
# 显示处理后的帧
cv2.imshow("Optical Tweezer Video Analysis", display_frame)
# 更新帧计数
self.current_frame_idx += 1
# 按键处理
key = cv2.waitKey(1) & 0xFF
if key == 27: # ESC键 - 退出
break
elif key == ord(' '): # 空格键 - 暂停/继续
pause = not pause
elif key == ord('s'): # 保存当前帧
cv2.imwrite(f"frame_{self.current_frame_idx}.png", frame)
logger.info(f"保存第 {self.current_frame_idx} 帧")
elif key == ord('a'): # 单步前进
pause = True
# 人工单步前进
ret, frame = self.cap.read()
if ret:
positions = self.detect_spheres(frame)
self.track_spheres(positions)
display_frame = self.visualize(frame, positions)
cv2.imshow("Optical Tweezer Video Analysis", display_frame)
self.current_frame_idx += 1
elif key == ord('r'): # 重置视频
self.cap.set(cv2.CAP_PROP_POS_FRAMES, 0)
self.current_frame_idx = 0
self.trajectories = {}
self.next_track_id = 1
logger.info("重置视频分析")
# 保存分析结果
self.save_analysis_results()
except Exception as e:
logger.exception("发生未预期错误:")
finally:
self.cleanup()
def cleanup(self):
"""资源清理"""
if hasattr(self, 'cap'):
self.cap.release()
if self.video_writer:
self.video_writer.release()
cv2.destroyAllWindows()
logger.info("系统资源已释放")
def main():
# 设置命令行参数
parser = argparse.ArgumentParser(description='光学镊子系统视频测试工具')
parser.add_argument('input', help='输入视频文件路径')
parser.add_argument('-o', '--output', help='输出视频文件路径')
args = parser.parse_args()
try:
logger.info("启动光学镊子系统视频测试...")
tester = OpticalTweezerVideoTester(
video_path=args.input,
output_path=args.output
)
tester.run()
except Exception as e:
logger.error(f"系统启动失败: {e}")
if __name__ == "__main__":
main()
加上机器学习,改写代码
最新发布