如何在 pyqt 中讀取串列埠傳輸的影象

2022-10-15 21:00:40

前言

這學期選修了嵌入式系統的課程,大作業選擇的題目是人臉口罩檢測。由於課程提供的開發板搭載的晶片是 STM32F103ZET6,跑不動神經網路,所以打算將 OV7725 拍攝到的影象通過串列埠傳輸給上位機處理。關於人臉口罩檢測可以參見上一篇部落格《如何使用 Yolov4 訓練人臉口罩檢測模型》,本篇部落格的程式碼放在了 https://github.com/zhiyiYo/Face-Mask-Detector,下面進入正題。

串列埠傳輸影象

為了讓上位機知道什麼時候開始傳輸一張影象,需要在傳輸畫素之前先發一段 header,這裡使用的是 image:。在 header 的最後新增換行符的理由是方便上位機可以一次讀入一行字串來和 image: 進行比對。

傳送完 header 就可以開始傳輸畫素了。OV7725 輸出的影象色彩模式為 RGB565,影象深度只有 16 位,用 2 個位元組表示一個畫素。在讀取攝像頭的 FIFO 晶片中儲存的畫素時,先讀到的是畫素的高 8 位(R<<5 | G[:3]),接著是低 8 位(G[3:]<<3 | B)。使用 HAL_UART_Transmit() 將畫素傳送給上位機即可,這裡沒有使用 printf 來傳輸,因為 printf 速度慢一些。每傳輸完一列畫素就傳送一個換行符,這樣上位機通過 serial.readline() 讀取 header 的時候就不會把上一張影象最後一列的畫素給讀進來了。

// 傳送幀頭
printf("image:\n");

// 在 LCD 上顯示影象並將畫素髮給上位機
uint8_t high, low;
for (uint32_t i = 0; i < HEIGHT; ++i)
{
    // 一列資料
    for (uint32_t j = 0; j < WIDTH; ++j)
    {
        // 讀顏色的高八位
        setReadClock(GPIO_PIN_RESET);
        // high = GPIOC->IDR & 0XFF;
        high = GPIOC->IDR & 0XFF;
        setReadClock(GPIO_PIN_SET);

        // 讀顏色的低八位
        setReadClock(GPIO_PIN_RESET);
        low = GPIOC->IDR & 0XFF;
        setReadClock(GPIO_PIN_SET);

        lcd_->drawPoint((high << 8) | low);

        // 傳送畫素給上位機
        HAL_UART_Transmit(&huart1, &high, 1, 100);
        HAL_UART_Transmit(&huart1, &low, 1, 100);
    }
    printf("\n");
}

串列埠讀取影象

如果直接在主執行緒讀取串列埠的資料會造成主介面卡頓,所以建立一個子執行緒 SerialThread 來讀圖,每讀完一張影象就傳送 loadImageFinished 訊號給主介面來顯示影象。

在讀取影象的畫素之前需要將 s.readline()[:-1].decode("utf-8", "replace")image: 進行比較,如果相等就說明下面拿到的就是影象資料,否則當前讀取的是還沒傳輸完成的影象的畫素。之後使用 s.read(column_len) 讀入一列畫素並將其新增到列表 data 中,由於 OV7725 工作在 QVGA 模式,輸出的影象是 320*240 解析度的,一個畫素 2 位元組,data 的長度等於 320*240*2 即 153600 就表明完成了一張完整影象的讀取。接下來把 RGB565 的畫素縮放到 RGB888 的畫素並組裝為 320*240的影象即可。

實驗過程中發現波特率不能取太高,否則讀取的影象會有奇怪的條紋,所以這裡使用的波特率為 1500000。

# coding: utf-8
from PIL import Image
import numpy as np
from PyQt5.QtCore import QThread, pyqtSignal
from PyQt5.QtGui import QPixmap, QImage
from serial import Serial


def imageToQPixmap(image: Image.Image):
    """ 將影象轉換為 `QPixmap`

    Parameters
    ----------
    image: `~PIL.Image` or `np.ndarray`
        RGB 影象
    """
    image = np.array(image)  # type:np.ndarray
    h, w, c = image.shape
    format = QImage.Format_RGB888 if c == 3 else QImage.Format_RGBA8888
    return QPixmap.fromImage(QImage(image.data, w, h, c * w, format))


def rgb565ToImage(pixels: list) -> QPixmap:
    """ 將 RGB565 影象轉換為 RGB888 """
    image = []
    for i in range(0, len(pixels), 2):
        pixel = (pixels[i] << 8) | pixels[i+1]
        r = pixel >> 11
        g = (pixel >> 5) & 0x3f
        b = pixel & 0x1f
        r = r * 255.0 / 31.0
        g = g * 255.0 / 63.0
        b = b * 255.0 / 31.0
        image.append([r, g, b])

    image = np.array(image, dtype=np.uint8).reshape(
        (240, 320, 3)).transpose((1, 0, 2))
    return imageToQPixmap(Image.fromarray(image))


class SerialThread(QThread):
    """ 串列埠執行緒 """

    loadImageFinished = pyqtSignal(QPixmap)

    def __init__(self, parent=None):
        super().__init__(parent)
        self.serial = Serial(baudrate=1500000)
        self.isStopped = False

    def run(self):
        """ 將串列埠傳輸的位元組轉換為影象 """
        data = []
        self.serial.port = config.get(config.serialPort)

        with self.serial as s:
            while not self.isStopped:
                if not s.isOpen():
                    s.open()

                # 等待 header
                header = s.readline()[:-1]
                if header.decode("utf-8", "replace") != "image:":
                    continue

                # 讀入畫素,丟棄換行符
                column_len = 320*2+1
                while len(data) < 2*320*240:
                    image_line = s.read(column_len)
                    data.extend(image_line[:-1])

                self.loadImageFinished.emit(rgb565ToImage(data))
                data.clear()

    def stop(self):
        """ 停止從串列埠讀取影象 """
        self.isStopped = True
        self.serial.close()

    def loadImage(self):
        """ 開始從串列埠讀取影象 """
        self.isStopped = False
        self.start()

軟體介面如下圖所示,只要點選工具列最左側的攝像頭按鈕就能從串列埠讀取影象。

後記

至此使用 pyqt 讀取串列埠傳輸影象的方法就介紹完畢了,以上~~