将python程序变成可执行程序 | 进阶篇

发布时间:2023年12月28日

系列文章目录

第一章 将python程序变成可执行程序 | 基础篇
第二章 将python程序变成可执行程序 | 进阶篇



前言

上一篇粗略的讲了下如何将python程序打包成可执行程序,并展示了在ubuntu和windows两种平台下的操作。
本篇我们上升一点难度,不再用hello,world这种打印语句了,编写一个真正“有用”的程序。


一、编写一个小程序

从事深度学习的工作中,难免需要进行素材筛选,如何筛选有用的视频对后期训练网络非常重要。打开视频一个个看,再关闭后一个个复制粘贴非常的麻烦。基于此,我想编写一个程序,实现以下功能:

  • 主要功能是对视频进行筛选,将某个目录下需要使用的视频保存在新的目录中,将打不开的视频文件进行删除
  • 用几个按键来实现前后视频的切换,使用帮助提示,保存,记录筛选位置,保存等
  • 用滚动条来实现查看视频某帧的状态

由于该程序主要让同事帮我筛选,所以只讲在windows下的打包过程。
(1)打开win菜单中的anaconda powershell ,如下图所示
当然前提你要在windows中已经安装好anaconda了,这可翻看我之前写的博客。
在这里插入图片描述

(2)创建一个新的虚拟环境
下面命令创建了一个名为video_player的使用python3.8的基础环境

conda create -n video_player python=3.8

(3)激活环境

conda activate video_player

(4)安装所需库
首先思考需求和问题,我们需要对视频进行筛选,自然是需要一个显示框,需要显示帮助提示,那肯定需要对话框,需要有界面,那肯定用一个显示界面的库,就用自带的tkinter吧,反正我们也不写多复杂的。处理视频那自然要用到opencv。用shutil库我们可以实现文件的拷贝移动。除此之外,我们还需要读取进行筛选视频所在的路径,保存上次筛选视频的序号…这些操作自然需要对文本文件进行读写,我们需要使用configparser库对一个配置文件进行配置读取。
上面这几个库除了opencv,都自带了,所以我们只用安装下opencv即可
在这里插入图片描述

python -m pip install opencv-python

(5)安装打包库
不能忘了安装pyinstall

python -m pip install pyinstaller

二、代码实现

(1)创建一个播放器(VideoPlayer)类,在初始化的时候读取同目录下的config.cfg文件中的配置项

class VideoPlayer:
    def __init__(self):
        self.config = configparser.ConfigParser()
        self.config.read('config.cfg')
        # 需要筛选视频文件所在的根目录root_dir
        self.root_dir = self.config.get('Paths', 'root_dir')
        # 需要保存的视频文件所在的根目录
        self.save_dir = self.config.get('Paths', 'save_dir')
        if not os.path.exists(self.save_dir):
            os.makedirs(self.save_dir)
        # 获取筛选筛选视频
        self.video_list = self.get_video_list()
        # 读取上次筛选的视频次序
        self.current_index = self.config.getint('Playback', 'current_index', fallback=0)

(2) 实现视频显示与可以拉动的状态条(position)

def play_video(self, index):
        video_path = self.video_list[index]

        # 尝试打开视频文件
        cap = cv2.VideoCapture(video_path)
        if not cap.isOpened():
            # 打不开视频说明其损坏,跳出对话框删除这个文件
            messagebox.showerror("Error", f"无法打开视频文件: {video_path}。该文件将被删除。")
            try:
                os.remove(video_path)
                messagebox.showinfo("Info", f"文件已删除: {video_path}")
                self.video_list.pop(index)  # 从视频列表中移除已删除的视频
                self.prev_video()  # 跳转到上一个视频
            except Exception as e:
                messagebox.showerror("Error", f"删除文件时出错: {e}")
            return

        cv2.namedWindow('Video Player')

        def on_change(_):
            pass
        #  用滚动条的方式展示视频的每一帧
        cv2.createTrackbar('Position', 'Video Player', 0, int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) - 1, on_change)
        cv2.setTrackbarPos('Position', 'Video Player', self.config.getint('Playback', 'current_position', fallback=0))
        
        # 如果直接关闭窗口,需要保存当前的视频次序
        while True:
            if cv2.getWindowProperty('Video Player', cv2.WND_PROP_VISIBLE) < 1:  # 检查窗口是否被关闭
                self.save_current_state()
                self.root.destroy()
                raise SystemExit

            position = cv2.getTrackbarPos('Position', 'Video Player')
            cap.set(cv2.CAP_PROP_POS_FRAMES, position)

            ret, frame = cap.read()

            if not ret:
                break

            cv2.imshow('Video Player', frame)
            cv2.setWindowTitle('Video Player', f'Current Video: {video_path}')

            key = cv2.waitKey(30)
            # 用按键实现视频控制
            if key == ord('s'):
                self.save_video(video_path)
                break
            elif key == ord('d'):
                self.next_video()
                break
            elif key == ord('a'):
                self.prev_video()
                break
            elif key == ord('h'):
                self.show_help()
                self.show_help()
                break
            elif key == 27:
                self.save_current_state()
                self.root.destroy()  

        cap.release()
        cv2.destroyAllWindows()

(3) 函数实现保存视频、后一个视频、前一个视频、提示帮助、保存当前状态

	def save_video(self, video_path):
        copying_dialog = tk.Toplevel()
        copying_dialog.title('Copying Video...')
        copying_label = tk.Label(copying_dialog, text=f'copy {video_path} to {self.save_dir}')
        copying_label.pack()

        shutil.copy(video_path, self.save_dir)

        copying_dialog.destroy()
        messagebox.showinfo('Save Video', f'视频保存到 {self.save_dir}目录')

    def next_video(self):
        if self.current_index < len(self.video_list) - 1:
            self.current_index += 1
        else:
            messagebox.showinfo('End of Videos', '已经是最后一个视频了.')

    def prev_video(self):
        if self.current_index > 0:
            self.current_index -= 1
        else:
            messagebox.showinfo('Start of Videos', '已经是第一个视频了.')


    def show_help(self):
        help_text = (
            '视频筛选工具 \n'
            '使用说明:在config.cfg文件中将需要筛选视频的根目录填写到root_dir中,save_dir填写的是保存视频的目录\n'
            '按键说明:\n'
            '按键s: 保存视频\n'
            '按键d: 下一个视频\n'
            '按键a: 上一个视频\n'
            '按键h: 帮助\n'
            '连按两次Esc键: 退出程序\n'
            '注意:切勿直接按窗口右上角X直接关闭窗口!!!'
            '\t\t\t\t\t\t\t\t\t\t\t\tedit by Pelly'
        )
        messagebox.showinfo('Video Player Help', help_text)
    def save_current_state(self):
        # 保存当前状态到配置文件
        self.config.set('Playback', 'current_index', str(self.current_index))
        with open('config.cfg', 'w') as config_file:
            self.config.write(config_file)

(4)最后的完整代码如下所示:

import cv2
import configparser
import tkinter as tk
from tkinter import messagebox
import shutil
import os
import glob


class VideoPlayer:
    def __init__(self):
        self.config = configparser.ConfigParser()
        self.config.read('config.cfg')

        self.root_dir = self.config.get('Paths', 'root_dir')
        self.save_dir = self.config.get('Paths', 'save_dir')
        if not os.path.exists(self.save_dir):
            os.makedirs(self.save_dir)
        self.video_list = self.get_video_list()
        self.current_index = self.config.getint('Playback', 'current_index', fallback=0)
        self.is_playing = True
        self.root = tk.Tk()
        self.root.withdraw()
        self.root.after(100, self.update, self.root)

    def get_video_list(self):
        return glob.glob(f'{self.root_dir}/*.mp4')

    def play_video(self, index):
        video_path = self.video_list[index]

        # 尝试打开视频文件
        cap = cv2.VideoCapture(video_path)
        if not cap.isOpened():
            messagebox.showerror("Error", f"无法打开视频文件: {video_path}。该文件将被删除。")
            try:
                os.remove(video_path)
                messagebox.showinfo("Info", f"文件已删除: {video_path}")
                self.video_list.pop(index)  # 从视频列表中移除已删除的视频
                self.prev_video()  # 跳转到上一个视频
            except Exception as e:
                messagebox.showerror("Error", f"删除文件时出错: {e}")
            return

        cv2.namedWindow('Video Player')

        def on_change(_):
            pass

        cv2.createTrackbar('Position', 'Video Player', 0, int(cap.get(cv2.CAP_PROP_FRAME_COUNT)) - 1, on_change)
        cv2.setTrackbarPos('Position', 'Video Player', self.config.getint('Playback', 'current_position', fallback=0))

        while True:
            if cv2.getWindowProperty('Video Player', cv2.WND_PROP_VISIBLE) < 1:  # 检查窗口是否被关闭
                self.save_current_state()
                self.root.destroy()
                raise SystemExit

            position = cv2.getTrackbarPos('Position', 'Video Player')
            cap.set(cv2.CAP_PROP_POS_FRAMES, position)

            ret, frame = cap.read()

            if not ret:
                break

            cv2.imshow('Video Player', frame)
            cv2.setWindowTitle('Video Player', f'Current Video: {video_path}')

            key = cv2.waitKey(30)

            if key == ord('s'):
                self.save_video(video_path)
                break
            elif key == ord('d'):
                self.next_video()
                break
            elif key == ord('a'):
                self.prev_video()
                break
            elif key == ord('h'):
                self.show_help()
                self.show_help()
                break
            elif key == 27:
                self.save_current_state()
                self.root.destroy()  

        cap.release()
        cv2.destroyAllWindows()

    def save_video(self, video_path):
        copying_dialog = tk.Toplevel()
        copying_dialog.title('Copying Video...')
        copying_label = tk.Label(copying_dialog, text=f'copy {video_path} to {self.save_dir}')
        copying_label.pack()

        shutil.copy(video_path, self.save_dir)

        copying_dialog.destroy()
        messagebox.showinfo('Save Video', f'视频保存到 {self.save_dir}目录')

    def next_video(self):
        if self.current_index < len(self.video_list) - 1:
            self.current_index += 1
        else:
            messagebox.showinfo('End of Videos', '已经是最后一个视频了.')

    def prev_video(self):
        if self.current_index > 0:
            self.current_index -= 1
        else:
            messagebox.showinfo('Start of Videos', '已经是第一个视频了.')


    def show_help(self):
        help_text = (
            '视频筛选工具 \n'
            '使用说明:在config.cfg文件中将需要筛选视频的根目录填写到root_dir中,save_dir填写的是保存视频的目录\n'
            '按键说明:\n'
            '按键s: 保存视频\n'
            '按键d: 下一个视频\n'
            '按键a: 上一个视频\n'
            '按键h: 帮助\n'
            '连按两次Esc键: 退出程序\n'
            '\t\t\t\t\t\t\t\t\t\t\t\tedit by Pelly'
        )
        messagebox.showinfo('Video Player Help', help_text)

    def update(self, root):
        if self.is_playing:
            self.play_video(self.current_index)

        root.after(100, self.update, root)

    def save_current_state(self):
        # 保存当前状态到配置文件
        self.config.set('Playback', 'current_index', str(self.current_index))
        with open('config.cfg', 'w') as config_file:
            self.config.write(config_file)


if __name__ == '__main__':
    player = VideoPlayer()
    player.root.mainloop()

(5)新建配置文件
新建一个名为config.cfg的文本文件,把下面几行填入,也就是说我们要读取目录src_videos中的所有mp4视频,将想要保存的放到dst_videos目录中。

[Paths]
root_dir = src_videos
save_dir = dst_videos

二、打包程序

在这个 程序中,我们依然只使用了一个代码文件
(1)打包程序的目录结构
如下图所示,只有一个代码文件,一个配置文件
在这里插入图片描述

(2)打包成一个可执行文件
打包成一个文件比较方便,但意味着这个exe文件会很大
在这里插入图片描述

pyinstaller --onefile --noconsole video_filter_tool.py

(4)生成的exe文件会最终出现在dist下
我们将这个exe文件拿出来放在同级目录下,因为我们这个可执行文件启动时需要读取cfg文件,以及筛选目录和保存目录(我没填绝对路径,填的相对路径,如果填绝对路径就不用把dst_videos和src_video)
在这里插入图片描述

(5)第二步也可以替换成打包成一个目录
打包成一个目录的情况下,exe文件会比较小,启动也更快,不过所用到的库之类的会出现在目录下
在这里插入图片描述

pyinstaller --onedir --noconsole video_filter_tool.py

(6)双击运行可执行程序
在这里插入图片描述

总结

本文主要介绍了在windows平台如何将一个真正有功能的python程序打包成一个可执行程序的过程,比对了两种打包方式的优缺点。

文章来源:https://blog.csdn.net/PellyKoo/article/details/135112183
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。