Wednesday, 7 November 2012

Gambas: Improving the PhotoViewer

By making the transition from Gambas 2 to Gambas 3, I've been able to increase the functionality of my Photo Viewer application, and make a few improvements along the way.

 

A new Gambas component has allowed me to add image histograms, rotation and normalisation to my project.


This new component is called gb.image.effect, and although it includes methods to despeckle, emboss, invert, oil-paint and solarize an image, I've only made use of a couple of the available methods.

We have now been using the original PhotoViewer (http://captainbodgit.blogspot.co.uk/2012/09/gambas-1-photoviewer.html) for a couple of months and I've had a chance to assess feedback from my user-base (...by which I mean that I've had a few cutting remarks from Lady Bodgit).

Just revisiting the initial work-flow requirements, I've now added 3 more operations:-
  1. Review each photo in turn
  2. Delete the "no-hope" pictures
  3. Rename any we wanted to keep that did not require edits
  4. Rotate images as necessary (usually 90deg CW)
  5. Assess exposure by viewing image histogram
  6. Normalise if necessary
  7. Launch GIMP for those that did require further editing
  8. Show our best efforts to long suffering friends & relatives
  9. Review camera settings when asked "How did you do that?"
In addition, there are 3 practical requirements to consider:-
  1. The image should be as large as possible
  2. Stepping through the images should be easy (e.g. by using the arrow keys)
  3. The camera (EXIF) data should be retained following simple edits

Folder & File Selection

Experience now shows that the FileChooser component was not a good choice for this application. It takes up too much screen space!

The simple solution was to use a DirChooser to select the target directory, and only display it when the user selects a directory. By using a DrawingArea to display each photo in turn, and setting the controls property Focus=TRUE it is then just a matter of writing some code in the controls KeyPress event.

.
strDir = DirSelect.Value
astrFiles = New String[]
astrFiles = Dir(strDir, "*.{jpg,jpeg,JPG,JPEG}").Sort()
.
.
Public Sub daPhoto_KeyPress()
.
.
 Select Case Key.Code
  Case Key.Right, Key.Down 
  Inc intCurrentFile

  Case Key.Left, Key.Up
  Dec intCurrentFile
.
.
 End Select
 LoadPhoto(strDir & "/" & astrFiles[intCurrentFile]
End

The Camera (EXIF) Data

The grid containing the camera (exif) data has also been moved off the main form, and is now launched on its own form, where the property Stacking=Above. This avoids it getting "lost" and allows it to be moved around the screen or easily closed when not required. The new histogram is handled and displayed in the same way.

Other Hot-keys

Since the daPhoto_KeyPress event is responding to our keys, I've also expanded the Select Case section above to include the following keys:-
  • PageUp = show first photo
  • PageDown = show last photo
  • Del = delete the current photo
  • R or r = rename the current photo
  • H or h = display the histogram
  • D or d = display EXIF data

This is in addition to the traditional top menu bar, and a right-click pop-up menu which is enabled by setting the main forms property PopupMenu=mnuPop (the name of the menu from the menu editor).

Image Histogram

Gambas 3 allows you to extract pixel intensity data for red, blue and green channels (although at the time of writing, the red & blue channels appear to be swapped over).

Most image editors (e.g. The GIMP) provide displays of this data in the form of histograms. The basic use of this information is to determine if the photo was correctly exposed by viewing a combined RGB histogram (see Exposure Explained: http://captainbodgit.blogspot.co.uk/2012/10/photography-1-exposure-explained.html).

I decided to include 5 histogram options, which the user selects (one at a time) by clicking on the histogram. The default histo is the sum of RGB:-
.
 hImage = Image.Load(strDir & "/" & astrFiles[intCurrentFile])  'current photo
 hHisto = hImage.Histogram()
 For index = 0 To 255
   lngRGBPixel = 0   'lngRGBPixel will be the plot value
   Select Case intHistoType
     Case RED_HISTO 
    '1 = Red Histo
       lngRGBPixel = hHisto[Image.Red, index]
     Case GREEN_HISTO  
'2 = Green Histo
       lngRGBPixel = hHisto[Image.Green, index]
     Case BLUE_HISTO
    '3 = Blue Histo
       lngRGBPixel = hHisto[Image.Blue, index]
     Case RGB_MAX_HISTO
'4 = Max value from R, G or B
       lngRGBPixel = hHisto[Image.Red, index]
       If hHisto[Image.Green, index] > lngRGBPixel Then
         lngRGBPixel = hHisto[Image.green, index]
       Endif
       If hHisto[Image.Blue, index] > lngRGBPixel Then
         lngRGBPixel = hHisto[Image.Blue, index]
       Endif
     Case Else   '0 = RGB_SUM_HISTO {Sum(RGB)}
       lngRGBPixel = hHisto[Image.Red, index] + hHisto[Image.Green, index]                + hHisto[Image.Blue, index]
   End Select

   'create array of plot values
   myHisto[index] = lngRGBPixel
   If lngRGBPixel > lngMaxPixel Then
   

     'Save largest (lngMaxPixel) for scaling the histo plot area
     lngMaxPixel = lngRGBPixel
   Endif
 Next
 
....'draw histo

Normalisation

This is about taking the image pixel range and stretching it to fill the available range (e.g. if your image only contained pixels values in the range 50 to 200, the "Normalize" option with stretch the range to cover 0 to 255, improving the photo by increasing the tonal range.

This can be implemented something like this:-
.
.
 hImage = Image.Load(strDir & "/" & astrFiles[intCurrentFile])  'current photo
 hImage.Normalize
 DrawPhoto(hImage)  'DrawPhoto scales the image & draws on daPhoto DrawingArea

Image Rotation

This is also quite simple:-
.
 hImage = Image.Load(strDir & "/" & astrFiles[intCurrentFile])  'current photo
 hImage.Rotate(Rad(intAngle))  'intAngle = 90 for CCW or 270 for CW
'now scale and draw on daPhoto
.

Saving Changes & Exif Data
To save changes back to the current file name, you can simply:-

 hImage.Save(strDir & "/" & astrFiles[intCurrentFile])

However, this only saves the image, not the Exif data.

I can't see a direct way of saving the image + Exif in Gambas, so I use another program called: jhead

First thing to do is check if jhead is installed when PhotoViewer is launched:-

 Public Sub _new()
 Dim strTest As String

 Exec ["whereis", "jhead"] To strTest
 If Instr(strTest, "bin/jhead") = 0 Then
   Message.Info("Please install: jhead", "close")
   Quit
 Endif

When the user tries to edit a file (i.e. rotate or normalise), we can save a copy of the file in a temporary directory, and then once the edit is complete, we copy the exif data from the temporary copy to the newly edited version. Something like this:-

 strNewFile = strDir & "/" & astrFiles[intCurrentFile]
 Try Exec ["jhead", "-te", strOrgFileCopy, strNewFile]
 If ERROR Then
   Message.Error("Don't Panic!", "close")
 Endif

Just to clarify, the jhead syntax for exif data copy is:-
jhead -te sourceFile destinationFile
Only the exif data is changed in the destinationFile, not the jpeg image.

Conclusion

Its tempting to use all the features of the gb.Image.Effect component in this project and create a graphics editor. But the requirement was just for an app that would match our work-flow when assessing SLR photos, so any further changes are likely to be minor. However, I'll certainly spend some more time experimenting with this interesting Gambas component.

PhotoViewer 3


No comments:

Post a Comment