161 lines
5.3 KiB
PowerShell
161 lines
5.3 KiB
PowerShell
<#
|
|
.SYNOPSIS
|
|
Extracts specific range of frames of a video file to PNG images using ffmpeg.
|
|
.PARAMETER Video
|
|
Path to the input video file. Can be absolute or relative to current directory.
|
|
.PARAMETER Images
|
|
Path to output PNG images files base name. Can be absolute or relative to current directory.
|
|
If not provided, the output files will be in subdirectory 'Frames' and named as the video file with a frame number suffix padded with zeros to 4 digits.
|
|
.PARAMETER Frames
|
|
0-based frames index to extract. Supports the following formats:
|
|
- start-end: Extract frames from start to end (e.g., "100-200")
|
|
- positive_number: Extract frames from 0 to that number (e.g., "50")
|
|
- -negative_number: Extract last N frames (e.g., "-10")
|
|
- %: Represents the last frame index. Can be used alone or in ranges (e.g., "%", "10-%", "-%")
|
|
- all: Extract all frames (same as "0-%")
|
|
.DESCRIPTION
|
|
Extracts specific range of frames of a video file to PNG images using ffmpeg.
|
|
Supports -WhatIf and -Confirm for safe execution.
|
|
#>
|
|
[CmdletBinding(SupportsShouldProcess)]
|
|
param(
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$Video,
|
|
|
|
[Parameter(Mandatory = $false)]
|
|
[string]$Images = '',
|
|
|
|
[Parameter(Mandatory = $true)]
|
|
[string]$Frames = ''
|
|
)
|
|
|
|
Set-StrictMode -Version Latest
|
|
$ErrorActionPreference = 'Stop'
|
|
|
|
$VideoPath = $Video
|
|
|
|
if (-not (Test-Path -LiteralPath $VideoPath)) {
|
|
Write-Error ('Video file not found: {0}' -f $VideoPath)
|
|
exit 1
|
|
}
|
|
|
|
$PSNativeCommandUseErrorActionPreference = $false
|
|
|
|
$totalFramesOutput = & ffprobe -v error -select_streams v:0 -count_frames -show_entries stream=nb_read_frames -of csv=p=0 -- $VideoPath 2>&1
|
|
$ffprobeExit = $LASTEXITCODE
|
|
|
|
if ($ffprobeExit -ne 0) {
|
|
$totalFramesOutput | ForEach-Object { Write-Error $_ }
|
|
Write-Error ('ffprobe failed to read video file: {0}' -f $VideoPath)
|
|
exit 1
|
|
}
|
|
|
|
$totalFrames = $totalFramesOutput | Select-Object -First 1
|
|
|
|
if (-not $totalFrames -or $totalFrames -notmatch '^\d+$') {
|
|
Write-Error ('Could not determine frame count for: {0}' -f $VideoPath)
|
|
exit 1
|
|
}
|
|
|
|
$totalFramesInt = [int]$totalFrames
|
|
$maxFrameIndex = $totalFramesInt - 1
|
|
|
|
$startFrame = 0
|
|
$endFrame = 0
|
|
|
|
# Replace % with actual last frame index, and handle 'all' keyword
|
|
$framesPattern = $Frames -replace '%', $maxFrameIndex
|
|
if ($framesPattern -ieq 'all') {
|
|
$framesPattern = "0-$maxFrameIndex"
|
|
}
|
|
|
|
if ($framesPattern -match '^(\d+)-(\d+)$') {
|
|
$startFrame = [int]$matches[1]
|
|
$endFrame = [int]$matches[2]
|
|
}
|
|
elseif ($framesPattern -match '^-(\d+)$') {
|
|
$negativeCount = [int]$matches[1]
|
|
$startFrame = [Math]::Max(0, $maxFrameIndex - $negativeCount)
|
|
$endFrame = $maxFrameIndex
|
|
}
|
|
elseif ($framesPattern -match '^\d+$') {
|
|
$positiveCount = [int]$framesPattern
|
|
$startFrame = 0
|
|
$endFrame = [Math]::Min($positiveCount, $maxFrameIndex)
|
|
}
|
|
else {
|
|
Write-Error ('Invalid Frames format: {0}. Expected: start-end, positive number, negative number, % for last frame, or "all"' -f $Frames)
|
|
exit 1
|
|
}
|
|
|
|
if ($startFrame -lt 0 -or $startFrame -gt $maxFrameIndex) {
|
|
Write-Error ('Start frame {0} is out of range. Valid range: 0 to {1}' -f $startFrame, $maxFrameIndex)
|
|
exit 1
|
|
}
|
|
|
|
if ($endFrame -lt $startFrame -or $endFrame -gt $maxFrameIndex) {
|
|
Write-Error ('End frame {0} is out of range. Valid range: {1} to {2}' -f $endFrame, $startFrame, $maxFrameIndex)
|
|
exit 1
|
|
}
|
|
|
|
$frameCount = $endFrame - $startFrame + 1
|
|
|
|
if ([string]::IsNullOrWhiteSpace($Images)) {
|
|
$videoBaseName = [System.IO.Path]::GetFileNameWithoutExtension($Video)
|
|
$outputDir = Split-Path -Parent $VideoPath
|
|
$Images = Join-Path $outputDir 'Frames' ($videoBaseName + '_%04d.png')
|
|
} else {
|
|
$outputPattern = $Images
|
|
|
|
if ($outputPattern -notmatch '%\d+d') {
|
|
$outputPattern = $outputPattern -replace '\.png$', ''
|
|
$outputPattern = $outputPattern + '_%04d.png'
|
|
}
|
|
|
|
$Images = $outputPattern
|
|
}
|
|
|
|
$outputDir = Split-Path -Parent $Images
|
|
if ($outputDir -and -not (Test-Path -LiteralPath $outputDir)) {
|
|
if ($PSCmdlet.ShouldProcess($outputDir, 'Create output directory')) {
|
|
New-Item -ItemType Directory -Path $outputDir -Force | Out-Null
|
|
Write-Host ('Created output directory: {0}' -f $outputDir)
|
|
}
|
|
}
|
|
|
|
Write-Host ('Extracting frames from video: {0}' -f $Video)
|
|
Write-Host (' Frame range: {0} to {1} ({2} frames)' -f $startFrame, $endFrame, $frameCount)
|
|
|
|
$outputBaseName = $Images -replace '%\d+d\.png$', ''
|
|
$outputExtension = '.png'
|
|
|
|
for ($frameIndex = $startFrame; $frameIndex -le $endFrame; $frameIndex++) {
|
|
$outputFile = ('{0}{1:D4}{2}' -f $outputBaseName, $frameIndex, $outputExtension)
|
|
|
|
$videoFilter = 'select=eq(n\,{0})' -f $frameIndex
|
|
|
|
$ffmpegArgs = @(
|
|
'-i', $VideoPath
|
|
'-vf', $videoFilter
|
|
'-vsync', 'vfr'
|
|
'-frames:v', '1'
|
|
'-q:v', '1'
|
|
'--', $outputFile
|
|
)
|
|
|
|
if ($PSCmdlet.ShouldProcess($outputFile, ('Extract frame {0} to PNG' -f $frameIndex))) {
|
|
Write-Host (' Extracting frame {0} to: {1}' -f $frameIndex, (Split-Path -Leaf $outputFile))
|
|
|
|
$ffmpegOutput = & ffmpeg @ffmpegArgs 2>&1
|
|
$ffmpegExit = $LASTEXITCODE
|
|
|
|
if ($ffmpegExit -ne 0) {
|
|
$ffmpegOutput | ForEach-Object { Write-Error $_ }
|
|
Write-Error ('ffmpeg failed to extract frame {0}' -f $frameIndex)
|
|
exit 1
|
|
}
|
|
}
|
|
}
|
|
|
|
Write-Host ('Frames extracted successfully')
|