Skip to content

Instantly share code, notes, and snippets.

@pr3sidentspence
Last active December 15, 2018 01:51
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pr3sidentspence/84af7b1eca2a0f1ae416dab2950c5b83 to your computer and use it in GitHub Desktop.
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…
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