117 lines
4.1 KiB
PowerShell
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)
|
|
}
|