Last active
December 15, 2018 01:51
-
-
Save pr3sidentspence/84af7b1eca2a0f1ae416dab2950c5b83 to your computer and use it in GitHub Desktop.
A powershell script that 1) takes scans of multiple docutments (postcards) on a dark grey background 2) splits them into individual images 3) straightens them if needed 4) crops them with about 50px of the background remaining as a border 5) cleans up said background in the case of a certain set of cards 6) checks with Microsoft's Cognitive Serv…
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
param( | |
[string]$ext = "bmp", | |
[float]$triplethresh = 0.09, | |
[float]$singlethresh = 0.04, | |
[string]$colour = 'rgb(55,60,56)', | |
[Parameter(Mandatory = $true)][int]$tempNum, | |
[string]$stopAfter = "", | |
[float]$rotThresh = 2, | |
[int]$max = 3, | |
[bool]$keepText = $false, | |
[switch]$noSort, | |
[switch]$justSort, | |
[switch]$justAutoRot | |
) | |
# tripleCardToReady.ps1 | |
# - powershell 5.1 script that takes scans of postcards or other documents | |
# on a dark grey background and splits them up into 1 image per document, | |
# straightens them if needed, crops them with about 50 pixels of the | |
# background showing, and converts the final output to jpeg format at | |
# 100% quality. | |
# | |
# Looks for scans in $PSScriptRoot\raw where $PSScriptRoot is the directory it was run from | |
# | |
# -adds a -1, -2, etc. suffix to the original file name, one for each document | |
# it finds, unless the original file names following the formula: | |
# [B][.][.][\-][\d*][\-][\d*][\-][\d*][\-][.*].[ext] | |
# (e.g., B1A-0007-008-009-front.bmp) | |
# in which case the output would be three files: | |
# B1A007-front.jpg B1A008-front.jpg B1A008-front.jpg | |
# | |
# -many of the raw scans this was created to process were scanned using a template, | |
# this script attempts to clean that up a little bit by replacing as much template | |
# as possible with a grey matching the background colour. | |
# TODO: Often what leads to false gaps being found is a strip of a document that is | |
# TODO wide enough, uniform enough, and has a value (Red + Green + Blue) that is | |
# TODO similar enough to the value of the background colour to cause a low standard | |
# TODO deviation for the pixel lines in it. | |
# | |
# TODO This strip and the background are *usually* different colours, though (greenery | |
# TODO in tinted historic postcards is a frequent culprit). If the script were to check | |
# TODO each line for each colour channel and see all three agree that it's a gap, this | |
# TODO could reduce false gap detection (obscure Sci-Fi joke coming up) | |
# TODO i.e., the Beserker Brain of gap finders. | |
[float]$3plThresh = $triplethresh | |
[float]$1glThresh = $singlethresh | |
# TODO: add template values for other templates as found | |
# define crop coordinates for template crops | |
#template reads h0 y0 h1 y1 h2 y2 | |
if ($tempNum -eq 1) { | |
$template = ( (2391, 0), (2437, 2246), (2488, 4526) ) | |
} | |
elseif ($tempNum -eq 2) { | |
$template = ( (2374, 0), (2300, 2285), (2290, 4575) ) | |
} | |
elseif ($tempNum -eq 3) { | |
$template = ( (2373, 0), (2400, 2274), (2461, 4544) ) | |
} | |
Write-Host "using template $tempNum" | |
function main { | |
Get-ChildItem -Path $PSScriptRoot\raw -Filter *.$ext | | |
ForEach-Object { | |
$fileName = $_.BaseName | |
Write-Host "" | |
Write-Host Starting work on $_ | |
if ($fileName -match ("(B.{1,3})\-(\d*)\-(\d*)\-(\d*)\-(.*)")) { | |
$binder = $matches[1] | |
$bIDnums = ($matches[2], $matches[3], $matches[4]) | |
$side = $matches[5] | |
Write-Host Cleaning up template for $_ | |
if ($tempNum -eq 1) { | |
& magick raw/$_ -fill "$colour" -stroke "$colour" -draw "rectangle 0,0 4960,100" -draw "path 'M 80,104 3520,104 80,109 Z'" -draw "rectangle 0,6812 4960,7015" -draw "rectangle 0,0 76,7015" -draw "rectangle 0,2250 4960,2391" -draw "rectangle 0,4525 4960,4684" -draw "path 'M 88,4684 3520,4684 3520,4699 Z'" -draw "rectangle 76,4684 88,6812" -draw "rectangle 76,106 80,2245" -draw "rectangle 4500,0 4960,7015" greyTemp\$_ | |
} | |
elseif ($tempNum -eq 2) { | |
& magick raw/$_ -fill "$colour" -stroke "$colour" -draw "rectangle 0,0 4960,81" -draw "rectangle 0,6864 4960,7015" -draw "rectangle 0,0 66,7015" -draw "rectangle 0,2284 4960,2375" -draw "rectangle 0,4574 4960,4669" -draw "rectangle 4791,0 4960,7015" greyTemp\$_ | |
} | |
elseif ($tempNum -eq 3) { | |
#& magick $_ -fill "$colour" -stroke "$colour" -draw "rectangle 0,0 4960,81" -draw "rectangle 0,6864 4960,7015" -draw "rectangle 0,0 66,7015" -draw "rectangle 0,2284 4960,2375" -draw "rectangle 0,4574 4960,4669" -draw "rectangle 4791,0 4960,7015" greyTemp\$_ | |
& magick raw/$_ -fill "$colour" -stroke "$colour" -draw "rectangle 0,0 4960,99" -draw "rectangle 0,6837 4960,7015" -draw "rectangle 0,0 76,7015" -draw "rectangle 0,2284 4960,2375" -draw "rectangle 0,4556 4960,4673" -draw "rectangle 4690,0 4960,7015" greyTemp\$_ | |
} | |
Write-Host Splitting $_ into 3 separate files | |
Write-Host "" | |
for ($i = 0; $i -lt 3; $i++ ) { | |
$bIDnum = $bidNums[$i] | |
$bSingleName = ("{0}{1}-{2}.{3}" -f $binder, $bIDnum, $side, $ext) | |
$templateCropParams = ("4960x{0}+0+{1}" -f $template[$i][0], $template[$i][1]) | |
magick.exe greyTemp\$_ -crop $templateCropParams singles\$bSingleName | |
} | |
#Remove-Item -Path $PSScriptRoot\greyTemp\*.* | |
} | |
else { | |
Write-Host Analyzing scan of loose documents in $_ | |
& magick raw/$_ -alpha off -resize 10% -crop x1 +repage -format "%[fx:standard_deviation]\n" info: >$PSScriptRoot/txt/$fileName.txt | |
[System.Collections.ArrayList]$tripGapInfo = findGaps "$fileName.txt" | |
tripleChop "$fileName" $tripGapInfo | |
} | |
if ($stopAfter -eq "singles") { | |
Write-Host stopAfter singles argument provided, exiting. | |
exit | |
} | |
else { | |
makeSmalls | |
} | |
} | |
} | |
function findGaps($textFile) { | |
[bool]$inGap = $false | |
[int]$gap = 0 | |
[int]$card = 0 | |
[int]$lineCounter = 0 | |
[int]$lnFactor = 0 | |
[float]$threshold = 0 | |
[bool]$single = $false | |
[int]$giCount = 0 | |
[System.Collections.ArrayList]$gapInfo = @() | |
if ($textFile -match "row|col") { | |
$threshold = $1glThresh | |
$lnFactor = 5 | |
$single = $true | |
} | |
else { | |
$threshold = $3plThresh | |
$lnFactor = 10 | |
} | |
$lines = [System.IO.File]::ReadLines( "$PSScriptRoot\txt\$textFile" ) | |
foreach ($line in $lines) { | |
if ($line -match ".*e.*") { | |
$line = 0 | |
} | |
if ($line -lt $threshold) { | |
if ($gap -eq $lnFactor) { | |
if (!$inGap) { | |
$gapInfo += ((($lineCounter - $lnFactor) * $lnFactor) + 10) | |
$inGap = $true | |
} | |
} | |
$gap++ | |
$card = 0 | |
} | |
else { | |
if ($card -eq $lnFactor) { | |
if ($inGap) { | |
$gapInfo += ((($lineCounter - $lnFactor) * $lnFactor) + 10) | |
$inGap = $false | |
} | |
} | |
$card++ | |
$gap = 0 | |
} | |
$lineCounter++ | |
} | |
$lineCounter++ | |
# last line is not recorded as a gap end so we have to add the last line of the scan as a gap end. | |
$gapInfo += (($lineCounter - $lnFactor) * $lnFactor) | |
Write-Host -NoNewline "gap starts & ends: " | |
foreach ($i in $gapInfo) { | |
Write-Host -NoNewline ", $i" | |
} | |
Write-Host "" | |
[int]$giCount = $gapInfo.Count | |
[int]$foundCards = ($giCount - 2) / 2 | |
if ($foundCards -eq 1 -OR $foundCards -eq 2 -OR $foundCards -eq 3) { | |
Write-Host -ForegroundColor Green ("found {0} gaps - or {1} documents - which is a number that's expected (yay!)" -f ($giCount / 2), $foundCards) | |
$3plThresh = $triplethresh | |
$1glThresh = $singlethresh | |
return $gapInfo | |
} | |
# TODO: Use an attempt counter instead of the amount of change in the thresholds from the starting. | |
# TODO a) because there's an off by one issue that bothers me, and | |
# TODO b) because it could get stuck in an oscillation, even though it's pretty unlikely. | |
elseif ([math]::abs($1glThresh - $singlethresh) -le 0.03) { | |
# Brain tells me this should be 0.02, but it was only running twice with that | |
if ($foundCards -lt 1) { | |
Write-Host -ForegroundColor Yellow ('no documents were found in {0} - possibly there was not enough contrast with the background' -f $textFile) | |
Write-Host -ForegroundColor Yellow ('threshold was $threshold - trying again with a threshold of {0}' -f ($threshold += 0.01)) | |
$1glThresh += 0.01 | |
$3plThresh += 0.01 | |
} | |
elseif ($foundCards -gt $max) { | |
Write-Host -ForegroundColor Yellow ('Found more documents ({0}) than the maximum expected ({1})' -f $foundCards, $max) | |
Write-Host -ForegroundColor Yellow ('threshold was $threshold - trying again with a threshold of {0}' -f ($threshold -= 0.01)) | |
$1glThresh -= 0.01 | |
$3plThresh -= 0.01 | |
} | |
findGaps $textFile | |
} | |
else { | |
if ($foundCards -lt 1) { | |
[string]$errorSubMsg = 'no' | |
} | |
if ($foundCards -gt $max) { | |
[string]$errorSubMsg = ('more than the maximum expected number ({0}) of' -f $max) | |
} | |
Write-Host -ForegroundColor Red ('found {0} documents after three attempts - giving up on {1}' -f $errorSubMsg, $textFile) | |
$3plThresh = $triplethresh | |
$1glThresh = $singlethresh | |
} | |
} | |
function tripleChop($fileName, $gapInfo) { | |
$fileName = $_.BaseName | |
$gInfoLen = $gapInfo.Count | |
# The arraylist has two entries for each gap, there is always 1 more | |
# gap than there are cards. So starting with arraylist.Count (e.g. 8) take | |
# 2 off for the extra gap info (6), divide by two to get number of cards. | |
$numCards = (($gInfoLen - 2) / 2) | |
if ($numCards -gt $max) { | |
Write-Host -ForegroundColor Red ('more than the maximum expected number ({0}) of documents were detected - skipping' -f $max) | |
} | |
else { | |
Write-Host Cropping $_ into $numCards separate files | |
for ($i = 0; $i -lt (2 * $numCards); $i = $i + 2) { | |
$cardNum = ($i / 2) + 1 | |
$overGapStart = ($gapInfo[$i]) | |
$overGapEnd = ($gapInfo[($i + 1)] - 10) | |
$underGapStart = (($gapInfo[($i + 2)]) + 10) | |
$underGapEnd = (($gapInfo[($i + 3)]) - 10) | |
if ($cardNum -eq 3) { | |
$underGapEnd = 7015 | |
} | |
if ($numCards -eq 1) { | |
$cropStart = $overGapEnd - 100 | |
$cropEnd = $underGapStart + 100 | |
} | |
else { | |
$cropStart = $overGapStart | |
$cropEnd = $underGapEnd | |
} | |
$length = $cropEnd - $cropStart | |
$cropParams = ("x{0}+0+{1}" -f $length, $cropStart) | |
$output = ("{0}/singles/{1}-{2}.{3}" -f $PSScriptRoot, $fileName, $cardNum, $ext) | |
& magick.exe $_ "-crop" $cropParams $output | |
} | |
} | |
} | |
function makeSmalls { | |
$fixTempBG = '' | |
[bool]$berman = $false | |
#Remove-Item -Path $PSScriptRoot\txt\* -Force # Empties txt-temp dir | |
Get-ChildItem $PSScriptRoot\singles -Filter *.$ext | | |
Foreach-Object { | |
$smallsName = $_.BaseName | |
if ($smallsName -match ("B.{1,3}\d*\-.*")) { | |
$berman = $true | |
$fixTempBG = ('-fuzz 20% -fill "{0}" -opaque black' -f $colour) | |
} | |
$imArgs = ('singles\{0} -resize 20% {1} small\{0}' -f $_ , $fixTempBG) | |
# Sometimes I get in interpretation hell and the following two lines get me out | |
$command = ('magick.exe {0}' -f $imArgs) | |
cmd.exe /c $command | |
# There is no point in trying to rotate the Berman cards, deskew picks up the | |
# template and masks as being straight and doesn't do anything, or gets it completely wrong. | |
if (!$berman ) { | |
rotator $_ | |
} | |
& magick.exe small\$_ -alpha off -crop 1x +repage -format "%[fx:standard_deviation]\n" info: >$PSScriptRoot/txt/$smallsName-cols.txt | |
& magick.exe small\$_ -alpha off -crop x1 +repage -format "%[fx:standard_deviation]\n" info: >$PSScriptRoot/txt/$smallsName-rows.txt | |
Write-Host Determining where to crop sides of $_ | |
[System.Collections.ArrayList]$singGapInfo = findgaps "$smallsName-cols.txt" | |
Write-Host Cropping $_ sides | |
Write-Host Determining where to crop top and bottom of $_ | |
singleChop $singGapInfo "cols" | |
[System.Collections.ArrayList]$singGapInfo = findgaps "$smallsName-rows.txt" | |
Write-Host Cropping $_ top and bottom | |
singleChop $singGapInfo "rows" | |
if ($_ -match "B..\d\d\d") { | |
Write-Host This is a Berman card. Attempting to clean up the border. | |
if ($tempNum -eq 1) { | |
$magArgs = ('magick.exe ready\{0}.jpg -stroke {1} -fill none -strokewidth 86 -draw "rectangle -4,-4,%[fx:w],%[fx:h]" ready\{0}.jpg' -f $smallsName, "$colour") | |
} | |
elseif ($tempNum -eq 2) { | |
$magArgs = ('magick.exe ready\{0}.jpg -stroke {1} -fill none -strokewidth 84 -draw "rectangle -5,-5,%[fx:w],%[fx:h]" ready\{0}.jpg' -f $smallsName, "$colour") | |
} | |
elseif ($tempNum -eq 3) { | |
#these are test numbers for now. | |
$magArgs = ('magick.exe ready\{0}.jpg -stroke {1} -fill none -strokewidth 84 -draw "rectangle -5,-5,%[fx:w],%[fx:h]" ready\{0}.jpg' -f $smallsName, "$colour") | |
} | |
cmd.exe /c $magArgs | |
} | |
Write-Host Finished work on $_ | |
Write-Host "" | |
} | |
Remove-Item -Path $PSScriptRoot\singles\*.* | |
} | |
function rotator { | |
Write-Host Determining if $_ needs straightening | |
$rotation = & magick small\$_ -background $colour -deskew 20% -print %[deskew:angle]\n null: | |
if ($rotation -lt $rotThresh) { | |
& magick small\$_ -background "$colour" -rotate $rotation small\$_ | |
& magick singles\$_ -background "$colour" -rotate $rotation singles\$_ | |
Write-Host $rotation degrees of rotation applied | |
} | |
else { | |
Write-Host the $rotation value was over the rotation threshold of $rotThresh | |
} | |
} | |
function singleChop($gapInfo, $cORr) { | |
[string]$fileName = $_.BaseName | |
[string]$inSingle = ("{0}\singles\{1}" -f $PSScriptRoot, $_) | |
[bool]$allGood = $false | |
[string]$imCommand = "" | |
# The arraylist has two elements for each gap, there is always 1 more | |
# gap than there are cards. So starting with arraylist.Count (e.g. 4) | |
# take 2 off for the extra gap info (2), divide by 2 to get number of cards. | |
$numCards = ($gapInfo.Count - 2) / 2 | |
if ($numCards -eq 1) { | |
$overGapEnd = $gapInfo[1] - 5 | |
$underGapStart = $gapInfo[2] + 5 | |
$cropStart = $overGapEnd - 50 | |
$cropEnd = $underGapStart + 30 | |
$length = $cropEnd - $cropStart | |
$allGood = $true | |
$imCommand = "-crop" | |
} | |
if ($cORr -eq "cols") { | |
if (!$allGood) { | |
Write-Host -ForegroundColor Red "function singleChop didn't get the right number of parameters - skipping cropping sides" | |
return | |
} | |
$cropParams = ("{0}x+{1}+0" -f $length, $cropStart) | |
$output = ("{0}/singles/{1}.{2}" -f $PSScriptRoot, $fileName, $ext) | |
} | |
elseif ($cORr -eq "rows") { | |
if ($allGood) { | |
$cropParams = ("x{0}+0+{1}" -f $length, $cropStart) | |
} | |
else { | |
Write-Host -ForegroundColor Red "function singleChop didn't get the right number of parameters - skipping cropping top & bottom" | |
$cropParams = "" | |
} | |
$output = ("{0}/ready/{1}.jpg" -f $PSScriptRoot, $fileName) | |
} | |
Write-host magick.exe $inSingle $imCommand $cropParams $output | |
& magick.exe $inSingle $imCommand $cropParams $output | |
} | |
function cdmSort { | |
# sorts and renames files as needed to import as compound itens into CONTENTdm (OCLC's ) digital repository | |
Write-Host "now sorting the finished images for CONTENTdm import" | |
Get-ChildItem -Path $PSScriptRoot\ready -Filter *.jpg | | |
ForEach-Object { | |
if ($_.BaseName -match '^(.*)\-(?:((?:front)|(?:f)|(?:1)|(?:a)|(?:1st))|((?:back)|(?:b)|(?:2)))$') { | |
$cardID = $matches[1] | |
Write-Host this looks like it`s ID should be $cardID | |
New-Item -ItemType directory -Path $PSScriptRoot\ready\$cardID -Force | Out-Null | |
New-Item -ItemType directory -Path $PSScriptRoot\ready\$cardID\scans -Force | Out-Null | |
if ($matches[2]) { | |
[string]$dest = "$PSScriptRoot\ready\$cardID\scans\1_front.jpg" | |
Write-Host "$_ matches the pattern for a document front --- moving it to $dest and renaming it to 1_front.jpg" | |
} | |
elseif ($matches[3]) { | |
[string]$dest = "$PSScriptRoot\ready\$cardID\scans\2_back.jpg" | |
Write-Host "$_ matches the pattern for a document front --- moving it to $dest and renaming it to 2_back.jpg" | |
} | |
Move-Item -Path ready\$_ -Destination $dest | |
} | |
} | |
} | |
function callMsAPI { | |
$imagePath = "$PSScriptRoot\small\testimage-$i.jpg" | |
$Uri = 'https://westus.api.cognitive.microsoft.com/vision/v1.0/analyze?visualFeatures=Description' # make sure your endpoint matches you key | |
$Headers = @{ | |
'Ocp-Apim-Subscription-Key' = '<subscription-key' #replace with yours | |
} | |
$Response = Invoke-RestMethod -Uri $Uri -Method Post -InFile $imagePath -Headers $Headers -ContentType 'application/octet-stream' | |
return $Response.description.captions.confidence | |
} | |
function autoOrient { | |
Get-ChildItem $PSScriptRoot\ready -Filter *-front.jpg | | |
ForEach-Object { | |
$i = 0 | |
$highScore = 0 | |
$testSCore = 0 | |
[int]$bestRot | |
for ($i = 0; $i -le 270; $i = $i + 90) { | |
$out = "$PSScriptRoot\small\testimage-$i.jpg" | |
if ($i -eq 0) { | |
cmd /c "magick.exe $PSScriptRoot\ready\$_ -resize 20% $out" | |
} | |
else { | |
cmd /c "magick.exe $PSScriptRoot\small\testimage-0.jpg -rotate $i $out" | |
} | |
Write-Host -NoNewline "sending $_.BaseName rotated $i degrees as testimage-$i.jpg to callMsAPI for analysis..." | |
$testScore = callMsAPI($i) | |
Write-Host "score: $testScore" | |
if ($testScore -gt $highScore) { | |
$highScore = $testScore | |
$bestRot = $i | |
} | |
# Sleep for 3 seconds as to not breech Microsoft's Free Tier rate limit. 3 might be overkill. | |
# Allowed 20/min. | |
Start-Sleep 3 | |
} | |
if ($bestRot -eq 0) { | |
Write-Host -ForegroundColor Green "$_ is already rightside up" | |
} | |
else { | |
Write-Host -ForegroundColor Yellow " a rotation of $bestRot degrees scored highest for $_ - rotating..." | |
$autoRotCmd = ("magick.exe {0} -rotate $bestRot {0}" -f $_.FullName) | |
cmd /c $autoRotCmd | |
} | |
} | |
} | |
if ($stopAfter -eq "singles") { | |
Write-Host Stopping after singles | |
} | |
New-Item -ItemType directory -Path $PSScriptRoot/ready -Force | Out-Null | |
New-Item -ItemType directory -Path $PSScriptRoot/singles -Force | Out-Null | |
New-Item -ItemType directory -Path $PSScriptRoot/txt -Force | Out-Null | |
New-Item -ItemType directory -Path $PSScriptRoot/small -Force | Out-Null | |
New-Item -ItemType directory -Path $PSScriptRoot/greyTemp -Force | Out-Null | |
if ($justSort) { | |
Write-Host "-justsort switch detected, jumping straight to sorting" | |
cdmSort | |
} | |
elseif ($justAutoRot) { | |
Write-Host "-justautorot switch detected, jumping straight to auto rotate" | |
autoOrient | |
} | |
else { | |
main | |
autoOrient | |
if (!$noSort) { | |
cdmSort | |
} | |
} | |
# Remove-Item -Path $PSScriptRoot/greyTemp -Recurse | |
# Remove-Item -Path $PSScriptRoot/singles -Recurse | |
# Remove-Item -Path $PSScriptRoot/small -Recurse | |
# if (!$keepText) { | |
# Remove-Item -Path $PSScriptRoot/txt -Recurse | |
# } | |
Write-Host "that was the last $ext file in the directory - Done creating new images" | |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment