百度360必应搜狗淘宝本站头条
当前位置:网站首页 > 技术文章 > 正文

用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...