在使用Autodesk Inventor時(shí),會(huì)遇到這樣一個(gè)問題:一旦對(duì)導(dǎo)出的動(dòng)畫選擇渲染操作,動(dòng)畫畫面就會(huì)變得嚴(yán)重扭曲。即便不進(jìn)行渲染,當(dāng)動(dòng)畫播放速度過快,或者裝配體中有部件移動(dòng)、轉(zhuǎn)動(dòng)速度過快時(shí),導(dǎo)出的動(dòng)畫大概率也會(huì)出現(xiàn)各種狀況。好在Inventor提供了將視頻導(dǎo)出為圖片的功能,無論視頻本身是什么情況,采用這種方式導(dǎo)出,一般都不會(huì)出現(xiàn)問題。
![圖片[1]-AI神器:如何快速將圖片串聯(lián)成視頻的必備小工具](http://www.oilmaxhydraulic.com.cn/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
# --- 輔助函數(shù):解決中文路徑讀取問題 ---
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: 創(chuàng)建工作線程處理耗時(shí)任務(wù) ---
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請(qǐng)檢查文件是否存在或是否損壞。")
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("無法創(chuàng)建視頻文件,請(qǐng)檢查輸出路徑和權(quán)限。")
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)} 尺寸不一致,將調(diào)整為第一張圖片的尺寸。")
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("任務(wù)已取消。")
except Exception as e:
self.error.emit(f"生成視頻時(shí)發(fā)生未知錯(cuò)誤: {str(e)}")
def stop(self):
self.is_running = False
# --- 步驟 2: 創(chuàng)建主窗口 ---
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("圖片轉(zhuǎn)視頻工具 (從小到大排序)")
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("請(qǐng)選擇包含圖片的文件夾...")
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)
# 【關(guān)鍵修改】更新UI標(biāo)簽文本
main_layout.addWidget(QLabel("待處理圖片列表 (已按文件名中數(shù)字從小到大排序):"))
main_layout.addWidget(self.image_list_widget)
# 輸出設(shè)置
output_layout = QHBoxLayout()
self.output_path_edit = QLineEdit()
self.output_path_edit.setPlaceholderText("請(qǐng)選擇視頻輸出路徑及文件名...")
self.output_path_edit.setReadOnly(True)
select_output_btn = QPushButton("設(shè)置輸出")
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)
# 幀率設(shè)置
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)
# 操作按鈕和進(jìn)度條
action_layout = QHBoxLayout()
self.start_button = QPushButton("開始轉(zhuǎn)換")
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)
# 工具欄 (用于字體大小調(diào)整)
toolbar = QToolBar("設(shè)置")
self.addToolBar(toolbar)
toolbar.addWidget(QLabel(" 界面字號(hào): "))
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: 實(shí)現(xiàn)槽函數(shù)和核心邏輯 ---
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]
# 【關(guān)鍵修改】去掉 reverse=True,實(shí)現(xiàn)從小到大排序(升序)
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, "錯(cuò)誤", 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, "輸入錯(cuò)誤", "請(qǐng)先選擇一個(gè)有效的圖片文件夾。")
return
if not output_file:
QMessageBox.warning(self, "輸入錯(cuò)誤", "請(qǐng)?jiān)O(shè)置輸出視頻文件路徑。")
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("轉(zhuǎn)換中...")
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, "錯(cuò)誤", error_message)
self.reset_ui_state()
def reset_ui_state(self):
self.start_button.setText("開始轉(zhuǎn)換")
self.start_button.setEnabled(True)
self.progress_bar.setValue(0)
self.worker_thread = None
# --- 步驟 4: 設(shè)置保存、加載和字體調(diào)整 ---
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, '確認(rèn)退出',
'轉(zhuǎn)換任務(wù)仍在進(jìn)行中,確定要退出嗎?',
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: 運(yùn)行程序 ---
if __name__ == '__main__':
app = QApplication(sys.argv)
ex = ImageToVideoApp()
ex.show()
sys.exit(app.exec_())
? 版權(quán)聲明
THE END