clean and format codebase
Former-commit-id: 5d0497ac67199a7ea475849a6ec3f28df46371cb
This commit is contained in:
@ -1,223 +0,0 @@
|
||||
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()
|
BIN
colored.zip
Normal file
BIN
colored.zip
Normal file
Binary file not shown.
@ -1,611 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
"""
|
||||
滴定分析Demo程序
|
||||
从Videos目录获取最新视频,解析logs或直接处理视频,生成rate随时间变化的可视化图像
|
||||
"""
|
||||
|
||||
import os
|
||||
import re
|
||||
import cv2
|
||||
import time
|
||||
import numpy as np
|
||||
import matplotlib.pyplot as plt
|
||||
import matplotlib.dates as mdates
|
||||
from datetime import datetime, timedelta
|
||||
from pathlib import Path
|
||||
from typing import List, Tuple, Dict, Optional
|
||||
import logging
|
||||
from dataclasses import dataclass
|
||||
|
||||
# 设置中文字体
|
||||
plt.rcParams['font.sans-serif'] = ['SimHei', 'Microsoft YaHei']
|
||||
plt.rcParams['axes.unicode_minus'] = False
|
||||
|
||||
@dataclass
|
||||
class DataPoint:
|
||||
"""数据点结构"""
|
||||
timestamp: float
|
||||
relative_time: float # 相对于开始时间的秒数
|
||||
state: str
|
||||
rate: float
|
||||
volume: float
|
||||
mode: str = "FAST" # FAST, SLOW, ABOUT, CRAZY
|
||||
|
||||
class TitrationDemo:
|
||||
def __init__(self, base_dir: str = "c:/expiriment/ai-titration-main"):
|
||||
"""初始化Demo类"""
|
||||
self.base_dir = Path(base_dir)
|
||||
self.videos_dir = self.base_dir / "Videos"
|
||||
self.logs_dir = self.base_dir / "logs"
|
||||
|
||||
# 模式对应的速度映射 (ml/次)
|
||||
self.mode_speeds = {
|
||||
'FAST': 0.45, # 快速模式速度
|
||||
'SLOW': 0.05, # 慢速模式速度
|
||||
'ABOUT': 0.02, # 精密模式速度
|
||||
'CRAZY': 1.0, # 疯狂模式速度
|
||||
'END': 0.0 # 结束模式无速度
|
||||
}
|
||||
|
||||
# 模式颜色映射
|
||||
self.mode_colors = {
|
||||
'FAST': '#FF6B6B', # 红色
|
||||
'SLOW': '#4ECDC4', # 青色
|
||||
'ABOUT': '#45B7D1', # 蓝色
|
||||
'CRAZY': '#96CEB4', # 绿色
|
||||
'END': '#FFEAA7' # 黄色
|
||||
}
|
||||
|
||||
# 状态颜色映射
|
||||
self.state_colors = {
|
||||
'transport': '#95A5A6', # 灰色
|
||||
'middle': '#F39C12', # 橙色
|
||||
'about': '#E74C3C', # 深红色
|
||||
'colored': '#8E44AD' # 紫色
|
||||
}
|
||||
|
||||
# 设置日志
|
||||
logging.basicConfig(level=logging.INFO)
|
||||
self.logger = logging.getLogger(__name__)
|
||||
|
||||
def get_latest_video(self) -> Optional[Path]:
|
||||
"""获取最新的视频文件"""
|
||||
video_files = []
|
||||
for ext in ['*.mp4', '*.mkv', '*.avi']:
|
||||
video_files.extend(self.videos_dir.glob(ext))
|
||||
|
||||
if not video_files:
|
||||
self.logger.error("未找到任何视频文件")
|
||||
return None
|
||||
|
||||
# 按文件名中的时间戳排序,获取最新的
|
||||
def extract_timestamp(filename: str) -> datetime:
|
||||
# 提取形如 20250606_200940 的时间戳
|
||||
match = re.search(r'(\d{8}_\d{6})', filename)
|
||||
if match:
|
||||
return datetime.strptime(match.group(1), '%Y%m%d_%H%M%S')
|
||||
return datetime.min
|
||||
|
||||
latest_video = max(video_files, key=lambda x: extract_timestamp(x.name))
|
||||
self.logger.info(f"选择最新视频: {latest_video.name}")
|
||||
return latest_video
|
||||
|
||||
def get_corresponding_log(self, video_path: Path) -> Optional[Path]:
|
||||
"""获取对应的日志文件"""
|
||||
# 从视频文件名提取时间戳
|
||||
timestamp_match = re.search(r'(\d{8}_\d{6})', video_path.name)
|
||||
if not timestamp_match:
|
||||
return None
|
||||
|
||||
timestamp = timestamp_match.group(1)
|
||||
log_file = self.logs_dir / f"titration_{timestamp}.log"
|
||||
|
||||
if log_file.exists():
|
||||
self.logger.info(f"找到对应日志文件: {log_file.name}")
|
||||
return log_file
|
||||
else:
|
||||
self.logger.warning(f"未找到对应日志文件: {log_file.name}")
|
||||
return None
|
||||
|
||||
def parse_log_data(self, log_path: Path) -> List[DataPoint]:
|
||||
"""解析日志文件,提取数据点"""
|
||||
data_points = []
|
||||
start_time = None
|
||||
current_mode = "FAST"
|
||||
|
||||
with open(log_path, 'r', encoding='utf-8') as f:
|
||||
for line in f:
|
||||
# 解析时间戳
|
||||
timestamp_match = re.match(r'(\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2},\d{3})', line)
|
||||
if not timestamp_match:
|
||||
continue
|
||||
|
||||
timestamp_str = timestamp_match.group(1)
|
||||
timestamp = datetime.strptime(timestamp_str, '%Y-%m-%d %H:%M:%S,%f')
|
||||
timestamp_float = timestamp.timestamp()
|
||||
|
||||
if start_time is None:
|
||||
start_time = timestamp_float
|
||||
|
||||
relative_time = timestamp_float - start_time
|
||||
|
||||
# 解析模式变化
|
||||
if "进入slow模式" in line or "检测到middle" in line:
|
||||
current_mode = "SLOW"
|
||||
elif "进入about模式" in line or "检测到about" in line:
|
||||
current_mode = "ABOUT"
|
||||
elif "返回fast模式" in line or "退出middle检查" in line:
|
||||
current_mode = "FAST"
|
||||
elif "检测到colored" in line:
|
||||
current_mode = "END"
|
||||
|
||||
# 解析体积信息
|
||||
volume_match = re.search(r'当前体积:\s*([\d.]+)\s*ml', line)
|
||||
if volume_match:
|
||||
volume = float(volume_match.group(1))
|
||||
# 这里rate需要通过视频处理获得,暂时设置为0
|
||||
data_points.append(DataPoint(
|
||||
timestamp=timestamp_float,
|
||||
relative_time=relative_time,
|
||||
state="transport", # 默认状态
|
||||
rate=0.0, # 需要通过视频处理获得
|
||||
volume=volume,
|
||||
mode=current_mode
|
||||
))
|
||||
|
||||
return data_points
|
||||
|
||||
def interpolate_volume_data(self, data_points: List[DataPoint],
|
||||
interpolation_interval: float = 0.1) -> List[DataPoint]:
|
||||
"""
|
||||
通过mode求speed反推log两个体积数据点之间的数据
|
||||
|
||||
Args:
|
||||
data_points: 原始数据点列表
|
||||
interpolation_interval: 插值间隔(秒)
|
||||
|
||||
Returns:
|
||||
插值后的数据点列表
|
||||
"""
|
||||
if len(data_points) < 2:
|
||||
return data_points
|
||||
|
||||
interpolated_points = []
|
||||
|
||||
for i in range(len(data_points)):
|
||||
# 添加原始数据点
|
||||
interpolated_points.append(data_points[i])
|
||||
|
||||
# 如果不是最后一个点,进行插值
|
||||
if i < len(data_points) - 1:
|
||||
current_point = data_points[i]
|
||||
next_point = data_points[i + 1]
|
||||
|
||||
# 计算时间差和体积差
|
||||
time_diff = next_point.relative_time - current_point.relative_time
|
||||
volume_diff = next_point.volume - current_point.volume
|
||||
|
||||
# 如果时间差大于插值间隔,进行插值
|
||||
if time_diff > interpolation_interval:
|
||||
# 根据当前模式获取速度
|
||||
current_speed = self.mode_speeds.get(current_point.mode, 0.0)
|
||||
|
||||
# 计算需要插值的点数
|
||||
num_interpolations = int(time_diff / interpolation_interval)
|
||||
|
||||
for j in range(1, num_interpolations + 1):
|
||||
# 计算插值时间
|
||||
interp_time = current_point.relative_time + j * interpolation_interval
|
||||
|
||||
# 如果插值时间超过下一个点的时间,跳出
|
||||
if interp_time >= next_point.relative_time:
|
||||
break
|
||||
|
||||
# 根据模式和时间计算体积
|
||||
# 假设每次推进的时间间隔内,体积按照模式速度增长
|
||||
elapsed_time = interp_time - current_point.relative_time
|
||||
|
||||
# 估算体积增长(基于推进频率和速度)
|
||||
# 假设推进频率:FAST=1次/秒,SLOW=0.5次/秒,ABOUT=0.2次/秒
|
||||
push_frequency = {
|
||||
'FAST': 1.0,
|
||||
'SLOW': 0.5,
|
||||
'ABOUT': 0.2,
|
||||
'CRAZY': 2.0,
|
||||
'END': 0.0
|
||||
}
|
||||
|
||||
freq = push_frequency.get(current_point.mode, 1.0)
|
||||
estimated_volume = current_point.volume + elapsed_time * freq * current_speed
|
||||
|
||||
# 确保体积不超过下一个点的体积
|
||||
if estimated_volume > next_point.volume:
|
||||
estimated_volume = current_point.volume + (elapsed_time / time_diff) * volume_diff
|
||||
|
||||
# 创建插值数据点
|
||||
interp_point = DataPoint(
|
||||
timestamp=current_point.timestamp + elapsed_time,
|
||||
relative_time=interp_time,
|
||||
state=current_point.state, # 继承当前状态
|
||||
rate=0.0, # 插值点的rate需要通过视频获得
|
||||
volume=estimated_volume,
|
||||
mode=current_point.mode # 继承当前模式
|
||||
)
|
||||
|
||||
interpolated_points.append(interp_point)
|
||||
|
||||
self.logger.info(f"原始数据点: {len(data_points)}, 插值后数据点: {len(interpolated_points)}")
|
||||
return interpolated_points
|
||||
|
||||
def process_video_with_predictor(self, video_path: Path, sample_interval: int = 5) -> List[DataPoint]:
|
||||
"""使用predictor处理视频,提取rate数据"""
|
||||
# 导入main模块中的predictor方法
|
||||
import sys
|
||||
sys.path.append(str(self.base_dir))
|
||||
|
||||
try:
|
||||
from main import MAT
|
||||
from utils import History
|
||||
except ImportError:
|
||||
self.logger.error("无法导入MAT类或History类")
|
||||
return []
|
||||
|
||||
cap = cv2.VideoCapture(str(video_path))
|
||||
if not cap.isOpened():
|
||||
self.logger.error(f"无法打开视频文件: {video_path}")
|
||||
return []
|
||||
|
||||
fps = cap.get(cv2.CAP_PROP_FPS)
|
||||
total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
|
||||
|
||||
self.logger.info(f"视频信息: FPS={fps}, 总帧数={total_frames}")
|
||||
# 创建MAT实例用于predictor,简化初始化
|
||||
try:
|
||||
mat = MAT(videoSourceIndex=0, bounce_time=4, end_bounce_time=1)
|
||||
# 创建简化的历史记录管理器
|
||||
mat.history = History(max_window_size=100.0, base_time=5.0)
|
||||
except Exception as e:
|
||||
self.logger.error(f"MAT初始化失败: {e}")
|
||||
return []
|
||||
|
||||
data_points = []
|
||||
frame_count = 0
|
||||
base_calculated = False
|
||||
rates_for_base = []
|
||||
|
||||
# 用于模式推断的变量
|
||||
current_mode = "FAST"
|
||||
mode_change_frame = 0
|
||||
|
||||
while True:
|
||||
ret, frame = cap.read()
|
||||
if not ret:
|
||||
break
|
||||
|
||||
# 更密集的采样
|
||||
if frame_count % sample_interval == 0:
|
||||
try:
|
||||
# 调用predictor获取状态和rate
|
||||
state, rate = mat.predictor(frame)
|
||||
|
||||
relative_time = frame_count / fps
|
||||
|
||||
# 收集前50帧的rate值来计算base
|
||||
if not base_calculated and len(rates_for_base) < 50:
|
||||
if state == "transport": # 只用transport状态的rate计算base
|
||||
rates_for_base.append(rate)
|
||||
if len(rates_for_base) >= 50:
|
||||
base_rate = float(np.mean(rates_for_base))
|
||||
if mat.history:
|
||||
mat.history.base = base_rate
|
||||
base_calculated = True
|
||||
self.logger.info(f"计算得到基准rate: {base_rate:.4f}")
|
||||
|
||||
# 基于rate和状态推断模式
|
||||
if base_calculated and mat.history and mat.history.base:
|
||||
base = mat.history.base
|
||||
thresholds = (base * 5, base * 13, base * 20)
|
||||
|
||||
# 基于predictor返回的状态推断模式
|
||||
if state == "transport":
|
||||
if current_mode != "FAST" and frame_count - mode_change_frame > fps * 2: # 2秒稳定期
|
||||
current_mode = "FAST"
|
||||
mode_change_frame = frame_count
|
||||
elif state == "middle":
|
||||
if current_mode == "FAST":
|
||||
current_mode = "SLOW"
|
||||
mode_change_frame = frame_count
|
||||
elif state == "about":
|
||||
if current_mode in ["FAST", "SLOW"]:
|
||||
current_mode = "ABOUT"
|
||||
mode_change_frame = frame_count
|
||||
elif state == "colored":
|
||||
current_mode = "END"
|
||||
mode_change_frame = frame_count
|
||||
|
||||
data_points.append(DataPoint(
|
||||
timestamp=time.time(),
|
||||
relative_time=relative_time,
|
||||
state=state,
|
||||
rate=rate,
|
||||
volume=0.0, # 视频中无法直接获得体积信息
|
||||
mode=current_mode
|
||||
))
|
||||
|
||||
except Exception as e:
|
||||
self.logger.warning(f"处理帧 {frame_count} 时出错: {e}")
|
||||
continue
|
||||
|
||||
frame_count += 1
|
||||
|
||||
# 更频繁的进度显示
|
||||
if frame_count % (sample_interval * 50) == 0:
|
||||
progress = (frame_count / total_frames) * 100
|
||||
self.logger.info(f"处理进度: {progress:.1f}% - 已处理 {len(data_points)} 个数据点")
|
||||
|
||||
cap.release()
|
||||
cv2.destroyAllWindows()
|
||||
self.logger.info(f"视频处理完成,共获得 {len(data_points)} 个数据点")
|
||||
|
||||
return data_points
|
||||
|
||||
def _smooth_mode_changes(self, data_points: List[DataPoint]) -> List[DataPoint]:
|
||||
"""平滑模式变化,避免频繁跳转"""
|
||||
if len(data_points) < 3:
|
||||
return data_points
|
||||
|
||||
smoothed_points = data_points.copy()
|
||||
window_size = 5 # 平滑窗口大小
|
||||
|
||||
for i in range(window_size, len(data_points) - window_size):
|
||||
# 获取窗口内的模式
|
||||
window_modes = [p.mode for p in data_points[i-window_size:i+window_size+1]]
|
||||
# 使用众数作为当前点的模式
|
||||
mode_counts = {}
|
||||
for mode in window_modes:
|
||||
mode_counts[mode] = mode_counts.get(mode, 0) + 1
|
||||
most_common_mode = max(mode_counts.items(), key=lambda x: x[1])[0]
|
||||
smoothed_points[i].mode = most_common_mode
|
||||
|
||||
return smoothed_points
|
||||
|
||||
def merge_log_and_video_data(self, log_data: List[DataPoint],
|
||||
video_data: List[DataPoint]) -> List[DataPoint]:
|
||||
"""合并日志和视频数据"""
|
||||
if not log_data:
|
||||
return video_data
|
||||
if not video_data:
|
||||
return log_data
|
||||
|
||||
# 以日志数据为基础,补充视频中的rate信息
|
||||
merged_data = []
|
||||
|
||||
for log_point in log_data:
|
||||
# 找到时间最接近的视频数据点
|
||||
closest_video_point = min(video_data,
|
||||
key=lambda x: abs(x.relative_time - log_point.relative_time))
|
||||
|
||||
# 如果时间差在合理范围内,使用视频的rate数据
|
||||
if abs(closest_video_point.relative_time - log_point.relative_time) < 5.0:
|
||||
log_point.rate = closest_video_point.rate
|
||||
log_point.state = closest_video_point.state
|
||||
|
||||
merged_data.append(log_point)
|
||||
|
||||
return merged_data
|
||||
|
||||
def create_visualization(self, data_points: List[DataPoint],
|
||||
save_path: Optional[str] = None) -> None:
|
||||
"""创建可视化图表"""
|
||||
if not data_points:
|
||||
self.logger.error("没有数据点可供可视化")
|
||||
return
|
||||
|
||||
# 提取数据
|
||||
times = [point.relative_time for point in data_points]
|
||||
rates = [point.rate for point in data_points]
|
||||
volumes = [point.volume for point in data_points]
|
||||
modes = [point.mode for point in data_points]
|
||||
states = [point.state for point in data_points]
|
||||
|
||||
# 创建图表
|
||||
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(12, 10))
|
||||
|
||||
# 上图:Rate随时间变化,按Mode着色
|
||||
self._plot_rate_by_mode(ax1, times, rates, modes)
|
||||
|
||||
# 下图:Volume随时间变化,按State着色
|
||||
self._plot_volume_by_state(ax2, times, volumes, states)
|
||||
|
||||
# 调整布局
|
||||
plt.tight_layout()
|
||||
|
||||
# 保存或显示
|
||||
if save_path:
|
||||
plt.savefig(save_path, dpi=300, bbox_inches='tight')
|
||||
self.logger.info(f"图表已保存至: {save_path}")
|
||||
else:
|
||||
plt.show()
|
||||
|
||||
def _plot_rate_by_mode(self, ax, times: List[float], rates: List[float],
|
||||
modes: List[str]) -> None:
|
||||
"""绘制Rate随时间变化图,按Mode着色"""
|
||||
# 按模式分组绘制
|
||||
current_mode = None
|
||||
current_times = []
|
||||
current_rates = []
|
||||
|
||||
for i, (time, rate, mode) in enumerate(zip(times, rates, modes)):
|
||||
if mode != current_mode:
|
||||
# 绘制前一段
|
||||
if current_times and current_mode:
|
||||
ax.plot(current_times, current_rates,
|
||||
color=self.mode_colors.get(current_mode, '#000000'),
|
||||
linewidth=2, label=f'{current_mode} Mode' if current_mode not in ax.get_legend_handles_labels()[1] else "")
|
||||
|
||||
# 开始新的一段
|
||||
current_mode = mode
|
||||
current_times = [time]
|
||||
current_rates = [rate]
|
||||
else:
|
||||
current_times.append(time)
|
||||
current_rates.append(rate)
|
||||
|
||||
# 绘制最后一段
|
||||
if current_times and current_mode:
|
||||
ax.plot(current_times, current_rates,
|
||||
color=self.mode_colors.get(current_mode, '#000000'),
|
||||
linewidth=2, label=f'{current_mode} Mode' if current_mode not in ax.get_legend_handles_labels()[1] else "")
|
||||
|
||||
ax.set_xlabel('时间 (秒)')
|
||||
ax.set_ylabel('Rate')
|
||||
ax.set_title('Rate随时间变化 (按操作模式着色)')
|
||||
ax.legend(loc='upper right')
|
||||
ax.grid(True, alpha=0.3)
|
||||
|
||||
def _plot_volume_by_state(self, ax, times: List[float], volumes: List[float],
|
||||
states: List[str]) -> None:
|
||||
"""绘制Volume随时间变化图,按State着色"""
|
||||
# 过滤掉volume为0的点(来自视频数据)
|
||||
filtered_data = [(t, v, s) for t, v, s in zip(times, volumes, states) if v > 0]
|
||||
|
||||
if not filtered_data:
|
||||
ax.text(0.5, 0.5, '无体积数据', ha='center', va='center', transform=ax.transAxes)
|
||||
ax.set_xlabel('时间 (秒)')
|
||||
ax.set_ylabel('体积 (ml)')
|
||||
ax.set_title('体积随时间变化 (按检测状态着色)')
|
||||
return
|
||||
|
||||
f_times, f_volumes, f_states = zip(*filtered_data)
|
||||
|
||||
# 按状态分组绘制
|
||||
current_state = None
|
||||
current_times = []
|
||||
current_volumes = []
|
||||
|
||||
for time, volume, state in zip(f_times, f_volumes, f_states):
|
||||
if state != current_state:
|
||||
# 绘制前一段
|
||||
if current_times and current_state:
|
||||
ax.plot(current_times, current_volumes,
|
||||
color=self.state_colors.get(current_state, '#000000'),
|
||||
linewidth=2, marker='o', markersize=3,
|
||||
label=f'{current_state.title()} State' if current_state not in ax.get_legend_handles_labels()[1] else "")
|
||||
|
||||
# 开始新的一段
|
||||
current_state = state
|
||||
current_times = [time]
|
||||
current_volumes = [volume]
|
||||
else:
|
||||
current_times.append(time)
|
||||
current_volumes.append(volume)
|
||||
|
||||
# 绘制最后一段
|
||||
if current_times and current_state:
|
||||
ax.plot(current_times, current_volumes,
|
||||
color=self.state_colors.get(current_state, '#000000'),
|
||||
linewidth=2, marker='o', markersize=3,
|
||||
label=f'{current_state.title()} State' if current_state not in ax.get_legend_handles_labels()[1] else "")
|
||||
|
||||
ax.set_xlabel('时间 (秒)')
|
||||
ax.set_ylabel('体积 (ml)')
|
||||
ax.set_title('体积随时间变化 (按检测状态着色)')
|
||||
ax.legend(loc='lower right')
|
||||
ax.grid(True, alpha=0.3)
|
||||
|
||||
def run_demo(self, use_video: bool = True, sample_interval: int = 5) -> None:
|
||||
"""运行demo程序"""
|
||||
self.logger.info("开始运行滴定分析Demo...")
|
||||
|
||||
# 1. 获取最新视频
|
||||
latest_video = self.get_latest_video()
|
||||
if not latest_video:
|
||||
return
|
||||
|
||||
# 2. 获取对应日志
|
||||
log_file = self.get_corresponding_log(latest_video)
|
||||
|
||||
# 3. 解析数据
|
||||
log_data = []
|
||||
if log_file:
|
||||
self.logger.info("解析日志数据...")
|
||||
log_data = self.parse_log_data(log_file)
|
||||
|
||||
video_data = []
|
||||
if use_video:
|
||||
self.logger.info("处理视频数据...")
|
||||
video_data = self.process_video_with_predictor(latest_video, sample_interval)
|
||||
|
||||
# 4. 合并数据
|
||||
if log_data and video_data:
|
||||
self.logger.info("合并日志和视频数据...")
|
||||
final_data = self.merge_log_and_video_data(log_data, video_data)
|
||||
elif log_data:
|
||||
final_data = log_data
|
||||
elif video_data:
|
||||
final_data = video_data
|
||||
else:
|
||||
self.logger.error("没有可用的数据")
|
||||
return
|
||||
|
||||
# 5. 创建可视化
|
||||
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
||||
save_path = self.base_dir / f"rate_analysis_{timestamp}.png"
|
||||
|
||||
self.logger.info("创建可视化图表...")
|
||||
self.create_visualization(final_data, str(save_path))
|
||||
|
||||
self.logger.info("Demo运行完成!")
|
||||
|
||||
# 打印统计信息
|
||||
self._print_statistics(final_data)
|
||||
|
||||
def _print_statistics(self, data_points: List[DataPoint]) -> None:
|
||||
"""打印数据统计信息"""
|
||||
if not data_points:
|
||||
return
|
||||
|
||||
print("\n=== 数据统计信息 ===")
|
||||
print(f"总数据点数: {len(data_points)}")
|
||||
print(f"时间范围: {data_points[0].relative_time:.1f}s - {data_points[-1].relative_time:.1f}s")
|
||||
|
||||
# 模式统计
|
||||
mode_counts = {}
|
||||
for point in data_points:
|
||||
mode_counts[point.mode] = mode_counts.get(point.mode, 0) + 1
|
||||
|
||||
print("\n模式分布:")
|
||||
for mode, count in mode_counts.items():
|
||||
percentage = (count / len(data_points)) * 100
|
||||
print(f" {mode}: {count} 次 ({percentage:.1f}%)")
|
||||
|
||||
# Rate统计
|
||||
rates = [point.rate for point in data_points if point.rate > 0]
|
||||
if rates:
|
||||
print(f"\nRate统计:")
|
||||
print(f" 最小值: {min(rates):.4f}")
|
||||
print(f" 最大值: {max(rates):.4f}")
|
||||
print(f" 平均值: {np.mean(rates):.4f}")
|
||||
|
||||
# 体积统计
|
||||
volumes = [point.volume for point in data_points if point.volume > 0]
|
||||
if volumes:
|
||||
print(f"\n体积统计:")
|
||||
print(f" 最小值: {min(volumes):.2f} ml")
|
||||
print(f" 最大值: {max(volumes):.2f} ml")
|
||||
|
||||
|
||||
def main():
|
||||
"""主函数"""
|
||||
demo = TitrationDemo()
|
||||
|
||||
# 运行demo
|
||||
# use_video=True: 同时处理视频和日志数据
|
||||
# use_video=False: 仅使用日志数据
|
||||
# sample_interval: 视频采样间隔(每N帧处理一次)- 设置为更小的值获得更密集的数据点
|
||||
demo.run_demo(use_video=True, sample_interval=1) # 每3帧采样一次,大幅增加数据点
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
324
main.py
324
main.py
@ -1,19 +1,20 @@
|
||||
import cv2
|
||||
import atexit
|
||||
import logging
|
||||
import time
|
||||
from datetime import datetime
|
||||
import numpy as np
|
||||
import ch340
|
||||
import atexit
|
||||
import utils
|
||||
from utils import State, History,login_to_platform,send_data_to_platform
|
||||
from scipy.signal import find_peaks
|
||||
from typing import Optional
|
||||
import logging
|
||||
|
||||
import cv2
|
||||
import numpy as np
|
||||
from scipy.signal import find_peaks
|
||||
|
||||
import ch340
|
||||
import utils
|
||||
from utils import History, State, login_to_platform, send_data_to_platform
|
||||
|
||||
|
||||
class MAT:
|
||||
def __init__(self, videoSourceIndex=0, bounce_time=2,sensity=30):
|
||||
def __init__(self, videoSourceIndex=0, bounce_time=2, sensity=30):
|
||||
# 初始化logging
|
||||
utils.setup_logging()
|
||||
self.system_logger = logging.getLogger("System")
|
||||
@ -21,15 +22,15 @@ class MAT:
|
||||
self.endpoint_logger = logging.getLogger("Endpoint")
|
||||
self.volume_logger = logging.getLogger("Volume")
|
||||
|
||||
self.system_logger.info('正在初始化MAT系统...')
|
||||
self.system_logger.info("正在初始化MAT系统...")
|
||||
self.videoSourceIndex = videoSourceIndex
|
||||
self.cap = cv2.VideoCapture(videoSourceIndex, cv2.CAP_DSHOW)
|
||||
self.ch340 = ch340.CH340()
|
||||
self.total_volume = 0
|
||||
self.sensity = sensity
|
||||
|
||||
|
||||
self.state = State(bounce_time)
|
||||
|
||||
|
||||
atexit.register(self.ch340.stop)
|
||||
self.history = History(bounce_time)
|
||||
self.colored_volume = None
|
||||
@ -41,37 +42,41 @@ class MAT:
|
||||
self.control_logger.info("开始抽取12ml")
|
||||
self.ch340.max_speed()
|
||||
self.ch340.pull(vol=12)
|
||||
self.control_logger.info('完成抽取')
|
||||
self.control_logger.info("完成抽取")
|
||||
|
||||
def ch340_init(self):
|
||||
"初始化电机位置,防止过头导致抽取溶液不准。"
|
||||
# self.ch340.push(speed=1,t=1)
|
||||
self.ch340.pull(speed=1.2,vol=1.8)
|
||||
self.control_logger.info('电极位置已初始化')
|
||||
self.ch340.pull(speed=1.2, vol=1.8)
|
||||
self.control_logger.info("电极位置已初始化")
|
||||
|
||||
def ch340_push(self, speed:int|float=0.1):
|
||||
def ch340_push(self, speed: int | float = 0.1):
|
||||
"常规推送,保证推送时间为1s,推送体积=speed"
|
||||
self.ch340.push_async(speed=speed, t=1)
|
||||
|
||||
def process_left(self, now: float, value_only:Optional[bool]=False) -> Optional[float]:
|
||||
'''
|
||||
|
||||
def process_left(
|
||||
self, now: float, value_only: Optional[bool] = False
|
||||
) -> Optional[float]:
|
||||
"""
|
||||
计算当前时刻剩余体积,并更新到self.total_volume。
|
||||
|
||||
|
||||
@param now: 当前时间戳
|
||||
@param value_only: 如果为True,则只返回剩余体积,不更新状态
|
||||
|
||||
@return: 当前剩余体积,仅当value_only为True时返回
|
||||
'''
|
||||
"""
|
||||
if self.state.mode == State.Mode.ABOUT:
|
||||
return self.total_volume
|
||||
st = self.ch340.start
|
||||
if not value_only:
|
||||
self.ch340.stop()
|
||||
if not abs(self.ch340._get_time()-1) < 1e-3:
|
||||
self.control_logger.warning(f"CH340 timing issue detected: {self.ch340._get_time()}")
|
||||
if not value_only:
|
||||
self.ch340.stop()
|
||||
if not abs(self.ch340._get_time() - 1) < 1e-3:
|
||||
self.control_logger.warning(
|
||||
f"CH340 timing issue detected: {self.ch340._get_time()}"
|
||||
)
|
||||
return self.total_volume
|
||||
r = now - st
|
||||
ret = self.total_volume - (1-r) * self.speeds[self.state.mode.value]
|
||||
ret = self.total_volume - (1 - r) * self.speeds[self.state.mode.value]
|
||||
if value_only:
|
||||
return ret
|
||||
else:
|
||||
@ -83,109 +88,119 @@ class MAT:
|
||||
if not suc:
|
||||
self.control_logger.error("无法从摄像头捕获帧")
|
||||
return None
|
||||
|
||||
|
||||
# 录制当前帧到视频文件
|
||||
if hasattr(self, 'capFile') and self.capFile.isOpened():
|
||||
if hasattr(self, "capFile") and self.capFile.isOpened():
|
||||
self.capFile.write(im)
|
||||
|
||||
|
||||
ret, rate = self.predictor(im)
|
||||
if ret is None:
|
||||
return None
|
||||
now = time.time()
|
||||
val = self.process_left(now, value_only=True)
|
||||
|
||||
|
||||
# 确保val不为None
|
||||
if val is None:
|
||||
self.control_logger.warning("体积计算返回None,跳过本次记录")
|
||||
return ret
|
||||
|
||||
|
||||
# 更新滑动窗口历史记录 - 维护最近end_bounce_time内的状态
|
||||
self.history.add_record(now, ret, rate, val, im)
|
||||
|
||||
|
||||
if self.history.is_empty():
|
||||
self.control_logger.error("未预期的没有可用的历史记录")
|
||||
return ret
|
||||
|
||||
|
||||
|
||||
# === 状态进入逻辑 ===
|
||||
if now - self._start_time<10:
|
||||
if now - self._start_time < 10:
|
||||
self._display_status(im, ret, rate, val)
|
||||
return ret
|
||||
|
||||
if not self.edited and self.history.base is not None and rate>self.history.base * 2:
|
||||
|
||||
if (
|
||||
not self.edited
|
||||
and self.history.base is not None
|
||||
and rate > self.history.base * 2
|
||||
):
|
||||
self.edited = True
|
||||
self.speeds[0] /= 2
|
||||
|
||||
|
||||
# 1. middle: predictor返回middle立即进入slow状态
|
||||
if ret == "middle":
|
||||
if self.state.is_fast_mode():
|
||||
self.process_left(now)
|
||||
self.state.enter_middle_state(now)
|
||||
self.control_logger.info(f"检测到middle,立即进入slow模式,当前体积: {val:.2f} ml")
|
||||
|
||||
self.control_logger.info(
|
||||
f"检测到middle,立即进入slow模式,当前体积: {val:.2f} ml"
|
||||
)
|
||||
|
||||
# 2. about: 返回about,且处于middle则进入about状态
|
||||
elif ret == "about":
|
||||
if self.state.is_slow_mode():
|
||||
self.process_left(now)
|
||||
self.state.enter_about_state(now)
|
||||
self.control_logger.info(f"检测到about,进入about模式,当前体积: {val:.2f} ml")
|
||||
|
||||
|
||||
self.control_logger.info(
|
||||
f"检测到about,进入about模式,当前体积: {val:.2f} ml"
|
||||
)
|
||||
|
||||
# middle检查: 进入middle的bounce_time后,在最近bounce_time内middle比例<70%返回fast状态
|
||||
if self.state.should_check_middle_exit(now) and not self.history.last_end:
|
||||
# 计算最近bounce_time内的middle比例
|
||||
recent_records = self.history.get_recent_records(self.state.bounce_time, now)
|
||||
|
||||
recent_records = self.history.get_recent_records(
|
||||
self.state.bounce_time, now
|
||||
)
|
||||
|
||||
if recent_records:
|
||||
trans_ratio = self.history.get_state_ratio("transport", recent_records)
|
||||
|
||||
|
||||
if trans_ratio > 0.4:
|
||||
self.process_left(now)
|
||||
self.state.exit_middle_check()
|
||||
self.control_logger.info(f"middle比例{trans_ratio:.2%}<60%,退出middle检查,返回fast模式")
|
||||
|
||||
self.control_logger.info(
|
||||
f"middle比例{trans_ratio:.2%}<60%,退出middle检查,返回fast模式"
|
||||
)
|
||||
|
||||
if self.state.mode == State.Mode.ABOUT and self.state.about_check:
|
||||
h = self.history.get_recent_records(self.about_time/3,now)
|
||||
h = self.history.get_recent_records(self.about_time / 3, now)
|
||||
ratio = self.history.get_state_ratio("transport", h)
|
||||
self.history.about_history.append(ratio == 1)
|
||||
while len(self.history.about_history) > 5:
|
||||
self.history.about_history.pop(0)
|
||||
if len(self.history.about_history) == 5 and self.history.last_end < 2:
|
||||
if len(self.history.about_history) == 5 and self.history.last_end < 2:
|
||||
rate = sum(self.history.about_history) / len(self.history.about_history)
|
||||
if rate > 0.8:
|
||||
self.state.exit_about()
|
||||
self.control_logger.info(f"about比例{ratio:.2%}<80%,退出about检查,返回middle模式")
|
||||
|
||||
self.control_logger.info(
|
||||
f"about比例{ratio:.2%}<80%,退出about检查,返回middle模式"
|
||||
)
|
||||
|
||||
ret = self.end_check()
|
||||
if ret == "colored":
|
||||
return ret
|
||||
if not self.history.last_end:
|
||||
self.history.last_end = max(self.history.last_end,ret)
|
||||
self.history.last_end = max(self.history.last_end, ret)
|
||||
self.state.about_check = False
|
||||
|
||||
|
||||
|
||||
# 显示状态信息
|
||||
self._display_status(im, ret, rate, val)
|
||||
return ret
|
||||
|
||||
def end_check(self,dep=0):
|
||||
'''终点检测,通过对图像直方图峰值分析,确认是否全局变色。'''
|
||||
|
||||
def end_check(self, dep=0):
|
||||
"""终点检测,通过对图像直方图峰值分析,确认是否全局变色。"""
|
||||
if not self.running:
|
||||
return "colored"
|
||||
if dep == 1:
|
||||
suc,im = self.cap.read()
|
||||
suc, im = self.cap.read()
|
||||
hsv = cv2.cvtColor(im, cv2.COLOR_BGR2HSV)
|
||||
|
||||
|
||||
# 降低饱和度通道 (S通道是索引1)
|
||||
hsv[:, :, 1] = hsv[:, :, 1] * 0.8
|
||||
|
||||
|
||||
# 转换回BGR颜色空间
|
||||
result = cv2.cvtColor(hsv, cv2.COLOR_HSV2BGR)
|
||||
name = f"colored_{datetime.now().strftime('%Y%m%d_%H%M%S')}.jpg"
|
||||
cv2.imwrite(name,result)
|
||||
cv2.imwrite(name, result)
|
||||
self.colored_im = name
|
||||
|
||||
|
||||
# 维护一个递归检测,重复4次后认定为终点。
|
||||
if dep > 3:
|
||||
self.colored_volume = self.total_volume
|
||||
@ -193,7 +208,7 @@ class MAT:
|
||||
self.running = False
|
||||
self.ch340.stop()
|
||||
return "colored"
|
||||
suc,im = self.cap.read()
|
||||
suc, im = self.cap.read()
|
||||
hsv = cv2.cvtColor(im, cv2.COLOR_BGR2HSV)
|
||||
s = hsv[:, :, 0]
|
||||
s = s[s > 0]
|
||||
@ -201,80 +216,105 @@ class MAT:
|
||||
hist = hist.flatten() # 转换为一维数组
|
||||
|
||||
# 峰值检测 - 找到直方图中的峰值
|
||||
peaks, properties = find_peaks(hist,
|
||||
height=np.max(hist) * 0.2, # 峰值高度至少是最大值的10%
|
||||
distance=10, # 峰值之间的最小距离
|
||||
prominence=np.max(hist) * 0.01) # 峰值的突出度
|
||||
if np.any(peaks>130):
|
||||
peaks, properties = find_peaks(
|
||||
hist,
|
||||
height=np.max(hist) * 0.2, # 峰值高度至少是最大值的10%
|
||||
distance=10, # 峰值之间的最小距离
|
||||
prominence=np.max(hist) * 0.01,
|
||||
) # 峰值的突出度
|
||||
if np.any(peaks > 130):
|
||||
if dep == 0:
|
||||
self.process_left(time.time())
|
||||
self.history.end_history.append(True)
|
||||
self.control_logger.info(f"检测到colored状态,end_check at {dep}")
|
||||
bgn = time.time()
|
||||
while time.time()-bgn < 2:
|
||||
suc,im = self.cap.read()
|
||||
if hasattr(self, 'capFile') and self.capFile.isOpened():
|
||||
while time.time() - bgn < 2:
|
||||
suc, im = self.cap.read()
|
||||
if hasattr(self, "capFile") and self.capFile.isOpened():
|
||||
self.capFile.write(im)
|
||||
cv2.putText(im, "ENDCHK", (10, 30),
|
||||
cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2)
|
||||
cv2.putText(
|
||||
im,
|
||||
"ENDCHK",
|
||||
(10, 30),
|
||||
cv2.FONT_HERSHEY_SIMPLEX,
|
||||
0.7,
|
||||
(0, 255, 0),
|
||||
2,
|
||||
)
|
||||
cv2.imshow("Frame", im)
|
||||
return self.end_check(dep+1)
|
||||
return self.end_check(dep + 1)
|
||||
# time.sleep(2)
|
||||
else:
|
||||
self.colored_im = None
|
||||
return dep
|
||||
|
||||
|
||||
def _display_status(self, im, detection_result, rate, volume):
|
||||
"""显示状态信息到图像上"""
|
||||
mode_color = {
|
||||
State.Mode.FAST: (255, 255, 255),
|
||||
State.Mode.SLOW: (10, 215, 255),
|
||||
State.Mode.ABOUT: (228,116,167),
|
||||
State.Mode.ABOUT: (228, 116, 167),
|
||||
}
|
||||
|
||||
status_text = f"Stat: {detection_result}, rate: {round(rate,2)}, Vol: {volume:.2f} ml"
|
||||
|
||||
status_text = (
|
||||
f"Stat: {detection_result}, rate: {round(rate,2)}, Vol: {volume:.2f} ml"
|
||||
)
|
||||
status_text += self.state.get_status_text()
|
||||
|
||||
cv2.putText(im, status_text, (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 0.7,
|
||||
mode_color[self.state.mode], 2)
|
||||
|
||||
cv2.putText(
|
||||
im,
|
||||
status_text,
|
||||
(10, 30),
|
||||
cv2.FONT_HERSHEY_SIMPLEX,
|
||||
0.7,
|
||||
mode_color[self.state.mode],
|
||||
2,
|
||||
)
|
||||
cv2.imshow("Frame", im)
|
||||
cv2.waitKey(1)
|
||||
|
||||
def predictor(self,im):
|
||||
'''主预测函数,分析图像并返回当前状态。当前,colored不使用这个函数。'''
|
||||
hsv = cv2.cvtColor(im,cv2.COLOR_BGR2HSV)
|
||||
s = hsv[:,:,1]
|
||||
mask = s>self.sensity
|
||||
cv2.imshow('mask',im*mask[:,:,np.newaxis])
|
||||
tot = mask.shape[0]*mask.shape[1]
|
||||
def predictor(self, im):
|
||||
"""主预测函数,分析图像并返回当前状态。当前,colored不使用这个函数。"""
|
||||
hsv = cv2.cvtColor(im, cv2.COLOR_BGR2HSV)
|
||||
s = hsv[:, :, 1]
|
||||
mask = s > self.sensity
|
||||
cv2.imshow("mask", im * mask[:, :, np.newaxis])
|
||||
tot = mask.shape[0] * mask.shape[1]
|
||||
val = np.sum(mask)
|
||||
rate = val/tot
|
||||
rate = val / tot
|
||||
if self.history.base is not None:
|
||||
base = self.history.base
|
||||
thr = (min(0.05,base*5), min(0.2,base*13), 0.5)
|
||||
thr = (min(0.05, base * 5), min(0.2, base * 13), 0.5)
|
||||
# thr = (base*5, base*13, 0.5)
|
||||
if rate < thr[0]:
|
||||
return "transport",rate
|
||||
elif rate <thr[1]:
|
||||
return "middle",rate
|
||||
return "transport", rate
|
||||
elif rate < thr[1]:
|
||||
return "middle", rate
|
||||
elif rate < thr[2]:
|
||||
return "about",rate
|
||||
return "about", rate
|
||||
else:
|
||||
return "colored",rate
|
||||
return "colored", rate
|
||||
else:
|
||||
return "transport",rate
|
||||
|
||||
return "transport", rate
|
||||
|
||||
def __del__(self):
|
||||
self.cap.release()
|
||||
if hasattr(self, 'capFile') and self.capFile.isOpened():
|
||||
if hasattr(self, "capFile") and self.capFile.isOpened():
|
||||
self.capFile.release()
|
||||
cv2.destroyAllWindows()
|
||||
|
||||
|
||||
def format_date_time(self, timestamp):
|
||||
return datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
def run(self, quick_speed=0.2, slow_speed=0.05, end_speed=0.02, mid_time=0.5, about_time=1, cap_dir="Videos"):
|
||||
def run(
|
||||
self,
|
||||
quick_speed=0.2,
|
||||
slow_speed=0.05,
|
||||
end_speed=0.02,
|
||||
mid_time=0.5,
|
||||
about_time=1,
|
||||
cap_dir="Videos",
|
||||
):
|
||||
self.running = True
|
||||
self.start_time = time.time()
|
||||
self.speeds: list[float] = [quick_speed, slow_speed, end_speed]
|
||||
@ -282,19 +322,19 @@ class MAT:
|
||||
self.edited = False
|
||||
self.volume_list = []
|
||||
self.color_list = []
|
||||
|
||||
|
||||
if cap_dir is not None:
|
||||
vid_name = f"{cap_dir}/{datetime.now().strftime('%Y%m%d_%H%M%S')}.mkv"
|
||||
else:
|
||||
vid_name = "Disabled"
|
||||
|
||||
|
||||
# 记录实验开始
|
||||
experiment_params = {
|
||||
"视频源索引 ": self.videoSourceIndex,
|
||||
"防抖时间 ": self.state.bounce_time,
|
||||
"快速模式速度": f"{quick_speed} ml/次",
|
||||
"慢速模式速度": f"{slow_speed} ml/次",
|
||||
"录制视频 ": vid_name
|
||||
"录制视频 ": vid_name,
|
||||
}
|
||||
self.system_logger.info("实验参数:")
|
||||
for key, value in experiment_params.items():
|
||||
@ -302,50 +342,56 @@ class MAT:
|
||||
fps = int(self.cap.get(cv2.CAP_PROP_FPS)) or 30
|
||||
width = int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH))
|
||||
height = int(self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
|
||||
|
||||
|
||||
# 初始化视频录制器
|
||||
if vid_name != "Disabled":
|
||||
fourcc = cv2.VideoWriter.fourcc(*'x264') # 使用更兼容的编码器
|
||||
fourcc = cv2.VideoWriter.fourcc(*"x264") # 使用更兼容的编码器
|
||||
self.capFile = cv2.VideoWriter(vid_name, fourcc, fps, (width, height))
|
||||
if not self.capFile.isOpened():
|
||||
self.system_logger.error(f"无法打开视频录制器: {vid_name}")
|
||||
del self.capFile
|
||||
else:
|
||||
self.system_logger.info(f"视频录制已初始化")
|
||||
|
||||
|
||||
self.ch340_init()
|
||||
cnt = 0
|
||||
self._start_time = time.time()
|
||||
self.about_time=about_time
|
||||
|
||||
self.about_time = about_time
|
||||
|
||||
while self.running:
|
||||
if not self.ch340.running:
|
||||
if 12 * cnt - self.total_volume < 0.5:
|
||||
self.ch340_pull()
|
||||
cnt += 1
|
||||
|
||||
|
||||
should_push = False
|
||||
if self.state.is_fast_mode():
|
||||
should_push = True
|
||||
elif self.state.is_slow_mode() and \
|
||||
time.time() - self.ch340.start > mid_time:
|
||||
elif (
|
||||
self.state.is_slow_mode()
|
||||
and time.time() - self.ch340.start > mid_time
|
||||
):
|
||||
|
||||
should_push = True
|
||||
elif self.state.is_about_mode() and \
|
||||
time.time() - self.ch340.start > about_time:
|
||||
|
||||
elif (
|
||||
self.state.is_about_mode()
|
||||
and time.time() - self.ch340.start > about_time
|
||||
):
|
||||
|
||||
if not self.state.about_first_flag:
|
||||
self.state.about_check = True
|
||||
else:
|
||||
self.state.about_first_flag = False
|
||||
should_push = True
|
||||
|
||||
|
||||
if should_push:
|
||||
speed = self.speeds[self.state.mode.value]
|
||||
self.volume_logger.info(f"当前体积: {self.total_volume:.2f} ml, 加入速度: {speed:.2f} ml/次")
|
||||
self.volume_logger.info(
|
||||
f"当前体积: {self.total_volume:.2f} ml, 加入速度: {speed:.2f} ml/次"
|
||||
)
|
||||
self.ch340_push(speed)
|
||||
self.total_volume += speed
|
||||
self.volume_list.append(round(self.total_volume,2))
|
||||
self.volume_list.append(round(self.total_volume, 2))
|
||||
try:
|
||||
self.color_list.append(self.state.mode.name)
|
||||
except Exception as e:
|
||||
@ -354,12 +400,12 @@ class MAT:
|
||||
if self._pred() is None:
|
||||
self.control_logger.error("预测失败,跳过当前帧")
|
||||
continue
|
||||
|
||||
|
||||
# 释放视频录制器
|
||||
if hasattr(self, 'capFile') and self.capFile.isOpened():
|
||||
if hasattr(self, "capFile") and self.capFile.isOpened():
|
||||
self.capFile.release()
|
||||
self.system_logger.info(f"视频录制完成: {vid_name}")
|
||||
|
||||
|
||||
# 实验结束,记录结果
|
||||
experiment_results = {
|
||||
"总体积 ": f"{self.total_volume:.2f} ml",
|
||||
@ -369,12 +415,12 @@ class MAT:
|
||||
"滴定速率": f"{self.total_volume / (time.time() - self.start_time):.2f} mL/s",
|
||||
}
|
||||
upload_data = {
|
||||
"start_time":self.format_date_time(self.start_time),
|
||||
"end_time":self.format_date_time(time.time()),
|
||||
"volume_record": f'{self.volume_list}',
|
||||
'voltage_record': f'{[]}',
|
||||
'color_record': f'{self.color_list}',
|
||||
'final_volume': f'{self.colored_volume}',
|
||||
"start_time": self.format_date_time(self.start_time),
|
||||
"end_time": self.format_date_time(time.time()),
|
||||
"volume_record": f"{self.volume_list}",
|
||||
"voltage_record": f"{[]}",
|
||||
"color_record": f"{self.color_list}",
|
||||
"final_volume": f"{self.colored_volume}",
|
||||
}
|
||||
self.system_logger.info("实验结果:")
|
||||
for key, value in experiment_results.items():
|
||||
@ -382,27 +428,23 @@ class MAT:
|
||||
|
||||
return upload_data, self.colored_im
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
token = login_to_platform("13504022184","password")
|
||||
|
||||
token = login_to_platform("13504022184", "password")
|
||||
|
||||
# 创建MAT类的实例并运行
|
||||
mat = MAT(
|
||||
videoSourceIndex = 1,
|
||||
bounce_time=2,
|
||||
sensity = 32
|
||||
)
|
||||
|
||||
mat = MAT(videoSourceIndex=1, bounce_time=2, sensity=32)
|
||||
|
||||
mat.state.mode = State.Mode.FAST
|
||||
|
||||
|
||||
final_data, finish_picture = mat.run(
|
||||
slow_speed = 0.05,
|
||||
quick_speed = 1.0,
|
||||
slow_speed=0.05,
|
||||
quick_speed=1.0,
|
||||
about_time=3,
|
||||
# cap_dir=None
|
||||
)
|
||||
with open("log.json","w") as f:
|
||||
with open("log.json", "w") as f:
|
||||
import json
|
||||
|
||||
json.dump(final_data, f, indent=4)
|
||||
send_data_to_platform(token, final_data, finish_picture)
|
||||
|
||||
|
||||
|
68
test.py
68
test.py
@ -1,68 +0,0 @@
|
||||
import cv2
|
||||
import numpy as np
|
||||
from matplotlib import pyplot as plt
|
||||
from scipy.signal import find_peaks
|
||||
|
||||
cap = cv2.VideoCapture(2) # 使用摄像头0,通常更稳定
|
||||
cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640) # 降低分辨率提高处理速度
|
||||
cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)
|
||||
|
||||
# 预先创建图形窗口,避免重复创建
|
||||
fig, ax = plt.subplots(figsize=(10, 4))
|
||||
plt.ion()
|
||||
ax.set_title('Saturation Channel Histogram')
|
||||
ax.set_xlabel('Saturation Value')
|
||||
ax.set_ylabel('Pixel Count')
|
||||
ax.set_xlim(0, 255)
|
||||
|
||||
while True:
|
||||
ret, frame = cap.read()
|
||||
if not ret:
|
||||
print("Failed to grab frame")
|
||||
break
|
||||
|
||||
cv2.imshow("Camera Feed", frame)
|
||||
|
||||
# 直接提取饱和度通道,避免完整HSV转换
|
||||
hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
|
||||
s = hsv[:, :, 0]
|
||||
s = s[s > 0] # 只保留非零饱和度值,减少噪声
|
||||
# 使用更高效的直方图计算
|
||||
hist = cv2.calcHist([s], [0], None, [256], [0, 256])
|
||||
hist = hist.flatten() # 转换为一维数组
|
||||
|
||||
# 峰值检测 - 找到直方图中的峰值
|
||||
peaks, properties = find_peaks(hist,
|
||||
height=np.max(hist) * 0.5, # 峰值高度至少是最大值的10%
|
||||
distance=10, # 峰值之间的最小距离
|
||||
prominence=np.max(hist) * 0.2) # 峰值的突出度
|
||||
|
||||
# 清除旧数据并绘制新直方图
|
||||
ax.clear()
|
||||
ax.plot(hist, 'b-', linewidth=1)
|
||||
|
||||
# 标注峰值
|
||||
if len(peaks) > 0:
|
||||
print(peaks)
|
||||
ax.text(0.5, 1.05, f'Found {len(peaks)} peaks')
|
||||
ax.plot(peaks, hist[peaks], 'ro', markersize=8, label=f'Peaks ({len(peaks)})')
|
||||
# 在峰值处添加文字标注
|
||||
for i, peak in enumerate(peaks):
|
||||
ax.annotate(f'Peak {i+1}\n({peak}, {int(hist[peak])})',
|
||||
xy=(peak, hist[peak]),
|
||||
xytext=(peak, hist[peak] + np.max(hist) * 0.1),
|
||||
ha='center', va='bottom',
|
||||
bbox=dict(boxstyle='round,pad=0.3', facecolor='yellow', alpha=0.7),
|
||||
arrowprops=dict(arrowstyle='->', color='red'))
|
||||
|
||||
|
||||
plt.draw()
|
||||
plt.pause(0.1) # 确保图形更新
|
||||
|
||||
key = cv2.waitKey(1) & 0xFF
|
||||
if key == ord('q'):
|
||||
break
|
||||
|
||||
cap.release()
|
||||
cv2.destroyAllWindows()
|
||||
plt.close('all')
|
11
test2.py
11
test2.py
@ -1,11 +0,0 @@
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
cap = cv2.VideoCapture(2) # 使用摄像头0,通常更稳定
|
||||
im = cap.read()[1]
|
||||
hsv = cv2.cvtColor(im, cv2.COLOR_BGR2HSV)
|
||||
h, s, v = cv2.split(hsv)
|
||||
pink_mask = (((h >= 300/360) | (h <= 20/360)) & (s >= 0.15) & (v >= 0.2))
|
||||
cv2.imshow("Pink Mask", im * pink_mask[:,:,np.newaxis]) # 显示掩码
|
||||
cv2.waitKey(0) # 等待按键
|
||||
cap.release()
|
23
tmp.py
23
tmp.py
@ -1,23 +0,0 @@
|
||||
from datetime import datetime
|
||||
import json
|
||||
import requests
|
||||
import time
|
||||
from utils import login_to_platform,send_data_to_platform
|
||||
|
||||
|
||||
token = login_to_platform("13504022184","password")
|
||||
|
||||
def format_date_time(timestamp):
|
||||
return datetime.fromtimestamp(timestamp).strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
print(token)
|
||||
final_data ={
|
||||
'start_time': '2025-06-17 14:59:33',
|
||||
'end_time': '2025-06-17 15:01:18',
|
||||
'volume_record': '[0.8, 1.6, 2.4000000000000004, 3.2, 4.0, 4.8, 5.6, 6.3999999999999995, 7.199999999999999, 7.999999999999999, 8.799999999999999, 9.6, 10.4, 11.200000000000001, 12.000000000000002, 12.800000000000002, 13.200000000000003, 13.600000000000003, 14.000000000000004, 14.400000000000004, 14.800000000000004, 15.200000000000005, 15.600000000000005, 16.000000000000004, 16.400000000000002, 16.8, 17.2, 17.599999999999998, 17.999999999999996, 18.399999999999995, 18.799999999999994, 19.199999999999992, 19.59999999999999, 19.99999999999999, 20.399999999999988, 20.799999999999986, 21.199999999999985, 21.599999999999984, 21.999999999999982, 22.39999999999998, 22.79999999999998, 23.199999999999978, 23.599999999999977, 23.999999999999975, 24.399999999999974, 24.799999999999972, 25.19999999999997, 25.59999999999997, 25.999999999999968, 26.399999999999967, 26.799999999999965, 27.199999999999964, 27.599999999999962, 27.99999999999996, 28.39999999999996, 28.447450351715048, 28.49745035171505, 28.54745035171505, 28.59745035171505, 28.98371173143383, 28.781944286823233, 28.761131591796836]',
|
||||
'voltage_record': "[]",
|
||||
'color_record': "['transport', 'transport', 'transport', 'transport', 'transport', 'transport', 'transport', 'transport', 'transport', 'transport', 'transport', 'transport', 'transport', 'transport', 'transport', 'transport', 'transport', 'transport', 'transport', 'transport', 'transport', 'transport', 'transport', 'transport', 'transport', 'transport', 'transport', 'transport', 'transport', 'transport', 'transport', 'transport', 'transport', 'transport', 'transport', 'transport', 'transport', 'transport', 'transport', 'transport', 'transport', 'transport', 'transport', 'transport', 'transport', 'transport', 'transport', 'transport', 'transport', 'transport', 'transport', 'transport', 'transport', 'transport', 'transport', 'middle', 'transport', 'transport', 'transport', 'transport', 'middle', 'colored']",
|
||||
'final_volume': "28.761131591796836"
|
||||
}
|
||||
# print(json.dumps(final_data))
|
||||
send_data_to_platform(token, final_data, "im.jpg")
|
144
tmp2.py
144
tmp2.py
@ -1,144 +0,0 @@
|
||||
import numpy as np
|
||||
import cv2
|
||||
import matplotlib.pyplot as plt
|
||||
from matplotlib.animation import FuncAnimation
|
||||
from collections import deque
|
||||
import threading
|
||||
import time
|
||||
|
||||
class DistributionChangeDetector:
|
||||
def __init__(self, baseline_windows: list[np.ndarray]):
|
||||
"""
|
||||
参数 baseline_windows: List of arrays,代表初始稳定期的多个窗口
|
||||
"""
|
||||
self.baseline = self._compute_baseline(baseline_windows)
|
||||
|
||||
def _compute_stats(self, window: np.ndarray) -> tuple[float, float, float]:
|
||||
"""返回 (P_under30, std, mode)"""
|
||||
p_under30 = np.mean(window < 30)
|
||||
std = np.std(window, ddof=1)
|
||||
|
||||
# 快速估计众数:最大 bin 的中心
|
||||
hist, bin_edges = np.histogram(window, bins=50)
|
||||
max_bin_index = np.argmax(hist)
|
||||
mode_est = (bin_edges[max_bin_index] + bin_edges[max_bin_index + 1]) / 2
|
||||
return p_under30, std, mode_est
|
||||
|
||||
def _compute_baseline(self, windows: list[np.ndarray]) -> tuple[np.ndarray, np.ndarray]:
|
||||
"""
|
||||
返回 baseline 向量 (P0, σ0, mode0) 和对应标准差(用于归一化)
|
||||
"""
|
||||
stats = np.array([self._compute_stats(w) for w in windows])
|
||||
mean = stats.mean(axis=0)
|
||||
std = stats.std(axis=0) + 1e-6 # 防止除0
|
||||
return mean, std
|
||||
|
||||
def update(self, window: np.ndarray) -> float:
|
||||
"""
|
||||
输入:当前窗口数据(长度 = 窗口大小)
|
||||
输出:变化分数(越大表示分布越偏离基准)
|
||||
"""
|
||||
x = np.array(self._compute_stats(window))
|
||||
mean, std = self.baseline
|
||||
norm_diff = (x - mean) / std
|
||||
change_score = np.linalg.norm(norm_diff)
|
||||
return float(change_score)
|
||||
|
||||
def hsv_score(s:np.ndarray):
|
||||
mask = s>30
|
||||
tot = len(mask)
|
||||
val = np.sum(mask)
|
||||
rate = val/tot
|
||||
return rate
|
||||
|
||||
class RealTimePlotter:
|
||||
def __init__(self, max_points=200):
|
||||
self.max_points = max_points
|
||||
self.scores = deque(maxlen=max_points)
|
||||
self.scores2 = deque(maxlen=max_points)
|
||||
self.times = deque(maxlen=max_points)
|
||||
self.start_time = time.time()
|
||||
|
||||
# 设置图形
|
||||
plt.ion() # 打开交互模式
|
||||
self.fig, (self.ax,self.ax2) = plt.subplots(1,2,figsize=(10, 6))
|
||||
self.line, = self.ax.plot([], [], 'b-', linewidth=2)
|
||||
self.line2, = self.ax2.plot([], [], 'b-', linewidth=2)
|
||||
self.ax.set_xlabel('Time (s)')
|
||||
self.ax.set_ylabel('Change Score')
|
||||
self.ax.set_title('Real-time Distribution Change Detection')
|
||||
self.ax.grid(True)
|
||||
self.ax2.grid(True)
|
||||
|
||||
def update_plot(self, score,s_score):
|
||||
current_time = time.time() - self.start_time
|
||||
self.scores.append(score)
|
||||
self.scores2.append(s_score)
|
||||
self.times.append(current_time)
|
||||
|
||||
# 更新数据
|
||||
self.line.set_data(list(self.times), list(self.scores))
|
||||
self.line2.set_data(list(self.times), list(self.scores2))
|
||||
|
||||
# 自动调整坐标轴
|
||||
if len(self.times) > 1:
|
||||
self.ax.set_xlim(min(self.times), max(self.times))
|
||||
self.ax2.set_xlim(min(self.times), max(self.times))
|
||||
self.ax.set_ylim(0,100)
|
||||
# self.ax.set_ylim(min(self.scores) * 0.95, max(self.scores) * 1.05)
|
||||
self.ax2.set_ylim(0,1)
|
||||
|
||||
# 刷新图形
|
||||
self.fig.canvas.draw()
|
||||
self.fig.canvas.flush_events()
|
||||
|
||||
|
||||
def gen_data():
|
||||
cap = cv2.VideoCapture(1)
|
||||
while True:
|
||||
ret, frame = cap.read()
|
||||
if not ret:
|
||||
break
|
||||
cv2.imshow("Camera Feed", frame)
|
||||
hsv = cv2.cvtColor(frame, cv2.COLOR_BGR2HSV)
|
||||
s = hsv[:, :, 1] # 直接提取饱和度通道
|
||||
s = s[s > 0] # 只保留非零饱和度值,减少噪声
|
||||
yield s
|
||||
if cv2.waitKey(1) & 0xFF == ord('a'):
|
||||
break
|
||||
cap.release()
|
||||
cv2.destroyAllWindows()
|
||||
|
||||
|
||||
def main():
|
||||
# 初始化数据生成器
|
||||
gen = gen_data()
|
||||
|
||||
# 获取基线数据
|
||||
print("收集基线数据...")
|
||||
baseline_data = [next(gen) for _ in range(30*5)]
|
||||
|
||||
# 初始化检测器和绘图器
|
||||
det = DistributionChangeDetector(baseline_data)
|
||||
plotter = RealTimePlotter()
|
||||
|
||||
print("开始实时检测和绘图...")
|
||||
|
||||
try:
|
||||
for x in gen:
|
||||
score = det.update(x)
|
||||
score2 = hsv_score(x)
|
||||
plotter.update_plot(score,score2)
|
||||
|
||||
# 小延时以控制更新频率
|
||||
time.sleep(0.01)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
print("停止检测")
|
||||
finally:
|
||||
plt.ioff() # 关闭交互模式
|
||||
plt.show() # 保持最终图形显示
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
188
utils.py
188
utils.py
@ -1,20 +1,23 @@
|
||||
import base64
|
||||
import json
|
||||
import logging
|
||||
import os
|
||||
import time
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from enum import Enum
|
||||
from dataclasses import dataclass
|
||||
from typing import List, Optional,Literal
|
||||
from typing import List, Literal, Optional
|
||||
|
||||
import numpy as np
|
||||
import time
|
||||
import requests
|
||||
import json
|
||||
import base64
|
||||
|
||||
|
||||
@dataclass
|
||||
class HistoryRecord:
|
||||
"""历史记录的单个条目"""
|
||||
|
||||
timestamp: float
|
||||
state: Literal["transport","middle","about","colored"]
|
||||
state: Literal["transport", "middle", "about", "colored"]
|
||||
rate: float
|
||||
volume: float
|
||||
image: np.ndarray
|
||||
@ -22,18 +25,18 @@ class HistoryRecord:
|
||||
|
||||
class History:
|
||||
"""滑动窗口历史记录管理类"""
|
||||
|
||||
def __init__(self, max_window_size: float = 5.0, base_time = 5.0):
|
||||
|
||||
def __init__(self, max_window_size: float = 5.0, base_time=5.0):
|
||||
"""
|
||||
初始化历史记录管理器
|
||||
|
||||
|
||||
Args:
|
||||
max_window_size: 最大窗口时间长度(秒)
|
||||
base_time: 基准时间长度(秒)
|
||||
display: 是否开启实时可视化显示
|
||||
"""
|
||||
if base_time > max_window_size:
|
||||
max_window_size = base_time+0.2
|
||||
max_window_size = base_time + 0.2
|
||||
# raise ValueError("Base time must be less than or equal to max window size.")
|
||||
self.records: List[HistoryRecord] = []
|
||||
self.max_window_size = max_window_size
|
||||
@ -42,22 +45,32 @@ class History:
|
||||
self.base = None
|
||||
self._base_time = base_time
|
||||
self._base_cnt = 0
|
||||
self.end_history:list[bool] = []
|
||||
self.end_history: list[bool] = []
|
||||
self.last_end = 0
|
||||
|
||||
def add_record(self, timestamp: float, state: str, rate: float, volume: float, image: np.ndarray):
|
||||
|
||||
def add_record(
|
||||
self,
|
||||
timestamp: float,
|
||||
state: str,
|
||||
rate: float,
|
||||
volume: float,
|
||||
image: np.ndarray,
|
||||
):
|
||||
"""添加新的历史记录"""
|
||||
record = HistoryRecord(timestamp, state, rate, volume, image)
|
||||
self.records.append(record)
|
||||
self._cleanup_old_records(timestamp)
|
||||
if self.base is None and (timestamp-self._base_time)>= self.records[0].timestamp:
|
||||
if (
|
||||
self.base is None
|
||||
and (timestamp - self._base_time) >= self.records[0].timestamp
|
||||
):
|
||||
if self._base_cnt < 30:
|
||||
self._base_cnt+=1
|
||||
self._base_cnt += 1
|
||||
return
|
||||
base_records = self.get_recent_records(self._base_time, timestamp)
|
||||
self.base = sum([rec.rate for rec in base_records]) / len(base_records)
|
||||
get_endpoint_logger().info("Base rate calculated: %.2f", self.base)
|
||||
|
||||
|
||||
def _cleanup_old_records(self, current_time: float):
|
||||
"""清理过期的历史记录"""
|
||||
cutoff_time = current_time - self.max_window_size
|
||||
@ -66,112 +79,131 @@ class History:
|
||||
while self.records and self.records[0].timestamp < cutoff_time:
|
||||
self.records.pop(0)
|
||||
return True
|
||||
|
||||
def get_records_in_timespan(self, start_time: float, end_time: Optional[float] = None) -> List[HistoryRecord]:
|
||||
|
||||
def get_records_in_timespan(
|
||||
self, start_time: float, end_time: Optional[float] = None
|
||||
) -> List[HistoryRecord]:
|
||||
"""获取指定时间段内的记录"""
|
||||
if end_time is None:
|
||||
return [record for record in self.records if record.timestamp >= start_time]
|
||||
else:
|
||||
return [record for record in self.records
|
||||
if start_time <= record.timestamp <= end_time]
|
||||
|
||||
def get_recent_records(self, duration: float, current_time: float) -> List[HistoryRecord]:
|
||||
return [
|
||||
record
|
||||
for record in self.records
|
||||
if start_time <= record.timestamp <= end_time
|
||||
]
|
||||
|
||||
def get_recent_records(
|
||||
self, duration: float, current_time: float
|
||||
) -> List[HistoryRecord]:
|
||||
"""获取最近指定时间长度内的记录"""
|
||||
start_time = current_time - duration
|
||||
return self.get_records_in_timespan(start_time, current_time)
|
||||
|
||||
def get_state_ratio(self, target_state: str, records: Optional[List[HistoryRecord]] = None) -> float:
|
||||
|
||||
def get_state_ratio(
|
||||
self, target_state: str, records: Optional[List[HistoryRecord]] = None
|
||||
) -> float:
|
||||
"""计算指定状态在记录中的比例"""
|
||||
if records is None:
|
||||
records = self.records
|
||||
|
||||
|
||||
if not records:
|
||||
return 0.0
|
||||
|
||||
|
||||
target_count = sum(1 for record in records if record.state == target_state)
|
||||
return target_count / len(records)
|
||||
|
||||
|
||||
def get_states_by_type(self, target_state: str) -> List[float]:
|
||||
"""获取所有指定状态的时间戳"""
|
||||
return [record.timestamp for record in self.records if record.state == target_state]
|
||||
|
||||
def find_record_by_timestamp(self, target_timestamp: float) -> Optional[HistoryRecord]:
|
||||
return [
|
||||
record.timestamp for record in self.records if record.state == target_state
|
||||
]
|
||||
|
||||
def find_record_by_timestamp(
|
||||
self, target_timestamp: float
|
||||
) -> Optional[HistoryRecord]:
|
||||
"""根据时间戳查找记录"""
|
||||
for record in self.records:
|
||||
if record.timestamp == target_timestamp:
|
||||
return record
|
||||
return None
|
||||
|
||||
|
||||
def is_empty(self) -> bool:
|
||||
"""检查历史记录是否为空"""
|
||||
return len(self.records) == 0
|
||||
|
||||
|
||||
|
||||
class State:
|
||||
"""滴定状态管理类"""
|
||||
|
||||
class Mode(Enum):
|
||||
FAST = 0 # 快速模式
|
||||
SLOW = 1 # 慢速模式 (middle)
|
||||
ABOUT = 2 # 接近终点模式
|
||||
|
||||
FAST = 0 # 快速模式
|
||||
SLOW = 1 # 慢速模式 (middle)
|
||||
ABOUT = 2 # 接近终点模式
|
||||
|
||||
def __init__(self, bounce_time=1):
|
||||
self.mode = self.Mode.FAST
|
||||
self.bounce_time = bounce_time
|
||||
|
||||
|
||||
# 状态检查标志
|
||||
self.in_middle_check = False
|
||||
self.in_end_check = False
|
||||
self.about_check = False
|
||||
self.about_first_flag = False
|
||||
|
||||
|
||||
# 时间记录
|
||||
self.middle_detected_time:Optional[float] = None
|
||||
|
||||
self.middle_detected_time: Optional[float] = None
|
||||
|
||||
def is_fast_mode(self):
|
||||
return self.mode == self.Mode.FAST
|
||||
|
||||
|
||||
def is_slow_mode(self):
|
||||
return self.mode == self.Mode.SLOW
|
||||
|
||||
|
||||
def is_about_mode(self):
|
||||
return self.mode == self.Mode.ABOUT
|
||||
|
||||
|
||||
def enter_middle_state(self, current_time):
|
||||
"""进入middle状态 - 立即切换到slow模式并开始检查"""
|
||||
self.mode = self.Mode.SLOW
|
||||
self.in_middle_check = True
|
||||
self.middle_detected_time = current_time
|
||||
|
||||
|
||||
def enter_about_state(self, current_time):
|
||||
"""从middle状态进入about状态"""
|
||||
if self.mode == self.Mode.SLOW:
|
||||
self.mode = self.Mode.ABOUT
|
||||
|
||||
|
||||
def exit_middle_check(self):
|
||||
"""退出middle检查状态,返回fast模式"""
|
||||
self.in_middle_check = False
|
||||
self.middle_detected_time = None
|
||||
self.mode = self.Mode.FAST
|
||||
|
||||
|
||||
def exit_about(self):
|
||||
"""about状态退出"""
|
||||
self.about_check = False
|
||||
self.about_first_flag = True
|
||||
if self.mode == self.Mode.ABOUT:
|
||||
self.mode = self.Mode.SLOW
|
||||
|
||||
|
||||
def should_check_middle_exit(self, current_time):
|
||||
"""检查是否应该进行middle退出检查"""
|
||||
return (self.in_middle_check and
|
||||
self.middle_detected_time is not None and
|
||||
current_time - self.middle_detected_time > self.bounce_time and
|
||||
(self.mode == self.Mode.SLOW))
|
||||
|
||||
return (
|
||||
self.in_middle_check
|
||||
and self.middle_detected_time is not None
|
||||
and current_time - self.middle_detected_time > self.bounce_time
|
||||
and (self.mode == self.Mode.SLOW)
|
||||
)
|
||||
|
||||
def get_status_text(self):
|
||||
"""获取状态显示文本"""
|
||||
status = []
|
||||
current_time = time.time()
|
||||
if self.in_middle_check and current_time - self.middle_detected_time > self.bounce_time:
|
||||
if (
|
||||
self.in_middle_check
|
||||
and current_time - self.middle_detected_time > self.bounce_time
|
||||
):
|
||||
status.append("MIDCHK")
|
||||
return ", " + ", ".join(status) if status else ""
|
||||
|
||||
@ -179,41 +211,42 @@ class State:
|
||||
def login_to_platform(username, password):
|
||||
"""登录到平台获取token"""
|
||||
try:
|
||||
wwwF = {'userName': username, 'password': password}
|
||||
url = 'https://jingsai.mools.net/api/login'
|
||||
wwwF = {"userName": username, "password": password}
|
||||
url = "https://jingsai.mools.net/api/login"
|
||||
|
||||
response = requests.post(url, wwwF,timeout=2)
|
||||
response = requests.post(url, wwwF, timeout=2)
|
||||
if response is None:
|
||||
print("错误",'网络连接失败 ')
|
||||
print("错误", "网络连接失败 ")
|
||||
return None
|
||||
request = json.loads(response.text)
|
||||
if request['code'] == 1:
|
||||
if request["code"] == 1:
|
||||
# 登陆成功
|
||||
# print("登陆成功",'登陆成功')
|
||||
# 从服务器获取到的数据中找到token
|
||||
token = request['token']
|
||||
token = request["token"]
|
||||
print("成功", "登录成功!")
|
||||
return token
|
||||
elif request['code'] == 2:
|
||||
print("错误",'用户名或密码错误!')
|
||||
elif request["code"] == 2:
|
||||
print("错误", "用户名或密码错误!")
|
||||
return None
|
||||
else:
|
||||
print("错误",'登陆失败 ')
|
||||
print("错误", "登陆失败 ")
|
||||
return None
|
||||
except Exception as e:
|
||||
print(e)
|
||||
print("错误", f"发送数据时出错:{e}")
|
||||
return None
|
||||
|
||||
|
||||
def send_data_to_platform(token, data, picture):
|
||||
"""将数据发送到平台"""
|
||||
try:
|
||||
# if 1:
|
||||
# if 1:
|
||||
# 打开图片文件并转换为 Base64 编码
|
||||
with open(picture, "rb") as picture_file:
|
||||
picture_data = picture_file.read()
|
||||
base64_encoded_picture = base64.b64encode(picture_data).decode("utf-8")
|
||||
|
||||
|
||||
# print(base64_encoded_picture)
|
||||
|
||||
# 更新数据字典,添加 Base64 编码的图片
|
||||
@ -223,7 +256,7 @@ def send_data_to_platform(token, data, picture):
|
||||
# 设置请求头
|
||||
headers = {
|
||||
"Authorization": f"Bearer {token}",
|
||||
"Content-Type": "application/json"
|
||||
"Content-Type": "application/json",
|
||||
}
|
||||
|
||||
url = "https://jingsai.mools.net/api/upload-record"
|
||||
@ -234,11 +267,14 @@ def send_data_to_platform(token, data, picture):
|
||||
request = json.loads(response.text)
|
||||
# print(request['code'])
|
||||
# 检查响应
|
||||
if request['code'] == 1:
|
||||
if request["code"] == 1:
|
||||
print("提交成功", "提交成功")
|
||||
|
||||
else:
|
||||
print("错误", f"网络请求失败,状态码:{response.status_code}\n错误信息:{response.text}")
|
||||
print(
|
||||
"错误",
|
||||
f"网络请求失败,状态码:{response.status_code}\n错误信息:{response.text}",
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
raise e
|
||||
@ -248,7 +284,7 @@ def send_data_to_platform(token, data, picture):
|
||||
def setup_logging(log_level=logging.INFO, log_dir="logs"):
|
||||
"""
|
||||
设置logging配置,创建不同模块的logger
|
||||
|
||||
|
||||
Args:
|
||||
log_level: 日志级别,默认INFO
|
||||
log_dir: 日志文件存储目录,默认"logs"
|
||||
@ -256,19 +292,19 @@ def setup_logging(log_level=logging.INFO, log_dir="logs"):
|
||||
# 创建日志目录
|
||||
if not os.path.exists(log_dir):
|
||||
os.makedirs(log_dir)
|
||||
|
||||
|
||||
# 获取当前时间作为日志文件名
|
||||
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
log_file = os.path.join(log_dir, f"titration_{timestamp}.log")
|
||||
|
||||
|
||||
# 配置根logger
|
||||
logging.basicConfig(
|
||||
level=log_level,
|
||||
format='%(asctime)s - %(name)8s - %(levelname)7s - %(message)s',
|
||||
format="%(asctime)s - %(name)8s - %(levelname)7s - %(message)s",
|
||||
handlers=[
|
||||
logging.FileHandler(log_file, encoding='utf-8'),
|
||||
logging.StreamHandler() # 同时输出到控制台
|
||||
]
|
||||
logging.FileHandler(log_file, encoding="utf-8"),
|
||||
logging.StreamHandler(), # 同时输出到控制台
|
||||
],
|
||||
)
|
||||
|
||||
|
||||
return log_file
|
||||
|
70
vid_chk.py
70
vid_chk.py
@ -1,57 +1,67 @@
|
||||
import cv2
|
||||
import numpy as np
|
||||
|
||||
import ch340
|
||||
|
||||
flag = False
|
||||
vidId = 1
|
||||
cap = cv2.VideoCapture(vidId, cv2.CAP_DSHOW)
|
||||
while not flag:
|
||||
suc,im = cap.read()
|
||||
cv2.imshow("image",im)
|
||||
suc, im = cap.read()
|
||||
cv2.imshow("image", im)
|
||||
k = cv2.waitKey(0)
|
||||
if k & 0xff == ord('q'):
|
||||
if k & 0xFF == ord("q"):
|
||||
# cap.release()
|
||||
# cv2.destroyAllWindows()
|
||||
flag=True
|
||||
flag = True
|
||||
else:
|
||||
vidId+=1
|
||||
vidId += 1
|
||||
cap.release()
|
||||
cap.open(vidId, cv2.CAP_DSHOW)
|
||||
print(f"使用摄像头索引: {vidId}")
|
||||
|
||||
import ch340
|
||||
|
||||
base = 30
|
||||
pump = ch340.CH340()
|
||||
while True:
|
||||
suc,im = cap.read()
|
||||
hsv = cv2.cvtColor(im,cv2.COLOR_BGR2HSV)
|
||||
s = hsv[:,:,1]
|
||||
mask = s>base
|
||||
cv2.imshow('mask',im*mask[:,:,np.newaxis])
|
||||
tot = mask.shape[0]*mask.shape[1]
|
||||
suc, im = cap.read()
|
||||
hsv = cv2.cvtColor(im, cv2.COLOR_BGR2HSV)
|
||||
s = hsv[:, :, 1]
|
||||
mask = s > base
|
||||
cv2.imshow("mask", im * mask[:, :, np.newaxis])
|
||||
tot = mask.shape[0] * mask.shape[1]
|
||||
val = np.sum(mask)
|
||||
rate = val/tot
|
||||
thr = (0.05,0.2, 0.5)
|
||||
rate = val / tot
|
||||
thr = (0.05, 0.2, 0.5)
|
||||
ret = ""
|
||||
if rate < thr[0]:
|
||||
ret = "transport"
|
||||
elif rate <thr[1]:
|
||||
ret = "middle"
|
||||
ret = "transport"
|
||||
elif rate < thr[1]:
|
||||
ret = "middle"
|
||||
elif rate < thr[2]:
|
||||
ret = "about"
|
||||
ret = "about"
|
||||
else:
|
||||
ret = "colored"
|
||||
im = cv2.putText(im, f"Rate: {rate:.2},base={base},{ret}", (10, 30), cv2.FONT_HERSHEY_SIMPLEX, 1, (0, 255, 0), 2)
|
||||
cv2.imshow('image',im)
|
||||
ret = "colored"
|
||||
im = cv2.putText(
|
||||
im,
|
||||
f"Rate: {rate:.2},base={base},{ret}",
|
||||
(10, 30),
|
||||
cv2.FONT_HERSHEY_SIMPLEX,
|
||||
1,
|
||||
(0, 255, 0),
|
||||
2,
|
||||
)
|
||||
cv2.imshow("image", im)
|
||||
k = cv2.waitKey(1)
|
||||
if k & 0xff == ord('w'):
|
||||
base+=1
|
||||
elif k & 0xff == ord('s'):
|
||||
base-=1
|
||||
elif k & 0xff == ord('a'):
|
||||
pump.push_async(speed=0.1,vol=0.1)
|
||||
elif k & 0xff == ord('q'):
|
||||
if k & 0xFF == ord("w"):
|
||||
base += 1
|
||||
elif k & 0xFF == ord("s"):
|
||||
base -= 1
|
||||
elif k & 0xFF == ord("a"):
|
||||
pump.push_async(speed=0.1, vol=0.1)
|
||||
elif k & 0xFF == ord("q"):
|
||||
print("Camera index = ", vidId)
|
||||
print("k =",base)
|
||||
print("k =", base)
|
||||
cap.release()
|
||||
cv2.destroyAllWindows()
|
||||
break
|
||||
break
|
||||
|
Reference in New Issue
Block a user