基于Vue+Python+Orange Pi Zero3的完整视频监控方案

以下是基于Vue+Python+Orange Pi Zero3的完整视频监控方案,包含两端完整代码和详细注释,确保各环节清晰可懂。

一、整体方案架构

复制代码
Orange Pi Zero3 (服务端)          Vue前端 (客户端)
┌───────────────────────┐       ┌───────────────────┐
│ 1. Python视频流服务    │       │ 1. 视频播放组件    │
│    - 读取摄像头        │◄─────►│ 2. 控制按钮        │
│    - 提供MJPEG流      │       │ 3. 状态显示        │
│    - 处理视频帧        │       │                   │
└───────────────────────┘       └───────────────────┘

二、Orange Pi端(Python服务端)

1. 环境准备
bash 复制代码
# 安装依赖
sudo apt update && sudo apt upgrade -y
sudo apt install python3 python3-pip -y
# 安装Python库(opencv用于摄像头操作,flask提供web服务)
pip3 install opencv-python flask flask-cors numpy

安装过程中会报错,需要创建并激活一个独立的虚拟环境,在其中安装包(不会影响系统 Python):

bash 复制代码
# 创建虚拟环境(路径可自定义,比如 ~/myenv)
python3 -m venv ~/myenv

# 激活虚拟环境
source ~/myenv/bin/activate  # Linux/Mac 系统
# 激活后命令行前会显示 (myenv) 表示成功

# 此时可正常使用 pip 安装包
pip install 你需要的包名

# 退出虚拟环境(可选)
deactivate

由于镜像问题下载的时候可能会因为超时报错,所以我选择切换仓库镜像提高下载的速率:

bash 复制代码
pip install opencv-python -i https://mirrors.aliyun.com/pypi/simple/
pip install flask -i https://mirrors.aliyun.com/pypi/simple/
pip install  flask-cors -i https://mirrors.aliyun.com/pypi/simple/
pip install numpy -i https://mirrors.aliyun.com/pypi/simple/
pip install requests -i https://mirrors.aliyun.com/pypi/simple/
2. 完整Python服务代码(video_server.py
python 复制代码
# -*- coding: utf-8 -*-
import cv2  # 摄像头操作库
import numpy as np  # 图像处理
from flask import Flask, Response, jsonify  # Web服务
from flask_cors import CORS  # 解决跨域问题
import threading  # 多线程处理(避免阻塞)
import time  # 时间相关
import requests  # 发送HTTP请求

# 初始化Flask应用
app = Flask(__name__)
# 允许跨域访问(前端Vue和后端Python可能不同端口)
CORS(app, resources=r"/*")

# -------------------------- 配置参数 --------------------------
# CAMERA_INDEX = 0  # 摄像头设备索引(通常为0)
DEFAULT_WIDTH = 640  # 默认宽度
DEFAULT_HEIGHT = 480  # 默认高度
DEFAULT_FPS = 15  # 默认帧率
SERVER_PORT = 5000  # 服务端口
# --------------------------------------------------------------

# 全局变量(多线程共享)
frame = None  # 当前视频帧
is_running = False  # 摄像头运行状态
lock = threading.Lock()  # 线程锁(防止资源竞争)
current_width = DEFAULT_WIDTH
current_height = DEFAULT_HEIGHT
current_fps = DEFAULT_FPS


def capture_frames():
    """
    从摄像头捕获视频帧并进行处理
    运行在独立线程中,避免阻塞Web服务
    """
    global frame, is_running, current_width, current_height, current_fps

    CAMERA_INDEX = 0  # 摄像头设备索引(通常为0)
    # 测试0-10之间的索引(通常足够覆盖大多数情况)
    for index in range(10):
        cap = cv2.VideoCapture(index, cv2.CAP_V4L2)
        if cap.isOpened():
            print(f"找到可用摄像头,索引:{index}")
            cap.release()  # 释放资源
            CAMERA_INDEX = index
            break
        elif index == 9:
            print(f"无可用摄像头")

    # 打开摄像头
    cap = cv2.VideoCapture(CAMERA_INDEX, cv2.CAP_V4L2)
    if not cap.isOpened():
        print(f"❌ 无法打开摄像头,请检查设备是否连接(video index: {CAMERA_INDEX})")
        is_running = False
        return

    # 设置摄像头参数
    cap.set(cv2.CAP_PROP_FRAME_WIDTH, current_width)
    cap.set(cv2.CAP_PROP_FRAME_HEIGHT, current_height)
    cap.set(cv2.CAP_PROP_FPS, current_fps)

    print(f"✅ 摄像头启动成功 (分辨率: {current_width}x{current_height}, 帧率: {current_fps})")
    is_running = True

    # 用于运动检测的背景帧
    background_frame = None

    # 获取初始帧以添加IP信息水印
    ip_info = get_ip_info()  # 获取公网IP信息

    while is_running:
        # 读取一帧画面
        ret, img = cap.read()
        if not ret:
            print("⚠️ 无法获取视频帧,尝试重连...")
            time.sleep(1)
            continue

        # ---------------------- 视频处理示例 ----------------------
        # 1. 添加IP水印
        cv2.putText(
            img,
            ip_info,
            (10, 30),  # 位置
            cv2.FONT_HERSHEY_SIMPLEX,  # 字体
            0.8,  # 大小
            (0, 255, 0),  # 颜色(绿)
            2  # 线条粗细
        )
        # 2. 添加时间水印
        current_time = time.strftime("%Y-%m-%d %H:%M:%S", time.localtime())
        cv2.putText(
            img,
            current_time,
            (10, 30+30),  # 位置
            cv2.FONT_HERSHEY_SIMPLEX,  # 字体
            0.8,  # 大小
            (0, 255, 0),  # 颜色(绿)
            2  # 线条粗细
        )

        # 2. 简单运动检测(可选)
        gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)  # 转为灰度图
        gray = cv2.GaussianBlur(gray, (21, 21), 0)  # 模糊处理降噪

        # 初始化背景帧
        if background_frame is None:
            background_frame = gray
            continue

        # 计算当前帧与背景帧的差异
        frame_delta = cv2.absdiff(background_frame, gray)
        thresh = cv2.threshold(frame_delta, 25, 255, cv2.THRESH_BINARY)[1]
        thresh = cv2.dilate(thresh, None, iterations=2)  # 膨胀处理

        # 检测运动区域
        contours, _ = cv2.findContours(thresh.copy(), cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)
        for c in contours:
            if cv2.contourArea(c) < 500:  # 忽略小面积变动(避免误判)
                continue
            (x, y, w, h) = cv2.boundingRect(c)
            cv2.rectangle(img, (x, y), (x + w, y + h), (0, 0, 255), 2)  # 画红色矩形
        # ---------------------------------------------------------

        # 线程安全地更新当前帧
        with lock:
            frame = img.copy()

        # 控制帧率(避免CPU占用过高)
        time.sleep(1.0 / current_fps)

    # 释放资源
    cap.release()
    print("🔌 摄像头已关闭")


def generate_stream():
    """
    生成MJPEG视频流
    前端通过HTTP请求获取该流并实时播放
    """
    global frame
    while True:
        with lock:
            # 检查是否有可用帧
            if frame is None:
                time.sleep(0.1)
                continue

            # 将OpenCV的BGR格式转为JPEG格式
            ret, buffer = cv2.imencode('.jpg', frame)
            if not ret:
                continue
            frame_bytes = buffer.tobytes()  # 转为字节流

        # 按照MJPEG格式协议返回(多部分替换格式)
        yield (b'--frame\r\n'
               b'Content-Type: image/jpeg\r\n\r\n' + frame_bytes + b'\r\n')

# 获取公网IP信息
def get_ip_info():
    try:
        # 访问 ipinfo.io,默认返回本机公网 IP 信息
        response = requests.get("https://ipinfo.io/json")
        response.raise_for_status()  # 检查请求是否成功
        data = response.json()

        # 提取关键信息
        # info = {
        #     "IP 地址": data.get("ip"),
        #     "国家": data.get("country"),
        #     "地区": data.get("region"),
        #     "城市": data.get("city"),
        #     "经纬度": data.get("loc"),  # 格式:纬度,经度
        #     "运营商": data.get("org"),
        #     "时区": data.get("timezone")
        # }
        # for key, value in info.items():
        #     print(f"{key}: {value}")
        return f'{data.get("country")} {data.get("region")} {data.get("city")}'
    except requests.exceptions.RequestException as e:
        print(f"查询失败:{e}")
        return None

# -------------------------- API接口 --------------------------
@app.route('/video_feed')
def video_feed():
    """视频流接口:供前端播放"""
    return Response(
        generate_stream(),
        mimetype='multipart/x-mixed-replace; boundary=frame'
    )


@app.route('/api/start', methods=['GET'])
def start_stream():
    """启动摄像头接口"""
    global is_running
    if not is_running:
        # 启动独立线程运行摄像头捕获函数
        threading.Thread(target=capture_frames, daemon=True).start()
        return jsonify({"status": "success", "message": "摄像头启动中..."})
    return jsonify({"status": "warning", "message": "摄像头已在运行"})


@app.route('/api/stop', methods=['GET'])
def stop_stream():
    """停止摄像头接口"""
    global is_running, frame
    if is_running:
        is_running = False
        frame = None  # 清空帧缓存
        return jsonify({"status": "success", "message": "摄像头已停止"})
    return jsonify({"status": "warning", "message": "摄像头未运行"})


@app.route('/api/status', methods=['GET'])
def get_status():
    """获取当前状态接口"""
    return jsonify({
        "is_running": is_running,
        "resolution": f"{current_width}x{current_height}",
        "fps": current_fps
    })


@app.route('/api/set_resolution/<int:w>/<int:h>', methods=['GET'])
def set_resolution(w, h):
    """设置分辨率接口(需要重启摄像头生效)"""
    global current_width, current_height
    # 简单验证分辨率是否合理
    if 320 <= w <= 1920 and 240 <= h <= 1080:
        current_width = w
        current_height = h
        return jsonify({"status": "success", "message": f"分辨率已设置为 {w}x{h},请重启摄像头"})
    return jsonify({"status": "error", "message": "分辨率范围无效(320-1920 x 240-1080)"})


# --------------------------------------------------------------


if __name__ == '__main__':
    print(f"🚀 视频监控服务启动中... 端口: {SERVER_PORT}")
    # 启动Web服务(host=0.0.0.0允许局域网访问)
    app.run(host='0.0.0.0', port=SERVER_PORT, debug=False)
3. 启动Python服务
bash 复制代码
# 直接运行(测试用)
python3 video_server.py

# 后台运行(生产用)
nohup python3 video_server.py > /var/log/orangepi_camera.log 2>&1 &
4. 设置开机自启(可选)

创建系统服务文件:

bash 复制代码
sudo nano /etc/systemd/system/orangepi-camera.service

内容如下:

ini 复制代码
[Unit]
Description=Orange Pi Camera Service
After=network.target

[Service]
User=orangepi
Group=orangepi
# 直接使用虚拟环境的python路径,替代系统python
ExecStart=/home/orangepi/python3/CAMERA/camera_video_server/myenv/bin/python /home/orangepi/python3/CAMERA/camera_video_server/video_server.py
WorkingDirectory=/home/orangepi/python3/CAMERA/camera_video_server
Restart=on-failure

[Install]
WantedBy=multi-user.target

启用服务:

bash 复制代码
sudo systemctl enable orangepi-camera
sudo systemctl start orangepi-camera

三、Vue前端(客户端)

1. 环境准备
bash 复制代码
# 创建Vue项目(如果没有)
vue create camera-monitor
cd camera-monitor

# 安装依赖(axios用于HTTP请求)
npm install axios --save
2. 完整Vue组件(src/components/CameraMonitor.vue
vue 复制代码
/**
* CameraMonitor.vue Orange Pi Zero3视频监控系统
* @Author ZhangJun
* @Date  2025/11/2 16:16
**/
<template>
  <div class="camera-monitor">
    <h1>Orange Pi Zero3视频监控系统</h1>

    <!-- 视频显示区域 -->
    <div class="video-container">
      <img
          ref="videoStream"
          class="video-feed"
          :src="streamUrl"
          alt="监控画面"
          v-if="isStreaming"
      >
      <div class="video-placeholder" v-else>
        <p>{{ placeholderText }}</p>
      </div>
    </div>

    <!-- 控制按钮区域 -->
    <div class="control-panel">
      <button
          @click="handleStart"
          :disabled="isStreaming || isLoading"
          class="btn start-btn"
      >
        <span v-if="!isLoading">启动监控</span>
        <span v-if="isLoading">启动中...</span>
      </button>

      <button
          @click="handleStop"
          :disabled="!isStreaming || isLoading"
          class="btn stop-btn"
      >
        停止监控
      </button>

      <div class="resolution-setting">
        <label>分辨率:</label>
        <select v-model="selectedResolution" @change="handleResolutionChange">
          <option value="320x240">320x240 (流畅)</option>
          <option value="640x480">640x480 (平衡)</option>
          <option value="1280x720">1280x720 (清晰)</option>
        </select>
      </div>
    </div>

    <!-- 状态信息区域 -->
    <div class="status-bar">
      <p>状态:{{ statusText }}</p>
      <p>当前分辨率:{{ currentResolution }}</p>
      <p>更新时间:{{ lastUpdateTime }}</p>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted, onBeforeUnmount } from 'vue';
import axios from 'axios';

// 核心状态
const isStreaming = ref(false);
const isLoading = ref(false);
const streamUrl = ref('');
const videoStream = ref(null);

// 配置信息
const orangePiIp = ref('127.0.0.1');
const serverPort = ref(5000);

// 显示信息
const statusText = ref('未连接');
const placeholderText = ref('请点击"启动监控"按钮开始');
const currentResolution = ref('640x480');
const selectedResolution = ref('640x480');
const lastUpdateTime = ref('');

// 定时器变量
let statusInterval= null;

orangePiIp.value=import.meta.env.VITE_SERVICE_IP

// 初始化视频流地址
const getStreamUrl = () => {
  return `http://${orangePiIp.value}:${serverPort.value}/video_feed`;
};

/** 检查摄像头当前状态 */
const checkStatus = async () => {
  try {
    const res = await axios.get(`http://${orangePiIp.value}:${serverPort.value}/api/status`);
    isStreaming.value = res.data.is_running;
    currentResolution.value = res.data.resolution;
    selectedResolution.value = res.data.resolution;
    lastUpdateTime.value = new Date().toLocaleString();
    statusText.value = isStreaming.value ? '监控中' : '已停止';
  } catch (err) {
    console.error('获取状态失败:', err);
    statusText.value = '无法连接到设备';
  }
};

/** 启动监控 */
const handleStart = async () => {
  isLoading.value = true;
  statusText.value = '正在启动摄像头...';
  try {
    const res = await axios.get(`http://${orangePiIp.value}:${serverPort.value}/api/start`);
    console.log('启动响应:', res.data);

    // 延迟刷新状态
    setTimeout(() => {
      checkStatus();
      isLoading.value = false;
    }, 1000);
  } catch (err) {
    console.error('启动失败:', err);
    statusText.value = '启动失败,请检查设备';
    isLoading.value = false;
  }
};

/** 停止监控 */
const handleStop = async () => {
  try {
    const res = await axios.get(`http://${orangePiIp.value}:${serverPort.value}/api/stop`);
    console.log('停止响应:', res.data);
    await checkStatus();
  } catch (err) {
    console.error('停止失败:', err);
    statusText.value = '停止失败';
  }
};

/** 切换分辨率 */
const handleResolutionChange = async () => {
  if (!selectedResolution.value) return;

  const [w, h] = selectedResolution.value.split('x').map(Number);
  try {
    const res = await axios.get(
        `http://${orangePiIp.value}:${serverPort.value}/api/set_resolution/${w}/${h}`
    );
    alert(res.data.message);

    // 如果正在运行,重启生效
    if (isStreaming.value) {
      await handleStop();
      setTimeout(() => handleStart(), 1000);
    }
  } catch (err) {
    console.error('设置分辨率失败:', err);
    alert('设置分辨率失败');
  }
};

// 生命周期钩子
onMounted(() => {
  streamUrl.value = getStreamUrl();
  checkStatus();
  statusInterval = setInterval(() => checkStatus(), 5000);
});

onBeforeUnmount(() => {
  if (statusInterval) {
    clearInterval(statusInterval);
  }
});
</script>

<style scoped>
/* 样式部分保持不变 */
.camera-monitor {
  max-width: 1200px;
  margin: 0 auto;
  padding: 20px;
  font-family: Arial, sans-serif;
}

h1 {
  color: #333;
  text-align: center;
  margin-bottom: 30px;
}

.video-container {
  width: 100%;
  background: #000;
  border-radius: 8px;
  overflow: hidden;
  min-height: 400px;
  position: relative;
}

.video-feed {
  width: 100%;
  height: auto;
  max-height: 800px;
}

.video-placeholder {
  width: 100%;
  height: 400px;
  display: flex;
  align-items: center;
  justify-content: center;
  color: #999;
}

.control-panel {
  margin-top: 20px;
  display: flex;
  flex-wrap: wrap;
  gap: 15px;
  align-items: center;
}

.btn {
  padding: 10px 20px;
  border: none;
  border-radius: 4px;
  cursor: pointer;
  font-size: 16px;
  transition: opacity 0.3s;
}

.btn:disabled {
  opacity: 0.6;
  cursor: not-allowed;
}

.start-btn {
  background: #4CAF50;
  color: white;
}

.stop-btn {
  background: #f44336;
  color: white;
}

.resolution-setting {
  margin-left: auto;
  display: flex;
  align-items: center;
  gap: 10px;
}

select {
  padding: 8px;
  border-radius: 4px;
  border: 1px solid #ddd;
}

.status-bar {
  margin-top: 20px;
  padding: 15px;
  background: #f5f5f5;
  border-radius: 4px;
  color: #666;
  font-size: 14px;
  display: flex;
  flex-wrap: wrap;
  gap: 20px;
}
</style>
3. 使用组件(src/App.vue
vue 复制代码
<template>
  <div id="app">
    <CameraMonitor />
  </div>
</template>

<script>
import CameraMonitor from './components/CameraMonitor.vue';

export default {
  name: 'App',
  components: {
    CameraMonitor
  }
};
</script>

<style>
#app {
  font-family: Avenir, Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  text-align: center;
  color: #2c3e50;
  margin-top: 30px;
}
</style>
4. 运行前端
bash 复制代码
# 开发环境运行
npm run serve

# 构建生产版本(可选)
npm run build

四、使用说明

  1. 配置修改

    • Python端:根据摄像头情况调整CAMERA_INDEX(通常为0)
    • Vue端:在CameraMonitor.vue中修改orangePiIp为你的Orange Pi局域网IP
  2. 启动流程

    • 先启动Orange Pi上的Python服务
    • 再启动Vue前端,访问前端页面(默认http://localhost:8080
    • 点击"启动监控"按钮开始显示画面
  3. 功能说明

    • 实时显示摄像头画面,带时间水印
    • 支持运动检测(自动框选移动目标)
    • 可切换分辨率(需重启生效)
    • 显示当前状态和设备信息

五、常见问题解决

  1. 摄像头无法启动

    • 检查设备是否存在:ls /dev/video*
    • 安装摄像头驱动(部分USB摄像头可能需要)
  2. 前端无法连接

    • 确认Orange Pi和前端设备在同一局域网
    • 检查防火墙:sudo ufw allow 5000
    • 替换正确的IP地址
  3. 画面卡顿

    • 降低分辨率或帧率
    • 检查网络稳定性(建议使用有线连接)

通过以上方案,你可以实现一个功能完整、可交互的视频监控系统,且代码结构清晰,便于后续扩展功能(如录像、截图、远程控制等)。

相关推荐
青瓷程序设计18 小时前
花朵识别系统【最新版】Python+TensorFlow+Vue3+Django+人工智能+深度学习+卷积神经网络算法
人工智能·python·深度学习
hyswl66618 小时前
2025年郑州开发小程序公司推荐
python·小程序
B站计算机毕业设计之家18 小时前
基于Python音乐推荐系统 数据分析可视化 协同过滤推荐算法 大数据(全套源码+文档)建议收藏✅
python·数据分析·推荐算法
code_Bo18 小时前
Ant Design Vue 日期选择器英文不变更中文问题
前端·vue.js·ant design
U***e6319 小时前
Vue自然语言
前端·javascript·vue.js
用户7851278147019 小时前
实战解析:淘宝/天猫商品描述API(taobao.item_get_desc)接口
python
codists19 小时前
Pycharm错误:JetBrains AI URL resolution failure
python
青瓷程序设计19 小时前
鱼类识别系统【最新版】Python+TensorFlow+Vue3+Django+人工智能+深度学习+卷积神经网络算法
人工智能·python·深度学习
c***979819 小时前
Vue性能优化实战
前端·javascript·vue.js
该用户已不存在19 小时前
Python正在死去,2026年Python还值得学吗?
后端·python