用Python讓電腦攝像頭實現掃二維碼
itomcoil 2025-05-24 14:42 2 浏览
import sys # 系統模組,用來存取命令列參數與系統功能
import cv2 # OpenCV,處理影像與相機操作
import numpy as np # Numpy,用來處理數值與陣列
from pyzbar import pyzbar # pyzbar 模組,用來解碼 QR Code 與條碼
# 匯入 PyQt6 所需的元件
from PyQt6.QtWidgets import (
QApplication, QWidget, QVBoxLayout, QLabel,
QPushButton, QTextEdit, QHBoxLayout, QFrame,
QFileDialog, QSpacerItem, QSizePolicy, QSlider
)
from PyQt6.QtGui import QImage, QPixmap, QPainter, QPen # 圖像顯示與繪圖工具
from PyQt6.QtCore import Qt, QTimer, QRect # 基本型別與計時器、矩形框
from PIL import Image, ImageDraw, ImageFont # PIL 用於繪製 QR Code 邊框與文字
# 主應用程式類別:QRCodeScannerApp,繼承 QWidget
class QRCodeScannerApp(QWidget):
def __init__(self):
super().__init__() # 初始化父類別
self.setWindowTitle("二維碼掃描器") # 設定視窗標題
self.setGeometry(500, 100, 705, 750) # 設定視窗位置與大小
# 初始化攝影機與其他功能的變數
self.camera_index = 0 # 攝影機編號(預設為 0)
self.capture = None # OpenCV 攝影機物件
self.timer = QTimer(self) # 建立 PyQt 的計時器
self.timer.timeout.connect(self.update_frame) # 設定定時器觸發時執行 update_frame()
self.logged_codes = set() # 儲存已辨識的 QR code(避免重複顯示)
self.brightness = 0 # 初始亮度值
self.contrast = 1.0 # 初始對比度值
self.manual_image = None # 手動選擇的圖片
self.manual_image_decoded = False # 是否已經辨識過手動圖片
# 框選辨識的相關參數
self.selecting = False # 是否正在框選
self.selection_start = None # 框選起始點
self.selection_end = None # 框選結束點
self.selection_rect = None # 框選矩形
# 新增反色功能相關變數
self.invert_enabled = False
# 新增縮放相關變數
self.zoom_factor = 1.0
self.min_zoom = 0.1
self.max_zoom = 5.0
self.init_ui() # 建立使用者介面
# 建立整個 UI 介面
def init_ui(self):
layout = QVBoxLayout() # 主垂直排列佈局
# 顯示標題文字
title_label = QLabel("<h2>二維碼掃描器</h2>")
title_label.setAlignment(Qt.AlignmentFlag.AlignCenter)
layout.addWidget(title_label)
# 用於顯示相機畫面的 QLabel
self.image_label = QLabel()
self.image_label.setFixedSize(640, 480) # 固定大小
self.image_label.setStyleSheet("background-color: black;") # 初始黑底
layout.addWidget(self.image_label)
# 按鈕群組
button_layout = QHBoxLayout()
self.start_button = QPushButton("開始掃描") # 開始按鈕
self.start_button.clicked.connect(self.start_scanning) # 綁定點擊事件
self.stop_button = QPushButton("停止掃描") # 停止按鈕
self.stop_button.clicked.connect(self.stop_scanning)
self.stop_button.setEnabled(False) # 初始為不可按
self.clear_log_button = QPushButton("清除日誌") # 清除日誌
self.clear_log_button.clicked.connect(self.clear_log)
self.manual_image_button = QPushButton("手動選擇圖片") # 手動載入圖片
self.manual_image_button.clicked.connect(self.select_image)
self.manual_select_button = QPushButton("框選區域識別") # 啟動框選辨識
self.manual_select_button.clicked.connect(self.enable_manual_selection)
# 新增反色按鈕
self.invert_button = QPushButton("反色")
self.invert_button.clicked.connect(self.toggle_invert)
button_layout.addWidget(self.invert_button)
# 將所有按鈕加入水平排版
button_layout.addWidget(self.start_button)
button_layout.addWidget(self.stop_button)
button_layout.addWidget(self.clear_log_button)
button_layout.addWidget(self.manual_image_button)
button_layout.addWidget(self.manual_select_button)
layout.addLayout(button_layout)
# 亮度調整滑桿
brightness_layout = QHBoxLayout()
brightness_label = QLabel("亮度")
self.brightness_slider = QSlider(Qt.Orientation.Horizontal)
self.brightness_slider.setRange(-100, 100) # 範圍 -100 ~ 100
self.brightness_slider.setValue(0)
self.brightness_slider.valueChanged.connect(self.update_brightness)
brightness_layout.addWidget(brightness_label)
brightness_layout.addWidget(self.brightness_slider)
layout.addLayout(brightness_layout)
# 對比度調整滑桿
contrast_layout = QHBoxLayout()
contrast_label = QLabel("對比度")
self.contrast_slider = QSlider(Qt.Orientation.Horizontal)
self.contrast_slider.setRange(10, 300) # 10% 到 300%
self.contrast_slider.setValue(100)
self.contrast_slider.valueChanged.connect(self.update_contrast)
contrast_layout.addWidget(contrast_label)
contrast_layout.addWidget(self.contrast_slider)
layout.addLayout(contrast_layout)
# 日誌區域(顯示掃描結果)
layout.addWidget(QLabel("<b>掃描日誌:</b>"))
self.log_text = QTextEdit()
self.log_text.setReadOnly(True)
layout.addWidget(self.log_text)
# 增加空白區域讓排版更好看
spacer = QSpacerItem(20, 40, QSizePolicy.Policy.Minimum, QSizePolicy.Policy.Expanding)
layout.addItem(spacer)
# 加入邊框樣式的外框
main_frame = QFrame()
main_frame.setLayout(layout)
main_frame.setStyleSheet("border: 2px solid #4CAF50; border-radius: 5px; padding: 10px;")
# 加到主視窗佈局中
central_layout = QVBoxLayout(self)
central_layout.addWidget(main_frame)
self.setLayout(central_layout)
# 啟動攝影機掃描
def start_scanning(self):
self.manual_image = None # 清除手動載入的圖片
self.capture = cv2.VideoCapture(self.camera_index) # 開啟攝影機
if not self.capture.isOpened(): # 無法打開則顯示錯誤
self.log_message("無法開啟相機")
return
self.timer.start(30) # 啟動計時器,每 30ms 更新一張畫面
self.start_button.setEnabled(False)
self.stop_button.setEnabled(True)
self.log_message("開始掃描...")
# 停止攝影機與計時器
def stop_scanning(self):
if self.capture and self.capture.isOpened():
self.capture.release() # 釋放攝影機資源
self.timer.stop()
self.image_label.clear() # 清除畫面
self.image_label.setStyleSheet("background-color: black;")
self.start_button.setEnabled(True)
self.stop_button.setEnabled(False)
self.log_message("停止掃描")
# 每次定時器觸發時執行,更新畫面
def update_frame(self):
if self.capture and self.capture.isOpened():
ret, frame = self.capture.read()
if ret:
adjusted_frame = self.adjust_brightness_contrast(frame)
self.decode_qr_codes(adjusted_frame)
elif self.manual_image is not None and not self.manual_image_decoded:
adjusted_image = self.adjust_brightness_contrast(self.manual_image)
self.decode_qr_codes(adjusted_image)
elif self.manual_image is not None:
adjusted_image = self.adjust_brightness_contrast(self.manual_image)
rgb_image = cv2.cvtColor(adjusted_image, cv2.COLOR_BGR2RGB)
self.display_image(rgb_image)
# 依據滑桿設定亮度與對比度,並考慮反色
def adjust_brightness_contrast(self, frame):
frame = cv2.convertScaleAbs(frame, alpha=self.contrast, beta=self.brightness)
if self.invert_enabled:
frame = cv2.bitwise_not(frame)
return frame
# 解碼 QR Code 並畫上邊框與結果
def decode_qr_codes(self, frame):
gray = cv2.cvtColor(frame, cv2.COLOR_BGR2GRAY)
barcodes = pyzbar.decode(gray)
if not barcodes:
rgb_frame = cv2.cvtColor(frame, cv2.COLOR_BGR2RGB)
self.display_image(rgb_frame)
return
self.manual_image_decoded = True
img_pil = Image.fromarray(cv2.cvtColor(frame, cv2.COLOR_BGR2RGB))
draw = ImageDraw.Draw(img_pil)
try:
font = ImageFont.truetype("arial.ttf", 20)
except IOError:
font = ImageFont.load_default()
for barcode in barcodes:
barcode_data = barcode.data.decode("utf-8")
barcode_type = barcode.type
# 加入日誌(避免重複顯示)
if barcode_data not in self.logged_codes:
self.log_message(f"掃描到條碼: {barcode_data} ({barcode_type})")
self.logged_codes.add(barcode_data)
# 使用 polygon 畫出旋轉後的 QR code 邊框
points = barcode.polygon
if len(points) > 4:
hull = cv2.convexHull(np.array([(p.x, p.y) for p in points])).squeeze()
polygon = [(int(x), int(y)) for [x, y] in hull]
else:
polygon = [(point.x, point.y) for point in points]
if len(polygon) > 1:
draw.line(polygon + [polygon[0]], fill=(255, 0, 0), width=3)
# 顯示 QR code 資料文字在左上角或第一個點
if polygon:
text_position = (polygon[0][0], polygon[0][1] - 25)
draw.text(text_position, barcode_data, font=font, fill=(0, 255, 0))
result_frame = cv2.cvtColor(np.asarray(img_pil), cv2.COLOR_RGB2BGR)
rgb_frame = cv2.cvtColor(result_frame, cv2.COLOR_BGR2RGB)
self.display_image(rgb_frame)
# 顯示影像於 image_label
def display_image(self, rgb_frame):
# 應用縮放
if self.zoom_factor != 1.0:
h, w = rgb_frame.shape[:2]
new_h = int(h * self.zoom_factor)
new_w = int(w * self.zoom_factor)
rgb_frame = cv2.resize(rgb_frame, (new_w, new_h), interpolation=cv2.INTER_LINEAR)
self.current_display_image = rgb_frame.copy()
h, w, ch = rgb_frame.shape
bytes_per_line = ch * w
qt_image = QImage(rgb_frame.data, w, h, bytes_per_line, QImage.Format.Format_RGB888)
pixmap = QPixmap.fromImage(qt_image)
# 若有框選,畫出紅框
if self.selection_rect:
painter = QPainter(pixmap)
pen = QPen(Qt.GlobalColor.red, 2, Qt.PenStyle.DashLine)
painter.setPen(pen)
painter.drawRect(self.selection_rect)
painter.end()
scaled_pixmap = pixmap.scaled(self.image_label.size(), Qt.AspectRatioMode.KeepAspectRatio)
self.image_label.setPixmap(scaled_pixmap)
# 顯示訊息到日誌
def log_message(self, message):
self.log_text.append(message)
self.log_text.ensureCursorVisible()
# 清除日誌內容並重置已辨識條碼集合
def clear_log(self):
self.log_text.clear()
self.logged_codes.clear()
# 當視窗關閉時,自動釋放攝影機資源
def closeEvent(self, event):
self.stop_scanning() # 停止掃描與釋放攝影機
event.accept() # 接受關閉事件
# 讓使用者手動選擇一張圖片
def select_image(self):
self.stop_scanning() # 若正在掃描則先停止
file_dialog = QFileDialog(self)
file_dialog.setNameFilter("Image Files (*.png *.jpg *.bmp)") # 限定檔案類型
file_dialog.setFileMode(QFileDialog.FileMode.ExistingFile)
if file_dialog.exec(): # 若選擇成功
image_path = file_dialog.selectedFiles()[0]
self.load_manual_image(image_path)
# 載入並顯示手動選擇的圖片
def load_manual_image(self, image_path):
image = cv2.imread(image_path) # 使用 OpenCV 讀取圖片
if image is None: # 圖片載入失敗
self.log_message("無法打開圖片")
self.manual_image = None
self.image_label.clear()
self.image_label.setStyleSheet("background-color: black;")
return
# 成功載入
self.manual_image = image
self.manual_image_decoded = False # 尚未辨識
self.selection_rect = None # 清除框選區域
self.zoom_factor = 1.0 # 重置縮放比例
adjusted_image = self.adjust_brightness_contrast(self.manual_image)
rgb_image = cv2.cvtColor(adjusted_image, cv2.COLOR_BGR2RGB)
self.display_image(rgb_image)
self.log_message("已載入手動選擇的圖片")
# 使用者調整亮度滑桿時觸發
def update_brightness(self, value):
self.brightness = value
self.refresh_manual_image() # 重新顯示圖片
# 使用者調整對比度滑桿時觸發
def update_contrast(self, value):
self.contrast = value / 100.0 # 轉換為 0.1 ~ 3.0 間的比例
self.refresh_manual_image()
# 重新顯示已載入的手動圖片(依據亮度/對比度)
def refresh_manual_image(self):
if self.capture and self.capture.isOpened():
return # 若攝影機仍開啟,不處理
elif self.manual_image is not None and not self.manual_image_decoded:
adjusted_image = self.adjust_brightness_contrast(self.manual_image)
self.decode_qr_codes(adjusted_image)
elif self.manual_image is not None:
adjusted_image = self.adjust_brightness_contrast(self.manual_image)
rgb_image = cv2.cvtColor(adjusted_image, cv2.COLOR_BGR2RGB)
self.display_image(rgb_image)
# 啟用框選辨識模式
def enable_manual_selection(self):
if self.manual_image is not None:
self.selecting = True
self.selection_rect = None
self.log_message("請用滑鼠框選區域進行識別 (按 Esc 可取消)")
# 滑鼠按下事件 - 開始框選
def mousePressEvent(self, event):
if self.selecting and event.button() == Qt.MouseButton.LeftButton:
self.selection_start = event.position().toPoint() # 記錄起點
# 滑鼠移動事件 - 更新選取框
def mouseMoveEvent(self, event):
if self.selecting and self.selection_start:
self.selection_end = event.position().toPoint() # 記錄終點
self.selection_rect = QRect(self.selection_start, self.selection_end) # 建立矩形
self.display_image(self.current_display_image) # 顯示帶框的畫面
# 滑鼠放開事件 - 完成框選並執行辨識
def mouseReleaseEvent(self, event):
if self.selecting and event.button() == Qt.MouseButton.LeftButton and self.selection_rect:
self.process_selected_region() # 處理選取區域
self.selection_start = None
self.selection_end = None
self.selection_rect = None # 清除框選框(保持模式)
self.display_image(self.current_display_image)
# 按下鍵盤 Esc 取消框選模式
def keyPressEvent(self, event):
if event.key() == Qt.Key.Key_Escape:
self.selecting = False
self.selection_rect = None
self.display_image(self.current_display_image)
self.log_message("已取消框選模式")
# 處理框選的影像區域
def process_selected_region(self):
if not self.selection_rect or self.manual_image is None:
return
# 計算實際圖片與 QLabel 的比例
label_size = self.image_label.size()
img_h, img_w, _ = self.manual_image.shape
scale_x = img_w / self.image_label.width()
scale_y = img_h / self.image_label.height()
# 將框選的畫面轉換為對應圖片中的區域
x = int(self.selection_rect.x() * scale_x)
y = int(self.selection_rect.y() * scale_y)
w = int(self.selection_rect.width() * scale_x)
h = int(self.selection_rect.height() * scale_y)
roi = self.manual_image[y:y + h, x:x + w] # 擷取 ROI 區域
if roi.size == 0:
self.log_message("選取區域無效")
return
# 框選區域放大
self.zoom_factor = min(self.max_zoom, max(self.min_zoom, 2.0)) # 固定放大2倍
adjusted_roi = self.adjust_brightness_contrast(roi)
self.decode_qr_codes(adjusted_roi)
# 切換反色功能
def toggle_invert(self):
self.invert_enabled = not self.invert_enabled
if self.invert_enabled:
self.log_message("反色功能已開啟")
else:
self.log_message("反色功能已關閉")
self.refresh_manual_image()
# 鼠標滾輪事件 - 放大縮小圖像
def wheelEvent(self, event):
if self.manual_image is not None:
delta = event.angleDelta().y()
if delta > 0:
self.zoom_factor = min(self.max_zoom, self.zoom_factor * 1.1)
else:
self.zoom_factor = max(self.min_zoom, self.zoom_factor / 1.1)
self.refresh_manual_image()
# 主程式進入點,啟動應用程式
if __name__ == '__main__':
app = QApplication(sys.argv) # 建立 QApplication 物件
window = QRCodeScannerApp() # 建立主視窗
window.show() # 顯示視窗
sys.exit(app.exec()) # 執行應用程式主迴圈
相关推荐
- 点过的网页会变色?没错,这玩意把你的浏览记录漏光了
-
提起隐私泄露这事儿,托尼其实早就麻了。。。平时网购、换手机号、注册各种账号之类的都会咔咔泄露,根本就防不住。但托尼真是没想到,浏览器里会有一个看起来完全人畜无害的功能,也在偷偷泄露我们的个人隐私,而且...
- Axure教程:高保真数据可视化原型
-
本文将介绍如何制作Axure高保真数据可视化原型,供大家参考和学习。高保真数据可视化原型设计,称得上是Axure高阶水平。数据可视化在原型设计中是一个重要的分支,但是对于Axure使用者具有一定要求。...
- Flutter web开发中禁用浏览器后退按钮
-
路由采用的go-router路由框架:finalrootNavigatorKey=GlobalKey<NavigatorState>();finalGoRouterrouter...
- jQuery 控制属性和样式
-
标记的属性each()遍历元素:each(callback)方法主要用于对选择器进行遍历,它接受一个函数为参数,该函数接受一个参数,指代元素的序号。对于标记的属性而言,可以利用each()方法配合th...
- 微信小程序入门教程之二:页面样式
-
这个系列的上一篇教程,教大家写了一个最简单的Helloworld微信小程序。但是,那只是一个裸页面,并不好看。今天接着往下讲,如何为这个页面添加样式,使它看上去更美观,教大家写出实际可以使用的页...
- 如何在Windows11的任务栏中禁用和删除天气小部件图标?
-
微软该公司已在Windows11的任务栏中添加了一个天气小部件图标,作为小部件的入口点。这个功能与之前Win10上的新闻与资讯功能相同,但是有的用户不喜欢想要关闭,不知道如何操作,下面小编为大家带来...
- CSS伪类选择器大全:提升网页交互与样式的神奇工具
-
CSS伪类选择器是前端开发中不可或缺的强大工具,它们允许我们根据元素的状态、位置或用户行为动态地应用样式。本文将全面介绍常用的伪类选择器,并通过代码示例展示其实际应用场景。一、基础交互伪类1.超链接...
- 7个Axure使用小技巧
-
编辑导读:对于Axure原型工具,很少有产品经过系统学习,一般都是直接上手,边摸索边学习,这直接导致很多快捷操作被忽视。笔者在日常工作中总结出以下小技巧,希望对各位有帮助。之前整理了2期Axure的...
- JavaScript黑暗技巧:禁止浏览器点击“后退”按钮
-
浏览网页时,当从A页面点击跳转到B页面后,一般情况下,可以点击浏览器上的“后退”按钮返回A页面。如果进入B页面后,B页面想让访问者留下,禁止返回,是否可以实现呢?这简直是要控制浏览器的行为,虽然有些邪...
- 对齐PyTorch,一文详解OneFlow的DataLoader实现
-
撰文|赵露阳在最新的OneFlowv0.5.0版本中,我们增加了许多新特性,比如:新增动态图特性:OneFlow默认以动态图模式(eager)运行,与静态图模式(graph)相比,更容易搭建网...
- Python计算机视觉编程 第一章 基本的图像操作和处理
-
以下是使用Python进行基本图像操作和处理的示例代码:使用PIL库加载图像:fromPILimportImageimage=Image.open("image.jpg"...
- PyTorch 深度学习实战(31):可解释性AI与特征可视化
-
在上一篇文章中,我们探讨了模型压缩与量化部署技术。本文将深入可解释性AI与特征可视化领域,揭示深度学习模型的决策机制,帮助开发者理解和解释模型的内部工作原理。一、可解释性AI基础1.核心概念特征重要...
- 学习编程第177天 python编程 富文本框text控件的使用
-
今天学习的是刘金玉老师零基础Python教程第72期,主要内容是python编程富文本框text控件。一、知识点1.tag_config方法:利用某个别名作为标签,具体的对应标签的属性功能配置在后面参...
- 用Python讓電腦攝像頭實現掃二維碼
-
importsys#系統模組,用來存取命令列參數與系統功能importcv2#OpenCV,處理影像與相機操作importnumpyasnp#Numpy,用來處理數值與...
- 使用Transformer来做物体检测
-
作者:JacobBriones编译:ronghuaiyang导读这是一个Facebook的目标检测Transformer(DETR)的完整指南。介绍DEtectionTRansformer(D...
- 一周热门
- 最近发表
- 标签列表
-
- ps图案在哪里 (33)
- super().__init__ (33)
- python 获取日期 (34)
- 0xa (36)
- super().__init__()详解 (33)
- python安装包在哪里找 (33)
- linux查看python版本信息 (35)
- python怎么改成中文 (35)
- php文件怎么在浏览器运行 (33)
- eval在python中的意思 (33)
- python安装opencv库 (35)
- python div (34)
- sticky css (33)
- python中random.randint()函数 (34)
- python去掉字符串中的指定字符 (33)
- python入门经典100题 (34)
- anaconda安装路径 (34)
- yield和return的区别 (33)
- 1到10的阶乘之和是多少 (35)
- python安装sklearn库 (33)
- dom和bom区别 (33)
- js 替换指定位置的字符 (33)
- python判断元素是否存在 (33)
- sorted key (33)
- shutil.copy() (33)