第一章 将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程序打包成一个可执行程序的过程,比对了两种打包方式的优缺点。