在使用Autodesk Inventor时,会遇到这样一个问题:一旦对导出的动画选择渲染操作,动画画面就会变得严重扭曲。即便不进行渲染,当动画播放速度过快,或者装配体中有部件移动、转动速度过快时,导出的动画大概率也会出现各种状况。好在Inventor提供了将视频导出为图片的功能,无论视频本身是什么情况,采用这种方式导出,一般都不会出现问题。
![图片[1]-AI神器:如何快速将图片串联成视频的必备小工具](https://www.zywz6.com/wp-content/uploads/2025/07/d2b5ca33bd20250728165006-1024x707.png)
import sys
import os
import re
import cv2
import numpy as np
from PyQt5.QtWidgets import (
QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout,
QPushButton, QFileDialog, QLineEdit, QLabel, QListWidget,
QProgressBar, QSpinBox, QMessageBox, QFrame, QToolBar
)
from PyQt5.QtCore import QThread, pyqtSignal, QSettings, Qt
from PyQt5.QtGui import QFont
# --- 辅助函数:解决中文路径读取问题 ---
def imread_zh(file_path):
"""
使用 numpy 读取文件,然后用 OpenCV 解码,以支持非 ASCII 路径。
"""
try:
img_array = np.fromfile(file_path, dtype=np.uint8)
img = cv2.imdecode(img_array, cv2.IMREAD_COLOR)
return img
except Exception as e:
print(f"Error reading {file_path} with imread_zh: {e}")
return None
# --- 步骤 1: 创建工作线程处理耗时任务 ---
class Worker(QThread):
progress = pyqtSignal(int)
finished = pyqtSignal(str)
error = pyqtSignal(str)
def __init__(self, image_paths, output_path, fps):
super().__init__()
self.image_paths = image_paths
self.output_path = output_path
self.fps = fps
self.is_running = True
def run(self):
if not self.image_paths:
self.error.emit("没有找到可用的图片文件。")
return
try:
first_image = imread_zh(self.image_paths[0])
if first_image is None:
self.error.emit(f"无法读取第一张图片: {os.path.basename(self.image_paths[0])}\n请检查文件是否存在或是否损坏。")
return
height, width, _ = first_image.shape
fourcc = cv2.VideoWriter_fourcc(*'mp4v')
video_writer = cv2.VideoWriter(self.output_path, fourcc, self.fps, (width, height))
if not video_writer.isOpened():
self.error.emit("无法创建视频文件,请检查输出路径和权限。")
return
total_images = len(self.image_paths)
for i, image_path in enumerate(self.image_paths):
if not self.is_running:
break
frame = imread_zh(image_path)
if frame is None:
print(f"警告:跳过无法读取的图片 {os.path.basename(image_path)}")
continue
if frame.shape[0] != height or frame.shape[1] != width:
print(f"警告:图片 {os.path.basename(image_path)} 尺寸不一致,将调整为第一张图片的尺寸。")
frame = cv2.resize(frame, (width, height))
video_writer.write(frame)
percentage = int(((i + 1) / total_images) * 100)
self.progress.emit(percentage)
video_writer.release()
if self.is_running:
self.finished.emit(f"视频已成功生成!\n路径: {self.output_path}")
else:
os.remove(self.output_path)
self.finished.emit("任务已取消。")
except Exception as e:
self.error.emit(f"生成视频时发生未知错误: {str(e)}")
def stop(self):
self.is_running = False
# --- 步骤 2: 创建主窗口 ---
class ImageToVideoApp(QMainWindow):
def __init__(self):
super().__init__()
self.settings = QSettings("MyCompany", "ImageToVideoApp_Ascending")
self.worker_thread = None
self.initUI()
self.load_settings()
def initUI(self):
self.setWindowTitle("图片转视频工具 (从小到大排序)")
self.setGeometry(300, 300, 800, 600)
central_widget = QWidget()
self.setCentralWidget(central_widget)
main_layout = QVBoxLayout(central_widget)
# 文件夹选择
folder_layout = QHBoxLayout()
self.folder_path_edit = QLineEdit()
self.folder_path_edit.setPlaceholderText("请选择包含图片的文件夹...")
self.folder_path_edit.setReadOnly(True)
select_folder_btn = QPushButton("选择文件夹")
select_folder_btn.clicked.connect(self.select_folder)
folder_layout.addWidget(QLabel("图片文件夹:"))
folder_layout.addWidget(self.folder_path_edit)
folder_layout.addWidget(select_folder_btn)
main_layout.addLayout(folder_layout)
# 图片列表
self.image_list_widget = QListWidget()
self.image_list_widget.setAlternatingRowColors(True)
# 【关键修改】更新UI标签文本
main_layout.addWidget(QLabel("待处理图片列表 (已按文件名中数字从小到大排序):"))
main_layout.addWidget(self.image_list_widget)
# 输出设置
output_layout = QHBoxLayout()
self.output_path_edit = QLineEdit()
self.output_path_edit.setPlaceholderText("请选择视频输出路径及文件名...")
self.output_path_edit.setReadOnly(True)
select_output_btn = QPushButton("设置输出")
select_output_btn.clicked.connect(self.select_output_file)
output_layout.addWidget(QLabel("输出文件:"))
output_layout.addWidget(self.output_path_edit)
output_layout.addWidget(select_output_btn)
main_layout.addLayout(output_layout)
# 帧率设置
settings_layout = QHBoxLayout()
settings_layout.addWidget(QLabel("帧率 (FPS):"))
self.fps_spinbox = QSpinBox()
self.fps_spinbox.setRange(1, 120)
self.fps_spinbox.setValue(24)
settings_layout.addWidget(self.fps_spinbox)
settings_layout.addStretch()
main_layout.addLayout(settings_layout)
# 分割线
line = QFrame()
line.setFrameShape(QFrame.HLine)
line.setFrameShadow(QFrame.Sunken)
main_layout.addWidget(line)
# 操作按钮和进度条
action_layout = QHBoxLayout()
self.start_button = QPushButton("开始转换")
self.start_button.clicked.connect(self.start_conversion)
self.progress_bar = QProgressBar()
self.progress_bar.setValue(0)
action_layout.addWidget(self.start_button)
action_layout.addWidget(self.progress_bar)
main_layout.addLayout(action_layout)
# 工具栏 (用于字体大小调整)
toolbar = QToolBar("设置")
self.addToolBar(toolbar)
toolbar.addWidget(QLabel(" 界面字号: "))
self.font_size_spinbox = QSpinBox()
self.font_size_spinbox.setRange(8, 24)
self.font_size_spinbox.setSuffix(" pt")
self.font_size_spinbox.valueChanged.connect(self.change_font_size)
toolbar.addWidget(self.font_size_spinbox)
# --- 步骤 3: 实现槽函数和核心逻辑 ---
def select_folder(self):
folder = QFileDialog.getExistingDirectory(self, "选择图片文件夹")
if folder:
self.folder_path_edit.setText(folder)
self.populate_image_list(folder)
def _sort_key_numeric(self, filename):
numbers = re.findall(r'\d+', filename)
return int("".join(numbers)) if numbers else 0
def populate_image_list(self, folder_path):
self.image_list_widget.clear()
try:
files = os.listdir(folder_path)
image_extensions = ['.png', '.jpg', '.jpeg', '.bmp', '.tiff']
image_files = [f for f in files if os.path.splitext(f)[1].lower() in image_extensions]
# 【关键修改】去掉 reverse=True,实现从小到大排序(升序)
image_files.sort(key=self._sort_key_numeric)
if not image_files:
self.image_list_widget.addItem("文件夹中未找到支持的图片文件。")
else:
self.image_list_widget.addItems(image_files)
except Exception as e:
QMessageBox.critical(self, "错误", f"无法读取文件夹: {e}")
def select_output_file(self):
folder_name = os.path.basename(self.folder_path_edit.text() or "video")
default_path = os.path.join(self.folder_path_edit.text(), f"{folder_name}.mp4")
output_path, _ = QFileDialog.getSaveFileName(
self, "保存视频文件", default_path, "MP4 视频 (*.mp4);;所有文件 (*)"
)
if output_path:
self.output_path_edit.setText(output_path)
def start_conversion(self):
image_folder = self.folder_path_edit.text()
output_file = self.output_path_edit.text()
fps = self.fps_spinbox.value()
if not image_folder or not os.path.isdir(image_folder):
QMessageBox.warning(self, "输入错误", "请先选择一个有效的图片文件夹。")
return
if not output_file:
QMessageBox.warning(self, "输入错误", "请设置输出视频文件路径。")
return
image_paths = []
for i in range(self.image_list_widget.count()):
item_text = self.image_list_widget.item(i).text()
full_path = os.path.join(image_folder, item_text)
if os.path.exists(full_path):
image_paths.append(full_path)
if not image_paths:
QMessageBox.warning(self, "无图片", "在选定文件夹中没有找到可用的图片。")
return
self.start_button.setText("转换中...")
self.start_button.setEnabled(False)
self.progress_bar.setValue(0)
self.worker_thread = Worker(image_paths, output_file, fps)
self.worker_thread.progress.connect(self.update_progress)
self.worker_thread.finished.connect(self.on_conversion_finished)
self.worker_thread.error.connect(self.on_conversion_error)
self.worker_thread.start()
def update_progress(self, value):
self.progress_bar.setValue(value)
def on_conversion_finished(self, message):
QMessageBox.information(self, "完成", message)
self.reset_ui_state()
def on_conversion_error(self, error_message):
QMessageBox.critical(self, "错误", error_message)
self.reset_ui_state()
def reset_ui_state(self):
self.start_button.setText("开始转换")
self.start_button.setEnabled(True)
self.progress_bar.setValue(0)
self.worker_thread = None
# --- 步骤 4: 设置保存、加载和字体调整 ---
def change_font_size(self, size):
self.setStyleSheet(f"QWidget {{ font-size: {size}pt; }}")
def load_settings(self):
geometry = self.settings.value("geometry", self.saveGeometry())
self.restoreGeometry(geometry)
self.folder_path_edit.setText(self.settings.value("last_folder", ""))
self.output_path_edit.setText(self.settings.value("last_output", ""))
self.fps_spinbox.setValue(int(self.settings.value("last_fps", 24)))
font_size = int(self.settings.value("font_size", 10))
self.font_size_spinbox.setValue(font_size)
self.change_font_size(font_size)
if self.folder_path_edit.text():
self.populate_image_list(self.folder_path_edit.text())
def save_settings(self):
self.settings.setValue("geometry", self.saveGeometry())
self.settings.setValue("last_folder", self.folder_path_edit.text())
self.settings.setValue("last_output", self.output_path_edit.text())
self.settings.setValue("last_fps", self.fps_spinbox.value())
self.settings.setValue("font_size", self.font_size_spinbox.value())
def closeEvent(self, event):
if self.worker_thread and self.worker_thread.isRunning():
reply = QMessageBox.question(self, '确认退出',
'转换任务仍在进行中,确定要退出吗?',
QMessageBox.Yes | QMessageBox.No, QMessageBox.No)
if reply == QMessageBox.Yes:
self.worker_thread.stop()
self.worker_thread.wait()
else:
event.ignore()
return
self.save_settings()
super().closeEvent(event)
# --- 步骤 5: 运行程序 ---
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = ImageToVideoApp()
ex.show()
sys.exit(app.exec_())
© 版权声明
THE END