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