发帖
1 0 0

【安信可小安派BW21-CBV-Kit开发板】家庭视频监控与回放系统

wenfengand
中级会员

5

主题

3

回帖

369

积分

中级会员

积分
369
小安派·BW21-CBV-KIt 69 1 3 天前
本帖最后由 wenfengand 于 2025-3-23 18:06 编辑

一、背景
安信可推出的 BW21-CBV-Kit 小安派是一款面向智能感知领域的开源硬件开发平台,其核心配置包括:
  • AI视觉能力:可搭载本地AI图像识别算法,支持人脸识别、手势识别及物品识别等场景,通过200W像素摄像头实现高清图像采集;
  • 处理性能:采用双频Wi-Fi(2.4GHz+5GHz)与高性能处理器RTL8735B(500MHz),满足实时图像处理与无线传输需求;
  • 开源生态:基于Arduino开发环境,提供PWM控制、传感器驱动等丰富接口,支持用户自定义开发网络摄像头系统。

相较于传统闭源智能摄像头,BW21-CBV-Kit的创新价值体现在:
  • 打破系统限制:允许开发者绕过厂商绑定,自由构建本地化存储或私有云方案,规避云端数据泄露风险;
  • 扩展场景融合:通过兼容DHT温湿度传感器、超声波测距模块等外设,可扩展智能家居安防、环境监测等复合功能;
  • 开发效率优势:提供HomeAssistant接入、预置AI模型调用等工具链,显著降低家庭级智能摄像头的开发门槛。

该设备在智能家居、工业视觉检测、新零售行为分析等领域具有重要应用潜力,其开源特性尤其适合创客社区和中小型企业构建定制化视觉解决方案。目前很多厂家都有智能摄像头产品,但这些产品是闭源的,只能使用对应厂家的软件系统和云存储。小安派的摄像头正好可以用于搭建网络摄像头,然后开发对应的软件系统,供家庭使用。

二、目标
使用 BW21-CBV-Kit 小安派制作一款家庭安防摄像头,能够实现 7x24 小时视频录制与实时存储,支持网页端回放监控录像。网页端应尽可能简便易操作,能够根据日期来选择视频文件。

本项目的核心目标为基于小安派硬件平台开发智能化家庭安防监控系统,重点构建三大功能模块:
1、全天候音视频采集系统
  • 实现7×24小时不间断视频录制,采用H.264编码技术确保1080P高清画质;
  • 搭载人脸识别算法,智能监控可疑目标;
2、分层式存储架构

  • 构建集中存储机制:NAS 订阅 rtsp 音视频流并写入硬盘
3、云端交互系统

  • 搭建基于B/S架构的流媒体服务平台,支持 m3u8 视频回放
  • 开发视频检索模块,可按天检索监控视频

系统整体设计遵循"端-边-云"协同架构,在保证数据安全性的前提下,最终形成完整的家庭安防解决方案。
三、设计思路
3.1 硬件设计
采用 BW21-CBV-Kit 开发板,连接 GC2053 摄像头。以包装盒为载体,通过手工加工作为摄像头的外壳。

                               
登录/注册后可看大图


                               
登录/注册后可看大图


3.2 软件设计
3.2.1 单片机
在单片机端,采用了 rtsp 输出视频流,同时标注视频中的人脸。
  1. #include "WiFi.h"
  2. #include "StreamIO.h"
  3. #include "VideoStream.h"
  4. #include "RTSP.h"
  5. #include "NNFaceDetection.h"
  6. #include "VideoStreamOverlay.h"

  7. #define CHANNEL   0
  8. #define CHANNELNN 3

  9. // Lower resolution for NN processing
  10. #define NNWIDTH  576
  11. #define NNHEIGHT 320

  12. VideoSetting config(VIDEO_FHD, 30, VIDEO_H264, 0);
  13. VideoSetting configNN(NNWIDTH, NNHEIGHT, 10, VIDEO_RGB, 0);
  14. NNFaceDetection facedet;
  15. RTSP rtsp;
  16. StreamIO videoStreamer(1, 1);
  17. StreamIO videoStreamerNN(1, 1);

  18. char ssid[] = "Network_SSID";    // your network SSID (name)
  19. char pass[] = "Password";        // your network password
  20. int status = WL_IDLE_STATUS;

  21. IPAddress ip;
  22. int rtsp_portnum;

  23. void setup()
  24. {
  25.     Serial.begin(115200);

  26.     // attempt to connect to Wifi network:
  27.     while (status != WL_CONNECTED) {
  28.         Serial.print("Attempting to connect to WPA SSID: ");
  29.         Serial.println(ssid);
  30.         status = WiFi.begin(ssid, pass);

  31.         // wait 2 seconds for connection:
  32.         delay(2000);
  33.     }
  34.     ip = WiFi.localIP();

  35.     // Configure camera video channels with video format information
  36.     // Adjust the bitrate based on your WiFi network quality
  37.     config.setBitrate(2 * 1024 * 1024);    // Recommend to use 2Mbps for RTSP streaming to prevent network congestion
  38.     Camera.configVideoChannel(CHANNEL, config);
  39.     Camera.configVideoChannel(CHANNELNN, configNN);
  40.     Camera.videoInit();

  41.     // Configure RTSP with corresponding video format information
  42.     rtsp.configVideo(config);
  43.     rtsp.begin();
  44.     rtsp_portnum = rtsp.getPort();

  45.     // Configure face detection with corresponding video format information
  46.     // Select Neural Network(NN) task and models
  47.     facedet.configVideo(configNN);
  48.     facedet.setResultCallback(FDPostProcess);
  49.     facedet.modelSelect(FACE_DETECTION, NA_MODEL, DEFAULT_SCRFD, NA_MODEL);
  50.     facedet.begin();

  51.     // Configure StreamIO object to stream data from video channel to RTSP
  52.     videoStreamer.registerInput(Camera.getStream(CHANNEL));
  53.     videoStreamer.registerOutput(rtsp);
  54.     if (videoStreamer.begin() != 0) {
  55.         Serial.println("StreamIO link start failed");
  56.     }

  57.     // Start data stream from video channel
  58.     Camera.channelBegin(CHANNEL);

  59.     // Configure StreamIO object to stream data from RGB video channel to face detection
  60.     videoStreamerNN.registerInput(Camera.getStream(CHANNELNN));
  61.     videoStreamerNN.setStackSize();
  62.     videoStreamerNN.setTaskPriority();
  63.     videoStreamerNN.registerOutput(facedet);
  64.     if (videoStreamerNN.begin() != 0) {
  65.         Serial.println("StreamIO link start failed");
  66.     }

  67.     // Start video channel for NN
  68.     Camera.channelBegin(CHANNELNN);

  69.     // Start OSD drawing on RTSP video channel
  70.     OSD.configVideo(CHANNEL, config);
  71.     OSD.begin();
  72. }

  73. void loop()
  74. {
  75.     // Do nothing
  76. }

  77. // User callback function for post processing of face detection results
  78. void FDPostProcess(std::vector<FaceDetectionResult> results)
  79. {
  80.     int count = 0;

  81.     uint16_t im_h = config.height();
  82.     uint16_t im_w = config.width();

  83.     Serial.print("Network URL for RTSP Streaming: ");
  84.     Serial.print("rtsp://");
  85.     Serial.print(ip);
  86.     Serial.print(":");
  87.     Serial.println(rtsp_portnum);
  88.     Serial.println(" ");

  89.     printf("Total number of faces detected = %d\r\n", facedet.getResultCount());
  90.     OSD.createBitmap(CHANNEL);

  91.     if (facedet.getResultCount() > 0) {
  92.         for (int i = 0; i < facedet.getResultCount(); i++) {
  93.             FaceDetectionResult item = results[i];
  94.             // Result coordinates are floats ranging from 0.00 to 1.00
  95.             // Multiply with RTSP resolution to get coordinates in pixels
  96.             int xmin = (int)(item.xMin() * im_w);
  97.             int xmax = (int)(item.xMax() * im_w);
  98.             int ymin = (int)(item.yMin() * im_h);
  99.             int ymax = (int)(item.yMax() * im_h);

  100.             // Draw boundary box
  101.             printf("Face %ld confidence %d:\t%d %d %d %d\n\r", i, item.score(), xmin, xmax, ymin, ymax);
  102.             OSD.drawRect(CHANNEL, xmin, ymin, xmax, ymax, 3, OSD_COLOR_WHITE);

  103.             // Print identification text above boundary box
  104.             char text_str[40];
  105.             snprintf(text_str, sizeof(text_str), "%s %d", item.name(), item.score());
  106.             OSD.drawText(CHANNEL, xmin, ymin - OSD.getTextHeight(CHANNEL), text_str, OSD_COLOR_CYAN);

  107.             // Draw facial feature points
  108.             for (int j = 0; j < 5; j++) {
  109.                 int x = (int)(item.xFeature(j) * im_w);
  110.                 int y = (int)(item.yFeature(j) * im_h);
  111.                 OSD.drawPoint(CHANNEL, x, y, 8, OSD_COLOR_RED);
  112.                 count++;
  113.                 if (count == MAX_FACE_DET) {
  114.                     goto OSDUpdate;
  115.                 }
  116.             }
  117.         }
  118.     }

  119. OSDUpdate:
  120.     OSD.update(CHANNEL);
  121. }
复制代码


3.2.2 集中存储
使用 ffmpeg 订阅小安派开发板的 rtsp 音视频流,并按照指定的时长保存到本地
  1. ffmpeg -rtsp_transport tcp -i rtsp://192.168.123.6:554/mystream \
  2. -c copy \
  3. -f hls \
  4. -strftime 1 \
  5. -hls_time 60 \
  6. -hls_list_size 0 \
  7. -hls_flags delete_segments+append_list \
  8. -hls_segment_filename "./data/%Y%m%d/%Y%m%d_%H%M%S.ts" \
  9. "./data/20250323/playlist.m3u8" \
  10. -protocol_whitelist file,rtsp,tcp \
  11. -rw_timeout 5000000
复制代码

该脚本的主要功能如下:
1、RTSP转HLS
将RTSP直播流(如IP摄像头)转换为HLS格式,适用于Web播放(如HTML5视频标签)。
2、分片存储

  • 每3秒生成一个.ts视频分片(-hls_time 3)。
  • 分片文件按日期和时间命名,存储在./data/YYYYMMDD/目录下(如20250323_123456.ts)。

3、播放列表管理
  • playlist.m3u8记录所有分片信息,供播放器按需加载。
  • -hls_list_size 0表示播放列表保留全部分片(适合点播),若需限制分片数量(如直播),可设为具体值(如5)。
4、自动清理旧分片
  • -hls_flags delete_segments会删除播放列表中不再引用的旧分片文件,但需注意:
     当hls_list_size=0时,播放列表包含所有分片,此标志无效,可能导致磁盘占满。建议设为非零值(如5)以自动清理。
5、时间目录结构

利用strftime格式动态生成目录和文件名,例如:

  1. ./data/20250318/20250323_120000.ts
  2. ./data/20250318/20250323_120003.ts
复制代码

保存下来的 m3u8 文件目录结构如下图所示:

                               
登录/注册后可看大图


3.2.3 视频回放
目前只完成了视频的录制、存储,还需要对应的前端页面来展示视频,而数据的传输又需要一些后端软件来支持。
3.2.3.1 后端
基于Flask框架构建的视频流服务后端,核心功能是提供结构化的HLS流媒体服务。通过自动化目录扫描和动态路由机制,实现按日期分类的视频资源管理与分发,主要服务于需要按时间维度检索视频内容的场景。


1、数据发现层
  • 目录扫描:通过get_available_dates()函数动态探测../ffmpeg/data目录结构
  • 校验机制:同时验证目录命名合规性(YYYYMMDD格式)与播放列表完整性(存在playlist.m3u8)
  • 时间轴排序:对合法日期目录进行时间升序排列,确保前端时间线展示的时序正确性
2、接口服务层
  • 可视化入口:根路由提供index.html模板,支持前端界面集成
  • 结构化数据接口:/api/dates端点返回标准化的日期列表(YYYY-MM-DD格式),实现前后端数据分离
  • 智能格式转换:自动将存储格式的日期标识转换为用户友好的展示格式
3、流媒体服务层
  • 协议适配路由:/video/实现HLS协议的分流处理
  • M3U8索引分发:直接返回指定日期的播放列表文件
  • TS分片处理:通过serve_ts_files方法实现分片资源的精准定位与安全验证
  • 四重安全校验:包含文件名长度检测、日期格式验证、目录存在性检查、文件物理存在确认
  1. from flask import Flask, render_template, jsonify, send_from_directory
  2. import os
  3. from datetime import datetime

  4. app = Flask(__name__)
  5. DATA_DIR = '../ffmpeg/data'

  6. def get_available_dates():
  7.     dates = []
  8.     for dirname in os.listdir(DATA_DIR):
  9.         dir_path = os.path.join(DATA_DIR, dirname)
  10.         m3u8_path = os.path.join(dir_path, 'playlist.m3u8')
  11.         if os.path.isdir(dir_path) and os.path.exists(m3u8_path):
  12.             try:
  13.                 datetime.strptime(dirname, '%Y%m%d')
  14.                 dates.append(dirname)
  15.             except ValueError:
  16.                 continue
  17.     dates.sort(key=lambda x: datetime.strptime(x, '%Y%m%d'))
  18.     return dates

  19. @app.route('/')
  20. def index():
  21.     return render_template('index.html')

  22. @app.route('/api/dates')
  23. def api_dates():
  24.     dates = get_available_dates()
  25.     formatted_dates = [f"{d[:4]}-{d[4:6]}-{d[6:8]}" for d in dates]
  26.     return jsonify({'dates': formatted_dates})

  27. @app.route('/video/<filename>')
  28. def serve_m3u8(filename):
  29.     if ".ts" in filename:
  30.         return serve_ts_files(filename)
  31.     else:
  32.         dir_path = os.path.join(DATA_DIR, filename)
  33.         return send_from_directory(dir_path, 'playlist.m3u8')


  34. def serve_ts_files(filename):
  35.     # 从文件名中提取日期部分(前8位)
  36.     if len(filename) < 8:
  37.         return "Invalid filename", 400
  38.    
  39.     date_str = filename[:8]
  40.     try:
  41.         # 验证日期格式
  42.         datetime.strptime(date_str, '%Y%m%d')
  43.     except ValueError:
  44.         return "Invalid date format in filename", 400

  45.     # 构建文件路径
  46.     dir_path = os.path.join(DATA_DIR, date_str)

  47.     print("dir_path is ", dir_path)

  48.     if not os.path.isdir(dir_path):
  49.         return "Date directory not found", 404

  50.     file_path = os.path.join(dir_path, filename)

  51.     print("file_path is ", file_path)
  52.     if not os.path.exists(file_path):
  53.         return "File not found", 404

  54.     return send_from_directory(dir_path, filename)

  55. if __name__ == '__main__':
  56.     app.run(debug=True)
复制代码

3.2.3.2 前端
前端部分主要处理人机交互,采用HLS流媒体协议实现跨平台视频播放,构建了日期选择、播放控制、状态反馈的可视化操作界面,与后端服务形成完整流媒体解决方案。

1、呈现层设计

  • 响应式布局:通过容器最大宽度限制和自动外边距实现桌面端适配
  • 视觉层次区分:控制栏采用Flex布局保持元素间距,视频播放器全宽黑色背景强化视觉焦点
  • 状态可视化:动态显示播放进度与总时长,提升操作反馈感
2、流媒体适配层
  • 双模兼容机制:
    • 首选Hls.js库实现高级功能支持
    • 备选原生HTML5播放器保障Safari等环境可用性
  • 智能实例管理:HLS对象动态销毁重建,避免多流内存泄漏
  • 事件驱动加载:通过MEDIA_ATTACHED和MANIFEST_PARSED事件链确保加载顺序
3、控制逻辑层
  • 时序化操作流:选择日期 → 生成标准化请求 → 初始化播放引擎 → 自动播放
  • 播放状态联动:用户操作(播放/暂停)与视频元素状态实时同步
  • 智能日期转换:自动将展示格式日期转换为存储格式参数
  1. <!-- templates/index.html -->
  2. <!DOCTYPE html>
  3. <html>
  4. <head>
  5.     <title>视频监控系统</title>
  6.     <script src="https://cdn.jsdelivr.net/npm/hls.js@1.1.5"></script>
  7.     <style>
  8.         .container {
  9.             max-width: 800px;
  10.             margin: 20px auto;
  11.             padding: 20px;
  12.         }
  13.         .controls {
  14.             margin-bottom: 20px;
  15.             display: flex;
  16.             gap: 10px;
  17.             align-items: center;
  18.         }
  19.         #videoPlayer {
  20.             width: 100%;
  21.             background: #000;
  22.         }
  23.         button {
  24.             padding: 5px 15px;
  25.             cursor: pointer;
  26.         }
  27.     </style>
  28. </head>
  29. <body>
  30.     <div class="container">
  31.         <div class="controls">
  32.             <select id="dateSelect">
  33.                 <option value="">选择日期</option>
  34.             </select>
  35.             <button id="playBtn">播放</button>
  36.             <button id="pauseBtn">暂停</button>
  37.             <span id="status">准备就绪</span>
  38.         </div>
  39.         <video id="videoPlayer" controls></video>
  40.     </div>

  41.     <script>
  42.         const video = document.getElementById('videoPlayer');
  43.         let hls = null;
  44.         let currentDate = '';

  45.         // 初始化HLS支持检测
  46.         if (Hls.isSupported()) {
  47.             hls = new Hls();
  48.             hls.attachMedia(video);
  49.         }

  50.         // 加载可用日期
  51.         fetch('/api/dates')
  52.             .then(res => res.json())
  53.             .then(data => {
  54.                 const select = document.getElementById('dateSelect');
  55.                 data.dates.forEach(date => {
  56.                     const option = document.createElement('option');
  57.                     option.value = date;
  58.                     option.textContent = date;
  59.                     select.appendChild(option);
  60.                 });
  61.             });

  62.         // 日期选择事件
  63.         document.getElementById('dateSelect').addEventListener('change', function() {
  64.             const selectedDate = this.value;
  65.             if (!selectedDate) return;

  66.             currentDate = selectedDate.replace(/-/g, '');
  67.             const m3u8Url = `/video/${currentDate}`;
  68.             
  69.             if (Hls.isSupported()) {
  70.                 if (hls) {
  71.                     hls.destroy();
  72.                 }
  73.                 hls = new Hls();
  74.                 hls.attachMedia(video);
  75.                 hls.on(Hls.Events.MEDIA_ATTACHED, () => {
  76.                     hls.loadSource(m3u8Url);
  77.                     hls.on(Hls.Events.MANIFEST_PARSED, () => {
  78.                         video.play();
  79.                     });
  80.                 });
  81.             } else if (video.canPlayType('application/vnd.apple.mpegurl')) {
  82.                 video.src = m3u8Url;
  83.                 video.addEventListener('loadedmetadata', () => {
  84.                     video.play();
  85.                 });
  86.             }
  87.         });

  88.         // 播放控制
  89.         document.getElementById('playBtn').addEventListener('click', () => {
  90.             video.play();
  91.         });

  92.         document.getElementById('pauseBtn').addEventListener('click', () => {
  93.             video.pause();
  94.         });

  95.         // 更新状态显示
  96.         video.addEventListener('timeupdate', () => {
  97.             const status = document.getElementById('status');
  98.             status.textContent = `播放中 - ${formatTime(video.currentTime)}/${formatTime(video.duration)}`;
  99.         });

  100.         function formatTime(seconds) {
  101.             const date = new Date(0);
  102.             date.setSeconds(seconds);
  103.             return date.toISOString().substr(11, 8);
  104.         }
  105.     </script>
  106. </body>
  107. </html>
复制代码

总体效果:
https://www.bilibili.com/video/BV1zSXYYAEhJ/

四、未来展望
4.1 事件探测与快速跳转
目前只有视频画面,前端视频回放也只能手工跳转查看。后续可以通过人脸识别到家庭成员,定位陌生人出现的时间,然后在前端播放器添加事件轨,方便快速跳转。

4.2 接入 homeassistant
目前是自己写了前后端代码,相对比较独立,如果能嵌入到 hass,那么应用范围会更加广泛。









──── 0人觉得很赞 ────

使用道具 举报

不错不错~
您需要登录后才可以回帖 立即登录
高级模式
返回
统计信息
  • 会员数: 28167 个
  • 话题数: 39946 篇