For Luxocator's frontend, let's create a file called Luxocator.py
. This module depends on OpenCV, wxPython, and some of Python's standard OS and threading functionality. It also depends on all the other modules that we have written in this chapter. Add the following import statements at the top of the file:
import numpy import cv2 import os import threading import wx from HistogramClassifier import HistogramClassifier from ImageSearchSession import ImageSearchSession import PyInstallerUtils import ResizeUtils import WxUtils
Now, let's implement the Luxocator
class as a subclass of wx.Frame
, which represents a GUI frame such as the contents of a window. Most of our GUI code is in the Luxocator
class's __init__
method, which is therefore a big method but is not very complicated. Our GUI elements include a search control, previous and next buttons, a bitmap, and a label to show the classification result. All of these GUI elements are stored in the member variables. The bitmap is confined to a certain maximum size (by default, 768 pixels in the larger dimension) and the other elements are laid out below it.
Several methods are registered as callbacks to handle events such as the window closing, a search string being entered, or the next or previous button being clicked. Besides the GUI elements, other member variables include instances of our HistogramClassifier
and ImageSearchSession
classes. Here is the implementation of the initializer, interspersed with some remarks on the GUI elements that we are using:
For more information about using bitmaps, controls, and layouts in wxPython, refer to the official wiki at http://wiki.wxpython.org/.
class Luxocator(wx.Frame): def __init__(self, classifierPath, maxImageSize=768, verboseSearchSession=False, verboseClassifier=False): style = wx.CLOSE_BOX | wx.MINIMIZE_BOX | wx.CAPTION | \ wx.SYSTEM_MENU | wx.CLIP_CHILDREN wx.Frame.__init__(self, None, title='Luxocator', \ style=style) self.SetBackgroundColour(wx.Colour(232, 232, 232)) self._maxImageSize = maxImageSize border = 12 defaultQuery = 'luxury condo sales' self._index = 0 self._session = ImageSearchSession() self._session.verbose = verboseSearchSession self._session.search(defaultQuery) self._classifier = HistogramClassifier() self._classifier.verbose = verboseClassifier self._classifier.deserialize(classifierPath) self.Bind(wx.EVT_CLOSE, self._onCloseWindow)
The search control (coming up next) deserves special attention because it contains multiple controls within it and its behavior differs slightly across operating systems. It can have up to three sub-controls: a text field, a search button, and a cancel button. There can be a callback for the Enter key being pressed while the text field is active. If the search and cancel buttons are present, they have callbacks on being clicked. We can set up the search control and its callbacks as follows:
self._searchCtrl = wx.SearchCtrl( self, size=(self._maxImageSize / 3, -1), style=wx.TE_PROCESS_ENTER) self._searchCtrl.SetValue(defaultQuery) self._searchCtrl.Bind(wx.EVT_TEXT_ENTER, self._onSearchEntered) self._searchCtrl.Bind(wx.EVT_SEARCHCTRL_SEARCH_BTN, self._onSearchEntered) self._searchCtrl.Bind(wx.EVT_SEARCHCTRL_CANCEL_BTN, self._onSearchCanceled)
By contrast, the label, previous and next buttons, and bitmap do not have any sub-controls that need to concern us. We can set them up as follows:
self._labelStaticText = wx.StaticText(self) self._prevButton = wx.Button(self, label='Prev') self._prevButton.Bind(wx.EVT_BUTTON, self._onPrevButtonClicked) self._nextButton = wx.Button(self, label='Next') self._nextButton.Bind(wx.EVT_BUTTON, self._onNextButtonClicked) self._staticBitmap = wx.StaticBitmap(self)
Our controls are lined up horizontally, with the search control on the left edge of the window, the previous and next buttons on the right edge, and a label halfway in between the search control and previous button. We will use an instance of wx.BoxSizer
to define this horizontal layout:
controlsSizer = wx.BoxSizer(wx.HORIZONTAL) controlsSizer.Add(self._searchCtrl, 0, wx.ALIGN_CENTER_VERTICAL | wx.RIGHT, border) controlsSizer.Add((0, 0), 1) # Spacer controlsSizer.Add( self._labelStaticText, 0, wx.ALIGN_CENTER_VERTICAL) controlsSizer.Add((0, 0), 1) # Spacer controlsSizer.Add( self._prevButton, 0, wx.ALIGN_CENTER_VERTICAL | wx.LEFT | wx.RIGHT, border) controlsSizer.Add( self._nextButton, 0, wx.ALIGN_CENTER_VERTICAL)
The best thing about layouts (and Russian dolls) is that they can be nested, one inside another. Our horizontal layout of controls needs to appear below the bitmap. This relationship is a vertical layout, which we will define using another wx.BoxSizer
instance:
self._rootSizer = wx.BoxSizer(wx.VERTICAL) self._rootSizer.Add(self._staticBitmap, 0, wx.TOP | wx.LEFT | wx.RIGHT, border) self._rootSizer.Add(controlsSizer, 0, wx.EXPAND | wx.ALL, border) self.SetSizerAndFit(self._rootSizer) self._updateImageAndControls()
This is the end of the __init__
method.
As seen in the following code, we provide getters and setters for the verbose
property of our ImageSearchSession
instance and our HistogramClassifier
instance:
@property def verboseSearchSession(self): return self._session.verbose @verboseSearchSession.setter def verboseSearchSession(self, value): self._session.verbose = value @property def verboseClassifier(self): return self._classifier.verbose @verboseClassifier.setter def verboseClassifier(self, value): self._classifier.verbose = value
Our _onCloseWindow
callback just cleans up the application by calling the Destroy
method of the superclass. Here is the implementation:
def _onCloseWindow(self, event): self.Destroy()
Our _onSearchEntered
callback submits the query string via the search method ImageSearchSession
. Then, it calls a helper method, _updateImageAndControls
, which asynchronously fetches images and updates the GUI, as we will see later. Here is the implementation of _onSearchEntered
:
def _onSearchEntered(self, event): query = event.GetString() if len(query) < 1: return self._session.search(query) self._index = 0 self._updateImageAndControls()
Our _onSearchCanceled
callback simply clears the search control's text field, as seen in the following code:
def _onSearchCanceled(self, event): self._searchCtrl.Clear()
Our remaining GUI event callbacks _onNextButtonClicked
and _onPrevButtonClicked
check whether more results are available and if so, use the searchNext
or searchPrev
method of ImageSearchSession
. Then, using the _updateImageAndControls
helper method, images are fetched asynchronously and the GUI is updated. Here are the implementations of the callbacks:
def _onNextButtonClicked(self, event): self._index += 1 if self._index >= self._session.offset + \ self._session.numResultsReceived - 1: self._session.searchNext() self._updateImageAndControls() def _onPrevButtonClicked(self, event): self._index -= 1 if self._index < self._session.offset: self._session.searchPrev() self._updateImageAndControls()
The _disableControls
method disables the search control and the previous and next buttons, as follows:
def _disableControls(self): self._searchCtrl.Disable() self._prevButton.Disable() self._nextButton.Disable()
Conversely, the _enableControls
method enables the search control, the previous button (if we are not already at the first available search result), and the next button (if we are not already at the last available search result). Here is the implementation:
def _enableControls(self): self._searchCtrl.Enable() if self._index > 0: self._prevButton.Enable() if self._index < self._session.numResultsAvailable - 1: self._nextButton.Enable()
The _updateImageAndControls
method first disables the controls because we do not want to handle any new queries until the current query is handled. Then, a busy cursor is shown and another helper method, _updateImageAndControlsAsync
, is started on a background thread. Here is the implementation:
def _updateImageAndControls(self): # Disable the controls. self._disableControls() # Show the busy cursor. wx.BeginBusyCursor() # Get the image in a background thread. threading.Thread( target=self._updateImageAndControlsAsync).start()
The background method, _updateImageAndControlsAsync
, starts by fetching an image and converting it to the OpenCV format. If the image cannot be fetched and converted, an error message is used as the label. Otherwise, the image is classified and resized to an appropriate size for display. Then, the resized image and the classification label are passed to a third and final helper method, _updateImageAndControlsResync
, which updates the GUI on the main thread. Here is the implementation of _updateImageAndControlsAsync
:
def _updateImageAndControlsAsync(self): # Get the current image. image, url = self._session.getCvImageAndUrl( self._index % self._session.numResultsRequested) if image is None: # Provide an error message. label = 'Failed to decode image' else: # Classify the image. label = self._classifier.classify(image, url) # Resize the image while maintaining its aspect ratio. image = ResizeUtils.cvResizeAspectFill( image, self._maxImageSize) # Update the GUI on the main thread. wx.CallAfter(self._updateImageAndControlsResync, image, label)
The synchronous callback, _updateImageAndControlsResync
, hides the busy cursor, creates a wxPython bitmap from the fetched image (or just a black bitmap if no image was successfully fetched and converted), shows the image and its classification label, resizes GUI elements, re-enables controls, and refreshes the window. Here is its implementation:
def _updateImageAndControlsResync(self, image, label): # Hide the busy cursor. wx.EndBusyCursor() if image is None: # Provide a black bitmap. bitmap = wx.EmptyBitmap(self._maxImageSize, self._maxImageSize / 2) else: # Convert the image to bitmap format. bitmap = WxUtils.wxBitmapFromCvImage(image) # Show the bitmap. self._staticBitmap.SetBitmap(bitmap) # Show the label. self._labelStaticText.SetLabel(label) # Resize the sizer and frame. self._rootSizer.Fit(self) # Re-enable the controls. self._enableControls() # Refresh. self.Refresh()
When the image cannot be successfully fetched and converted, the user sees something like the following screenshot:
Conversely, when an image is successfully fetched and converted, the user sees the classification result, as shown in the following screenshot:
That completes the implementation of the Luxocator
class. Now, let's write a main method to set resource paths and launch an instance of Luxocator
:
def main(): os.environ['REQUESTS_CA_BUNDLE'] = \ PyInstallerUtils.resourcePath('cacert.pem') app = wx.App() luxocator = Luxocator( PyInstallerUtils.resourcePath('classifier.mat'), verboseSearchSession=False, verboseClassifier=False) luxocator.Show() app.MainLoop() if __name__ == '__main__': main()
Note that one of the resources is a certificate bundle called cacert.pem
. It is required by Requests in order to make an SSL connection, which is in turn required by Bing. You can find a copy of it inside this chapter's code bundle, which is downloadable from my website at http://nummist.com/opencv/7376_02.zip. Place cacert.pem
in the same folder as Luxocator.py
. Note that our code sets an environment variable, REQUESTS_CA_BUNDLE
, which is used by Requests to locate the certificate bundle.