Last Updated on 2021-05-13 by Clay
使用 PyInstaller 打包 Python 程式為 exe 的時候,常常碰到我們程式當中想要放圖片的時候。若是直接就使用 PyInstaller 打包,那麼我們常常會碰到執行檔案時的一個問題:『圖片顯示不出來。』
針對這個問題,其實網路上有不少辦法, 辦法從較簡單到困難,我認為有三種解決方法,並且我對第三種我認為比較好的辦法做了一點小改良。
雖然可能很多高手都知道這個辦法了,但還是容我獻醜,紀錄在這裡吧。
辦法 1 | 辦法 2 | 辦法 3 | |
難易度 | 簡單 | 簡單 | 較麻煩 |
作法 | 將圖片放到執行檔的目錄 | 使用絕對路徑讀取圖片 | 將圖片轉成 byte,放在程式碼中讀取 |
特性 | 在自己電腦上,執行檔跟圖片都不能改資料夾 | 只能用於自己的電腦 | 即使分享給別人仍然可用 |
以下我就簡單介紹這三種辦法。
事先準備
如果是想要了解 PyInstaller 運作,也許你可以參考我之前寫過的這篇《PyInstaller —— 如何將 Python 檔案打包成 exe 執行檔》。
如果想要學習 PyQt5 怎麼運作,也許可以參考我寫的《PyQt5 基本教學》。
那麼以下開始。
首先,我使用 Qt Designer 直接拉出了個簡單的視窗,上面只有一個元件: QLabel。
程式碼如下:
# -*- coding: utf-8 -*- from PyQt5 import QtCore, QtGui, QtWidgets class Ui_MainWindow(object): def setupUi(self, MainWindow): MainWindow.setObjectName("MainWindow") MainWindow.resize(796, 554) self.centralwidget = QtWidgets.QWidget(MainWindow) self.centralwidget.setObjectName("centralwidget") self.label = QtWidgets.QLabel(self.centralwidget) self.label.setGeometry(QtCore.QRect(170, 70, 431, 341)) self.label.setText("") self.label.setObjectName("label") MainWindow.setCentralWidget(self.centralwidget) self.retranslateUi(MainWindow) QtCore.QMetaObject.connectSlotsByName(MainWindow) def retranslateUi(self, MainWindow): _translate = QtCore.QCoreApplication.translate MainWindow.setWindowTitle(_translate("MainWindow", "MainWindow"))
這裡只是介面,我另外寫了個程式繼承這個界面的 class:
# -*- coding: utf-8 -*- import sys import cv2 from PyQt5 import QtWidgets from PyQt5.QtGui import * from test import Ui_MainWindow class MainWindow(QtWidgets.QMainWindow): def __init__(self): super(MainWindow, self).__init__() self.ui = Ui_MainWindow() self.ui.setupUi(self) image = cv2.imread('test.png') h, w, channel = image.shape bytesPerLine = 3*w image = QImage(image, w, h, bytesPerLine, QImage.Format_RGB888).rgbSwapped() image = QPixmap(image) self.ui.label.setPixmap(image) self.ui.label.setScaledContents(True) if __name__ == '__main__': app = QtWidgets.QApplication([]) window = MainWindow() window.show() sys.exit(app.exec_())
Output:
我是用 opencv 將圖片讀進來,然後再使用 QPixmap()
儲存圖片,最後顯示在我們圖形界面上的 Label 元件。
至此,準備工作告一段落。
辦法 1 —— 將圖片放到執行檔的目錄
首先我們使用 PyInstaller 打包我們的程式。
pyinstaller -F test_GUI.py --noconsole
(補充: -F 為『只打包成一個執行檔; --noconsole 為『執行檔案時取消黑視窗』』)
打包好後,我們來到打包好的 dist 目錄看看。
我在這裡只有這麼一個執行檔,雙擊執行。
這時候我們會看到這樣的『致命錯誤』—— 這是正常的,因為執行檔找不到我們要顯示的圖片。
所以辦法 1 來了 —— 我們將我們所要使用的爆炸圖片移到這個目錄底下。
這時候再執行一次。
這次就很順利地打開程式了。
不過以上的這個方法雖然簡單,卻不太推薦。
辦法 2 —— 使用絕對路徑讀取圖片
這個方法其實也是相當簡單。
image = cv2.imread('C:/Users/Clay/PycharmProjects/VideoScreenshot/test.png')
我們將讀取圖片從相對路徑更改為絕對路徑。然後我們再打包一次:
pyinstaller -F test_GUI.py --noconsole
這一次,我們在執行檔的目錄底下沒有圖片。
雙擊執行:
是可以顯示出圖片的,原因很簡單,因為我們使用『絕對路徑』指定了圖片的位置。但這個方法其實也不好,因為如果今天絕對路徑指定的圖片消失的話,這個程式照樣無法使用 —— 這意味著,我們仍然沒辦法分享這個程式給其他人使用。
辦法 3 —— 將圖片轉成 byte,放在程式碼中讀取
這個方法是我最推薦的方法,畢竟這是比較安全、可以分享給別人使用的作法。
首先,我們使用 base64 這個模組將圖片轉換成 byte:
# -*- coding: utf-8 -*- import base64 def pic2str(file, functionName): pic = open(file, 'rb') content = '{} = {}\n'.format(functionName, base64.b64encode(pic.read())) pic.close() with open('pic2str.py', 'a') as f: f.write(content) if __name__ == '__main__': pic2str('test.png', 'explode')
我所寫的 pic2str()
基本上只需要輸入『圖片路徑』以及『程式中要儲存圖片的變數名稱』兩項,就可以將圖片轉成的 byte 存入 "pic2str.py" 這個 py 檔當中的變數。
我們來看一下成果:
非常長,長到看不見盡頭。不過這就是我們的圖片。
然後我們回到原本的程式,稍微修改下:
# -*- coding: utf-8 -*- import sys import base64 from io import BytesIO from PyQt5 import QtWidgets from PyQt5.QtGui import * from test import Ui_MainWindow from PIL import Image, ImageQt from pic2str import explode class MainWindow(QtWidgets.QMainWindow): def __init__(self): super(MainWindow, self).__init__() self.ui = Ui_MainWindow() self.ui.setupUi(self) # Load byte data byte_data = base64.b64decode(explode) image_data = BytesIO(byte_data) image = Image.open(image_data) # PIL to QPixmap qImage = ImageQt.ImageQt(image) image = QPixmap.fromImage(qImage) # QPixmap to QLabel self.ui.label.setPixmap(image) self.ui.label.setScaledContents(True) if __name__ == '__main__': app = QtWidgets.QApplication([]) window = MainWindow() window.show() sys.exit(app.exec_())
別忘了要使用 "from pic2str import explode" 這行導入我們剛存下來的圖片 byte。
之間讀取的流程基本上為:
然後我們再次使用指令打包:
pyinstaller -F test_GUI.py --noconsole
這一次我們可以將爆炸(或你測試的圖片)通通刪掉,試跑看看:
如果能正常顯示,代表這個方法非常成功,我們可以任意分享給朋友了!
這個辦法之所以能成功,是因為 PyInstaller 並不是將 Python 檔打包為原生的 exe 檔,而是建制一個可以執行 Python 檔案的環境,所以通常這個 exe 檔都很肥。而圖片等等外部素材就是 PyInstaller 不會連結起來的物件之一,所以才會鑽這個漏洞,將圖片的 bytes 直接存入 py 檔中。
畢竟若是 py 檔都沒有成功包起來,代表這個模組真的有很大的漏洞。不過目前我使用 PyInstaller ,雖然遇到不少坑,但是基本上耐心點都是可以解決的。
如果你遇到了:
ValueError: unsupported image mode 'LA'
類似以上這樣的問題,也許你可以參考我所寫的《[Solved] ValueError: unsupported image mode ‘LA’》。