Криво собирается приложение Python

Написал приложение на Python, использовал Qt5 для удобного пользования программой, запуск через терминал проходит успешно и никаких ошибок не выдаёт, но при сборке в .ехе, приложение ломается. Ввожу в приложение нужные параметры и после этого: окно приложения (не отвечает), открывается 3 точно таких же окна.

Собирал код несколькими способами: auto-py-to-exe, pyinstaller - но всё такая же проблема

  • сообщение об ошибках отсутствуют
from PyQt5 import QtCore, QtGui, QtWidgets
from PyQt5.QtWidgets import QFileDialog
from PyQt5.QtGui import QIntValidator
from PyQt5.QtWidgets import QLineEdit
import cv2
import numpy as np
from tqdm import tqdm
from concurrent.futures import ProcessPoolExecutor


def read_frames(cap):
    while cap.isOpened():
        ret, frame = cap.read()

        if not ret:
            break
        yield frame


def process_frame(args):
    frame, frame_size, square_size, light_symbol, dark_symbol = args
    frame = cv2.resize(frame, frame_size)
    height, width, _ = frame.shape
    symbol_frame = np.zeros((height, width, 3), dtype=np.uint8)
    font = cv2.FONT_ITALIC

    for i in range(0, height, square_size):
        for j in range(0, width, square_size):
            square = frame[i:i + square_size, j:j + square_size]
            brightness = int(np.mean(square))
            if brightness > 128:
                symbol = light_symbol
                color = (255, 255, 255)
            else:
                symbol = dark_symbol
                color = (255, 255, 255)

            # Задаем координаты для рисования символа
            x, y = j, i
            symbol_frame = cv2.putText(symbol_frame, symbol, (x, y), font, 0.26, color, 0)

    return symbol_frame


class Ui_MainWindow(object):
    def setupUi(self, MainWindow):
        MainWindow.setObjectName("MainWindow")
        MainWindow.resize(465, 383)
        MainWindow.setMaximumSize(QtCore.QSize(1920, 1080))
        MainWindow.setContextMenuPolicy(QtCore.Qt.CustomContextMenu)
        MainWindow.setToolButtonStyle(QtCore.Qt.ToolButtonIconOnly)
        self.centralwidget = QtWidgets.QWidget(MainWindow)
        self.centralwidget.setObjectName("centralwidget")
        self.label = QtWidgets.QLabel(self.centralwidget)
        self.label.setGeometry(QtCore.QRect(10, 10, 211, 31))
        font = QtGui.QFont()
        font.setPointSize(20)
        self.label.setFont(font)
        self.label.setObjectName("label")
        self.label_2 = QtWidgets.QLabel(self.centralwidget)
        self.label_2.setGeometry(QtCore.QRect(10, 60, 171, 31))
        font = QtGui.QFont()
        font.setPointSize(20)
        self.label_2.setFont(font)
        self.label_2.setObjectName("label_2")
        self.label_3 = QtWidgets.QLabel(self.centralwidget)
        self.label_3.setGeometry(QtCore.QRect(10, 100, 211, 31))
        font = QtGui.QFont()
        font.setPointSize(20)
        self.label_3.setFont(font)
        self.label_3.setObjectName("label_3")
        self.label_4 = QtWidgets.QLabel(self.centralwidget)
        self.label_4.setGeometry(QtCore.QRect(10, 140, 211, 31))
        font = QtGui.QFont()
        font.setPointSize(20)
        self.label_4.setFont(font)
        self.label_4.setObjectName("label_4")
        self.label_5 = QtWidgets.QLabel(self.centralwidget)
        self.label_5.setGeometry(QtCore.QRect(10, 180, 211, 31))
        font = QtGui.QFont()
        font.setPointSize(20)
        self.label_5.setFont(font)
        self.label_5.setObjectName("label_5")
        self.label_6 = QtWidgets.QLabel(self.centralwidget)
        self.label_6.setGeometry(QtCore.QRect(10, 220, 211, 31))
        font = QtGui.QFont()
        font.setPointSize(20)
        self.label_6.setFont(font)
        self.label_6.setObjectName("label_6")

        self.spinBox = QtWidgets.QSpinBox(self.centralwidget)
        self.spinBox.setGeometry(QtCore.QRect(190, 60, 61, 31))
        self.spinBox.setObjectName("spinBox")
        self.spinBox.setMinimum(2)
        self.spinBox.setMaximum(50)

        self.plainTextEdit_2 = QLineEdit(self.centralwidget)
        self.plainTextEdit_2.setGeometry(QtCore.QRect(130, 100, 41, 31))
        self.plainTextEdit_2.setObjectName("plainTextEdit_2")
        self.plainTextEdit_2.setMaxLength(1)

        self.plainTextEdit_3 = QLineEdit(self.centralwidget)
        self.plainTextEdit_3.setGeometry(QtCore.QRect(140, 140, 41, 31))
        self.plainTextEdit_3.setObjectName("plainTextEdit_3")
        self.plainTextEdit_3.setMaxLength(1)

        self.plainTextEdit_4 = QLineEdit(self.centralwidget)
        self.plainTextEdit_4.setGeometry(QtCore.QRect(220, 180, 104, 31))
        self.plainTextEdit_4.setObjectName("plainTextEdit_4")
        int_validator_plainTextEdit_4 = QIntValidator()
        self.plainTextEdit_4.setValidator(int_validator_plainTextEdit_4)

        self.label_7 = QtWidgets.QLabel(self.centralwidget)
        self.label_7.setGeometry(QtCore.QRect(330, 180, 16, 31))
        font = QtGui.QFont()
        font.setPointSize(20)
        self.label_7.setFont(font)
        self.label_7.setObjectName("label_7")

        self.plainTextEdit_5 = QLineEdit(self.centralwidget)
        self.plainTextEdit_5.setGeometry(QtCore.QRect(350, 180, 104, 31))
        self.plainTextEdit_5.setObjectName("plainTextEdit_5")
        int_validator_plainTextEdit_5 = QIntValidator()
        self.plainTextEdit_5.setValidator(int_validator_plainTextEdit_5)

        self.pushButton = QtWidgets.QPushButton(self.centralwidget)
        self.pushButton.setGeometry(QtCore.QRect(10, 270, 191, 71))
        font = QtGui.QFont()
        font.setPointSize(16)
        self.pushButton.setFont(font)
        self.pushButton.setObjectName("pushButton")

        self.progressBar = QtWidgets.QProgressBar(self.centralwidget)
        self.progressBar.setGeometry(QtCore.QRect(250, 310, 181, 21))
        self.progressBar.setProperty("value", 0)
        self.progressBar.setObjectName("progressBar")

        self.plainTextEdit_6 = QtWidgets.QPlainTextEdit(self.centralwidget)
        self.plainTextEdit_6.setGeometry(QtCore.QRect(220, 10, 221, 31))
        self.plainTextEdit_6.setObjectName("plainTextEdit_6")

        self.plainTextEdit_7 = QtWidgets.QPlainTextEdit(self.centralwidget)
        self.plainTextEdit_7.setGeometry(QtCore.QRect(220, 230, 231, 31))
        self.plainTextEdit_7.setObjectName("plainTextEdit_7")

        MainWindow.setCentralWidget(self.centralwidget)
        self.menubar = QtWidgets.QMenuBar(MainWindow)
        self.menubar.setGeometry(QtCore.QRect(0, 0, 465, 21))
        self.menubar.setObjectName("menubar")
        MainWindow.setMenuBar(self.menubar)
        self.statusbar = QtWidgets.QStatusBar(MainWindow)
        self.statusbar.setObjectName("statusbar")
        MainWindow.setStatusBar(self.statusbar)
        self.retranslateUi(MainWindow)
        QtCore.QMetaObject.connectSlotsByName(MainWindow)

        self.plainTextEdit_6.mousePressEvent = self.choose_video
        self.plainTextEdit_7.mousePressEvent = self.choose_save_path
        self.pushButton.clicked.connect(self.process_data)
        self.progressBar.setVisible(False)

    def choose_video(self, event):
        options = QFileDialog.Options()
        options |= QFileDialog.DontUseNativeDialog
        file_name, _ = QFileDialog.getOpenFileName(None, "Выберите видео", "",
                                                   "Video Files (*.mp4 *.avi);;All Files (*)", options=options)
        if file_name:
            self.plainTextEdit_6.setPlainText(file_name)

    def choose_save_path(self, event):
        options = QFileDialog.Options()
        options |= QFileDialog.DontUseNativeDialog
        save_path, _ = QFileDialog.getSaveFileName(None, "Куда сохранить", "", "All Files (*)", options=options)
        if save_path:
            if not save_path.lower().endswith('.mp4'):
                save_path += '.mp4'
            self.plainTextEdit_7.setPlainText(save_path)

    def process_data(self):

        self.progressBar.setVisible(True)
        HEIGHT = int(self.plainTextEdit_4.text())
        WIDTH = int(self.plainTextEdit_5.text())

        mainVideo = self.plainTextEdit_6.toPlainText()
        outputVideo = self.plainTextEdit_7.toPlainText()

        cap = cv2.VideoCapture(mainVideo)

        # успешность открытия видео
        if not cap.isOpened():
            #print("Ошибка: не удалось открыть видео.")
            exit()

        total_frames = int(cap.get(cv2.CAP_PROP_FRAME_COUNT))
        output_video_path = outputVideo
        fourcc = cv2.VideoWriter_fourcc(*'mp4v')
        output_video_writer = cv2.VideoWriter(output_video_path, fourcc, cap.get(cv2.CAP_PROP_FPS), (WIDTH, HEIGHT))
        start_frame = 0
        cap.set(cv2.CAP_PROP_POS_FRAMES, start_frame)
        pbar = tqdm(total=total_frames)


        with ProcessPoolExecutor(max_workers=4) as executor:
            frames_chunk = []
            for frame in read_frames(cap):
                frames_chunk.append((frame, (WIDTH, HEIGHT), self.spinBox.value(), self.plainTextEdit_2.text(), self.plainTextEdit_3.text()))
                if len(frames_chunk) % 64 == 0:
                    for processed_frame in executor.map(process_frame, frames_chunk):
                        output_video_writer.write(processed_frame)
                        pbar.update()
                    frames_chunk = []

            for processed_frame in executor.map(process_frame, frames_chunk):
                output_video_writer.write(processed_frame)
                pbar.update()

        cap.release()
        output_video_writer.release()

    def retranslateUi(self, MainWindow):
        _translate = QtCore.QCoreApplication.translate
        MainWindow.setWindowTitle(_translate("MainWindow", "Sosiska"))
        self.label.setText(_translate("MainWindow", "Выберите видео"))
        self.label_2.setText(_translate("MainWindow", "Размер Сетки"))
        self.label_3.setText(_translate("MainWindow", "Символ 1"))
        self.label_4.setText(_translate("MainWindow", "Символ 2"))
        self.label_5.setText(_translate("MainWindow", "Размеры видео"))
        self.label_6.setText(_translate("MainWindow", "Куда сохранить"))
        self.label_7.setText(_translate("MainWindow", "x"))
        self.pushButton.setText(_translate("MainWindow", "Поехали"))

if __name__ == "__main__":
    import sys
    app = QtWidgets.QApplication(sys.argv)
    MainWindow = QtWidgets.QMainWindow()
    ui = Ui_MainWindow()
    ui.setupUi(MainWindow)
    MainWindow.show()
    sys.exit(app.exec_())

Возможно я неправильно собираю приложение и что-то важное не указал - кто знает, подскажите


Ответы (1 шт):

Автор решения: Amgarak

Костыль для всех страждущих .EXE на пайтоне, но вам лень\не получается разобраться почему ЭТО не работает.

Идем на python.org и качаем Windows embeddable package (64-bit) - Python>>> Downloads>>> Windows

Далее нужно установить для него pip и нужные библиотеки, но сперва проверим что всё работает как надо.

Мой путь до скаченного «карманного» пайтона:

C:\Users\Amgarak\Downloads\python-3.11.7-embed-amd64

Win+r -> cmd > cd C:\Users\Amgarak\Downloads\python-3.11.7-embed-amd64

Проверим и введём в консоль: python

Вывод: Python 3.11.7 (tags/v3.11.7:fa7a6f2, Dec 4 2023, 19:24:49) [MSC v.1937 64 bit (AMD64)] on win32

Отлично, всё работает. Введём в запустившийся интерпретатор следующие команды для выхода из него:

import sys
sys.exit()

Python 3.11.7

Прекрасно, нам снова доступна консоль.

А теперь давайте установим pip.

Переходим по ссылке: installation

Находим: Download the script, from https://bootstrap.pypa.io/get-pip.py.

Скачиваем скрипт\копируем в txt файл и называем get-pip.py

Помещаем get-pip.py в корень нашего «карманного» пайтона:

C:\Users\Amgarak\Downloads\python-3.11.7-embed-amd64\get-pip.py

Возвращаемся в нашу консоль и вводим: python get-pip.py

Не обращаем внимание на:

WARNING: The script wheel.exe is installed in 'C:\Users\Amgarak\Downloads\python-3.11.7-embed-amd64\Scripts' which is not on PATH.
  Consider adding this directory to PATH or, if you prefer to suppress this warning, use --no-warn-script-location.
WARNING: The scripts pip.exe, pip3.11.exe and pip3.exe are installed in 'C:\Users\Amgarak\Downloads\python-3.11.7-embed-amd64\Scripts' which is not on PATH.
  Consider adding this directory to PATH or, if you prefer to suppress this warning, use --no-warn-script-location.

Отлично, pip установлен, но для его работы нужно подкорректировать один файл.

Возвращаемся в каталог «карманного» пайтона C:\Users\Amgarak\Downloads\python-3.11.7-embed-amd64

Ищем файл python311._pth -> Открываем его любым txt-редактором -> добавляем строчку Lib\site-packages (на следующей строке после #import site)

Lib\site-packages

Проверим в консоли: python -m pip list

Вывод:

Package    Version
---------- -------
pip        23.3.2
setuptools 69.0.3
wheel      0.42.0

Отлично, всё работает!

Теперь установим какую-нибудь библиотеку: python -m pip install NuMPI

И снова проверим: python -m pip list

Вывод:

Package    Version
---------- -------
cftime     1.6.3
netCDF4    1.6.2
NuMPI      0.4.0
numpy      1.26.3
pip        23.3.2
scipy      1.11.4
setuptools 69.0.3
wheel      0.42.0

Прекрасно, почти всё готово. Теперь вам нужно установить таким же образом все необходимые библиотеки для вашего проекта.

После этого подготавливаем 2 файла:

start.py

import subprocess
import sys
import os

current_directory = getattr(sys, '_MEIPASS', os.getcwd())
folder_path = os.path.join(current_directory, 'python-3.11.7-embed-amd64\\bat.bat')

subprocess.run([folder_path], shell=True) # Выполнение бат-файла

bat.bat

@echo off
cd /d %~dp0 
python.exe main.py

bat.bat -> помещаем в «карманный» пайтон python-3.11.7-embed-amd64 прямо в корень, туда же помещаем и основной скрипт(main.py)\дополнительные файлы.

При запаковке в .exe указываем папку python-3.11.7-embed-amd64 как доп.ресурсы для запаковки, а файл для запуска указываем start.py

Логика какая: Паинсталлер запускает скрипт start.py, который запускает bat.bat, который запускает «карманный» пайтон, который запускает уже нужный скрипт.

Версия без .bat:

start.py

import subprocess
import sys
import os

current_directory = getattr(sys, '_MEIPASS', os.getcwd())
executable_path = os.path.join(current_directory, 'python-3.11.7-embed-amd64\\python.exe')
script_path = os.path.join(current_directory, 'python-3.11.7-embed-amd64\\main.py')

# Выполнение исполняемого файла
subprocess.run([executable_path, script_path], shell=True)

Логика какая: Паинсталлер запускает скрипт start.py, который запускает «карманный» пайтон, который запускает уже нужный скрипт.

auto-py-to-exe

Решение конечно так себе, но с ходу лечит многие проблемы с паинсталлером.

Из минусов: .exe больше весит, скрипт дольше запускается.
Из плюсов: это работает, если всё работало до запаковки в .exe

З.Ы. Дабы всё это безобразие запускалось немного быстрее, файл(ы) исходного скрипта main.py можно сперва сконвертировать в байт-код -> main.pyc

python -c "import py_compile; py_compile.compile('main.py', 'main.pyc')"
→ Ссылка