AI神器:如何快速将图片串联成视频的必备小工具

在使用Autodesk Inventor时,会遇到这样一个问题:一旦对导出的动画选择渲染操作,动画画面就会变得严重扭曲。即便不进行渲染,当动画播放速度过快,或者装配体中有部件移动、转动速度过快时,导出的动画大概率也会出现各种状况。好在Inventor提供了将视频导出为图片的功能,无论视频本身是什么情况,采用这种方式导出,一般都不会出现问题。

图片[1]-AI神器:如何快速将图片串联成视频的必备小工具
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
喜欢就支持一下吧
点赞1995 分享