Functions/GenXdev.AI.Queries/Invoke-ImageObjectsUpdate.ps1

################################################################################
<#
.SYNOPSIS
Updates object detection metadata for image files in a specified directory.
 
.DESCRIPTION
This function processes images in a specified directory to detect objects using
artificial intelligence. It creates JSON metadata files containing detected
objects, their positions, confidence scores, and labels. The function supports
batch processing with configurable confidence thresholds and can optionally
skip existing metadata files or retry previously failed detections.
 
.PARAMETER ImageDirectory
The directory path containing images to process. Can be relative or absolute
path. Default is the current directory.
 
.PARAMETER Recurse
If specified, processes images in the specified directory and all
subdirectories recursively.
 
.PARAMETER OnlyNew
If specified, only processes images that don't already have object metadata
files or have empty metadata files.
 
.PARAMETER RetryFailed
If specified, retries processing previously failed images that have empty
metadata files or contain error indicators.
 
.PARAMETER NoDockerInitialize
Skip Docker initialization when this switch is used. Used when already called
by parent function to avoid redundant container setup.
 
.PARAMETER ConfidenceThreshold
Minimum confidence threshold (0.0-1.0) for object detection. Objects detected
with confidence below this threshold will be filtered out. Default is 0.5.
 
.PARAMETER Force
Force rebuild of Docker container and remove existing data when this switch
is used. This will recreate the entire detection environment.
 
.PARAMETER UseGPU
Use GPU-accelerated version when this switch is used. Requires an NVIDIA GPU
with appropriate drivers and CUDA support.
 
.PARAMETER ContainerName
The name for the Docker container running the object detection service.
Default is "deepstack_face_recognition".
 
.PARAMETER VolumeName
The name for the Docker volume for persistent storage of detection models
and data. Default is "deepstack_face_data".
 
.PARAMETER ServicePort
The port number for the DeepStack service to listen on. Must be between
1 and 65535. Default is 5000.
 
.PARAMETER HealthCheckTimeout
Maximum time in seconds to wait for service health check before timing out.
Must be between 10 and 300 seconds. Default is 60.
 
.PARAMETER HealthCheckInterval
Interval in seconds between health check attempts when waiting for service
startup. Must be between 1 and 10 seconds. Default is 3.
 
.PARAMETER ImageName
Custom Docker image name to use instead of the default DeepStack image.
Allows using alternative object detection models or configurations.
 
.PARAMETER FacesPath
The path inside the container where faces and detection data are stored.
Default is "/datastore" which matches DeepStack's expected structure.
 
.EXAMPLE
Invoke-ImageObjectsUpdate -ImageDirectory "C:\Photos" -Recurse
 
This example processes all images in C:\Photos and all subdirectories using
default settings with 0.5 confidence threshold.
 
.EXAMPLE
Invoke-ImageObjectsUpdate "C:\Photos" -RetryFailed -OnlyNew
 
This example processes only new images and retries previously failed ones
in the C:\Photos directory using positional parameter syntax.
 
.EXAMPLE
Invoke-ImageObjectsUpdate -ImageDirectory "C:\Photos" -UseGPU `
    -ConfidenceThreshold 0.7
 
This example uses GPU acceleration with higher confidence threshold of 0.7
for more accurate but fewer object detections.
#>

function Invoke-ImageObjectsUpdate {

    [CmdletBinding()]
    [Alias("objectdetection")]

    param(
        #######################################################################
        [Parameter(
            Position = 0,
            Mandatory = $false,
            HelpMessage = "The directory path containing images to process"
        )]
        [string] $ImageDirectory = ".\",
        #######################################################################
        [Parameter(
            Position = 1,
            Mandatory = $false,
            HelpMessage = "Minimum confidence threshold for object detection"
        )]
        [ValidateRange(0.0, 1.0)]
        [double] $ConfidenceThreshold = 0.5,
        #######################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = "Recurse through subdirectories"
        )]
        [switch] $Recurse,
        #######################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = "Skip images that already have metadata files"
        )]
        [switch] $OnlyNew,
        #######################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = "Retry processing previously failed images"
        )]
        [switch] $RetryFailed,
        #######################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = "Skip Docker initialization for parent function calls"
        )]
        [switch] $NoDockerInitialize,
        #######################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = "Force rebuild Docker container and remove data"
        )]
        [Alias("ForceRebuild")]
        [switch] $Force,
        #######################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = "Use GPU-accelerated version requiring NVIDIA GPU"
        )]
        [switch] $UseGPU,
        #######################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = "The name for the Docker container"
        )]
        [ValidateNotNullOrEmpty()]
        [string] $ContainerName = "deepstack_face_recognition",
        #######################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = "The name for Docker volume for persistent storage"
        )]
        [ValidateNotNullOrEmpty()]
        [string] $VolumeName = "deepstack_face_data",
        #######################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = "The port number for the DeepStack service"
        )]
        [ValidateRange(1, 65535)]
        [int] $ServicePort = 5000,
        #######################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = "Maximum seconds to wait for service health check"
        )]
        [ValidateRange(10, 300)]
        [int] $HealthCheckTimeout = 60,
        #######################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = "Interval in seconds between health check attempts"
        )]
        [ValidateRange(1, 10)]
        [int] $HealthCheckInterval = 3,
        #######################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = "Custom Docker image name to use for detection"
        )]
        [ValidateNotNullOrEmpty()]
        [string] $ImageName,
        #######################################################################
        [Parameter(
            Mandatory = $false,
            HelpMessage = "Container path where faces are stored"
        )]
        [ValidateNotNullOrEmpty()]
        [string] $FacesPath = "/datastore"
        #######################################################################
    )
    begin {

        # convert the possibly relative path to an absolute path for reliable access
        $path = GenXdev.FileSystem\Expand-Path $ImageDirectory

        # ensure the target directory exists before proceeding with any operations
        if (-not [System.IO.Directory]::Exists($path)) {

            Microsoft.PowerShell.Utility\Write-Host (
                "The directory '$path' does not exist."
            )
            return
        }

        # output verbose information about the processing directory
        Microsoft.PowerShell.Utility\Write-Verbose (
            "Processing images for object detection in directory: $path"
        )
    }

    process {

        # retrieve all supported image files from the specified directory
        # applying recursion only if the recurse switch was provided
        Microsoft.PowerShell.Management\Get-ChildItem `
            -Path "$path\*.jpg", "$path\*.jpeg", "$path\*.png" `
            -Recurse:$Recurse `
            -File `
            -ErrorAction SilentlyContinue |
            Microsoft.PowerShell.Core\ForEach-Object {

            try {

                # store the full path to the current image for better readability
                $image = $PSItem.FullName

                # handle retry mode for previously failed images
                if ($RetryFailed) {

                    if ([System.IO.File]::Exists("$($image):objects.json")) {

                        # read existing metadata file content
                        $content = [System.IO.File]::ReadAllText(
                            "$($image):objects.json"
                        )

                        # check if file is empty or contains failure indicators
                        if ($content.StartsWith("{}") -or
                            $content -eq (
                                '{"predictions":null,"count":0,"objects":[]}'
                            )) {

                            # reset content to empty for reprocessing
                            $content = "{}"
                        }
                    }
                }

                # output verbose information about current image being processed
                Microsoft.PowerShell.Utility\Write-Verbose (
                    "Processing image for object detection: $image"
                )

                # remove read-only attribute if present to ensure file modification
                if ($PSItem.Attributes -band
                    [System.IO.FileAttributes]::ReadOnly) {

                    $PSItem.Attributes = $PSItem.Attributes -bxor
                        [System.IO.FileAttributes]::ReadOnly
                }

                # construct path for the metadata file
                $metadataFilePath = "$($image):objects.json"

                # check if a metadata file already exists for this image
                $fileExists = [System.IO.File]::Exists($metadataFilePath)

                # read existing content or use empty object as default
                $content = if ($fileExists) {
                    [System.IO.File]::ReadAllText($metadataFilePath)
                } else {
                    "{}"
                }

                # determine if we should process this image based on conditions
                if ((-not $OnlyNew) -or
                    (-not $fileExists) -or
                    ($content -eq "{}") -or
                    (-not $content.Contains("predictions"))) {

                    # create an empty metadata file as placeholder if missing
                    if (-not $fileExists) {

                        $null = [System.IO.File]::WriteAllText(
                            $metadataFilePath,
                            "{}"
                        )

                        # output verbose confirmation of metadata file creation
                        Microsoft.PowerShell.Utility\Write-Verbose (
                            "Created new object metadata file for: $image"
                        )
                    }

                    # obtain object detection data using ai detection technology
                    $objectData = GenXdev.AI\Get-ImageDetectedObjects `
                        -ImagePath $image `
                        -NoDockerInitialize:$NoDockerInitialize `
                        -ConfidenceThreshold $ConfidenceThreshold

                    # process the detection results into structured data format
                    $processedData = if ($objectData -and
                        $objectData.success -and
                        $objectData.predictions) {

                        # extract predictions array from detection results
                        $predictions = $objectData.predictions

                        # create array of object labels from predictions
                        $objectLabels = $predictions |
                            Microsoft.PowerShell.Core\ForEach-Object {
                                $_.label
                            }

                        # group objects by label to get counts
                        $objectCounts = $objectLabels |
                            Microsoft.PowerShell.Utility\Group-Object `
                                -NoElement

                        # construct structured data object with all metadata
                        $data = @{
                            success = $true
                            count = $predictions.Count
                            objects = $objectLabels
                            predictions = $predictions
                            object_counts = @{}
                        }

                        # populate object counts from grouped results
                        $objectCounts |
                            Microsoft.PowerShell.Core\ForEach-Object {
                                $data.object_counts[$_.Name] = $_.Count
                            }

                        $data
                    } else {

                        # create empty structure if no objects are detected
                        @{
                            success = $true
                            count = 0
                            objects = @()
                            predictions = @()
                            object_counts = @{}
                        }
                    }

                    # convert processed data to json format for storage
                    $objects = $processedData |
                        Microsoft.PowerShell.Utility\ConvertTo-Json `
                            -Depth 20 `
                            -WarningAction SilentlyContinue

                    # output verbose confirmation of detection analysis completion
                    Microsoft.PowerShell.Utility\Write-Verbose (
                        "Received object detection analysis for: $image"
                    )

                    try {

                        # re-parse and compress json for consistent formatting
                        $newContent = ($objects |
                            Microsoft.PowerShell.Utility\ConvertFrom-Json |
                            Microsoft.PowerShell.Utility\ConvertTo-Json `
                                -Compress `
                                -Depth 20 `
                                -WarningAction SilentlyContinue)

                        # ensure proper empty structure format
                        if ($newContent -eq (
                            '{"predictions":null,"count":0,"objects":[]}'
                        )) {

                            $newContent = (
                                '{"success":true,"count":0,"objects":[],' +
                                '"predictions":[],"object_counts":{}}'
                            )
                        }

                        # save the processed object data to metadata file
                        [System.IO.File]::WriteAllText(
                            $metadataFilePath,
                            $newContent
                        )

                        # output verbose confirmation of successful save
                        Microsoft.PowerShell.Utility\Write-Verbose (
                            "Successfully saved object metadata for: $image"
                        )
                    }
                    catch {

                        # log any errors that occur during metadata processing
                        Microsoft.PowerShell.Utility\Write-Warning (
                            "$PSItem`r`n$objects"
                        )
                    }
                }
            }
            catch {

                # log any errors that occur during image processing
                Microsoft.PowerShell.Utility\Write-Warning (
                    "Error processing image '$image': " +
                    "$($_.Exception.Message)"
                )
            }
        }
    }    end {
    }
}
################################################################################