"if all you have is a hammer, everything looks like a nail"
There is a strong tendendecy among programmers to try to redefine problems to fit their language of choice, but the fact is that different aspects of a problem are often best addressed by tailoring one's approach to that aspect rather than vice versa. The surge in popularity of Python stems from its ability to allow simple tasks to be coded simply while still scaling to larger and larger problems gracefully.
However, "pure" Python is not the right language for intense computation and the designers had the sense to provide a very solid mechanism for incorporating C/C++ libraries as fully-functional extensions to the environment. The Montage Project at IPAC/Caltech (with funding from the NSF) is using that framework to make their astronomical image processing software available as a Python module.
When it comes to user interfaces, the most common approach is, again, to extend Python, this time with Tkinter (embedded Tcl interpreter) or wxWidgets (C++), and to build all the GUI event handling in Python itself. This is perfectly viable, but I think there is another approach that may be better for high-end interfaces.
For a programmable, ubiquitous environment for GUI layout, data presentation and asynchronous event handling, one would be hard pressed to find anything that could compete with the modern browser/Javascript combination. When you add to that the vast array of Javascript libraries available (mostly free), the argument for using this as the primary display/GUI environment becomes overwhelming. It is also surprisingly easy: firing up a browser from Python is straightforward, and though the level of control Python can exert over the browser is currently a little lacking, the two-way communication between Python and the Javascript in the browser page is more than adequate.
Approaching the problem as peer-to-peer communication actually frees both Python and Javascript to do what they each do best; data manipulation in Python and GUI / rendering in Javascript.
With the C library Montage (hammer), Javascript GUI (screwdriver) and Python managment (pliers) we have a much more complete toolbox.
IMPORTANT : While what I have developed here is a perfectly usable tool in its own right, the underlying purpose I was tasked with was to build code that could be used as an exemplar for the above model and possibly a starting point for others who want to extend it for their own purposes. This is decidedly not an attempt to create some sort of, "do everything here," application (there are enough of those) but rather to provide tools and techniques that can be folded in to other work. Some examples of this may be found on other pages on this site.
For that reason, this write-up will take the time to explain some of the details of how the code works. Skip it if you just want to use the program but it is one of the main reasons for the work.
The browser really only knows how to talk to web servers, so before connecting to the browser, we start a server up inside Python. There are many of these, and our use case is not too advanced, so it probably does not matter too much. We went with Tornado, which may be overkill but gives us room to evolve.
We chose to start the web server in an second thread so that we could reserve the main thread for anything else we might want to do (we currently just drop it into an interactive Python interpreter after startup so that the user can type commands at us).
This web server is actually the captive entity: we start it on a random port on "localhost" so only our own processes will see it and only the browser we fire up will know the port (we will point the browser to that localhost port when we start it). So we create a working directory for data (main page, Javascript, images to display, etc.) we will want the browser to have. We clean this space up on exit. [Note: With the addition of remote browser support, that option replaces "localhost" with the full internet name of the machine. "localhost" is still used when we start the browser ourselves.]
The web server will serve this workspace at the root URL. If we have a file "index.html" in the workspace, the URL "localhost:12345/index.html" will return it (with 12345 replaced with the random port number we generated).
Assuming good content in the workspace, we can now fire up a browser to use it. This part of the system is the most primative in terms of the level of control we have. Basically, all Python can do is try to emit a system call to start a browser process, but with no real control other than to tell it what page to load at startup (our "localhost:12345" address). It would be nice to have more of a handshake (e.g. to affect page size, browser controls, etc.) but that does not exist at this point.
We use the standard Python webbrowser package for this, which makes a number of checks for the proper syntax for the startup call based on the browser and platform.
The browser loads our index.html file, which includes the GUI client Javascript code we provide. This code configures the display and sets up GUI event handlers. Both this Javascript and our web server have been configured to support the WebSocket interface, which means we can essentially send "commands" both directions.
In theory, we could send low-level events (e.g, slider bar updateds) in real time but we prefer not to burden Python with those so keep that logic in the browser. Our commands are things like "zoom in" (sent from the browser to Python when the user clicks that control) and "image <url>" (sent from Python to the browser when Python has prepared a new view of the image to display).
All commands are asynchronous. When the browser sends a "zoom" command it does not wait for the new image. Rather it goes back to waiting for the next event, which most likely will be the new "image" command from Python. To avoid extraneous events (attention spans being what they are), the GUI will sometime invoke a gray-out blocker element with a "wait" clock icon which will only be turned off when the right "response" comes along.
After the requisite command traffic to get the initial image displayed (Python does not know how big to make it until the browser sends its window size), the system gets to an essentially quiescent state; the browser is waiting for a user GUI action or a command from Python and Python is waiting for a command from the browser or (in our case) for the user to type a command at the interactive prompt.
It turns out that mViewer is fast enough to feed an interactive system and, in conjunction with a few other Montage modules (for image cutouts, resizing, and region statistics), makes a very useful visualization tool. Since there are so many options, however, an interactive tool either has to default most of them or provide controls for setting/adjusting them. The next page describes our control scheme. More ...
Grayscale output (single image) or full color (three overlayed images at different wavelengths as red, green and blue).
Data stretches customized to astronomical data, which tends to have a large number of pixels near the background level (often black sky) and small numbers of very bright pixels (e.g., stars).
Overlays of coordinate grids (astronomers use multiple coordinate systems aligned with the Earth, the Galaxy, and the Ecliptic). On top of that, some coordinate systems precess with time.
Overlays of source catalogs with symbol size scaled by source brightness. Like images, source brightnesses can cover several order of magnitude do symbol sizes are often scaled logarithmically and due to ancient conventions where a "first magnitude" object is brighter than a "second magnitude", the brightness scale is often in reverse order.
Overlays of image outlines. While there are few astronomical surveys that cover the sky, most image datasets are actually very spotty and only cover interesting patches. Outlines are often the best way to judge coverage.
Markers and labels for custom annotation.