基于电脑 BIOS SN 的智能摄像头管理工具
一款面向多设备管理场景的桌面端摄像头控制工具,通过电脑主板 BIOS 序列号(SN)作为唯一标识, 自动匹配并打开对应摄像头,解决多台电脑、多个摄像头环境下设备识别混乱的难题。 工具采用 PyQt5 构建现代化图形界面,搭配 OpenCV 实现高效视频采集与实时预览, 配置信息持久化为 JSON 文件,支持一次配置、长期使用,极大提升了批量设备运维的效率与准确性。
目录
功能特性
| |
|---|
| |
| SN 与摄像头索引的映射关系保存为 JSON 文件 |
| |
| |
| |
| |
环境要求
Python >= 3.8opencv-python >= 4.5PyQt5 >= 5.15Windows 操作系统(依赖 WMIC 命令)
快速开始
1. 安装依赖
pip install opencv-python PyQt5
2. 启动程序
python camera_by_sn.py
3. 使用流程
┌─────────────────────────────────────────────────────┐│ 启动程序 → 自动显示本机 SN ││ ↓ ││ 点击「扫描所有摄像头」→ 确认可用摄像头索引 ││ ↓ ││ 填写 SN + 选择摄像头索引 → 点击「保存配置」 ││ ↓ ││ 输入 SN → 点击「打开摄像头」→ 实时预览画面 │└─────────────────────────────────────────────────────┘
核心代码解析
Part 1:设备标识获取模块
本模块负责获取计算机的唯一硬件标识——BIOS 序列号,作为设备区分的核心依据。
import subprocessdefget_local_bios_sn():"""获取当前电脑的 BIOS 序列号"""try: result = subprocess.run( ['wmic', 'bios', 'get', 'serialnumber'], capture_output=True, text=True, timeout=10 ) lines = [l.strip() for l in result.stdout.strip().split('\n') if l.strip()]# 第一行是表头 "SerialNumber",第二行是实际序列号值if len(lines) >= 2:return lines[1]except Exception:passreturn""
技术要点:
- 通过
subprocess.run() 调用 Windows 系统命令 wmic bios get serialnumber - 设置
timeout=10 防止命令挂起阻塞主线程 capture_output=True 捕获标准输出,text=True 以字符串形式返回- 解析输出时跳过表头行,提取第二行作为实际 SN 值
配置文件读写模块:
import osimport jsonCONFIG_FILE = os.path.join(os.path.dirname(os.path.abspath(__file__)), "camera_config.json")defload_config():if os.path.exists(CONFIG_FILE):with open(CONFIG_FILE, "r", encoding="utf-8") as f:return json.load(f)return {}defsave_config(config):with open(CONFIG_FILE, "w", encoding="utf-8") as f: json.dump(config, f, indent=2, ensure_ascii=False)
- 配置文件路径基于脚本所在目录动态计算,避免工作目录差异带来的路径问题
- 使用
ensure_ascii=False 确保中文字符正常写入
Part 2:GUI 界面与交互逻辑
界面基于 PyQt5 构建,采用垂直布局嵌套水平布局的经典组合模式。
from PyQt5.QtWidgets import ( QApplication, QWidget, QVBoxLayout, QHBoxLayout, QLabel, QLineEdit, QPushButton, QSpinBox, QMessageBox)from PyQt5.QtCore import QTimer, QtclassCameraApp(QWidget):def__init__(self): super().__init__() self.setWindowTitle("摄像头 SN 配置工具") self.resize(860, 620) self.cap = None self.timer = QTimer() self.timer.timeout.connect(self.update_frame) self.config = load_config() self.init_ui()
界面结构层次:
QVBoxLayout (主布局)├── QHBoxLayout (SN 显示区)│ ├── QLabel "当前电脑 SN:"│ └── QLabel [动态显示本机 SN]├── QHBoxLayout (配置输入区)│ ├── QLabel + QLineEdit (SN 输入)│ ├── QLabel + QSpinBox (摄像头索引)│ └── QPushButton "保存配置"├── QHBoxLayout (操作区)│ ├── QLineEdit (SN 输入)│ ├── QPushButton "打开摄像头"│ ├── QPushButton "关闭摄像头"│ └── QPushButton "扫描所有摄像头"├── QLabel (视频画面显示区 640×360)└── QLabel (状态栏)
配置保存逻辑:
defsave_config_entry(self): sn = self.sn_input.text().strip()ifnot sn: QMessageBox.warning(self, "提示", "请输入电脑 SN")return cam_idx = self.cam_index_spin.value() self.config[sn] = cam_idx save_config(self.config) self.status_label.setText(f"已保存: SN={sn} → 摄像头索引={cam_idx}")
- 内存与持久化双写:同时更新运行时字典和磁盘 JSON 文件
Part 3:摄像头控制与视频流渲染
本模块包含摄像头的打开、关闭、扫描和实时视频帧渲染的全部逻辑。
打开摄像头(含预热机制):
import cv2from PyQt5.QtGui import QImage, QPixmapdefopen_camera(self): sn = self.open_sn_input.text().strip()ifnot sn: self.status_label.setText("请输入 SN")returnif sn notin self.config: self.status_label.setText(f"配置中未找到 SN: {sn},请先保存配置")return cam_idx = self.config[sn] self.stop_camera() self.cap = cv2.VideoCapture(cam_idx, cv2.CAP_DSHOW)if self.cap.isOpened():# 跳过前 10 帧(摄像头预热,避免黑屏)for _ in range(10): self.cap.read() self.status_label.setText(f"已打开摄像头 (SN: {sn}, 索引: {cam_idx})") self.timer.start(30) # 约 33fps 刷新else: self.status_label.setText(f"摄像头索引 {cam_idx} 打开失败,请确认设备可用或更换索引") self.cap = None
视频帧渲染(BGR → RGB → QPixmap):
defupdate_frame(self):if self.cap and self.cap.isOpened(): ret, frame = self.cap.read()if ret:# OpenCV 默认 BGR 格式,需转为 RGB 供 Qt 显示 frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) h, w, ch = frame.shape img = QImage(frame.data, w, h, ch * w, QImage.Format_RGB888) scaled = QPixmap.fromImage(img).scaled( self.video_label.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation ) self.video_label.setPixmap(scaled)
摄像头扫描(亮度检测):
defscan_cameras(self):"""扫描索引 0~9,逐个尝试打开并读取一帧,显示哪些有画面""" self.stop_camera() self.status_label.setText("正在扫描摄像头...") QApplication.processEvents() results = []for i in range(10): cap = cv2.VideoCapture(i, cv2.CAP_DSHOW)if cap.isOpened():for _ in range(5): cap.read() ret, frame = cap.read()if ret: gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) brightness = gray.mean() status = f"有画面 (亮度:{brightness:.0f})"if brightness > 5else"黑屏" results.append(f"索引 {i}: 可打开 - {status}")else: results.append(f"索引 {i}: 可打开 - 无法读取帧") cap.release()else: results.append(f"索引 {i}: 无法打开") QMessageBox.information(self, "摄像头扫描结果", "\n".join(results))
技术要点:
cv2.CAP_DSHOW:指定 DirectShow 后端,Windows 上性能和兼容性最佳- 预热跳帧:许多 USB 摄像头前几帧为黑色或曝光异常,跳过可提升用户体验
- QTimer 驱动渲染:以 30ms 间隔触发帧读取,避免阻塞 GUI 主线程
- 亮度检测:将帧转为灰度图后计算平均像素值,阈值 5 以下判定为黑屏
Qt.KeepAspectRatio:保持画面宽高比,Qt.SmoothTransformation 保证缩放画质
配置文件说明
程序运行后会在同目录下生成 camera_config.json:
{"5CD5145Y25": 0,"ABC1234567": 1,"XYZ9876543": 0}
知识点总结
1. Windows 硬件信息获取
| | |
|---|
| wmic bios get serialnumber | |
| wmic csproduct get uuid | |
| Get-PnpDevice -Class Camera | |
注:WMIC 在较新版本 Windows 中已标记为弃用,后续可迁移至 Get-CimInstance PowerShell 命令。
2. OpenCV 视频采集核心概念
- VideoCapture:视频捕获对象,支持索引(设备)和文件路径两种打开方式
- 后端选择:
CAP_DSHOW(DirectShow)、CAP_MSMF(Media Foundation)、CAP_V4L2(Linux) - 帧格式:OpenCV 默认使用 BGR 色彩空间,显示时需转为 RGB
- 资源释放:
cap.release() 释放设备独占,避免其他程序无法访问
3. PyQt5 GUI 开发模式
- 信号与槽机制:
button.clicked.connect(handler) 事件驱动编程 - QTimer 定时器:非阻塞的周期性任务调度,适合视频帧刷新
- 布局管理:
QVBoxLayout / QHBoxLayout 组合嵌套实现响应式界面 - 线程安全:GUI 更新必须在主线程执行,
QApplication.processEvents() 强制刷新
4. JSON 配置管理
json.dump() / json.load() 一行代码完成序列化与反序列化
拓展场景与测试步骤
拓展应用场景
| |
|---|
| 每台工位绑定 SN,启动即自动打开对应工业相机进行产品检测 |
| 机房多台主机统一配置,快速定位并查看指定主机的摄像头画面 |
| 诊室设备通过 SN 绑定,医护人员一键调取指定设备视频 |
| |
| |
测试步骤
Step 1:环境验证
# 确认 Python 版本python --version# 确认依赖安装pip show opencv-python PyQt5
Step 2:SN 获取验证
# 手动执行确认能获取到 SNwmic bios get serialnumber
Step 3:启动程序并验证界面
python camera_by_sn.py
检查项:
Step 4:摄像头扫描测试
Step 5:配置保存测试
- [ ] 检查同目录下生成
camera_config.json
Step 6:摄像头打开测试
Step 7:异常场景测试
📝 本工具为本地化方案,如需跨网络远程控制摄像头,请参考 camera_server.py + camera_client.py 的 C/S 架构实现。
"""根据电脑 BIOS SN 打开指定摄像头的 GUI 工具配置文件 camera_config.json 记录 SN 与摄像头索引的对应关系依赖: pip install opencv-python PyQt5"""import sysimport osimport jsonimport subprocessimport cv2from PyQt5.QtWidgets import (QApplication, QWidget, QVBoxLayout, QHBoxLayout,QLabel, QLineEdit, QPushButton, QSpinBox, QMessageBox)from PyQt5.QtGui import QImage, QPixmapfrom PyQt5.QtCore import QTimer, QtCONFIG_FILE = os.path.join(os.path.dirname(os.path.abspath(file)), "camera_config.json")def load_config():if os.path.exists(CONFIG_FILE):with open(CONFIG_FILE, "r", encoding="utf-8") as f:return json.load(f)return {}def save_config(config):with open(CONFIG_FILE, "w", encoding="utf-8") as f:json.dump(config, f, indent=2, ensure_ascii=False)def get_local_bios_sn():"""获取当前电脑的 BIOS 序列号"""try:result = subprocess.run(['wmic', 'bios', 'get', 'serialnumber'],capture_output=True, text=True, timeout=10)lines = [l.strip() for l in result.stdout.strip().split('\n') if l.strip()]# 第一行是 "SerialNumber",第二行是实际值if len(lines) >= 2:return lines[1]except Exception:passreturn ""class CameraApp(QWidget):def init(self):super().init()self.setWindowTitle("摄像头 SN 配置工具")self.resize(860, 620) self.cap = None self.timer = QTimer() self.timer.timeout.connect(self.update_frame) self.config = load_config() self.init_ui()def init_ui(self): layout = QVBoxLayout() # 当前电脑 SN 显示 local_sn = get_local_bios_sn() sn_display = QHBoxLayout() sn_display.addWidget(QLabel("当前电脑 SN:")) self.local_sn_label = QLabel(local_sn if local_sn else "获取失败") self.local_sn_label.setStyleSheet("font-weight: bold;") sn_display.addWidget(self.local_sn_label) sn_display.addStretch() layout.addLayout(sn_display) # 配置区域:SN + 摄像头索引 + 保存 config_layout = QHBoxLayout() config_layout.addWidget(QLabel("电脑 SN:")) self.sn_input = QLineEdit() self.sn_input.setPlaceholderText("输入目标电脑的 BIOS SN") self.sn_input.setText(local_sn) config_layout.addWidget(self.sn_input) config_layout.addWidget(QLabel("摄像头索引:")) self.cam_index_spin = QSpinBox() self.cam_index_spin.setRange(0, 10) config_layout.addWidget(self.cam_index_spin) self.btn_save = QPushButton("保存配置") self.btn_save.clicked.connect(self.save_config_entry) config_layout.addWidget(self.btn_save) layout.addLayout(config_layout) # 打开摄像头区域 open_layout = QHBoxLayout() open_layout.addWidget(QLabel("输入 SN 打开摄像头:")) self.open_sn_input = QLineEdit() self.open_sn_input.setPlaceholderText("输入 SN,自动匹配配置中的摄像头索引") self.open_sn_input.setText(local_sn) open_layout.addWidget(self.open_sn_input) self.btn_open = QPushButton("打开摄像头") self.btn_open.clicked.connect(self.open_camera) open_layout.addWidget(self.btn_open) self.btn_stop = QPushButton("关闭摄像头") self.btn_stop.clicked.connect(self.stop_camera) open_layout.addWidget(self.btn_stop) self.btn_scan = QPushButton("扫描所有摄像头") self.btn_scan.clicked.connect(self.scan_cameras) open_layout.addWidget(self.btn_scan) layout.addLayout(open_layout) # 视频画面 self.video_label = QLabel("摄像头画面") self.video_label.setMinimumSize(640, 360) self.video_label.setStyleSheet("background-color: #222; color: white;") self.video_label.setAlignment(Qt.AlignCenter) layout.addWidget(self.video_label) # 状态栏 self.status_label = QLabel("就绪") layout.addWidget(self.status_label) self.setLayout(layout)def save_config_entry(self): sn = self.sn_input.text().strip() if not sn: QMessageBox.warning(self, "提示", "请输入电脑 SN") return cam_idx = self.cam_index_spin.value() self.config[sn] = cam_idx save_config(self.config) self.status_label.setText(f"已保存: SN={sn} → 摄像头索引={cam_idx}")def open_camera(self): sn = self.open_sn_input.text().strip() if not sn: self.status_label.setText("请输入 SN") return if sn not in self.config: self.status_label.setText(f"配置中未找到 SN: {sn},请先保存配置") return cam_idx = self.config[sn] self.stop_camera() self.cap = cv2.VideoCapture(cam_idx, cv2.CAP_DSHOW) if self.cap.isOpened(): # 跳过前 10 帧(摄像头预热,避免黑屏) for _ in range(10): self.cap.read() self.status_label.setText(f"已打开摄像头 (SN: {sn}, 索引: {cam_idx})") self.timer.start(30) else: self.status_label.setText(f"摄像头索引 {cam_idx} 打开失败,请确认设备可用或更换索引") self.cap = Nonedef scan_cameras(self): """扫描索引 0~9,逐个尝试打开并读取一帧,显示哪些有画面""" self.stop_camera() self.status_label.setText("正在扫描摄像头...") QApplication.processEvents() results = [] for i in range(10): cap = cv2.VideoCapture(i, cv2.CAP_DSHOW) if cap.isOpened(): # 跳过几帧预热 for _ in range(5): cap.read() ret, frame = cap.read() if ret: # 判断是否为黑屏(平均亮度极低) gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY) brightness = gray.mean() status = f"有画面 (亮度:{brightness:.0f})" if brightness > 5 else "黑屏" results.append(f"索引 {i}: 可打开 - {status}") else: results.append(f"索引 {i}: 可打开 - 无法读取帧") cap.release() else: results.append(f"索引 {i}: 无法打开") info = "\n".join(results) QMessageBox.information(self, "摄像头扫描结果", info) self.status_label.setText("扫描完成,请根据结果选择正确的摄像头索引")def stop_camera(self): self.timer.stop() if self.cap: self.cap.release() self.cap = None self.video_label.clear() self.video_label.setText("摄像头画面")def update_frame(self): if self.cap and self.cap.isOpened(): ret, frame = self.cap.read() if ret: frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB) h, w, ch = frame.shape img = QImage(frame.data, w, h, ch * w, QImage.Format_RGB888) scaled = QPixmap.fromImage(img).scaled( self.video_label.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation ) self.video_label.setPixmap(scaled)def closeEvent(self, event): self.stop_camera() event.accept()if name == 'main':app = QApplication(sys.argv)window = CameraApp()window.show()sys.exit(app.exec_())