Functions/GenXdev.AI.Queries/Save-FoundImageObjects.ps1

###############################################################################
<#
.SYNOPSIS
Saves cropped object images from indexed image search results to files.
 
.DESCRIPTION
This function takes image search results and extracts individual detected
object regions, saving them as separate image files. It can search for objects
using various criteria including keywords, people, scenes, and metadata filters.
The function processes images with AI-detected object boundaries and crops them
to individual PNG files in the specified output directory.
 
.PARAMETER Any
Will match any of all the possible meta data types including descriptions,
keywords, people, objects, scenes, picture types, style types, and moods.
 
.PARAMETER DescriptionSearch
The description text to look for, wildcards allowed.
 
.PARAMETER Keywords
The keywords to look for, wildcards allowed.
 
.PARAMETER People
People to look for, wildcards allowed.
 
.PARAMETER Objects
Objects to look for, wildcards allowed.
 
.PARAMETER Scenes
Scenes to look for, wildcards allowed.
 
.PARAMETER PictureType
Picture types to filter by, wildcards allowed.
 
.PARAMETER StyleType
Style types to filter by, wildcards allowed.
 
.PARAMETER OverallMood
Overall moods to filter by, wildcards allowed.
 
.PARAMETER DatabaseFilePath
Path to the SQLite database file.
 
.PARAMETER Title
Title for the image gallery.
 
.PARAMETER Description
Description for the image gallery.
 
.PARAMETER Language
Language for descriptions and keywords.
 
.PARAMETER PathLike
Array of directory path-like search strings to filter images by path
(SQL LIKE patterns, e.g. '%\2024\%').
 
.PARAMETER InputObject
Accepts search results from a previous -PassThru call to regenerate the view.
 
.PARAMETER OutputDirectory
Directory to save cropped object images.
 
.PARAMETER PreferencesDatabasePath
Database path for preference data files.
 
.PARAMETER ImageDirectories
Array of directory paths to search for images.
 
.PARAMETER FacesDirectory
The directory containing face images organized by person folders.
 
.PARAMETER EmbedImages
Embed images as base64.
 
.PARAMETER NoFallback
Switch to disable fallback behavior.
 
.PARAMETER NeverRebuild
Never rebuild the image index database.
 
.PARAMETER ShowInBrowser
Show results in browser.
 
.PARAMETER SendKeyEscape
Send Escape key to browser.
 
.PARAMETER SendKeyHoldKeyboardFocus
Hold keyboard focus in browser.
 
.PARAMETER SendKeyUseShiftEnter
Use Shift+Enter for browser input.
 
.PARAMETER SendKeyDelayMilliSeconds
Delay in milliseconds for sending keys.
 
.PARAMETER NoBorders
Show images without borders.
 
.PARAMETER SideBySide
Show images side by side.
 
.PARAMETER AcceptLang
Accept-Language header for browser.
 
.PARAMETER Monitor
Monitor to use for browser window.
 
.PARAMETER ShowOnlyPictures
Show only pictures.
 
.PARAMETER Interactive
Enable interactive mode.
 
.PARAMETER Private
Open browser in private/incognito mode.
 
.PARAMETER Edge
Use Microsoft Edge browser.
 
.PARAMETER Chrome
Use Google Chrome browser.
 
.PARAMETER Chromium
Use Chromium browser.
 
.PARAMETER Firefox
Use Mozilla Firefox browser.
 
.PARAMETER ShowWindow
Show browser window.
 
.PARAMETER Left
Set browser window left position.
 
.PARAMETER Right
Set browser window right position.
 
.PARAMETER Top
Set browser window top position.
 
.PARAMETER Bottom
Set browser window bottom position.
 
.PARAMETER Centered
Center browser window.
 
.PARAMETER ApplicationMode
Open browser in application mode.
 
.PARAMETER NoBrowserExtensions
Disable browser extensions.
 
.PARAMETER DisablePopupBlocker
Disable popup blocker in browser.
 
.PARAMETER RestoreFocus
Restore focus to previous window.
 
.PARAMETER NewWindow
Open browser in new window.
 
.PARAMETER OnlyReturnHtml
Return only HTML from browser.
 
.PARAMETER FocusWindow
Focus browser window.
 
.PARAMETER SetForeground
Set browser window to foreground.
 
.PARAMETER Maximize
Maximize browser window.
 
.PARAMETER HasNudity
Filter images that contain nudity.
 
.PARAMETER NoNudity
Filter images that do NOT contain nudity.
 
.PARAMETER HasExplicitContent
Filter images that contain explicit content.
 
.PARAMETER NoExplicitContent
Filter images that do NOT contain explicit content.
 
.PARAMETER ForceIndexRebuild
Force rebuild of the image index database.
 
.PARAMETER GeoLocation
Geographic coordinates [latitude, longitude] to search near.
 
.PARAMETER GeoDistanceInMeters
Maximum distance in meters from GeoLocation to search for images.
 
.PARAMETER SaveUnknownPersons
Also save unknown persons detected as objects.
 
.PARAMETER SessionOnly
Use alternative settings stored in session for AI preferences like Language,
Image collections, etc.
 
.PARAMETER ClearSession
Clear alternative settings stored in session for AI preferences like Language,
Image collections, etc.
 
.PARAMETER SkipSession
Dont use alternative settings stored in session for AI preferences like
Language, Image collections, etc.
 
.EXAMPLE
Save-FoundImageObjects -Objects "car", "tree" -OutputDirectory "C:\CroppedObjects"
 
.EXAMPLE
saveimageObjects -Any "sunset" -SaveUnknownPersons
#>

function Save-FoundImageObjects {

    [CmdletBinding()]
    [System.Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSUseSingularNouns', '')]
    [OutputType([Object[]], [System.Collections.Generic.List[Object]], [string])]
    [Alias('saveimageObjects')]

    param(
        ###############################################################################
        [Parameter(
            Position = 0,
            Mandatory = $false,
            HelpMessage = 'Will match any of all the possible meta data types.'
        )]
        [string[]] $Any = @(),
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'The description text to look for, wildcards allowed.'
        )]
        [string[]] $DescriptionSearch = @(),
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'The keywords to look for, wildcards allowed.'
        )]
        [string[]] $Keywords = @(),
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'People to look for, wildcards allowed.'
        )]
        [string[]] $People = @(),
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Objects to look for, wildcards allowed.'
        )]
        [string[]] $Objects = @(),
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Scenes to look for, wildcards allowed.'
        )]
        [string[]] $Scenes = @(),
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Picture types to filter by, wildcards allowed.'
        )]
        [string[]] $PictureType = @(),
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Style types to filter by, wildcards allowed.'
        )]
        [string[]] $StyleType = @(),
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Overall moods to filter by, wildcards allowed.'
        )]
        [string[]] $OverallMood = @(),
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Path to the SQLite database file.'
        )]
        [string] $DatabaseFilePath,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Title for the image gallery.'
        )]
        [string] $Title,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Description for the image gallery.'
        )]
        [string] $Description,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Language for descriptions and keywords.'
        )]
        [ValidateSet(
            'Afrikaans',
            'Akan',
            'Albanian',
            'Amharic',
            'Arabic',
            'Armenian',
            'Azerbaijani',
            'Basque',
            'Belarusian',
            'Bemba',
            'Bengali',
            'Bihari',
            'Bosnian',
            'Breton',
            'Bulgarian',
            'Cambodian',
            'Catalan',
            'Cherokee',
            'Chichewa',
            'Chinese (Simplified)',
            'Chinese (Traditional)',
            'Corsican',
            'Croatian',
            'Czech',
            'Danish',
            'Dutch',
            'English',
            'Esperanto',
            'Estonian',
            'Ewe',
            'Faroese',
            'Filipino',
            'Finnish',
            'French',
            'Frisian',
            'Ga',
            'Galician',
            'Georgian',
            'German',
            'Greek',
            'Guarani',
            'Gujarati',
            'Haitian Creole',
            'Hausa',
            'Hawaiian',
            'Hebrew',
            'Hindi',
            'Hungarian',
            'Icelandic',
            'Igbo',
            'Indonesian',
            'Interlingua',
            'Irish',
            'Italian',
            'Japanese',
            'Javanese',
            'Kannada',
            'Kazakh',
            'Kinyarwanda',
            'Kirundi',
            'Kongo',
            'Korean',
            'Krio (Sierra Leone)',
            'Kurdish',
            'Kurdish (Soranî)',
            'Kyrgyz',
            'Laothian',
            'Latin',
            'Latvian',
            'Lingala',
            'Lithuanian',
            'Lozi',
            'Luganda',
            'Luo',
            'Macedonian',
            'Malagasy',
            'Malay',
            'Malayalam',
            'Maltese',
            'Maori',
            'Marathi',
            'Mauritian Creole',
            'Moldavian',
            'Mongolian',
            'Montenegrin',
            'Nepali',
            'Nigerian Pidgin',
            'Northern Sotho',
            'Norwegian',
            'Norwegian (Nynorsk)',
            'Occitan',
            'Oriya',
            'Oromo',
            'Pashto',
            'Persian',
            'Polish',
            'Portuguese (Brazil)',
            'Portuguese (Portugal)',
            'Punjabi',
            'Quechua',
            'Romanian',
            'Romansh',
            'Runyakitara',
            'Russian',
            'Scots Gaelic',
            'Serbian',
            'Serbo-Croatian',
            'Sesotho',
            'Setswana',
            'Seychellois Creole',
            'Shona',
            'Sindhi',
            'Sinhalese',
            'Slovak',
            'Slovenian',
            'Somali',
            'Spanish',
            'Spanish (Latin American)',
            'Sundanese',
            'Swahili',
            'Swedish',
            'Tajik',
            'Tamil',
            'Tatar',
            'Telugu',
            'Thai',
            'Tigrinya',
            'Tonga',
            'Tshiluba',
            'Tumbuka',
            'Turkish',
            'Turkmen',
            'Twi',
            'Uighur',
            'Ukrainian',
            'Urdu',
            'Uzbek',
            'Vietnamese',
            'Welsh',
            'Wolof',
            'Xhosa',
            'Yiddish',
            'Yoruba',
            'Zulu')]
        [string] $Language,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = ('Array of directory path-like search strings to ' +
                "filter images by path (SQL LIKE patterns, e.g. '%\\2024\\%')")
        )]
        [string[]] $PathLike = @(),
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            ValueFromPipeline = $true,
            HelpMessage = ('Accepts search results from a previous -PassThru ' +
                'call to regenerate the view.')
        )]
        [System.Object[]] $InputObject,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Directory to save cropped Object images.'
        )]
        [string] $OutputDirectory = '.\',
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Database path for preference data files'
        )]
        [Alias('DatabasePath')]
        [string] $PreferencesDatabasePath,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Array of directory paths to search for images.'
        )]
        [Alias('imagespath','directories','imgdirs','imagedirectory')]
        [string[]] $ImageDirectories,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = ('The directory containing face images organized by ' +
                'person folders.')
        )]
        [string] $FacesDirectory,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Geographic coordinates [latitude, longitude] to search near.'
        )]
        [double[]] $GeoLocation,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = ('Maximum distance in meters from GeoLocation to ' +
                'search for images.')
        )]
        [double] $GeoDistanceInMeters = 1000,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Delay in milliseconds for sending keys.'
        )]
        [Alias('DelayMilliSeconds')]
        [int] $SendKeyDelayMilliSeconds,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Accept-Language header for browser.'
        )]
        [Alias('lang','locale')]
        [string] $AcceptLang,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Monitor to use for browser window.'
        )]
        [Alias('m','mon')]
        [int] $Monitor,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Set browser window left position.'
        )]
        [int] $Left,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Set browser window right position.'
        )]
        [int] $Right,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Set browser window top position.'
        )]
        [int] $Top,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Set browser window bottom position.'
        )]
        [int] $Bottom,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Embed images as base64.'
        )]
        [switch] $EmbedImages,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Switch to disable fallback behavior.'
        )]
        [switch] $NoFallback,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Never rebuild the image index database.'
        )]
        [switch] $NeverRebuild,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Show results in browser.'
        )]
        [Alias('show','s')]
        [switch] $ShowInBrowser,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Send Escape key to browser.'
        )]
        [Alias('Escape')]
        [switch] $SendKeyEscape,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Hold keyboard focus in browser.'
        )]
        [Alias('HoldKeyboardFocus')]
        [switch] $SendKeyHoldKeyboardFocus,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Use Shift+Enter for browser input.'
        )]
        [Alias('UseShiftEnter')]
        [switch] $SendKeyUseShiftEnter,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Show images without borders.'
        )]
        [Alias('nb')]
        [switch] $NoBorders,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Show images side by side.'
        )]
        [Alias('sbs')]
        [switch] $SideBySide,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Show only pictures.'
        )]
        [Alias('NoMetadata','OnlyPictures')]
        [switch] $ShowOnlyPictures,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Enable interactive mode.'
        )]
        [Alias('i','editimages')]
        [switch] $Interactive,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Open browser in private/incognito mode.'
        )]
        [Alias('incognito','inprivate')]
        [switch] $Private,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Use Microsoft Edge browser.'
        )]
        [Alias('e')]
        [switch] $Edge,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Use Google Chrome browser.'
        )]
        [Alias('ch')]
        [switch] $Chrome,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Use Chromium browser.'
        )]
        [Alias('c')]
        [switch] $Chromium,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Use Mozilla Firefox browser.'
        )]
        [Alias('ff')]
        [switch] $Firefox,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Show browser window.'
        )]
        [Alias('sw')]
        [switch] $ShowWindow,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Center browser window.'
        )]
        [switch] $Centered,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Open browser in application mode.'
        )]
        [Alias('a','app','appmode')]
        [switch] $ApplicationMode,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Disable browser extensions.'
        )]
        [Alias('de','ne','NoExtensions')]
        [switch] $NoBrowserExtensions,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Disable popup blocker in browser.'
        )]
        [Alias('allowpopups')]
        [switch] $DisablePopupBlocker,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Restore focus to previous window.'
        )]
        [Alias('rf','bg')]
        [switch] $RestoreFocus,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Open browser in new window.'
        )]
        [Alias('nw','new')]
        [switch] $NewWindow,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Return only HTML from browser.'
        )]
        [switch] $OnlyReturnHtml,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Focus browser window.'
        )]
        [Alias('fw','focus')]
        [switch] $FocusWindow,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Set browser window to foreground.'
        )]
        [Alias('fg')]
        [switch] $SetForeground,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Maximize browser window.'
        )]
        [switch] $Maximize,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Filter images that contain nudity.'
        )]
        [switch] $HasNudity,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Filter images that do NOT contain nudity.'
        )]
        [switch] $NoNudity,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Filter images that contain explicit content.'
        )]
        [switch] $HasExplicitContent,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Filter images that do NOT contain explicit content.'
        )]
        [switch] $NoExplicitContent,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Force rebuild of the image index database.'
        )]
        [switch] $ForceIndexRebuild,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = 'Also save unknown persons detected as objects.'
        )]
        [switch] $SaveUnknownPersons,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = ('Use alternative settings stored in session for AI ' +
                'preferences like Language, Image collections, etc')
        )]
        [switch] $SessionOnly,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = ('Clear alternative settings stored in session for AI ' +
                'preferences like Language, Image collections, etc')
        )]
        [switch] $ClearSession,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = ('Dont use alternative settings stored in session ' +
                'for AI preferences like Language, Image collections, etc')
        )]
        [Alias('FromPreferences')]
        [switch] $SkipSession
        ###############################################################################
    )


    ###############################################################################
    begin {

        # copy parameters for the ai meta language function to resolve language
        $params = GenXdev.Helpers\Copy-IdenticalParamValues `
            -BoundParameters $PSBoundParameters `
            -FunctionName 'GenXdev.AI\Get-AIMetaLanguage' `
            -DefaultValues (Microsoft.PowerShell.Utility\Get-Variable `
                -Scope Local `
                -ErrorAction SilentlyContinue)

        # resolve the language parameter using ai meta language function
        $language = GenXdev.AI\Get-AIMetaLanguage @params

        # initialize information tracking object for processing statistics
        $info = @{
            resultCount = 0
        }

        # process the any parameter if provided to expand search criteria
        if ($null -ne $Any -and $Any.Length -gt 0) {

            # transform each entry in any parameter to include wildcards if needed
            $any = @($Any |
                    Microsoft.PowerShell.Core\ForEach-Object {

                        # trim whitespace from the entry
                        $entry = $_.Trim()

                        # add wildcards if no wildcard characters are present
                        if ($entry.IndexOfAny([char[]]@('*', '?')) -lt 0) {

                            "*$entry*"
                        }
                        else {
                            $_
                        }
                    })

            # merge any parameter values with existing search criteria arrays
            $descriptionSearch = $null -ne $DescriptionSearch ?
                ($DescriptionSearch + $Any) : $Any

            $keywords = $null -ne $Keywords ?
                ($Keywords + $Any) : $Any

            $people = $null -ne $People ?
                ($People + $Any) : $Any

            $objects = $null -ne $Objects ?
                ($Objects + $Any) : $Any

            $scenes = $null -ne $Scenes ?
                ($Scenes + $Any) : $Any

            $pictureType = $null -ne $PictureType ?
                ($PictureType + $Any) : $Any

            $styleType = $null -ne $StyleType ?
                ($StyleType + $Any) : $Any

            $overallMood = $null -ne $OverallMood ?
                ($OverallMood + $Any) : $Any
        }
    }
    ###############################################################################
    process {

        # ensure the output directory exists by expanding the path
        $outputDirectory = GenXdev.FileSystem\Expand-Path $OutputDirectory `
            -CreateDirectory

        Microsoft.PowerShell.Utility\Write-Verbose (
            "using output directory: $outputDirectory")

        # define internal function to save image objects from processed images
        function saveImage {

            param ($InputObject)

            # process each image object in the pipeline
            $InputObject |
                Microsoft.PowerShell.Core\ForEach-Object {

                    # get current image object and validate it has required data
                    $image = $_
                    if ($null -eq $image -or -not $image.path) { return }

                    Microsoft.PowerShell.Utility\Write-Verbose (
                        "processing image: $($image.path)")

                    # extract object detection data if available
                    $objects = $null
                    if ($image.objects -and $image.objects.objects) {
                        $objects = $image.objects.objects
                    }

                    # track coordinates of saved object rectangles to avoid duplicates
                    $savedObjectRects = @()

                    # process detected objects if any exist
                    if ($objects) {

                        # get the full path to the source image file
                        $imgPath = $image.path

                        try {
                            # load the source image using system drawing classes
                            $imgObj = [System.Drawing.Image]::FromFile($imgPath)

                            try {
                                # extract base filename without extension for output naming
                                $imgBase = [System.IO.Path]::GetFileNameWithoutExtension(
                                    $imgPath)

                                # initialize object index counter for unique naming
                                $objectIdx = 0

                                # iterate through each detected object to crop and save
                                foreach ($obj in $objects) {

                                    # calculate safe bounding rectangle coordinates within image bounds
                                    $x_min = [Math]::Max(0,
                                        [Math]::Min($obj.x_min, $imgObj.Width - 1))
                                    $y_min = [Math]::Max(0,
                                        [Math]::Min($obj.y_min, $imgObj.Height - 1))
                                    $x_max = [Math]::Max($x_min + 1,
                                        [Math]::Min($obj.x_max, $imgObj.Width))
                                    $y_max = [Math]::Max($y_min + 1,
                                        [Math]::Min($obj.y_max, $imgObj.Height))

                                    # calculate width and height of the crop rectangle
                                    $width = $x_max - $x_min
                                    $height = $y_max - $y_min

                                    # skip invalid rectangles with zero or negative dimensions
                                    if ($width -le 0 -or $height -le 0) { continue }

                                    # create rectangle objects for cropping operation
                                    $cropRect = [System.Drawing.Rectangle]::new(
                                        $x_min, $y_min, $width, $height)

                                    # create new bitmap to hold the cropped object
                                    $croppedBitmap = [System.Drawing.Bitmap]::new(
                                        $width, $height)

                                    # create graphics context for drawing the cropped region
                                    $croppedGraphics = [System.Drawing.Graphics]::FromImage(
                                        $croppedBitmap)

                                    # define destination rectangle for the cropped image
                                    $destRect = [System.Drawing.Rectangle]::new(
                                        0, 0, $width, $height)

                                    # draw the cropped portion of source image to new bitmap
                                    $null = $croppedGraphics.DrawImage($imgObj,
                                        $destRect, $cropRect,
                                        [System.Drawing.GraphicsUnit]::Pixel)

                                    # dispose graphics context to free resources
                                    $croppedGraphics.Dispose()

                                    # generate sanitized label for the detected object
                                    $objectLabel = if ($obj.label) {
                                        $obj.label
                                    } else {
                                        "object$objectIdx"
                                    }

                                    # remove invalid filename characters from object label
                                    $objectLabel = $objectLabel `
                                        -replace '[^\w\-_]', '_'

                                    # construct output filename with object information
                                    $outFile = Microsoft.PowerShell.Management\Join-Path `
                                        $OutputDirectory `
                                    ("${imgBase}_${objectLabel}_${objectIdx}.png")

                                    Microsoft.PowerShell.Utility\Write-Verbose (
                                        "saving object to: $outFile")

                                    # save the cropped bitmap as png file
                                    $croppedBitmap.Save($outFile,
                                        [System.Drawing.Imaging.ImageFormat]::Png)

                                    # dispose bitmap to free memory
                                    $croppedBitmap.Dispose()

                                    # record saved object coordinates for overlap checking
                                    $savedObjectRects += @{
                                        x_min = $x_min
                                        y_min = $y_min
                                        x_max = $x_max
                                        y_max = $y_max
                                    }

                                    # increment object index for next iteration
                                    $objectIdx++
                                }

                                # process unknown persons if the switch is enabled
                                if ($SaveUnknownPersons -and
                                    $image.people -and
                                    $image.people.predictions) {

                                    Microsoft.PowerShell.Utility\Write-Verbose (
                                        'processing unknown persons')

                                    # initialize person index counter for unique naming
                                    $personIdx = 0

                                    # iterate through detected person predictions
                                    foreach ($person in $image.people.predictions) {

                                        try {
                                            # calculate safe person bounding rectangle coordinates
                                            $x_min = [Math]::Max(0,
                                                [Math]::Min($person.x_min,
                                                    $imgObj.Width - 1))
                                            $y_min = [Math]::Max(0,
                                                [Math]::Min($person.y_min,
                                                    $imgObj.Height - 1))
                                            $x_max = [Math]::Max($x_min + 1,
                                                [Math]::Min($person.x_max,
                                                    $imgObj.Width))
                                            $y_max = [Math]::Max($y_min + 1,
                                                [Math]::Min($person.y_max,
                                                    $imgObj.Height))

                                            # calculate person rectangle dimensions
                                            $width = $x_max - $x_min
                                            $height = $y_max - $y_min

                                            # skip invalid person rectangles
                                            if ($width -le 0 -or $height -le 0) {
                                                continue
                                            }

                                            # check for overlap with previously saved objects
                                            $overlap = $false
                                            foreach ($rect in $savedObjectRects) {

                                                # test for rectangle intersection using bounds checking
                                                if ((($x_min -le $rect.x_max) -and
                                                    ($x_max -ge $rect.x_min)) -and
                                                    (($y_min -le $rect.y_max) -and
                                                    ($y_max -ge $rect.y_min))) {

                                                    $overlap = $true
                                                    break
                                                }
                                            }

                                            # skip persons that overlap with saved objects
                                            if ($overlap) { continue }

                                            # create crop rectangle for person detection
                                            $cropRect = [System.Drawing.Rectangle]::new(
                                                $x_min, $y_min, $width, $height)

                                            # create bitmap for cropped person image
                                            $croppedBitmap = [System.Drawing.Bitmap]::new(
                                                $width, $height)

                                            # create graphics context for person cropping
                                            $croppedGraphics = [System.Drawing.Graphics]::FromImage(
                                                $croppedBitmap)

                                            # define destination rectangle for person crop
                                            $destRect = [System.Drawing.Rectangle]::new(
                                                0, 0, $width, $height)

                                            # draw cropped person region to new bitmap
                                            $null = $croppedGraphics.DrawImage($imgObj,
                                                $destRect, $cropRect,
                                                [System.Drawing.GraphicsUnit]::Pixel)

                                            # dispose graphics context
                                            $croppedGraphics.Dispose()

                                            # construct filename for unknown person image
                                            $outFile = Microsoft.PowerShell.Management\Join-Path `
                                                $OutputDirectory `
                                            ("${imgBase}_unknownperson_${personIdx}.png")

                                            Microsoft.PowerShell.Utility\Write-Verbose (
                                                "saving unknown person to: $outFile")

                                            # save person bitmap as png file
                                            $croppedBitmap.Save($outFile,
                                                [System.Drawing.Imaging.ImageFormat]::Png)

                                            # dispose person bitmap
                                            $croppedBitmap.Dispose()

                                            # increment person index counter
                                            $personIdx++
                                        }
                                        catch {
                                            # log warning for person processing failures
                                            Microsoft.PowerShell.Utility\Write-Verbose (
                                                ('failed to crop/save unknown person ' +
                                                "for $($imgPath): $_"))
                                        }
                                    }
                                }
                            }
                            finally {
                                # ensure source image object is disposed to free memory
                                if ($null -ne $imgObj) {
                                    $imgObj.Dispose()
                                }
                            }
                        }
                        catch {
                            # log warning for general image processing failures
                            Microsoft.PowerShell.Utility\Write-Verbose (
                                ("failed to crop/save objects for $($imgPath): $_"))
                        }
                    }

                    # increment result counter for statistics tracking
                    $info.resultCount++

                    # output the processed image object to pipeline
                    Microsoft.PowerShell.Utility\Write-Output $image
                }
        }

        # process input based on whether explicit input objects are provided
        if ($null -ne $InputObject) {

            Microsoft.PowerShell.Utility\Write-Verbose (
                'processing provided input objects')

            # process each input object through the save image function
            $InputObject |
                Microsoft.PowerShell.Core\ForEach-Object { saveImage $_ }
        }
        else {
            Microsoft.PowerShell.Utility\Write-Verbose (
                'searching for indexed images')

            # copy parameters for find-indexedimage function call
            $params = GenXdev.Helpers\Copy-IdenticalParamValues `
                -BoundParameters $PSBoundParameters `
                -FunctionName 'GenXdev.AI\Find-IndexedImage' `
                -DefaultValues (Microsoft.PowerShell.Utility\Get-Variable `
                    -Scope Local `
                    -ErrorAction SilentlyContinue)

            # find indexed images and process each through save image function
            GenXdev.AI\Find-IndexedImage @params |
                Microsoft.PowerShell.Core\ForEach-Object { saveImage $_ }
        }
    }
    ###############################################################################
    end {

        # output processing statistics to verbose stream
        Microsoft.PowerShell.Utility\Write-Verbose (
            "processed $($info.resultCount) images")
    }
}
###############################################################################