Files
Video-Processing-Scripts/Get-LastFrame.ps1
T

117 lines
4.1 KiB
PowerShell

<#
.SYNOPSIS
Extracts a specific frame or the last frame of a video file to a PNG image using ffmpeg.
.PARAMETER Video
Path to the input video file. Can be absolute or relative to current directory.
.PARAMETER Image
Path to the output PNG image file. Can be absolute or relative to current directory.
Supports a printf-style numbering token (e.g. frame_%04d.png or C:\Output\frame_%04d.png), which is
replaced with the extracted 0-based frame index, zero-padded to the width given in the token
(%d = no padding, %04d = 4 digits). Without a token the path is used literally.
The output directory is created when it does not exist.
.PARAMETER Frame
0-based frame index to extract. Negative values count from the end:
-1 extracts the last frame (default), -2 extracts second-to-last, etc.
The resulting frame index is clamped to the valid range (0 to total frames - 1).
.DESCRIPTION
Extracts a specific frame of a video file to a PNG image using ffmpeg.
Supports negative frame indices to count from the end of the video.
Supports a printf-style numbering token in -Image, like Get-FramesRange.ps1.
Supports -WhatIf and -Confirm for safe execution.
#>
[CmdletBinding(SupportsShouldProcess)]
param(
[Parameter(Mandatory = $true)]
[string]$Video,
[Parameter(Mandatory = $true)]
[string]$Image,
[Parameter(Mandatory = $false)]
[int]$Frame = -1
)
Set-StrictMode -Version Latest
$ErrorActionPreference = 'Stop'
$VideoPath = $Video
if (-not (Test-Path $VideoPath)) {
Write-Error ('Video file not found: {0}' -f $VideoPath)
exit 1
}
$PSNativeCommandUseErrorActionPreference = $false
$totalFrames = & ffprobe -v error -select_streams v:0 -count_frames -show_entries stream=nb_read_frames -of csv=p=0 $VideoPath 2>&1 | 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
$frameToExtract = if ($Frame -lt 0) {
# Negative values count from the end
$calculatedFrame = $totalFramesInt + $Frame
# Clamp to valid range [0, maxFrameIndex]
[Math]::Max(0, [Math]::Min($calculatedFrame, $maxFrameIndex))
} else {
# Positive values are direct indices
if ($Frame -gt $maxFrameIndex) {
Write-Error ('Frame index {0} is out of range. Valid range: 0 to {1}' -f $Frame, $maxFrameIndex)
exit 1
}
$Frame
}
Write-Host ('Total frames: {0} - extracting frame index {1}' -f $totalFramesInt, $frameToExtract)
# Resolve any printf-style numbering token (%d, %04d) in -Image to the extracted frame
# index. The substitution is done here rather than left to ffmpeg: ffmpeg would treat the
# token as an image2 sequence counter starting at 1 instead of the actual video frame index.
$ImagePath = $Image
$tokenPattern = [regex]'%(\d*)d'
while ($ImagePath -match $tokenPattern) {
$padWidth = if ($Matches[1]) { [int]$Matches[1] } else { 0 }
$ImagePath = $tokenPattern.Replace($ImagePath, ([string]$frameToExtract).PadLeft($padWidth, '0'), 1)
}
if ($ImagePath -ne $Image) {
Write-Host ('Output pattern resolved to: {0}' -f $ImagePath)
}
$outputDir = Split-Path -Parent $ImagePath
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)
}
}
if ($PSCmdlet.ShouldProcess($ImagePath, ('Extract frame {0} to PNG' -f $frameToExtract))) {
$videoFilter = 'select=eq(n\,{0})' -f $frameToExtract
$ffmpegArgs = @(
'-y'
'-i', $VideoPath
'-vf', $videoFilter
'-vsync', 'vfr'
'-frames:v', '1'
'-q:v', '1'
'--', $ImagePath
)
$ffmpegStderr = & ffmpeg @ffmpegArgs 2>&1
if ($LASTEXITCODE -ne 0) {
$ffmpegStderr | ForEach-Object { Write-Error $_ }
Write-Error 'ffmpeg failed to extract the last frame.'
exit 1
}
Write-Host ('Frame saved to: {0}' -f $ImagePath)
}