惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

freeCodeCamp Programming Tutorials: Python, JavaScript, Git & More
I
InfoQ
H
Help Net Security
大猫的无限游戏
大猫的无限游戏
L
LangChain Blog
F
Full Disclosure
Apple Machine Learning Research
Apple Machine Learning Research
J
Java Code Geeks
CTFtime.org: upcoming CTF events
CTFtime.org: upcoming CTF events
WordPress大学
WordPress大学
N
Netflix TechBlog - Medium
博客园 - Franky
IT之家
IT之家
K
Kaspersky official blog
Simon Willison's Weblog
Simon Willison's Weblog
C
Cisco Blogs
钛媒体:引领未来商业与生活新知
钛媒体:引领未来商业与生活新知
T
The Exploit Database - CXSecurity.com
Know Your Adversary
Know Your Adversary
PCI Perspectives
PCI Perspectives
V2EX - 技术
V2EX - 技术
P
Proofpoint News Feed
Security Archives - TechRepublic
Security Archives - TechRepublic
The Last Watchdog
The Last Watchdog
V
Vulnerabilities – Threatpost
Vercel News
Vercel News
博客园 - 司徒正美
G
Google Developers Blog
cs.AI updates on arXiv.org
cs.AI updates on arXiv.org
H
Hackread – Cybersecurity News, Data Breaches, AI and More
Blog — PlanetScale
Blog — PlanetScale
Help Net Security
Help Net Security
S
Secure Thoughts
有赞技术团队
有赞技术团队
P
Privacy International News Feed
S
Schneier on Security
T
Troy Hunt's Blog
T
The Blog of Author Tim Ferriss
阮一峰的网络日志
阮一峰的网络日志
H
Heimdal Security Blog
Latest news
Latest news
S
Security @ Cisco Blogs
博客园 - 聂微东
The Hacker News
The Hacker News
B
Blog
Forbes - Security
Forbes - Security
A
About on SuperTechFans
Exploit-DB.com RSS Feed
Exploit-DB.com RSS Feed
罗磊的独立博客
雷峰网
雷峰网

博客园 - jack_Meng

油候脚本中,使用GM_info对象,打印脚本信息 使用油候脚本,移除页面无法选择文字 高中免费电子教辅资料合集 Windows防火墙关闭445端口:Windows在防火墙禁用echo-reply(type 0)、time-exceeded(type 11)、destination-unreachable(type 3)类型的ICMP包 WinForm 双屏开发中屏幕适配与窗口定位 高考志愿填报之计算机类专业院校报考指南(2026版) WebSocket 快速入门教程(附示例源码) 自动生成项目工具--AutoBuilder,一键干掉80%的重复CRUD工作 C#/.NET/.NET Core优秀项目和框架2026年5月简报 在Win10系统中,默认使用照片查看器 C#实现控制台多区域输出 解决: 您的连接不是私密连接,您目前无法访问 因为此网站使用了 HSTS 小说下载 网页版时间,可全屏显示 个人笔记本连接公共WIFI的安全措施 追更 HelloGitHub 一整年,年度盘点 基于Rust开发的m3u8下载器M3U8Quicker:支持断点续传、边下边播 新写了个直播录制工具,可录制抖音快手斗鱼直播 bing 每日一图 --- 桌面壁纸 一款基于 C# 开发的 Windows 10/11 系统增强优化工具 适合个人的免费域名 语雀里存了三年的笔记,导出到了本地插件----YuqueOut C# 也能像 Python 一样写脚本 | .NET 10 构建基于文件的应用 js 双击页面 开始/暂停 页面滚动 使用bat批量给txt追加内容 【译】告别繁琐查错:认识下新的 Visual Studio Debugger Agent Workflow 好消息,在 Visual Studio 中可以免费使用 GitHub Copilot 了! [C#] 零依赖高性能跨平台 Web 胶水库 -- PicoServer 我的第一款独立产品--TaskManager 写 EF Core 查询,优化查询语句 最新.NET新手入门学习网站合集(2026更新版) Avalonia UI:.NET 跨平台桌面开发的“真香”选择 一个 txt 生成并保存 mp3的 Python 脚本
Python摄像头监控:运动检测+自动录像+时间水印
jack_Meng · 2026-04-29 · via 博客园 - jack_Meng

用 Python 实现智能摄像头监控:运动检测 + 自动录像 + 时间水印

关键词:Python、OpenCV、运动检测、帧差法、视频录制、时间戳、安防监控、边缘计算

在家庭安防、宠物看护、仓库监控等场景中,一个轻量、可靠、无需联网的本地摄像头监控系统非常实用。本文将带你从零实现一个基于 Python + OpenCV 的智能运动检测与自动录像系统——它能在检测到画面变化时自动开始录制视频,运动停止后继续缓冲几秒,并在保存的视频左上角添加精确的时间水印。

更重要的是:录制的视频是原始画面,不包含任何检测框或调试信息,可直接用于存档或回放!


✅ 项目亮点

  • 三帧差分法:比传统两帧差分更稳定,减少“目标静止即消失”的问题
  • 仅录有效片段:只在检测到运动时录像,节省存储空间
  • ⏱️ 自动时间水印:每帧叠加 2026-01-12 21:07:35 格式时间戳
  • 自动归档:视频按时间命名,存入 recordings/ 目录
  • 即插即用:支持 USB 摄像头、树莓派 CSI 摄像头等
  • 资源安全释放:异常退出也能正确关闭文件和设备

核心原理:为什么用“三帧差分”?

传统的两帧差分法(当前帧 vs 上一帧)在目标静止时会丢失轮廓,导致漏检。而三帧差分法通过计算:

highlighter- Ini

diff1 = |frame(t) - frame(t-1)|
diff2 = |frame(t+1) - frame(t)|
motion = diff1 ∩ diff2

只有连续两帧都发生变化的区域才被认为是“真实运动”,有效抑制了噪声和短暂干扰。

配合形态学开运算 + 膨胀,还能去除小噪点并填充运动区域空洞,大幅提升检测鲁棒性。


完整代码解析

以下是经过优化的完整实现(已注释关键逻辑):

python

import cv2
import datetime
import os


CAMERA_INDEX = 1          
THRESHOLD = 25            
MIN_CONTOUR_AREA = 800    
RECORD_DIR = "recordings"
POST_MOTION_BUFFER_SEC = 3  
os.makedirs(RECORD_DIR, exist_ok=True)


cap = cv2.VideoCapture(CAMERA_INDEX)
if not cap.isOpened():
    raise IOError("无法打开摄像头,请检查设备连接")


ret, prev_frame = cap.read()
ret, curr_frame = cap.read()
ret, next_frame = cap.read()
if not all([ret, ret, ret]):
    raise RuntimeError("无法获取初始视频帧")


is_recording = False
video_writer = None
last_motion_time = datetime.datetime.now()

def three_frame_diff(prev, curr, nxt, thresh_val=25):
    """三帧差分 + 形态学优化"""
    gray_prev = cv2.cvtColor(prev, cv2.COLOR_BGR2GRAY)
    gray_curr = cv2.cvtColor(curr, cv2.COLOR_BGR2GRAY)
    gray_next = cv2.cvtColor(nxt, cv2.COLOR_BGR2GRAY)

    diff1 = cv2.absdiff(gray_curr, gray_prev)
    diff2 = cv2.absdiff(gray_next, gray_curr)

    _, bin1 = cv2.threshold(diff1, thresh_val, 255, cv2.THRESH_BINARY)
    _, bin2 = cv2.threshold(diff2, thresh_val, 255, cv2.THRESH_BINARY)

    combined = cv2.bitwise_and(bin1, bin2)
    kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
    combined = cv2.morphologyEx(combined, cv2.MORPH_OPEN, kernel)  
    combined = cv2.dilate(combined, kernel, iterations=1)         
    return combined

def add_timestamp_to_frame(frame):
    """在帧左上角添加时间水印(格式:2026-01-12 21:07:35)"""
    timestamp_str = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    
    cv2.putText(frame, timestamp_str, (10, 30),
                cv2.FONT_HERSHEY_SIMPLEX, 0.7, (255, 255, 255), 2, cv2.LINE_AA)
    cv2.putText(frame, timestamp_str, (10, 30),
                cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 0, 0), 1, cv2.LINE_AA)
    return frame

try:
    print("\n[*] 运动检测开始")
    while True:
        ret, raw_frame = cap.read()
        if not ret: break

        display_frame = raw_frame.copy()  
        motion_mask = three_frame_diff(prev_frame, curr_frame, next_frame, THRESHOLD)

        
        contours, _ = cv2.findContours(motion_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
        motion_detected = False
        for cnt in contours:
            if cv2.contourArea(cnt) > MIN_CONTOUR_AREA:
                x, y, w, h = cv2.boundingRect(cnt)
                cv2.rectangle(display_frame, (x, y), (x + w, y + h), (0, 255, 0), 2)
                cv2.putText(display_frame, "MOTION!", (x, y - 10),
                            cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2)
                motion_detected = True

        
        prev_frame = curr_frame.copy()
        curr_frame = next_frame.copy()
        ret, next_frame = cap.read()
        if not ret: break

        current_time = datetime.datetime.now()

        
        if motion_detected:
            last_motion_time = current_time
            if not is_recording:
                timestamp = current_time.strftime("%Y%m%d_%H%M%S")
                video_path = os.path.join(RECORD_DIR, f"motion_{timestamp}.mp4")
                fourcc = cv2.VideoWriter_fourcc(*'mp4v')
                h, w = raw_frame.shape[:2]
                video_writer = cv2.VideoWriter(video_path, fourcc, 20.0, (w, h))
                is_recording = True
                print(f"[+] 开始录制: {video_path}, {current_time.strftime('%Y-%m-%d %H:%M:%S')}")

        
        if is_recording:
            frame_to_save = raw_frame.copy()
            frame_to_save = add_timestamp_to_frame(frame_to_save)
            video_writer.write(frame_to_save)

            
            if (current_time - last_motion_time).total_seconds() > POST_MOTION_BUFFER_SEC:
                video_writer.release()
                is_recording = False
                print(f"[-] 停止录制, {current_time.strftime('%Y-%m-%d %H:%M:%S')}")

        
        
        
        

finally:
    
    if is_recording:
        video_writer.release()
    cap.release()
    cv2.destroyAllWindows()

注意:代码中已注释掉 cv2.imshow 部分,使其可在无图形界面的服务器或树莓派后台运行。如需调试,取消注释即可。

点击查看代码

highlighter- Python

import cv2
import datetime
import os


CAMERA_INDEX = 1          
THRESHOLD = 25            
MIN_CONTOUR_AREA = 800    
RECORD_DIR = "recordings"
POST_MOTION_BUFFER_SEC = 3  
os.makedirs(RECORD_DIR, exist_ok=True)


cap = cv2.VideoCapture(CAMERA_INDEX)
if not cap.isOpened():
    raise IOError("无法打开摄像头,请检查设备连接")


ret, prev_frame = cap.read()
ret, curr_frame = cap.read()
ret, next_frame = cap.read()
if not all([ret, ret, ret]):
    raise RuntimeError("无法获取初始视频帧")


is_recording = False
video_writer = None
last_motion_time = datetime.datetime.now()

def three_frame_diff(prev, curr, nxt, thresh_val=25):
    """三帧差分法 + 形态学优化"""
    gray_prev = cv2.cvtColor(prev, cv2.COLOR_BGR2GRAY)
    gray_curr = cv2.cvtColor(curr, cv2.COLOR_BGR2GRAY)
    gray_next = cv2.cvtColor(nxt, cv2.COLOR_BGR2GRAY)

    diff1 = cv2.absdiff(gray_curr, gray_prev)
    diff2 = cv2.absdiff(gray_next, gray_curr)

    _, bin1 = cv2.threshold(diff1, thresh_val, 255, cv2.THRESH_BINARY)
    _, bin2 = cv2.threshold(diff2, thresh_val, 255, cv2.THRESH_BINARY)

    combined = cv2.bitwise_and(bin1, bin2)
    kernel = cv2.getStructuringElement(cv2.MORPH_ELLIPSE, (5, 5))
    combined = cv2.morphologyEx(combined, cv2.MORPH_OPEN, kernel)
    combined = cv2.dilate(combined, kernel, iterations=1)
    return combined

def add_timestamp_to_frame(frame):
    """在帧左上角添加中文格式时间水印"""
    timestamp_str = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
    
    cv2.putText(
        frame,
        timestamp_str,
        (10, 30),
        cv2.FONT_HERSHEY_SIMPLEX,
        0.7,
        (255, 255, 255),   
        2,
        cv2.LINE_AA
    )
    cv2.putText(
        frame,
        timestamp_str,
        (10, 30),
        cv2.FONT_HERSHEY_SIMPLEX,
        0.7,
        (0, 0, 0),         
        1,
        cv2.LINE_AA
    )
    return frame

try:
    print("\n[*] 运动检测开始")
    while True:
        ret, raw_frame = cap.read()  
        if not ret:
            break

        
        display_frame = raw_frame.copy()

        
        motion_mask = three_frame_diff(prev_frame, curr_frame, next_frame, THRESHOLD)

        
        contours, _ = cv2.findContours(motion_mask, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
        motion_detected = False
        for cnt in contours:
            if cv2.contourArea(cnt) > MIN_CONTOUR_AREA:
                x, y, w, h = cv2.boundingRect(cnt)
                cv2.rectangle(display_frame, (x, y), (x + w, y + h), (0, 255, 0), 2)
                cv2.putText(display_frame, "MOTION!", (x, y - 10),
                            cv2.FONT_HERSHEY_SIMPLEX, 0.7, (0, 255, 0), 2)
                motion_detected = True

        
        prev_frame = curr_frame.copy()
        curr_frame = next_frame.copy()
        ret, next_frame = cap.read()
        if not ret:
            break

        current_time = datetime.datetime.now()

        
        if motion_detected:
            last_motion_time = current_time
            if not is_recording:
                timestamp = current_time.strftime("%Y%m%d_%H%M%S")
                video_path = os.path.join(RECORD_DIR, f"motion_{timestamp}.mp4")
                fourcc = cv2.VideoWriter_fourcc(*'mp4v')
                height, width = raw_frame.shape[:2]
                video_writer = cv2.VideoWriter(video_path, fourcc, 20.0, (width, height))
                is_recording = True
                timestamp_str = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
                print(f"[+] 开始录制: {video_path},{timestamp_str}")

        
        if is_recording:
            frame_to_save = raw_frame.copy()
            frame_to_save = add_timestamp_to_frame(frame_to_save)
            video_writer.write(frame_to_save)

            
            if (current_time - last_motion_time).total_seconds() > POST_MOTION_BUFFER_SEC:
                video_writer.release()
                is_recording = False
                timestamp_str = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
                print(f"[-] 停止录制,{timestamp_str}")

        
        
        
        
        

finally:
    if is_recording:
        video_writer.release()
    cap.release()
    cv2.destroyAllWindows()

️ 使用指南

1. 安装依赖

bash

pip install opencv-python

2. 修改摄像头索引

  • CAMERA_INDEX = 0:笔记本内置摄像头
  • CAMERA_INDEX = 1:外接 USB 摄像头
  • 在 Linux 下可通过 ls /dev/video* 查看设备

3. 调整灵敏度

  • 提高灵敏度:降低 THRESHOLD(如 15)或减小 MIN_CONTOUR_AREA
  • 降低误报:提高 THRESHOLD(如 40)或增大 MIN_CONTOUR_AREA

4. 运行

bash

python motion_monitor.py

录制的视频将自动保存在 recordings/ 文件夹中,命名如:

highlighter-

motion_20260112_210735.mp4

应用场景扩展

  • 家庭宠物监控:记录猫咪捣乱瞬间
  • 无人值守店铺:夜间异常闯入报警
  • 实验室/机房:设备状态异常记录
  • 树莓派 + 移动电源:便携式野外监控站

若需进一步升级,可考虑:

  • 添加微信/邮件通知(使用 smtplib 或企业微信 API)
  • 支持 RTSP 网络摄像头
  • 集成 YOLO 进行人/车分类
  • 使用 FFmpeg 转 H.264 降低体积

✅ 总结

本文提供了一个轻量、高效、生产可用的 Python 视频监控方案。它不依赖深度学习模型,资源占用低,适合部署在边缘设备上。通过三帧差分与智能录像策略,既保证了检测准确性,又避免了无效视频堆积。

真正的智能,不在于复杂,而在于恰到好处的自动化。


欢迎点赞、收藏、转发!
如果你有改进想法或遇到问题,欢迎在评论区交流

__EOF__

  • 本文作者: Dapenson
  • 本文链接: https://www.cnblogs.com/dapenson/p/19474335
  • 关于博主: 评论和私信会在第一时间回复。或者直接私信我。
  • 版权声明: 本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
  • 声援博主: 如果您觉得文章对您有帮助,可以点击文章右下角推荐一下。

=======================================================================================