Skip to content
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
111 changes: 68 additions & 43 deletions scripts/Rename-Photos.ps1
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
Renames odometer photos using EXIF date/time, GPS-matched location, and OCR odometer reading.

.DESCRIPTION
For each IMG_*.jpg in the target folder:
For each IMG_*.jpg or IMG_*.jpeg in the target folder:
1. Reads DateTimeOriginal from EXIF via ExifTool
2. Reads GPS coordinates from EXIF via ExifTool
3. Matches GPS to the nearest known location from locations.json
Expand Down Expand Up @@ -295,9 +295,14 @@ function Get-OdometerReading {
Loads the image via WinRT BitmapDecoder, runs it through OcrEngine, and
selects the longest digit run as the odometer value.

Decodes the image at a configurable scale percentage before OCR. Downscaling
normalises digit size for close-up photos and smooths pixelation artefacts
from digital displays. Defaults to 25% (suitable for 4032x3024 source images).
Runs up to four OCR passes at 100%, 75%, 50%, and 25% of the original image
dimensions, returning on the first pass that yields a digit group of 4 or more
characters (confidence "ok"). If no pass reaches that threshold the best
low-confidence result is returned; if all passes find no digits at all,
confidence is "none".

EXIF orientation is respected on every pass so iPhone photos are correctly
rotated before recognition.

Requires Windows PowerShell 5.1. Returns Confidence="error" immediately
under PowerShell 6+, which lacks the required WinRT APIs.
Expand All @@ -316,8 +321,7 @@ function Get-OdometerReading {
Digits (string[]), and Error (string) fields.
#>
param(
[string]$ImagePath,
[int] $ScalePct = 25
[string]$ImagePath
)

$result = [PSCustomObject]@{
Expand Down Expand Up @@ -374,37 +378,60 @@ function Get-OdometerReading {
return $result
}

$scale = [Math]::Min(1.0, $ScalePct / 100.0)
$transform = [Windows.Graphics.Imaging.BitmapTransform]::new()
$transform.ScaledWidth = [uint32][Math]::Max(1, [Math]::Round($decoder.PixelWidth * $scale))
$transform.ScaledHeight = [uint32][Math]::Max(1, [Math]::Round($decoder.PixelHeight * $scale))
$transform.InterpolationMode = [Windows.Graphics.Imaging.BitmapInterpolationMode]::Linear
$fallback = $null

foreach ($scalePct in @(100, 75, 50, 25)) {
$scale = $scalePct / 100.0
$transform = [Windows.Graphics.Imaging.BitmapTransform]::new()
$transform.ScaledWidth = [uint32][Math]::Max(1, [Math]::Round($decoder.PixelWidth * $scale))
$transform.ScaledHeight = [uint32][Math]::Max(1, [Math]::Round($decoder.PixelHeight * $scale))
$transform.InterpolationMode = [Windows.Graphics.Imaging.BitmapInterpolationMode]::Linear

$bitmap = Invoke-WinRTAsync ($decoder.GetSoftwareBitmapAsync(
[Windows.Graphics.Imaging.BitmapPixelFormat]::Bgra8,
[Windows.Graphics.Imaging.BitmapAlphaMode]::Premultiplied,
$transform,
[Windows.Graphics.Imaging.ExifOrientationMode]::RespectExifOrientation,
[Windows.Graphics.Imaging.ColorManagementMode]::DoNotColorManage
)) ([Windows.Graphics.Imaging.SoftwareBitmap])

$ocrResult = Invoke-WinRTAsync ($engine.RecognizeAsync($bitmap)) ([Windows.Media.Ocr.OcrResult])
$rawText = $ocrResult.Text
$digitMatches = [regex]::Matches($rawText, '\d+')
Write-Verbose " OCR pass $scalePct%: '$rawText'"

if ($digitMatches.Count -eq 0) { continue }

$digits = @($digitMatches | ForEach-Object { $_.Value })
$best = $digitMatches | Sort-Object Length | Select-Object -Last 1

if ($best.Length -ge 4) {
$winStream.Dispose()
$netStream.Dispose()
$result.Reading = $best.Value
$result.Confidence = 'ok'
$result.RawText = $rawText
$result.Digits = $digits
return $result
}

$bitmap = Invoke-WinRTAsync ($decoder.GetSoftwareBitmapAsync(
[Windows.Graphics.Imaging.BitmapPixelFormat]::Bgra8,
[Windows.Graphics.Imaging.BitmapAlphaMode]::Premultiplied,
$transform,
[Windows.Graphics.Imaging.ExifOrientationMode]::IgnoreExifOrientation,
[Windows.Graphics.Imaging.ColorManagementMode]::DoNotColorManage
)) ([Windows.Graphics.Imaging.SoftwareBitmap])
if ($null -eq $fallback) {
$fallback = [pscustomobject]@{ Reading = $best.Value; RawText = $rawText; Digits = $digits }
}
}

$winStream.Dispose()
$netStream.Dispose()

$ocrResult = Invoke-WinRTAsync ($engine.RecognizeAsync($bitmap)) ([Windows.Media.Ocr.OcrResult])
$result.RawText = $ocrResult.Text

$digitMatches = [regex]::Matches($result.RawText, '\d+')
if ($digitMatches.Count -eq 0) {
$result.Confidence = 'none'
if ($null -ne $fallback) {
$result.Reading = $fallback.Reading
$result.Confidence = 'low'
$result.RawText = $fallback.RawText
$result.Digits = $fallback.Digits
return $result
}

$result.Digits = @($digitMatches | ForEach-Object { $_.Value })
$best = $digitMatches | Sort-Object Length | Select-Object -Last 1
$result.Reading = $best.Value
$result.Confidence = if ($best.Length -ge 4) { 'ok' } else { 'low' }

$result.Confidence = 'none'
return $result
}
catch {
Expand All @@ -427,19 +454,14 @@ function Add-OcrToPhotoContext {
.PARAMETER Photo
The photo context object created by New-PhotoContext.

.PARAMETER OcrScalePct
Percentage of original image dimensions to decode at before running OCR.
Passed through to Get-OdometerReading.

.OUTPUTS
None. Modifies Photo.OCR in place.
#>
param(
[pscustomobject]$Photo,
[int] $OcrScalePct = 25
[pscustomobject]$Photo
)

$result = Get-OdometerReading -ImagePath $Photo.File.FullName -ScalePct $OcrScalePct
$result = Get-OdometerReading -ImagePath $Photo.File.FullName
$Photo.OCR.Reading = $result.Reading
$Photo.OCR.Confidence = $result.Confidence
$Photo.OCR.RawText = $result.RawText
Expand Down Expand Up @@ -831,8 +853,6 @@ if (-not $PSBoundParameters.ContainsKey('ProximityThresholdMiles') -and $setting
if (-not $PSBoundParameters.ContainsKey('MaxSpeedMph') -and $settings.ContainsKey('MaxSpeedMph')) { $MaxSpeedMph = [double]$settings['MaxSpeedMph'] }
$roadFactor = if ($settings.ContainsKey('RoadFactor')) { [double]$settings['RoadFactor'] } else { 1.25 }
$tolerancePct = if ($settings.ContainsKey('TolerancePct')) { [double]$settings['TolerancePct'] } else { 0.20 }
$ocrScalePct = if ($settings.ContainsKey('OcrScalePercent')) { [int]$settings['OcrScalePercent'] } else { 25 }

$fallbackLocation = if ($settings.ContainsKey('FallbackLocation') -and $settings['FallbackLocation']) {
$settings['FallbackLocation']
} else { "Unknown" }
Expand Down Expand Up @@ -877,9 +897,9 @@ $auditLog = Join-Path $logsDir "rename-log.json"
Write-Information "[Rename-Photos] Starting - source: $Source" -InformationAction Continue

# Collect source photos before the fallback chain so temporal filtering is available.
$photos = @(Get-ChildItem -Path $Source -Filter "IMG_*.jpeg")
$photos = @(Get-ChildItem -Path $Source -File | Where-Object { $_.Name -match '^IMG_.*\.(jpg|jpeg)$' })
if ($photos.Count -eq 0) {
Write-Information "No IMG_*.jpeg files found in $Source" -InformationAction Continue
Write-Information "No IMG_*.jpg or IMG_*.jpeg files found in $Source" -InformationAction Continue
exit 0
}

Expand Down Expand Up @@ -1014,7 +1034,7 @@ foreach ($file in $photos) {
}

# 6. OCR reading
Add-OcrToPhotoContext -Photo $photo -OcrScalePct $ocrScalePct
Add-OcrToPhotoContext -Photo $photo
Write-Verbose " OCR result: reading=$($photo.OCR.Reading) confidence=$($photo.OCR.Confidence)"

if ($photo.OCR.Confidence -eq 'none' -or $photo.OCR.Confidence -eq 'error') {
Expand Down Expand Up @@ -1073,9 +1093,14 @@ foreach ($file in $photos) {
if (Test-Path $auditLog) {
try { $existing = @(Get-Content $auditLog -Raw | ConvertFrom-Json) } catch {}
}
$existing = @($existing | Where-Object { $_.OriginalFile -ne $entry.OriginalFile })
$existing = @($existing | Where-Object { -not $_.PSObject.Properties['OriginalFile'] -or $_.OriginalFile -ne $entry.OriginalFile })
$existing += $entry
ConvertTo-Json -InputObject $existing | Out-File $auditLog -Encoding utf8
$auditJson = if ($existing.Count -eq 1) {
"[$(ConvertTo-Json -InputObject $existing[0])]"
} else {
ConvertTo-Json -InputObject $existing
}
$auditJson | Out-File $auditLog -Encoding utf8
}

$processedCount = @($photos | Where-Object { $_.Name -notmatch '^\d{6}-\d{4} ' }).Count
Expand Down
Loading