Saturday, April 26, 2014

PowerShell functions using TessNet2 for OCR

#This script requires TessNet2 to be installed, run this script to install TessNet2. 

<#
     #Here is an example using these 2 functions to create a jpg from text then using TessNet2 to convert it back to text. 
     #Start by creating an image with text in it.  So we can use our OCR function on it.  
     Create-ImageFromText -FileName "$home\HelloWorld.jpg" -Text "Hello World From Tessnet2."
     #Uncomment this line if you want the image to pop up.
     & "$home\HelloWorld.jpg"
     $Result = Get-TextTextImage "$home\HelloWorld.jpg"
#>

Function Get-TextTextImage{
    [CmdletBinding()]
    Param(  [Parameter(Mandatory=$True,Position=1)] [string]$FileName)
    #Code was derived from the example on http://www.pixel-technology.com/freeware/tessnet2/
    $image = [System.Drawing.Bitmap]("$home\HelloWorld.jpg" )
    #The first time the Dll is used we have to deem it as same by unblocking-file.  No need to include it after the first run.
    Unblock-File "$Home\Programming\TessNet2\TessNetAssemblies\Release64\tessnet2_64.dll"
    [void][System.Reflection.Assembly]::LoadFrom("$Home\Programming\TessNet2\TessNetAssemblies\Release64\tessnet2_64.dll")
    $ocr = New-Object tessnet2.Tesseract
    ##############################the file location should point to the parent directory where the TessData pack was unzipped
    $void = $ocr.Init("$home\Programming\TessNet2\tessdata", "eng", $false)
    $result = $ocr.DoOCR($image, [System.Drawing.Rectangle]::Empty)
    $ocr.Dispose()
    $image.Dispose()
    return $result
}

Function Create-ImageFromText{
    [CmdletBinding()]
    Param(  [Parameter(Mandatory=$True,Position=1)] [string]$FileName,
            [Parameter(Mandatory=$True)]            [string]$Text)
     #Code to create an image from any given text was derived from Keith Hills reply on
    $TextToPutInImage = $Text
    $filename = $FileName 
    $length = $($TextToPutInImage.Length*30)
    Add-Type -AssemblyName System.Drawing
    $bmp = new-object System.Drawing.Bitmap $length ,90 
    $font = new-object System.Drawing.Font Consolas,24 
    $brushBg = [System.Drawing.Brushes]::Black 
    $brushFg = [System.Drawing.Brushes]::White 
    $graphics = [System.Drawing.Graphics]::FromImage($bmp) 
    $graphics.FillRectangle($brushBg,0,0,$bmp.Width,$bmp.Height) 
    $graphics.DrawString($TextToPutInImage,$font,$brushFg,10,10) 
    $graphics.Dispose() 
    $bmp.Save($filename,[ System.Drawing.Imaging.ImageFormat]::Jpeg) 
    $graphics.Dispose()
    $bmp.Dispose()
    $font.Dispose()
}

Installation Script For TessNet2

#This script downloads and installs TessNet2 (OCR), it must be run with admin privledges
#This script downloads and installs 7-zip(used to install TessNet2) and Tessnet2. 
#The 7-zip downloaded and installed is for 64 bit computers in C:\Program Files\7-Zip
#The Tessnet2 and TessNet2 English data are stored in $Home\Programming\TessNet2 directory.

$Date = [System.datetime]::now.ToString("yyyy-MM-dd")
$Junk = "$Home\Junk$Date"
if(!$(Test-Path $Junk)) { $void = New-Item -ItemType directory -Path $Junk }

#If 7-zip is not installed (it is needed to extract the data .gz file) 
if(!$(Test-Path "C:\Program Files\7-Zip")) {
    write-host "Downloading 7-Zip"
    $webclient = New-Object System.Net.WebClient
    $file = "$Junk\7z920-x64.msi"
    $webclient.DownloadFile($url,$file)

    write-host "Installing 7-Zip"
    Start-Process -file "$Junk\7z920-x64.msi" `
                  -arg ' /qn INSTALLDIR="C:\Program Files\7-Zip" ' `
                  -passthru | wait-process
}

#Create a programming folder in our Home directory.
$pgmDirectory = "$Home" + "\programming"
if(!$(Test-Path $pgmDirectory)) { $void = New-Item -ItemType directory -Path $pgmDirectory }

#Create a TessNet2 folder in our Programming directory.
$TNetDirectory = $pgmDirectory + "\TessNet2"
if(!$(Test-Path $TNetDirectory)) { $void=New-Item -ItemType directory -Path $TNetDirectory }

#If the TessNet language pack is not downloaded yet, download and extract it.
$TDataDirectory = $TNetDirectory + "\tessdata"
if(!$(Test-Path $TDataDirectory)) {
    write-host "Downloading TessNet2 Data"
    $webclient = New-Object System.Net.WebClient
    $file = "$Junk\tesseract-2.00.eng.tar.gz"
    $webclient.DownloadFile($url,$file)
    write-host "Installing TessNet2 Data"
    $void = & "C:\Program Files\7-Zip\7z.exe" "x" "$Junk\tesseract-2.00.eng.tar.gz" "-o$Junk"
    $void = & "C:\Program Files\7-Zip\7z.exe" "x" "$Junk\tesseract-2.00.eng.tar" "-o$TNetDirectory"
}

#If the TessNetAssemblies are not downloaded yet, download and extract them
$TAssDirectory = $TNetDirectory + "\TessNetAssemblies"
if(!$(Test-Path $TAssDirectory)) {
    write-host "Downloading TessNet2 Assemblies"
    $void=New-Item -ItemType directory -Path $TAssDirectory
    $webclient = New-Object System.Net.WebClient
    $file = "$Junk\tessnet2.zip"
    $webclient.DownloadFile($url,$file)
    write-host "Installing TessNet2 Assemblies"
    $void = & "C:\Program Files\7-Zip\7z.exe" "x" "$Junk\tessnet2.zip" "-o$TAssDirectory"
}

Remove-Item $Junk -Force -Recurse

<#
#Run this commented out section to uninstall TessNet2 (Ensure any applications (including PowerShell) that have used it are closed first).  
#This uninstalled assummes that you have it installed in the same directories this script installed it to. 
$unstlDirectory = "$Home" + "\programming" + "\TessNet2"
Remove-Item $unstlDirectory -Force -Recurse
#>

Wednesday, April 23, 2014

PowerShell example to convert text on PDF to MP3 (OCR)

My wife mentioned that she wanted a device that scans books/documents in and converts it to an mp3 file.  She is on the road a fair bit and would like to read (have read to her) work material while she is driving.
When she stated that the device was $3000 I was floored, I didn't think it would be all that hard to throw something together that would accomplish this task.  I threw this example together in PowerShell but will create a more complete and accurate version created in C#.
The steps below describes the complete process from taking a PDF to MP3, if you only want to pull the text from a jpg run step 1 & 6, if you only want to convert a wav file to MP3 do steps 3 and 8, for PDF to Jpg conversion step 2 and 4 are needed.


Installation Scripts
The first 3 scripts download and install assemblies/programs we need to convert your pdf(s) to an MP3 audio file (All downloads use the 64 bit versions, if your system is not 64 bit you will have to download the additional assemblies/programs manually).  The scripts create a 'programming' folder and install these programs to that directory.  The scripts also installs 7-zip if it is not already installed in order to unpackage these tools.  Each script will contain a commented section at the end which will remove the installed package when run.  No commented section removes 7-zip, this will need to be done manually via add remove programs.
Step 1. Install TessNet2 to convert image to text.
Step 2. Install GhostScript to convert PDF to JPG.
Step 3. Install ffmpeg to convert Wav file to MP3.


The rest of the scripts(functions) need to loaded/run whenever PowerShell is reopened, they will remain in memory for the existence of the PowerShell session.
Step 4. PowerShell function utilizes GhostScript to convert PDF to an image.
Step 5. PowerShell function alters an image, in this example I use it to pull out a specific part of an image.
Step 6. PowerShell functions that will utilize TessNet2 to pull the text from the image. (OCR)
Step 7. PowerShell function that converts Text to a Wav file.
Step 8. Powershell function that converts the Wav file to an MP3.

After running each of the above scripts you can give the bellow example a try.  The example script grabs a scanned pdf from a book in the users Documents library (Scan0002.pdf) and converts the text on the page to an mp3 file which is saved in the users Music library.

 #Scan0002.pdf is a sample document that can be downloaded from here http://drive.google.com/file/d/0B0ZMfV3A7dAhbTZPOXFENkluUGs/edit?usp=sharing
 #To convert your own pdf to mp3 specify the location of the pdf below.  
 $original = Get-ChildItem "$home\Documents\Scan0002.pdf"
 #convert-PdfToImage converts pdf to jpg, the new jpg file name keeps the base name of the file and adds jpg as the extension.
 #Tweaking with the resolution results in better results depending on you scanners quality.
 $jpg = convert-PdfToImage -PDFLocation $original -Resolution 300  

 $clean = "$home\Documents\scan0002Clean.jpg"  
 #Use the Update-ImageColor function to create an image that only contains the page itself. 
 #With the scanner bed noise removed it removes a lot of noise and improves the accuracy. 
 
 #The parameters for Update-ImageColor are hard coded for the example page from this book, pages from a different book may need different parameters to set it to actual page size.   
 #I think it will be fun to create a powershell script to automatically recognize the page size and pull it automatically so no parameters are needed, I'll post it when I create it.
 $void = Update-ImageColor -CurrentFileName $jpg[$jpg.Count-1].FullName -NewFileName $clean -WidthStartPosition 125 -Width 1610 -height 2400 -heighttartPosition 160  
 #Get each word, and all the words properties(location, accuracy confidence, size, etc.) from the jpg image.
 $PageWords = Get-TextFromImage $clean  
 [string]$Sentences = ""  
 #concatonate the words into one long string.
 $PageWords | %{$Sentences += $_.text + " "} 
 #Words that end with '-' are words that continue on the next line, so remove '- ' to concatenate the word.
 $Sentences = $Sentences.Replace("- ","")  
 #Convert $Sentences to a wav file.  We are using Zira's voice to use a different voice just select the voice 
 #that displays when you type the -Voice parameter. 
 Convert-TextToWavFile -Text $Sentences -OutputLocation "$home\Music\BookReading.wav" -Voice Zira  
 #Convert the wav file to an mp3 file and delete the wave file.
 Convert-Audio -path "$home\Music" -Source "BookReading.wav" -DeleteOriginal $true  


Note: if you can scan the image in as a jpg instead of a pdf you do not have to use the convert-PdfToImage function which will speed up the process and reduce any image 'noise' that may be incurred with the pdf to jpg coversion.  I noticed a slight improvement in quality when skipping the pdf to jpg conversion.
If you want you can try the jpg file of the same page and skip the call to convert-PdfToImage to see the difference.  The page is located here. https://drive.google.com/file/d/0B0ZMfV3A7dAhWlZWdWQ0M2VNaGM/edit?usp=sharing

Food For Thought
If you are scanning many documents to convert to an MP3 and you want to automate automate the above process you could set a SystemFileWatcher over the scan directory and set it up so that if new documents stopped being scanned for 5 minutes it would move them all to a "Converted" directory and kick off converting all the documents/images to 1 MP3 File.  To see an example of a powershell script that uses SystemFileWatcher you could view this PowerShell example script. http://programmingthisandthat.blogspot.ca/2014/04/create-delegates-that-will-be-used-when.html

Powershell function to create Wav file from text

Function Convert-TextToWavFile{
    [CmdletBinding()]
    Param(  [Parameter(Mandatory=$True)] [string]$OutputLocation,
            [Parameter(Mandatory=$True)] [string]$Text ,
            [ValidateSet("David","Hazel","Zira")]
            [String] $Voice = "David")

    Add-Type -AssemblyName System.speech
    $speak = New-Object System.Speech.Synthesis.SpeechSynthesizer
    $speak.SetOutputToWaveFile($OutputLocation)
    $voiceInfo = $speak.GetInstalledVoices() | ?{$_.VoiceInfo.Name.Contains($Voice)} #Get the specified voice
    $speak.SelectVoice($voiceInfo.VoiceInfo.Name)  #load the voice we want to use.
    $speak.speak($text)
    $speak.Dispose()
}

Powershell function to convert Wav file to MP3 using ffmpeg


Function Convert-Audio{
    [CmdletBinding()]
    Param(  [Parameter(Mandatory=$True,Position=1)] [string]$path,
            [string]$Source = '.wav', #The source or input file format
            $rate = '192k', #The encoding bit rate
            $DeleteOriginal = $true)

$results = @()
#This script was derived from Scott Wood's post at this site http://blog.abstractlabs.net/2013/01/batch-converting-wma-or-wav-to-mp3.html
#Thanks Scott.
Get-ChildItem -Path:$path -Include:"*$Source" -Recurse | ForEach-Object -Process: {
        $file = $_.Name.Replace($_.Extension,'.mp3')
        $input = $_.FullName
        $output = $_.DirectoryName
        $output = "$output\$file"
#-i Input file path
#-id3v2_version Force id3 version so windows can see id3 tags
#-f Format is MP3
#-ab Bit rate
#-ar Frequency
# Output file path
#-y Overwrite the destination file without confirmation
        $arguments = "-i `"$input`" -id3v2_version 3 -f mp3 -ab $rate -ar 44100 `"$output`" -y"
        $ffmpeg = ".'C:\Users\Greg\Programming\ffmpeg\bin\ffmpeg.exe'"
       
        #Hide the output
        $Status = Invoke-Expression "$ffmpeg $arguments 2>&1"
        $t = $Status[$Status.Length-2].ToString() + " " + $Status[$Status.Length-1].ToString()
        $results += $t.Replace("`n","")
       
        #Delete the old file when finished if so requested
        if($DeleteOriginal -and $t.Replace("`n","").contains("%")) {
            Remove-Item -Path:$_
        }
    }
    if ($results) {
        return $results
    }
    else {
        return "No file found"
    }
}

Installation Script for ffmpeg

#This script downloads and installs ffmpeg WAV to MP3 converter, it must be run with admin privledges
#This script downloads and installs 7-zip(used to install ffmpeg)
#The 7-zip downloaded and installed is for 64 bit computers in C:\Program Files\7-Zip
#The ffmpeg is stored in $Home\Programming\ffmpeg directory.

$Date = [System.datetime]::now.ToString("yyyy-MM-dd")
$Junk = "$Home\Junk$Date"
if(!$(Test-Path $Junk)) { $void = New-Item -ItemType directory -Path $Junk }

#If 7-zip is not installed (it is needed to extract the data .gz file)
if(!$(Test-Path "C:\Program Files\7-Zip")) {
    write-host "Downloading 7-Zip"
    $webclient = New-Object System.Net.WebClient
    $url = "http://downloads.sourceforge.net/sevenzip/7z920-x64.msi"
    $file = "$Junk\7z920-x64.msi"
    $webclient.DownloadFile($url,$file)

    write-host "Installing 7-Zip"
    Start-Process -file "$Junk\7z920-x64.msi" `
                  -arg ' /qn INSTALLDIR="C:\Program Files\7-Zip" ' `
                  -passthru | wait-process
}

#Create a programming folder in our Home directory.
$pgmDirectory = "$Home" + "\programming"
if(!$(Test-Path $pgmDirectory)) { $void = New-Item -ItemType directory -Path $pgmDirectory }

#If the ffmpeg is not downloaded yet, download and extract it.
$ffmpeg = "$pgmDirectory\ffmpeg"
if(!$(Test-Path $ffmpeg)) {
    write-host "Downloading ffmpeg"
    $webclient = New-Object System.Net.WebClient
    $url = "http://ffmpeg.zeranoe.com/builds/win64/shared/ffmpeg-20140424-git-443936d-win64-shared.7z"
    $file = "$Junk\ffmpeg-20140424-git-443936d-win64-shared.7z"
    $webclient.DownloadFile($url,$file)
    write-host "Installing ffmpeg"
    $void = & "C:\Program Files\7-Zip\7z.exe" "x" "$file" "-o$Junk"
    $dir = $(Get-ChildItem -Directory -Path $junk | ?{$_.Name.ToUpper().Contains("FFMPEG")})[0]
    $Location = $dir.FullName
    if(!$(Test-Path $ffmpeg)) { $void = New-Item -ItemType directory -Path $ffmpeg }
    Copy-Item "$Location\*" $ffmpeg -Recurse
}
Write-Host "ffmpeg install is completed"

Remove-Item $Junk -Force -Recurse

<#
#Run this commented out section to uninstall FFmpeg (All programs that used this assembly (including Powershell) must be closed before running this script).
#This uninstalled assummes that you have it installed in the same directories this script installed it to.
$unstlDirectory = "$Home" + "\programming" + "\ffmpeg"
Remove-Item $unstlDirectory -Force -Recurse
#>

Sunday, April 20, 2014

Function Update-ImageColor{

[CmdletBinding()]

Param( [Parameter(Mandatory=$True,Position=1)] [string]$CurrentFileName,

[Parameter(Mandatory=$True,Position=1)] [string]$NewFileName,

[System.Drawing.Color]$newColor = [System.Drawing.Color]::Black,

[int]$MinCombined = -1,

[int]$MaxCombined = 800,

[int]$MaxRed = 400,

[int]$MaxGreen = 400,

[int]$MaxBlue = 400,

[int]$MinRed = -1,

[int]$MinGreen = -1,

[int]$MinBlue = -1,

[int]$Width=0,

[int]$height=0,

[int]$WidthStartPosition=0,

[int]$heighttartPosition=0,

[int]$ZoomFactor=1)

try {

$scrBitmap = [System.Drawing.Bitmap]($CurrentFileName)

$ActualColor = New-Object System.Drawing.Color

#make an empty bitmap the same size as scrBitmap

if($Width -eq 0){$Width = $scrBitmap.Width}

if($height -eq 0){$height = $scrBitmap.Height}

$newBitmap = new-object System.Drawing.Bitmap $Width, $height

for ($i = 0; $i -lt $newBitmap.Width; $i++) {

for ($j = 0; $j -lt $newBitmap.Height; $j++) {

#get the pixel from the scrBitmap image

$actulaColor = $scrBitmap.GetPixel($i+$WidthStartPosition, $j+$heighttartPosition);

if(($actulaColor.R -gt $MaxRed -or $actulaColor.R -lt $MinRed) -or

($actulaColor.G -gt $MaxGreen -or $actulaColor.G -lt $MinGreen) -or

($actulaColor.B -gt $MaxBlue -or $actulaColor.B -lt $MinBlue) -or

($actulaColor.R + $actulaColor.G + $actulaColor.B) -lt $MinCombined -or

($actulaColor.R + $actulaColor.G + $actulaColor.B) -gt $MaxCombined){

$newBitmap.SetPixel($i, $j, $newColor)



}

else {

$newBitmap.SetPixel($i, $j, $actulaColor)



}

}

}

Remove-Item $NewFileName -Force -ErrorAction SilentlyContinue

#Size newSize = new Size((int)(originalBitmap.Width * zoomFactor), (int)(originalBitmap.Height * zoomFactor));

#Bitmap bmp = new Bitmap(originalBitmap, newSize);

$newBitmap.Save($NewFileName,[System.Drawing.Imaging.ImageFormat]::Jpeg)

$newBitmap.Dispose()

$scrBitmap.Dispose()

return "$NewFileName : Conversion Complete."



}

catch [Net.WebException] {

return $_.Exception



}

}