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

###############################################################################
<#
.SYNOPSIS
Saves cropped face images from indexed image search results.
 
.DESCRIPTION
This function takes image search results and extracts/saves individual face regions as separate image files.
It can search for faces using various criteria and save them to a specified output directory.
#>

function Save-FoundImageFaces {

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

    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 = "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 = "Path to the SQLite database file."
        )]
        [string] $DatabaseFilePath,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = "Show results in a browser gallery."
        )]
        [Alias("show", "s")]
        [switch] $ShowInBrowser,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = "Return image data as objects."
        )]
        [switch] $PassThru,
        ###############################################################################
        [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 = "Browser accept-language header."
        )]
        [string] $AcceptLang,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = "Monitor to use for display."
        )]
        [int] $Monitor,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = "Initial width of browser window."
        )]
        [int] $Width,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = "Initial height of browser window."
        )]
        [int] $Height,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = "Initial X position of browser window."
        )]
        [int] $X,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = "Initial Y position of browser window."
        )]
        [int] $Y,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = "Enable interactive browser features."
        )]
        [Alias("i", "editimages")]
        [switch] $Interactive,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = "Open in private/incognito mode."
        )]
        [switch] $Private,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = "Force enable debugging port."
        )]
        [switch] $Force,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = "Open in Microsoft Edge."
        )]
        [switch] $Edge,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = "Open in Google Chrome."
        )]
        [switch] $Chrome,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = "Open in Chromium-based browser."
        )]
        [switch] $Chromium,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = "Open in Firefox."
        )]
        [switch] $Firefox,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = "Open in all browsers."
        )]
        [switch] $All,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = "Open in fullscreen mode."
        )]
        [switch] $FullScreen,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = "Place window on left side."
        )]
        [switch] $Left,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = "Place window on right side."
        )]
        [switch] $Right,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = "Place window on top."
        )]
        [switch] $Top,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = "Place window on bottom."
        )]
        [switch] $Bottom,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = "Center the window."
        )]
        [switch] $Centered,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = "Hide browser controls."
        )]
        [switch] $ApplicationMode,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = "Disable browser extensions."
        )]
        [switch] $NoBrowserExtensions,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = "Disable popup blocker."
        )]
        [switch] $DisablePopupBlocker,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = "Restore PowerShell focus."
        )]
        [switch] $RestoreFocus,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = "Create new browser window."
        )]
        [switch] $NewWindow,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = "Only return HTML."
        )]
        [switch] $OnlyReturnHtml,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = "Embed images as base64."
        )]
        [switch] $EmbedImages,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = "Force rebuild of the image index database."
        )]
        [switch] $ForceIndexRebuild,
        ###############################################################################
        [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.")
        )]
        [object[]] $InputObject,
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = "Directory to save cropped face images."
        )]
        [string] $OutputDirectory = ".\",
        ###############################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = "Also save unknown persons detected as objects."
        )]
        [switch] $SaveUnknownPersons,
        ########################################################################
        # Use alternative settings stored in session for AI preferences like Language, Image collections, etc
        [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 {
        $params = GenXdev.Helpers\Copy-IdenticalParamValues `
            -BoundParameters $PSBoundParameters `
            -FunctionName "GenXdev.AI\Get-AIMetaLanguage" `
            -DefaultValues (Microsoft.PowerShell.Utility\Get-Variable -Scope Local -ErrorAction SilentlyContinue)
        $Language = GenXdev.AI\Get-AIMetaLanguage @params -Language (
            [String]::IsNullOrWhiteSpace($Language) ?
            (GenXdev.Helpers\Get-DefaultWebLanguage) :
            $Language
        )


        $info = @{
            resultCount = 0
        }

        if ($null -ne $Any -and
            $Any.Length -gt 0) {

            $Any = @($Any | Microsoft.PowerShell.Core\ForEach-Object {

                $entry = $_.Trim()

                if ($entry.IndexOfAny([char[]]@('*', '?')) -lt 0) {

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

            # if Any parameter is used, treat it as a set of keywords
            $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 output directory exists
        $OutputDirectory = GenXdev.FileSystem\Expand-Path $OutputDirectory -CreateDirectory

        # if InputObject is provided, convert each item to an image object
        function saveImage {

            param ($InputObject)

            $InputObject | Microsoft.PowerShell.Core\ForEach-Object {
                $image = $_
                if ($null -eq $image -or -not $image.path) { return }

                # Handle face/people recognition data
                $people = $null
                if ($image.people -and $image.people.predictions) {
                    $people = $image.people.predictions
                } elseif ($image.faces -and $image.faces.predictions) {
                    $people = $image.faces.predictions
                }
                $savedFaceRects = @()
                if ($people) {
                    $imgPath = $image.path
                    try {
                        $imgObj = [System.Drawing.Image]::FromFile($imgPath)
                        try {
                            $imgBase = [System.IO.Path]::GetFileNameWithoutExtension($imgPath)
                            $faceIdx = 0
                            foreach ($face in $people) {
                                $x_min = [Math]::Max(0, [Math]::Min($face.x_min, $imgObj.Width - 1))
                                $y_min = [Math]::Max(0, [Math]::Min($face.y_min, $imgObj.Height - 1))
                                $x_max = [Math]::Max($x_min + 1, [Math]::Min($face.x_max, $imgObj.Width))
                                $y_max = [Math]::Max($y_min + 1, [Math]::Min($face.y_max, $imgObj.Height))
                                $width = $x_max - $x_min
                                $height = $y_max - $y_min
                                if ($width -le 0 -or $height -le 0) { continue }
                                $cropRect = [System.Drawing.Rectangle]::new($x_min, $y_min, $width, $height)
                                $croppedBitmap = [System.Drawing.Bitmap]::new($width, $height)
                                $croppedGraphics = [System.Drawing.Graphics]::FromImage($croppedBitmap)
                                $destRect = [System.Drawing.Rectangle]::new(0, 0, $width, $height)
                                $null = $croppedGraphics.DrawImage($imgObj, $destRect, $cropRect, [System.Drawing.GraphicsUnit]::Pixel)
                                $croppedGraphics.Dispose()
                                $faceLabel = if ($face.userid) { $face.userid } else { "face$faceIdx" }
                                $faceLabel = $faceLabel -replace '[^\w\-_]', '_'
                                $outFile = Microsoft.PowerShell.Management\Join-Path $OutputDirectory ("${imgBase}_${faceLabel}_${faceIdx}.png")
                                $croppedBitmap.Save($outFile, [System.Drawing.Imaging.ImageFormat]::Png)
                                $croppedBitmap.Dispose()
                                $savedFaceRects += @{x_min=$x_min;y_min=$y_min;x_max=$x_max;y_max=$y_max}
                                $faceIdx++
                            }
                            # Save unknown persons if requested
                            if ($SaveUnknownPersons -and $image.objects -and $image.objects.objects) {
                                $objIdx = 0
                                foreach ($obj in $image.objects.objects) {
                                    if ($obj.label -eq 'person') {
                                        try {
                                            $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))
                                            $width = $x_max - $x_min
                                            $height = $y_max - $y_min
                                            if ($width -le 0 -or $height -le 0) { continue }
                                            # Check if this object overlaps with any known face
                                            $overlap = $false
                                            foreach ($rect in $savedFaceRects) {
                                                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
                                                }
                                            }
                                            if ($overlap) { continue }
                                            $cropRect = [System.Drawing.Rectangle]::new($x_min, $y_min, $width, $height)
                                            $croppedBitmap = [System.Drawing.Bitmap]::new($width, $height)
                                            $croppedGraphics = [System.Drawing.Graphics]::FromImage($croppedBitmap)
                                            $destRect = [System.Drawing.Rectangle]::new(0, 0, $width, $height)
                                            $null = $croppedGraphics.DrawImage($imgObj, $destRect, $cropRect, [System.Drawing.GraphicsUnit]::Pixel)
                                            $croppedGraphics.Dispose()
                                            $outFile = Microsoft.PowerShell.Management\Join-Path $OutputDirectory ("${imgBase}_unknownperson_${objIdx}.png")
                                            $croppedBitmap.Save($outFile, [System.Drawing.Imaging.ImageFormat]::Png)
                                            $croppedBitmap.Dispose()
                                            $objIdx++
                                        }
                                        catch {
                                            Microsoft.PowerShell.Utility\Write-Warning "Failed to crop/save unknown person for $($imgPath): $_"
                                        }
                                    }
                                }
                            }
                        }
                        finally {
                            if ($null -ne $ImageObj) { $imageObj.Dispose() }
                        }
                    }
                    catch {
                        Microsoft.PowerShell.Utility\Write-Warning "Failed to crop/save faces for $($imgPath): $_"
                    }
                }
                $info.resultCount++
                Microsoft.PowerShell.Utility\Write-Output $image
            }
        }

        if ($null -ne $InputObject) {

            $InputObject | Microsoft.PowerShell.Core\ForEach-Object { saveImage $_ }
        }
        else
         {
            $params = GenXdev.Helpers\Copy-IdenticalParamValues `
            -BoundParameters $PSBoundParameters `
            -FunctionName "GenXdev.AI\Find-IndexedImage" `
            -DefaultValues (
                Microsoft.PowerShell.Utility\Get-Variable -Scope Local -ErrorAction SilentlyContinue
            )

            GenXdev.AI\Find-IndexedImage @params | Microsoft.PowerShell.Core\ForEach-Object { saveImage $_ }
         }
    }
}
################################################################################