A NASA TV Still Frame Viewer in Python
I wrote Spacestills, a Python program for viewing NASA TV still frames.
The main window of Spacestills running on Replit. |
As a hobbyist wishing to improve my Python programming skills, for some time I’ve wanted to work on learning projects more substantial than code snippets, throwaway tools, or short scripts.
Spacestillschecks several boxes. The problem domain is one of my primary interests, space exploration. At about 350 lines of code, it’s a non-trivial system with a GUI. It accesses the network to download data from the web. Finally, the program relies on a few Python libraries.
About the program
Spacestills periodically downloads NASA TV still frames from a web feed and displays them in a GUI.
The program allows to correct the aspect ratio of the frames and save them in PNG format. It downloads the latest frame automatically and gives the option to reload manually, disable the automatic reload, or change the download frequency.
As a learning exercise, Spacestillsis a basic program with minimal features.
However, it does something useful: capturing and saving images of space events NASA TV covers. To complement the commentary and discussion, space enthusiasts often live blog events by sharing to social networks or forums the screenshots they manually take from NASA TV. Spacestillsspares the effort of using screen capture tools and saves image files that are ready for sharing.
Visit the Spacestills project site for the full source code as well as the installation and usage instructions. You can run Spacestills online on Replit.
Development environment
I developed Spacestills with Replit. Replit is a development, deployment, and collaboration environment in the cloud that supports dozens of programming languages and frameworks, including Python. As a Chrome OS and cloud enthusiast, I love Replit because it works fully in the browser and there’s nothing to download or install.
The full workspace where I developed and maintain Spacestills is available on Replit where you can also run the program online.
On my ASUS Chromebox 3, which I use for working on Spacestills with Replit, I could have run in a Crostini Linux container a traditional Python IDE such as PyCharm. But Replit is perfect for my needs, is not overkill, starts up fast, and demands fewer resources.
Resources and dependencies
Spacestills relies on a number of resources and Python libraries.
This is by design. I wanted a programming project simple enough to complete at my learning stage, but complex enough to pull together network data and a few Python libraries.
NASA TV feed
The Kennedy Space Center website has a page with a selection of NASA video feeds, including the NASA TV Public Channel. The feeds show the latest still frames and are updated automatically.
Each feed comes with frames in three sizes and Spacestills relies on the largest NASA TV feed featuring 704x408 pixel frames. The maximum update frequency is once every 45 seconds. Therefore, retrieving the latest still frame is as simple as downloading a JPEG image from the feed’s URL.
The raw images are stretched vertically and look odd. So the program can correct the aspect ratio by squeezing the images and producing an undistorted 16:9 version.
Python
Although Spacestills may work with earlier versions, I recommend using Python 3.6. The program doesn't use any language features specific to version 3.6 or later, but PySimpleGUI will eventually require 3.6.
Libraries
Spacestills depends on these Python libraries:
- Pillow: image processing
- PySimpleGUI: GUI framework (Spacestills uses the Tkinter backend)
- Requests: HTTP requests
Design
The program displays one image at a time, a still frame from the NASA TV feed.
I decided to store the image in memory for simplicity and performance. This does away with the complexity of keeping track of temporary files. Memory storage is an acceptable tradeoff because the single image is small and doesn’t raise resource usage concerns.
I could simplify the logic by having the program unconditionally download, resize, and display a new still frame from the feed, even when the user changes the aspect ratio.
Aside from performance issues, this may create inconsistencies. What if the user wanted to resize the currently displayed image when a new one was available in the feed? Downloading unconditionally would discard the image the user still wanted. Instead, Spacestills caches the original image of the current frame and resizes a copy.
The cache solves another problem. Cycling several times between making the same bitmap larger and smaller degrades quality and introduces artifacts, so Spacestills fetches the cached original whenever it needs to change the size.
Image representation and storage
The PySimpleGUI Image user interface element is a natural choice for visualizing a still frame.
Since PySimpleGUI’s Tkinter backend accepts few image formats as input to the PySimpleGUI Image element, so there are constraints on the format for storing the image downloaded from NASA TV’s feed. Since PySimpleGUI.Image doesn’t support JPEG Spacestills converts the downloaded image to PNG which PySimpleGUI.Image accepts.
The program stores the image data in a Pillow PIL.Image object. The library has convenient methods for creating images from files, resizing images, and saving them to files.
The StillFrame class
StillFrame is a Spacestills class that holds a still frame downloaded from the NASA TV feed.
The StillFrame.image attribute stores the image as a PIL.Image instance in PNG format and all the StillFrame methods maintain the format invariant, converting to PNG if necessary. The cached original image, again a PIL.Image instance, goes in the StillFrame.original_image attribute.
The class has methods for returning the raw image data PySimpleGUI.Image needs, calculating the aspect ratio, resizing the image, and converting to PNG.
The decision to keep the image in memory brings the side benefit of simplifying some methods, for example StillFrame.bytes() that returns the raw bytes of a frame image. The implementation relies on Python’s BytesIO binary streams and is straightforward:
def bytes(self):
file = BytesIO()
self.image.save(file, 'png')
file.seek(0)
return file.read()
The StillFrame.topng() method for converting the image to PNG was originally private. Since Spacestills has no APIs or clients, private methods and attributes would probably over-engineer the program at this early stage. If I decide to re-use some of the code, for example, to employ a different GUI or visualization frontend, I may provide better encapsulation.
Why use a class at all? Isn’t it overkill for managing a cache and not much else?
I experimented with holding a still frame in a PIL.Image and using Python’s ability to dynamically create a new attribute for the cache without changing the original class. This way the image processing methods became ordinary functions, taking the image as an argument.
However, I didn’t see a clear advantage of one solution over the other and went with the class-based one. This leaves the door open to storing additional state, for example metadata such as the download time.
Still frame download and processing
Function download_image() is at the core of the program as it downloads an image from the feed and returns a PIL.Image instance. The function creates a PIL.Image that reads from a BytesIO stream which, in turn, reads the response content an HTTP request to the feed URL returns.
In case of network errors or other issues, download_image() returns a blank image with a blue background. This way no empty area is left in the GUI where the image is supposed to be. Function make_blank_image() takes care of creating and returning the blank image.
To refresh the view with the latest still frame, either automatically or manually by the user, the code calls function refresh(). The function has to be called with an argument indicating whether to resize the still frame if the aspect ratio correction option is active. refresh() can update the PySimpleGUI.Image element that displays the still frame with just one line of code:
window['-IMAGE-'].update(data=still.bytes())
The code retrieves the element identified by the key -IMAGE- from the window dictionary, then updates its data parameter with the raw bytes of the still frame still, which holds the current still frame. The data parameter of the PySimpleGUI.Image element accepts the bytes of an image.
Function change_aspect_ratio() does the actual resizing of the still frame and updates the PySimplegUI.Image element with similar code.
To save the still frame, the program calls the straightforward function save(). It takes as arguments a StillFrame instance and a filename returned by the PySimpleGUI popup_get_file() function, which presents to the user a file save dialog.
The image download loop
Spacestills runs a loop that downloads the still frames from the feed and displays them, unless the user turns off the automatic update option. The user can also change the frame update frequency or manually reload the frame.
The download loop is implemented in part as a clause of the program’s event loop, a feature of the PySimpleGUI GUI framework. The download loop relies also on the next_timeout() function, to calculate the time when to download the next frame, and the timeout_due() predicate to check whether the next download is due.
The event loop
The program’s main() function mostly contains PySimpleGUI boilerplate code to create a window from a layout and run the event loop.
The loop branches on the event and values read from the program window, updates some state, and calls the functions to perform the requested actions. There’s a clause for every user interface element plus a check of whether the next automatic image reload is due.
GUI layout
I experimented with several GUI layouts, and none were completely satisfactory.
Aside from the natural grouping of the auto-reload settings, I found no good way of further organizing the user interface elements into groups or hierarchies that would make the program more intuitive. In the end I went with a layout that arranges the elements in two rows, the first one containing all the elements except for the auto-reload settings that go on the second row.
Open issues
Spacestills is my largest Python project so far, and I’m mostly satisfied. It has all the features I planned, works well, and the code is moderately Pythonic.
However, the system has a few issues and ways it didn’t turn out as expected.
Overall, Spacestills feels like a beginner project, which is not necessarily a bad thing at my learning stage. A first hint is some identifiers don’t have consistent names and others aren’t descriptive enough.
I also feel the design leaves something to be desired. I’m not sure I picked the right abstractions, functions, and classes to implement the logic. This is likely a challenge beginners face when scaling their code to larger systems.
The event loop seems long. But wrapping its functionality in several small functions for the sake of brevity wouldn’t probably help much. Although not encapsulated, the current code is straightforward, makes the logic clear, and improves readability.
Speaking of design and abstractions, the way refresh() and other functions depend on the layout of the GUI via hard-coded window keys seems off. It’s in part a consequence of the way the domain logic interleaves with framework code. I can’t think of a better way of decoupling the references to user interface elements from the domain logic.
The download loop is based on polling. Comparing times in a tight loop feels suboptimal, and I do not know the impact on performance. But the program seems responsive enough.
There’s a minor issue I’m not sure is a bug of my program, of PySimpleGUI, or my misunderstanding of the framework. Typing a filename in the save dialog without going through the browse dialog doesn't add the default .png extension.
Finally, I added comprehensive docstrings and some comments, as well as a README file. Is all this documentation overkill for such a small program? I don’t know, but I wanted to practice with thoroughly documenting a system.
Although I’m aware of these issues and problems, I’m unable to figure better ways of addressing them. This is likely another consequence of my early stage of learning. Hey, it’s progress!
Possible improvements
Spacestills has room for improvement beyond fixing the open issues.
A further step may be to break down the code from a monolithic, single-file program into modules implementing different parts of the system. This would probably simplify the tests, which are currently missing altogether.
Modularizing the program would bring another benefit. Once the domain logic is sufficiently decoupled from the presentation, it shouldn’t be difficult to provide an alternate user interface such as a web frontend.
Spacestills is a step of my journey to using Python with Replit in the cloud.