我写了一段脚本,请你根据我提供的代码写出详细逻辑和实现的功能:#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
"""
import os, sys, json, time, cv2, numpy as np, serial, threading, gc
from datetime import datetime
from collections import deque
import logging
import configparser
# ----------- 日志 -----------
log_file = os.path.join(os.path.dirname(sys.executable), 'run.log') \
if getattr(sys, 'frozen', False) else 'run.log'
logging.basicConfig(
level=logging.INFO,
format='%(asctime)s.%(msecs)03d | %(levelname)s | %(message)s',
datefmt='%Y-%m-%d %H:%M:%S',
handlers=[logging.FileHandler(log_file, encoding='utf-8'),
logging.StreamHandler()]
)
# ----------- 配置 -----------
CONFIG_NAME = 'config.ini'
def load_config():
cfg = configparser.ConfigParser()
if not os.path.exists(CONFIG_NAME):
cfg['rtsp'] = {'url': 'rtsp://admin:tp123456@192.168.0.46:554/ch1/main/av_stream'}
cfg['serial'] = {'com': 'COM1', 'baud': '115200'}
with open(CONFIG_NAME, 'w', encoding='utf-8') as f:
cfg.write(f)
logging.info("已生成默认配置文件 %s,下次启动可修改", CONFIG_NAME)
cfg.read(CONFIG_NAME, encoding='utf-8')
return cfg
CFG = load_config()
RTSP_URL = CFG.get('rtsp', 'url')
SERIAL_COM = CFG.get('serial', 'com')
SERIAL_BAUD= CFG.getint('serial', 'baud')
# ----------- illumin 线程 -----------
class IlluminReader:
def __init__(self, com, baud, buf_dur=5):
self.com = com
self.baud = baud
self.buf_dur= buf_dur
self.ser = None
self.buf = deque() # (ts, value)
self.lock = threading.Lock()
self.running= True
self.thread = None
# 对外
def start(self):
try:
self.ser = serial.Serial(self.com, self.baud, timeout=1)
cmd = "while :;do ubus call image get_illumin;sleep 1;done &\n"
self.ser.write(cmd.encode())
logging.info("串口 %s 已打开并发送循环命令", self.com)
except Exception as e:
logging.error("串口打开失败: %s", e)
return False
self.thread = threading.Thread(target=self._loop, daemon=True)
self.thread.start()
return True
def stop(self):
self.running = False
if self.thread: self.thread.join(2)
if self.ser and self.ser.is_open: self.ser.close()
def get(self, target_time):
with self.lock:
if not self.buf: return None, None, None
best = min(self.buf, key=lambda x: abs(x[0] - target_time))
return best[1], best[0], abs(best[0] - target_time)
# 内部
def _loop(self):
while self.running:
try:
line = self.ser.readline().decode('utf-8', errors='ignore').strip()
if not line: continue
val = self._parse(line)
if val is None: continue
now = time.time()
with self.lock:
self.buf.append((now, val))
while self.buf and now - self.buf[0][0] > self.buf_dur:
self.buf.popleft()
except Exception as e:
logging.error("illumin 读线程异常: %s", e)
time.sleep(1)
@staticmethod
def _parse(line):
try:
if line.startswith('{') and line.endswith('}'):
return float(json.loads(line)['illumin'])
import re
nums = re.findall(r'\d+\.?\d*', line)
return float(nums[0]) if nums else None
except Exception:
return None
# ----------- 检测器 -----------
class ColorTransitionDetector:
def __init__(self):
self.cap = None
self.frame_buffer = deque()
self.timestamp_buffer= deque()
self.buffer_size = 0
self.current_state = None
self.color_counter = 0
self.bw_counter = 0
self.transition_log = []
self.COLOR_THRESHOLD = 15
self.BW_THRESHOLD = 5
self.CONSEC_FRAMES = 3
self.PRE_TIME = 3
self.POST_TIME = 5
self.INITIAL_FRAMES = 5
self.init_sats = []
self.state_ok = False
self.frame_count = 0
self.last_frame_time= time.time()
self.frame_interval = 0.1
self.illumin = IlluminReader(SERIAL_COM, SERIAL_BAUD)
self.pending = []
self._chk_dirs()
def _chk_dirs(self):
self.save_dir = os.path.join(os.path.dirname(sys.executable), 'screens') \
if getattr(sys, 'frozen', False) else 'screens'
os.makedirs(self.save_dir, exist_ok=True)
def init_cam(self):
logging.info("连接摄像头...")
self.cap = cv2.VideoCapture(RTSP_URL)
if not self.cap.isOpened():
logging.error("摄像头打开失败")
return False
self.cap.set(cv2.CAP_PROP_BUFFERSIZE, 1)
fps = max(self.cap.get(cv2.CAP_PROP_FPS), 8)
self.buffer_size = int((self.PRE_TIME + self.POST_TIME) * fps)
self.frame_buffer = deque(maxlen=self.buffer_size)
self.timestamp_buffer = deque(maxlen=self.buffer_size)
self.frame_interval = 1.0 / fps
logging.info("缓冲区帧数: %d", self.buffer_size)
return True
def reconnect(self):
if self.cap: self.cap.release()
time.sleep(2)
return self.init_cam()
def calc_sat(self, frame):
try:
small = cv2.resize(frame, (320, 240))
hsv = cv2.cvtColor(small, cv2.COLOR_BGR2HSV)
return np.mean(hsv[:, :, 1])
except Exception as e:
logging.error("饱和度计算失败: %s", e)
return 0
def detect(self, sat):
if not self.state_ok: return False, ""
detected = False
ttype = ""
if self.current_state == "bw":
if sat > self.COLOR_THRESHOLD:
self.color_counter += 1; self.bw_counter = 0
if self.color_counter >= self.CONSEC_FRAMES:
ttype, detected, self.current_state = "btc", True, "color"
self.color_counter = 0
else:
self.color_counter = 0
else:
if sat < self.BW_THRESHOLD:
self.bw_counter += 1; self.color_counter = 0
if self.bw_counter >= self.CONSEC_FRAMES:
ttype, detected, self.current_state = "ctb", True, "bw"
self.bw_counter = 0
else:
self.bw_counter = 0
return detected, ttype
def proc_frame(self, frame):
self.frame_count += 1
now = time.time()
elapsed = now - self.last_frame_time
if elapsed < self.frame_interval: time.sleep(self.frame_interval - elapsed)
self.last_frame_time = now
small = cv2.resize(frame, (0, 0), fx=0.5, fy=0.5)
self.frame_buffer.append(small.copy())
self.timestamp_buffer.append(now)
sat = self.calc_sat(small)
if not self.state_ok:
self.init_sats.append(sat)
if len(self.init_sats) >= self.INITIAL_FRAMES:
avg = np.mean(self.init_sats)
self.current_state = "color" if avg > self.COLOR_THRESHOLD else "bw"
self.state_ok = True
logging.info("初始状态: %s (avg=%.2f)", self.current_state, avg)
detected, ttype = self.detect(sat)
if detected:
ts_str = datetime.now().strftime('%Y%m%d_%H%M%S_%f')[:-3]
self.transition_log.append(f"{ts_str} {ttype}")
logging.info("检测到 %s 饱和度=%.2f", ttype.upper(), sat)
# 3 s 前 illumin
ill, ill_ts, diff = self.illumin.get(now - 3)
if ill is not None:
logging.info("转换前3秒 illumin=%.2f (时间差=%.2fs)", ill, diff)
else:
logging.warning("未找到转换前3秒 illumin 记录")
self.save_pre(now, ts_str, ttype)
self.pending.append((now + self.POST_TIME, ts_str, ttype))
return sat, detected, small
def save_pre(self, trans_time, ts_str, ttype):
if not self.frame_buffer: return
target = trans_time - self.PRE_TIME
idx = 0
for i, t in enumerate(self.timestamp_buffer):
if t >= target: idx = i; break
frame = self.frame_buffer[idx]
h = int(self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
w = int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH))
full = cv2.resize(frame, (w, h), interpolation=cv2.INTER_CUBIC)
path = os.path.join(self.save_dir, f"{ts_str}_{ttype}_pre{self.PRE_TIME}s.jpg")
if cv2.imwrite(path, full, [cv2.IMWRITE_JPEG_QUALITY, 90]):
logging.info("已保存前截图: %s", os.path.basename(path))
def save_post(self, frame, ts_str, ttype):
h = int(self.cap.get(cv2.CAP_PROP_FRAME_HEIGHT))
w = int(self.cap.get(cv2.CAP_PROP_FRAME_WIDTH))
full = cv2.resize(frame, (w, h), interpolation=cv2.INTER_CUBIC)
path = os.path.join(self.save_dir, f"{ts_str}_{ttype}_post{self.POST_TIME}s.jpg")
if cv2.imwrite(path, full, [cv2.IMWRITE_JPEG_QUALITY, 90]):
logging.info("已保存后截图: %s", os.path.basename(path))
def run(self):
if not self.init_cam(): return
if not self.illumin.start():
logging.warning("illumin 不可用")
try:
while True:
ret, frame = self.cap.read()
if not ret:
if not self.reconnect(): time.sleep(5)
continue
sat, detected, small = self.proc_frame(frame)
# 检查后截图
now = time.time()
for t in list(self.pending):
if now >= t[0]:
self.save_post(frame, t[1], t[2])
self.pending.remove(t)
# 可视化
display = small.copy()
color = (0, 0, 255) if detected else (0, 255, 0)
cv2.putText(display, f"sat={sat:.1f}", (10, 30),
cv2.FONT_HERSHEY_SIMPLEX, 0.7, color, 2)
cv2.imshow("ColorTransitionDetector", display)
if cv2.waitKey(1) & 0xFF == ord('q'):
break
finally:
self.illumin.stop()
if self.cap: self.cap.release()
cv2.destroyAllWindows()
# ----------- 启动 -----------
if __name__ == "__main__":
ColorTransitionDetector().run()
最新发布