param( [Parameter(Mandatory = $true)] [string]$TargetSignaturePath, [Parameter(Mandatory = $true)] [string]$CandidateExtractionRoot, [string]$CandidateViewsJson = "", [string]$OutputPath = "", [double]$PrimitiveWeight = 0.15, [double]$DirectionWeight = 0.15, [double]$LineLengthWeight = 0.15, [double]$CircleRadiusWeight = 0.03, [double]$ArcRadiusWeight = 0.03, [double]$ArcLengthWeight = 0.04, [double]$RelativeMidpointWeight = 0.30, [double]$RelativeFeatureWeight = 0.15 ) $ErrorActionPreference = "Stop" function Read-JsonFile([string]$Path) { if (-not (Test-Path -LiteralPath $Path)) { throw "File does not exist: $Path" } return Get-Content -LiteralPath $Path -Raw | ConvertFrom-Json } function Get-PropertyValue($Object, [string]$Name) { if ($null -eq $Object) { return $null } $property = $Object.PSObject.Properties[$Name] if ($null -eq $property) { return $null } return $property.Value } function Convert-ToNumberMap($Object) { $result = [ordered]@{} if ($null -eq $Object) { return $result } foreach ($property in $Object.PSObject.Properties) { $value = 0.0 if ($null -ne $property.Value) { $value = [double]$property.Value } $result[$property.Name] = $value } return $result } function Get-Map($Signature, [string]$Name) { return Convert-ToNumberMap (Get-PropertyValue $Signature $Name) } function Get-RelativeMap($Signature, [string]$Name) { $relative = Get-PropertyValue $Signature "relative_line_coordinates" return Convert-ToNumberMap (Get-PropertyValue $relative $Name) } function Get-MapSum($Map) { $sum = 0.0 foreach ($key in $Map.Keys) { $sum += [double]$Map[$key] } return $sum } function Get-HistogramSimilarity($CandidateMap, $TargetMap) { $keys = @($CandidateMap.Keys + $TargetMap.Keys | Select-Object -Unique) if ($keys.Count -eq 0) { return 1.0 } $intersection = 0.0 $union = 0.0 foreach ($key in $keys) { $candidateValue = 0.0 $targetValue = 0.0 if ($CandidateMap.Contains($key)) { $candidateValue = [double]$CandidateMap[$key] } if ($TargetMap.Contains($key)) { $targetValue = [double]$TargetMap[$key] } $intersection += [Math]::Min($candidateValue, $targetValue) $union += [Math]::Max($candidateValue, $targetValue) } if ($union -le 0.0) { return 1.0 } return $intersection / $union } function Get-CountSimilarity([double]$CandidateCount, [double]$TargetCount) { $maxValue = [Math]::Max($CandidateCount, $TargetCount) if ($maxValue -le 0.0) { return 1.0 } return [Math]::Min($CandidateCount, $TargetCount) / $maxValue } function Get-CandidateMetadataMap([string]$Path) { $map = @{} if ([string]::IsNullOrWhiteSpace($Path) -or -not (Test-Path -LiteralPath $Path)) { return $map } $rawItems = Read-JsonFile $Path $items = if ($rawItems -is [array]) { $rawItems } else { @($rawItems) } foreach ($item in $items) { $index = [int](Get-PropertyValue $item "index") $map[[string]$index] = $item } return $map } $target = Read-JsonFile $TargetSignaturePath $candidateRoot = Resolve-Path -LiteralPath $CandidateExtractionRoot if ([string]::IsNullOrWhiteSpace($OutputPath)) { $OutputPath = Join-Path $candidateRoot.Path "candidate-ranking-relative.json" } $weights = [ordered]@{ primitive_counts = $PrimitiveWeight line_direction_counts = $DirectionWeight line_length_histogram = $LineLengthWeight circle_radius_histogram = $CircleRadiusWeight arc_radius_histogram = $ArcRadiusWeight arc_length_histogram = $ArcLengthWeight relative_midpoint_histogram = $RelativeMidpointWeight relative_feature_histogram = $RelativeFeatureWeight } $weightSum = 0.0 foreach ($value in $weights.Values) { $weightSum += [double]$value } if ([Math]::Abs($weightSum - 1.0) -gt 0.000001) { throw "Weights must sum to 1.0. Current sum: $weightSum" } $metadata = Get-CandidateMetadataMap $CandidateViewsJson $targetMaps = @{ primitive_counts = Get-Map $target "primitive_counts" line_direction_counts = Get-Map $target "line_direction_counts" line_length_histogram = Get-Map $target "line_length_histogram" circle_radius_histogram = Get-Map $target "circle_radius_histogram" arc_radius_histogram = Get-Map $target "arc_radius_histogram" arc_length_histogram = Get-Map $target "arc_length_histogram" relative_midpoint_histogram = Get-RelativeMap $target "relative_midpoint_histogram" relative_feature_histogram = Get-RelativeMap $target "feature_histogram" } $signatureFiles = @(Get-ChildItem -LiteralPath $candidateRoot.Path -Recurse -Filter "main-view-signature.json" | Sort-Object FullName) if ($signatureFiles.Count -eq 0) { throw "No candidate signatures found under: $($candidateRoot.Path)" } $ranking = @() foreach ($signatureFile in $signatureFiles) { $candidate = Read-JsonFile $signatureFile.FullName $directoryName = Split-Path -Leaf (Split-Path -Parent $signatureFile.FullName) $index = $null if ($directoryName -match '^(\d+)_') { $index = [int]$Matches[1] } $candidateMaps = @{ primitive_counts = Get-Map $candidate "primitive_counts" line_direction_counts = Get-Map $candidate "line_direction_counts" line_length_histogram = Get-Map $candidate "line_length_histogram" circle_radius_histogram = Get-Map $candidate "circle_radius_histogram" arc_radius_histogram = Get-Map $candidate "arc_radius_histogram" arc_length_histogram = Get-Map $candidate "arc_length_histogram" relative_midpoint_histogram = Get-RelativeMap $candidate "relative_midpoint_histogram" relative_feature_histogram = Get-RelativeMap $candidate "feature_histogram" } $componentScores = [ordered]@{} $score = 0.0 foreach ($key in $weights.Keys) { $component = Get-HistogramSimilarity $candidateMaps[$key] $targetMaps[$key] $componentScores[$key] = $component $score += $component * [double]$weights[$key] } $candidatePrimitiveCounts = $candidateMaps["primitive_counts"] $targetPrimitiveCounts = $targetMaps["primitive_counts"] $candidateLineCount = if ($candidatePrimitiveCounts.Contains("line")) { [double]$candidatePrimitiveCounts["line"] } else { 0.0 } $targetLineCount = if ($targetPrimitiveCounts.Contains("line")) { [double]$targetPrimitiveCounts["line"] } else { 0.0 } $meta = $null if ($null -ne $index -and $metadata.ContainsKey([string]$index)) { $meta = $metadata[[string]$index] } $ranking += [pscustomobject][ordered]@{ rank = 0 index = $index name = $directoryName candidate_view_name = Get-PropertyValue $meta "name" direction_name = Get-PropertyValue $meta "direction_name" roll_degrees = Get-PropertyValue $meta "roll_deg" score = $score component_scores = $componentScores candidate_line_count = $candidateLineCount target_line_count = $targetLineCount line_count_score = Get-CountSimilarity $candidateLineCount $targetLineCount candidate_relative_midpoint_bins = (Get-MapSum $candidateMaps["relative_midpoint_histogram"]) target_relative_midpoint_bins = (Get-MapSum $targetMaps["relative_midpoint_histogram"]) signature_path = $signatureFile.FullName } } $sorted = @($ranking | Sort-Object -Property @{ Expression = { [double]$_.score }; Descending = $true }, @{ Expression = { [int]$_.index }; Ascending = $true }) for ($i = 0; $i -lt $sorted.Count; $i++) { $sorted[$i].rank = $i + 1 } $result = [ordered]@{ ok = $true created_at = (Get-Date).ToString("o") target_signature_path = (Resolve-Path -LiteralPath $TargetSignaturePath).Path candidate_extraction_root = $candidateRoot.Path candidate_count = $sorted.Count weights = $weights ranking = $sorted } $outputDirectory = Split-Path -Parent $OutputPath if (-not [string]::IsNullOrWhiteSpace($outputDirectory)) { New-Item -ItemType Directory -Force -Path $outputDirectory | Out-Null } $result | ConvertTo-Json -Depth 12 | Set-Content -LiteralPath $OutputPath -Encoding UTF8 $sorted | Select-Object -First 10 rank,index,name,score,candidate_line_count,target_line_count,line_count_score | Format-Table -AutoSize Write-Host "Ranking written: $OutputPath"