AI神器:如何快速將圖片串聯(lián)成視頻的必備小工具

在使用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)成視頻的必備小工具
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_())
------本頁內(nèi)容已結(jié)束,喜歡請(qǐng)分享------
溫馨提示:由于項(xiàng)目或工具都有失效性,如遇到不能做的項(xiàng)目或不能使用的工具,可以根據(jù)關(guān)鍵詞在站點(diǎn)搜索相關(guān)內(nèi)容,查看最近更新的或者在網(wǎng)頁底部給我們留言反饋。
? 版權(quán)聲明
THE END
喜歡就支持一下吧
點(diǎn)贊1995 分享