Former-commit-id: 79f9eff524ee3778b6e010e5fc56d2060118c0fd
This commit is contained in:
2025-06-03 12:26:07 +08:00
parent 6cab5e12f4
commit 3c32c03c45
7 changed files with 247 additions and 43 deletions

2
.gitignore vendored
View File

@ -10,3 +10,5 @@ logs
Videos
*.build
*.dist
HCHO
upx.exe

View File

@ -0,0 +1,14 @@
状态管理逻辑:
- 滑动窗口
维护一个滑动窗口history记录最近end_bounce_time内的状态。
状态包括:时间,浓度,状态,当前截图
- 进入
1. middle: predictor返回middle立即进入slow状态。
2. about: 返回about且处于middle则进入about状态
3. end返回end置end_check标记开始检查
- check
1. middle: 进入middle的bounce_time后在最近bounce_time内middle比例<70%返回fast状态
2. about随着middle退出一起退出不单独处理
3. end进入end之后的end_bounce_time如果end_bounce_time内end比例<80%则实验终止逻辑否则从history中找到第二个end并继续check逻辑

223
analyze_rate_volume.py Normal file
View File

@ -0,0 +1,223 @@
import os
import cv2
import numpy as np
import matplotlib.pyplot as plt
import re
from datetime import datetime
import main as _main
import argparse
class RateVolumeAnalyzer:
def __init__(self):
self.mat = _main.MAT() # 创建MAT实例以使用predictor方法
def parse_log_file(self, log_path):
"""解析日志文件,提取体积和时间信息"""
volumes = []
timestamps = []
with open(log_path, 'r', encoding='utf-8') as f:
for line in f:
# 匹配体积信息的行
if '当前体积:' in line:
# 提取时间戳
time_match = re.search(r'(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}),\d+', line)
# 提取体积
volume_match = re.search(r'当前体积: ([\d.]+) ml', line)
if time_match and volume_match:
timestamp = datetime.strptime(time_match.group(1), '%Y-%m-%d %H:%M:%S')
volume = float(volume_match.group(1))
timestamps.append(timestamp)
volumes.append(volume)
return timestamps, volumes
def extract_frames_from_video(self, video_path, timestamps, start_time):
"""从视频中提取对应时间点的帧"""
cap = cv2.VideoCapture(video_path)
if not cap.isOpened():
print(f"无法打开视频文件: {video_path}")
return []
fps = cap.get(cv2.CAP_PROP_FPS)
frames = []
for timestamp in timestamps:
# 计算相对于实验开始的秒数
seconds_from_start = (timestamp - start_time).total_seconds()
frame_number = int(seconds_from_start * fps)
# 设置视频位置到指定帧
cap.set(cv2.CAP_PROP_POS_FRAMES, frame_number)
ret, frame = cap.read()
if ret:
frames.append(frame)
else:
frames.append(None) # 如果无法读取帧添加None
cap.release()
return frames
def calculate_rates(self, frames):
"""使用predictor方法计算每帧的rate"""
rates = []
states = []
for frame in frames:
if frame is not None:
try:
state, rate = self.mat.predictor(frame)
rates.append(rate)
states.append(state)
except Exception as e:
print(f"计算rate时发生错误: {e}")
rates.append(None)
states.append(None)
else:
rates.append(None)
states.append(None)
return rates, states
def plot_rate_volume_curve(self, volumes, rates, states, log_filename):
"""绘制rate-体积曲线"""
# 过滤掉None值
valid_data = [(v, r, s) for v, r, s in zip(volumes, rates, states)
if r is not None and s is not None]
if not valid_data:
print("没有有效的数据点可以绘制")
return
volumes_valid, rates_valid, states_valid = zip(*valid_data)
plt.figure(figsize=(12, 8))
# 根据状态用不同颜色绘制点
colors = {'transport': 'blue', 'middle': 'orange', 'about': 'purple', 'colored': 'red'}
for state in colors:
state_volumes = [v for v, s in zip(volumes_valid, states_valid) if s == state]
state_rates = [r for r, s in zip(rates_valid, states_valid) if s == state]
if state_volumes:
plt.scatter(state_volumes, state_rates,
c=colors[state], label=state, alpha=0.7, s=50)
# 绘制连接线
plt.plot(volumes_valid, rates_valid, 'k-', alpha=0.3, linewidth=1)
plt.xlabel('体积 (ml)', fontsize=12)
plt.ylabel('Rate', fontsize=12)
plt.title(f'Rate-体积曲线 ({log_filename})', fontsize=14)
plt.legend()
plt.grid(True, alpha=0.3)
# 保存图片
output_filename = f"rate_volume_curve_{log_filename.replace('.log', '.png')}"
plt.savefig(output_filename, dpi=300, bbox_inches='tight')
plt.show()
print(f"图片已保存为: {output_filename}")
# 打印统计信息
print(f"\n统计信息:")
print(f"总数据点: {len(valid_data)}")
for state in colors:
count = sum(1 for s in states_valid if s == state)
if count > 0:
print(f"{state}: {count} 个点")
def analyze_experiment(self, timestamp_str=None):
"""分析指定时间戳的实验,如果不指定则分析最新的实验"""
logs_dir = "logs"
videos_dir = "Videos"
if timestamp_str:
log_file = f"titration_{timestamp_str}.log"
video_file = f"{timestamp_str}.mp4"
else:
# 找到最新的日志文件
log_files = [f for f in os.listdir(logs_dir) if f.endswith('.log')]
if not log_files:
print("没有找到日志文件")
return
log_files.sort()
log_file = log_files[-1]
# 提取时间戳来找对应的视频文件
timestamp_match = re.search(r'titration_(\d{8}_\d{6})\.log', log_file)
if timestamp_match:
video_file = f"{timestamp_match.group(1)}.mp4"
else:
print("无法从日志文件名提取时间戳")
return
log_path = os.path.join(logs_dir, "titration_20250529_191634.log")
video_path = os.path.join(videos_dir, "tmp.mp4")
if not os.path.exists(log_path):
print(f"日志文件不存在: {log_path}")
return
if not os.path.exists(video_path):
print(f"视频文件不存在: {video_path}")
return
print(f"分析实验: {log_file}")
print(f"对应视频: {video_file}")
# 解析日志文件
timestamps, volumes = self.parse_log_file(log_path)
if not timestamps:
print("日志文件中没有找到体积数据")
return
print(f"找到 {len(timestamps)} 个数据点")
# 获取实验开始时间
start_time = timestamps[0]
# 从视频中提取帧
print("正在从视频中提取帧...")
frames = self.extract_frames_from_video(video_path, timestamps, start_time)
# 计算rate
print("正在计算rate值...")
rates, states = self.calculate_rates(frames)
# 绘制曲线
print("正在绘制rate-体积曲线...")
self.plot_rate_volume_curve(volumes, rates, states, log_file)
def main():
parser = argparse.ArgumentParser(description='分析滴定实验的rate-体积曲线')
parser.add_argument('--timestamp', type=str, help='指定实验时间戳 (格式: YYYYMMDD_HHMMSS)')
parser.add_argument('--list', action='store_true', help='列出所有可用的实验')
args = parser.parse_args()
analyzer = RateVolumeAnalyzer()
if args.list:
# 列出所有可用的实验
logs_dir = "logs"
if os.path.exists(logs_dir):
log_files = [f for f in os.listdir(logs_dir) if f.endswith('.log')]
log_files.sort()
print("可用的实验:")
for log_file in log_files:
timestamp_match = re.search(r'titration_(\d{8}_\d{6})\.log', log_file)
if timestamp_match:
timestamp = timestamp_match.group(1)
print(f" {timestamp}")
return
analyzer.analyze_experiment(args.timestamp)
if __name__ == "__main__":
main()

3
ch340_gui.exe Normal file
View File

@ -0,0 +1,3 @@
version https://git-lfs.github.com/spec/v1
oid sha256:fc7d1088d4f1edb2c939f12a2d5cd136142744c6ddd23835f91710a0383d295f
size 21382656

12
main.py
View File

@ -121,15 +121,15 @@ class MAT:
recent_history = [(t, state, v, i) for t, state, v, i in self.history if t >= bounce_start_time]
if recent_history:
middle_count = sum(1 for _, state, _, _ in recent_history if state == "transport")
middle_ratio = middle_count / len(recent_history)
trans_count = sum(1 for _, state, _, _ in recent_history if state == "transport")
trans_ratio = trans_count / len(recent_history)
if middle_ratio > 0.3:
if trans_ratio > 0.3:
self.process_left(now)
self.state.exit_middle_check()
# about状态随middle一起退出
self.state.exit_about_with_middle()
self.control_logger.info(f"middle比例{middle_ratio:.2%}<70%退出middle检查返回fast模式")
self.control_logger.info(f"middle比例{trans_ratio:.2%}<70%退出middle检查返回fast模式")
# end检查: 进入end之后的end_bounce_time如果end比例<80%,则重置;否则终止实验
if self.state.should_check_end_result(now):
@ -174,10 +174,6 @@ class MAT:
self._display_status(im, ret, rate, val)
return ret
def _reset_colored_detection(self, current_time, colored_ratio, current_volume):
"""重置colored检测状态 - 已被新逻辑替代,保留以避免错误"""
pass
def _display_status(self, im, detection_result, rate, volume):
"""显示状态信息到图像上"""
mode_color = {

View File

@ -2,3 +2,4 @@ numpy
opencv-python
pyserial
matplotlib
PySide6

View File

@ -1,35 +0,0 @@
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
测试logging系统的功能
"""
import utils
def test_logging():
"""测试logging系统"""
# 初始化logging
log_file = utils.setup_logging()
print(f"日志文件已创建: {log_file}")
# 获取各个模块的logger
system_logger = utils.get_system_logger()
hardware_logger = utils.get_hardware_logger()
vision_logger = utils.get_vision_logger()
control_logger = utils.get_control_logger()
endpoint_logger = utils.get_endpoint_logger()
volume_logger = utils.get_volume_logger()
# 测试各个模块的日志输出
system_logger.info("系统初始化完成")
hardware_logger.info("硬件连接正常")
vision_logger.debug("图像处理开始")
control_logger.warning("控制逻辑警告")
endpoint_logger.error("终点检测错误")
volume_logger.info("体积计算: 10.5 ml")
print("日志测试完成请检查logs目录下的日志文件。")
if __name__ == "__main__":
test_logging()