From 3919c9b7f868eaacd936453eca40672853051858 Mon Sep 17 00:00:00 2001 From: Jaime Florian Date: Thu, 18 Apr 2024 20:17:49 -0700 Subject: [PATCH] feat: removed findstr dependency, added linux and mac support - Replaced findstr with regex to find the duration of the video. - Added new logic for finding the ffmpeg executable to use, falling back to an ffmpeg available on PATH in case there is not one available in the bin folder. --- .gitignore | 3 + README.md | 70 +++++++---- ReferenceImporterMain.py | 13 +- reference_importer/imageSequence.py | 41 ------- .../__init__.py | 0 reference_importer_main/imageSequence.py | 114 ++++++++++++++++++ .../ui/__init__.py | 0 .../ui/main_dialog_ui.py | 2 +- .../ui/main_dialog_ui.ui | 0 9 files changed, 171 insertions(+), 72 deletions(-) delete mode 100644 reference_importer/imageSequence.py rename {reference_importer => reference_importer_main}/__init__.py (100%) create mode 100644 reference_importer_main/imageSequence.py rename {reference_importer => reference_importer_main}/ui/__init__.py (100%) rename {reference_importer => reference_importer_main}/ui/main_dialog_ui.py (99%) rename {reference_importer => reference_importer_main}/ui/main_dialog_ui.ui (100%) diff --git a/.gitignore b/.gitignore index d00cd32..43bd746 100644 --- a/.gitignore +++ b/.gitignore @@ -10,6 +10,9 @@ __pycache__/ .Python build/ bin/ +bin_linux/ +bin_mac/ +bin_windows/ develop-eggs/ dist/ downloads/ diff --git a/README.md b/README.md index 391a519..74f6593 100644 --- a/README.md +++ b/README.md @@ -1,27 +1,45 @@ # Reference Importer -## Description -Script that automates the importing of video reference into Maya, automatically creating an image sequence from a video file and creating an image plane with the sequence. -# Installation -## 1. Download the correct version from the Releases tab ->If Using Maya 2021 or under, download ***ReferenceImporter.zip***. - ->If Using Maya 2022 or higher, download ***ReferenceImporter_Python3.zip***. -## 2. Copy the contents of the zip to you Maya scripts folder ->found under 'Documents/maya/***InsertMayaVerion***/scripts' -## 2. Run the following command in Maya - import reference_importer - reference_importer.run() -make sure to run it using Python - -# Usage - -## for a demonstration of usage please watch the video : - -[![Script Walkthrough Video](https://img.youtube.com/vi/ObX9NU2BmZo/0.jpg)](https://www.youtube.com/watch?v=ObX9NU2BmZo "Script Walkthrough Video") -### 1. Choose a video file -### 2. Set the desired trimming -> Use format HH:MM:SS.ms for the timecode -### 3. Set output sequence name and format -### 4. Set output directory for the sequence -### 5. (Optional) auto creation of image plane in Maya for the sequence -### 6. Import the video!! +Tool to help artists import videos as image sequence in Maya without going through a +video editing package. Supports multiple file formats, length trimming and specifying a +target frame rate. + +## Features +- Video to image sequence conversion, for improved Reference Plane performance. +- Automatic frame rate conversion (Forget about using Premiere Pro to change the frame rate of your reference!). +- Automatic creation of a Reference Plane in Maya. +- Multiplatform support (Windows, Mac and Linux). +- [Ffmpeg](https://ffmpeg.org/) as the backend engine for video conversion. +- [Qt.py](https://github.com/mottosso/Qt.py) abstraction for multiple Maya versions support (2022+). + +## Installation +1. Find the latest version under the [Releases](https://github.com/JaimeFlorian27/reference-importer/releases) section. +2. Download the right distribution for your platform. + - Distributions use the following naming pattern: `reference_importer__.zip` +3. Extract the zip file as a folder named `reference_importer` in your `$MAYA_APP_DIR//scripts` folder. + - ie: `/home/jaime/Maya/2024/scripts/reference_importer` + - [What is my MAYA_APP_DIR?](https://help.autodesk.com/view/MAYAUL/2024/ENU/?guid=GUID-228CCA33-4AFE-4380-8C3D-18D23F7EAC72) + +## Usage + +To launch Reference Importer, run the following Python script: +``` +import reference_importer +reference_importer.run() +``` + +### Workflow + +1. Select a video file in the UI. +2. Set the desired start and end timecode. + - End timecode will be populated by default to the end of the video. + > Use HH:MM:SS.ms as the timecode format. +3. Set output image sequence name. +4. Set the output format. +4. Select the output directory for the sequence. +4. Set the desired target frame rate. +5. (Optional) Enable creation of an Image Plane in Maya for the sequence. +6. Import the video! + +#### Demonstration Video: + +[![Reference Importer demonstration video](https://img.youtube.com/vi/ObX9NU2BmZo/0.jpg)](https://www.youtube.com/watch?v=ObX9NU2BmZo "Script Walkthrough Video") diff --git a/ReferenceImporterMain.py b/ReferenceImporterMain.py index fe62828..bd8d1fb 100644 --- a/ReferenceImporterMain.py +++ b/ReferenceImporterMain.py @@ -3,12 +3,17 @@ # Video Demonstration: https://www.youtube.com/watch?v=ObX9NU2BmZo import sys +import os +import platform from pathlib import Path +# Allow standalone to show in macOS +if platform.system() == "Darwin": + os.environ["QT_MAC_WANTS_LAYER"] = "1" + VENDOR_PATH = Path(__file__).parent.resolve() / "vendor" sys.path.insert(0, str(VENDOR_PATH)) -import os import re from Qt import QtCore,QtWidgets from Qt.QtCompat import wrapInstance @@ -21,7 +26,7 @@ except ImportError: IN_MAYA = False -from .reference_importer import ImageSequencer, Ui +from .reference_importer_main import ImageSequencer, Ui @@ -94,7 +99,7 @@ def SetInput(self): filename = filename[0] if filename != "": - duration = self.imageSequencer.getDuration(filename) + duration = self.imageSequencer.get_duration(filename) self.ui.lineEdit_end_trim.setText(duration) self.ui.lineEdit.setText(filename) def SetOutput(self): @@ -155,7 +160,7 @@ def CreateImageSequence(self): output_name = self.ui.lineEdit_2.text()+"_%03d"+output_ext output_file = os.path.join(output_dir, output_name) - self.imageSequencer.createSequence(input_file,frameRate, + self.imageSequencer.create_sequence(input_file,frameRate, trim_start,trim_end, output_file) diff --git a/reference_importer/imageSequence.py b/reference_importer/imageSequence.py deleted file mode 100644 index 53b948c..0000000 --- a/reference_importer/imageSequence.py +++ /dev/null @@ -1,41 +0,0 @@ -# -*- coding: utf-8 -*- -from pathlib import Path -import subprocess - - - -class ImageSequencer(): - - def __init__(self, video_file="", - output_name="", - padding = "%03d" , - output_frameRate= 24): - - self.output_frameRate = output_frameRate - self.video_file = video_file - self.output_name= output_name - self.padding = padding - self.trim_start = 0 - self.trim_end = 0 - self.ffmpeg_path = Path(__file__).parent.parent.resolve() / "bin" / "ffmpeg" - - def getDuration(self, video_file): - command = (u'"%s" -i "%s" 2>&1 | findstr "Duration"' %(self.ffmpeg_path,video_file)) - try: - process = subprocess.check_output(command, shell=True) - process = str(process).strip() - process = process.split(" ") - process = process[3][0:11] - except Exception as e: - raise e - return process - def createSequence(self,input_file, frameRate,start_trim,end_trim, output_file): - command = ('"%s" -i "%s" -r %s -vf scale=1280:-1 -q:v 3 -ss %s -to %s "%s"' % (self.ffmpeg_path,input_file,frameRate,start_trim,end_trim,output_file)) - try: - subprocess.run(command, shell=True) - except Exception as e: - raise e - - -if __name__ == "__main__": - pass diff --git a/reference_importer/__init__.py b/reference_importer_main/__init__.py similarity index 100% rename from reference_importer/__init__.py rename to reference_importer_main/__init__.py diff --git a/reference_importer_main/imageSequence.py b/reference_importer_main/imageSequence.py new file mode 100644 index 0000000..9ab69a0 --- /dev/null +++ b/reference_importer_main/imageSequence.py @@ -0,0 +1,114 @@ +from __future__ import annotations +import re +import shutil +import subprocess +import platform +from pathlib import Path + +TIMECODE_REGEX = r"Duration: (\d{2}:\d{2}:\d{2}.\d)" + + +class FfmpegError(Exception): + ... + +class MissingFfmpeg(FfmpegError): + ... + + +class ImageSequencer: + def __init__( + self, + video_file: str = "", + output_name: str = "", + padding: str = "%03d", + output_frame_rate: int = 24, + ) -> None: + self.output_frame_rate = output_frame_rate + self.video_file = video_file + self.output_name = output_name + self.padding = padding + self.trim_start = 0 + self.trim_end = 0 + self.ffmpeg_path = self.get_ffmpeg_path() + + @classmethod + def get_ffmpeg_path(cls) -> Path: + """Resolves the path to ffmpeg. + + The script first attempts to use the ffmpeg available in the bin folder, + otherwise it fallbacks to the first match of ffmpeg avaiable in Path. + + Raises: + MissingFfmpeg: If no ffmpeg binaries are available. + + """ + _ffmpeg_binary = f"ffmpeg{'.exe' if platform.system() == 'Windows' else ''}" + ffmpeg_executable = Path(__file__).parent.parent.resolve() / "bin" / _ffmpeg_binary + if not ffmpeg_executable.exists(): + ffmpeg_executable = shutil.which("ffmpeg") + + if not ffmpeg_executable: + raise MissingFfmpeg() + return Path(ffmpeg_executable) + + def get_duration(self, video_file: str) -> str: + """Gets the duration of the video from the output of ffmpeg -i. + + Args: + video_file: Video file. + + Raises: + FfmpegError: If ffmpeg errors out and does not yield an output. + ValueError: If there is no match for the duration in ffmpeg's output. + + Returns: + Duration of the video in timecode. + """ + command = f'"{self.ffmpeg_path}" -i "{video_file}"' + try: + output = str( + subprocess.run( + command, + shell=True, + stderr=subprocess.STDOUT, + stdout=subprocess.PIPE, + check=False, + ), + ) + + except subprocess.CalledProcessError as e: + _except_msg: str = "Unable to get ffmpeg output" + raise FfmpegError(_except_msg) from e + + # re.search searches across the whole string (multiline output). + match: re.Match | None = re.search(TIMECODE_REGEX, output) + + if not match: + _except_msg: str = f"Unable to find Duration for video {video_file}" + raise ValueError(_except_msg) + + # the timecode is the first group of the match. + duration: str = str(match.groups(0)[0]) + return duration + + def create_sequence( + self, + input_file: str, + frame_rate: int, + start_trim: str, + end_trim: str, + output_file: str, + ): + command = ( + f'"{self.ffmpeg_path}" -i "{input_file}" -r {frame_rate}' + f" -vf scale=1280:-1 -q:v 3 -ss {start_trim} -to {end_trim} " + f'"{output_file}"' + ) + try: + subprocess.run(command, shell=True, check=False) + except subprocess.CalledProcessError as e: + raise FfmpegError from e + + +if __name__ == "__main__": + pass diff --git a/reference_importer/ui/__init__.py b/reference_importer_main/ui/__init__.py similarity index 100% rename from reference_importer/ui/__init__.py rename to reference_importer_main/ui/__init__.py diff --git a/reference_importer/ui/main_dialog_ui.py b/reference_importer_main/ui/main_dialog_ui.py similarity index 99% rename from reference_importer/ui/main_dialog_ui.py rename to reference_importer_main/ui/main_dialog_ui.py index 9b4070f..ef5af94 100644 --- a/reference_importer/ui/main_dialog_ui.py +++ b/reference_importer_main/ui/main_dialog_ui.py @@ -148,7 +148,7 @@ def __init__(self, Dialog): # setupUi def setTexts(self, Dialog): - Dialog.setWindowTitle(u"Reference Importer v1.1.2") + Dialog.setWindowTitle(u"Reference Importer v1.2.0") self.groupBox_input.setTitle(u"Input") self.label_video_file.setText(u"Video File") self.pushButton_fileExplorer_input.setText(u"Open...") diff --git a/reference_importer/ui/main_dialog_ui.ui b/reference_importer_main/ui/main_dialog_ui.ui similarity index 100% rename from reference_importer/ui/main_dialog_ui.ui rename to reference_importer_main/ui/main_dialog_ui.ui