Files
Video-Processing-Scripts/Get-Frames.ps1
T
2026-05-19 14:55:09 -04:00

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')