Chapter 4, Maximizing Your Data: Histograms and Stretching
If you've read the previous three chapters of this tutorial, you've no doubt noticed that I have been hammering home one point
more than any other: when capturing, calibrating, and stacking your images, do NOT mess with the histogram! Making sure you
don't touch the histogram in the first stages of processing insures that all of the data you've captured in your hard-fought hours of imaging is available for the latter stages of processing. Now it's time to take a good, close look at that data, see how it can be manipulated to show an object in the best way, and make those changes to the image data.
First, however, another warning: if you are taking images with the intent of doing brightness measurements (photometry) of stars, asteroids, or other objects, you're already done. Don't do ANY histogram manipulation of your image. Accurate photometry requires that you calibrate your images with both darks and flats, and then do NO histogram stretching or manipulation. Doing so will throw off the pixel values in the image that are required to do brightness measurements. From this point on out, everything we'll be doing is for "pretty pictures" -- not for scientific purposes. Fair warning.
Histograms Revisited
Let's start out with a brief review of histograms. A histogram is simply a graphical representation of the range of pixel values in an image:

An image histogram display from MaximDL/CCD software
The histogram display above is from a "raw" CCD image. The histogram is a 2D graph -- the horizontal axis represents the brightness values in the image's pixels, from 0 to 65,535. The vertical axis represents *how many* pixels in the image have that value. Looking at the histogram above, you can see that there is a large peak at the low end, where the values in the pixels are closer to zero (or black), and the values gradually slope off as the pixel values get higher. The peak isn't at zero (totally black), so there is some sky background and the image probably hasn't been calibrated with a dark frame. There are a fairly large number of pixels in the image that are brighter than the peak (which represents the majority of the background pixels), and not very many pixels that are really bright (though there is a small peak close to 65,535 -- which indicates some bright stars). Once you get the hang of interpreting histograms, it's fairly easy to tell a lot about an image without even seeing the actual image. Let's look at another histogram:

A more evenly-spread histogram display
Once again, "reading" this histogram tells us that the peak of values (the majority of the background) is much closer to zero than in the previous example, there's a much larger range of medium-brightness pixels, with the slope off towards the brigher (and less numerous) pixels less steep than in the one above. Looking at this histogram, it's clear that the image has been "stretched" -- if you look carefully at the first histogram above, you can see that the very thin peak area actually rises up from the left and then slopes down to the right before taking the steep plunge downward and to the right. The second example has actually just taken that small area of the first histogram and spread it out over more of the pixel values.
That's what "stretching" an image is all about. You want to manipulate the pixel data in the image so that the values you care about (usually values above the background but not bright like stars) take up more of the image's possible dynamic range than they originally did. You "stretch" the section of the image that contains the object data so that it fills up more of the pixel range than the background or the bright values from stars. Stretching parts of the histogram will give more emphasis to the range of pixels you stretch than to other ranges. Your goal is to show all the possible brightness variations of the object you're imaging without blackening the background or saturating all of the star images.

What parts of a histogram mean in an image...
The somewhat busy picture above shows how the parts of the histogram "map" to what you see in an image. The part of the graph at the far left edge show the count of very-nearly-black, dark pixels in the background. The part of the histogram to the right of that, just before the "peak," shows the majority of the dark-but-not-too-dark background pixels. The peak of the histogram, for this image, indicates the fainter nebulosity that makes up the majority of the image. To the right of that, it shows the brighter (getting closer to white) nebulous areas and dimmer stars, and finally all the way to the right shows the brightest stars in the image. This is a well-balanced image, with the background sky dark but not black, showing a wide range of nebula values from dim to bright (remember, this area was *stretched* to show it off better), and only a couple of the very brightest stars fully saturated to pure white. The shape of this histogram is something you should remember -- it's a good example of how most of the images you work with should have their histograms look. The image takes full advantage of the 16-bit dynamic range of the camera's capabilities, and is tonally smooth without a harsh look, yet has enough contrast to clearly discern very faint features from the background.
I hope now you understand what a histogram really is, and have a little better understanding of how to "read" one. Now let's move on to the techniques you can use to go from that first, raw histogram to the final one!
Stretching
"Stretching" is the process of mapping a range of brightness values in an image to a new range of brightness values. The software you use takes the old pixels values within a certain range, and through a mathematical function gives all of the pixels new brightness values. There are lots of ways to stretch an image -- let's start with the simplest of all, a linear stretch.


A starting histogram, and the image it's taken from
The histogram above left, and the image just to the right of it, represent a sigma-summed 4 hours of exposure of the Horsehead and Flame Nebula region. All of the individual images making up the sigma sum (each 10 minutes long) has been dark-frame subtracted. Looking at the histogram, you can see that there are no pixels with values below about 400, and some bright pixel values around 62,000 (so even the bright stars aren't saturated). Most of the rest of the pixels, which some the nebulosity in this region, are clustered around the 1000-18000 value range. There is a large "gap" in the histogram, with very few pixels, from above 18000 until you hit much higher values.
In MaximDL/CCD (and most other astro image processing programs), there are two kinds of stretches: a "screen stretch," which affects how the image pixel values are mapped to new values for display on the screen, and an actual stretch "operation," which modifies the data in the image file so the pixels have new values. You can change the screen stretch, which changes how the image is displayed ONLY, by moving the black point (red) and white point (green) sliders on the histogram display, and the program will display the image using the new settings without affecting the actual image. The image above is currently displayed with the black point set at 408 (the lowest value that any pixels in the image have), and the white point set at 62,453 (the highest value that any of the pixels in the image has). What that does is make most of the image "invisible" -- only the brightest white stars can be seen. This happens because computer displays are almost always 8-bits per color devices, while the image is 16-bits monochrome. To display the 16-bit image on your computer screen, the software maps all of the 65,536 possible 16-bit values in the image down into 256-possible 8-bit values so your computer can display it. That means that a range of 256 values in the original image will all map to a single 8-bit color value. With most of the pixels in this image having values down near the lower (darker) end, the "interesting" pixels all map to black or very nearly black, and we can't see them on our screen. Let's move the sliders a bit (remember, we're only affecting the *display* of the image right now, not the actual data) so we can see the image a little better.


Changing Screen Stretch by moving the sliders, and how it affects the image
By moving the black point slider down to 84, and the white point slider town to 3163, we have changed the range of pixels that we're asking the software to use to map to the 8-bit display. Instead of mapping some 62,000 values to 256 values, now we're only mapping a small range (84 to 3163) of pixel values, which are really the ones that interest us. Since the number of output values remained the same (256), but the number of possible input values shrunk dramatically (62,000 to 3079), instead of mapping ranges of 256 values from the image to one output value we're now mapping ranges of 12 values to one output value. The software *stretches* the image's pixels to fit the new display parameters, and we can see much more of the image. Remember that nothing in the original image data has changed -- all 16-bits worth of data are still there -- only the way we're seeing it on screen has changed.

By zooming in on only the range inside the new black/white points,
we can see the histogram of the range of pixels we're displaying!
The histogram above is the exact same as the previous one, except we've zoomed in on the range of pixels that we're mapping to the display. We've "stretched" the most interesting part of the histogram out horizontally by remapping the black and white points, changing the way the image appears. Notice how much more like the "ideal" histogram mentioned above this looks...
The screen stretch is a "linear stretch" -- it simply maps the input values to the output values linearly, taking every 12 values from the input (in the case above) and giving them new values for display. Values from about 84 to 95 (AND anything BELOW 84) in the original image will map to 0 (pure black) for display, from 96 to 107 will map to 1 for display, 108 to 119 will map to 2 for display, etc. Note also that we set the white point to 3163...this means that pixels in the original image with values from about 3152 AND ABOVE will all be mapped to 255, or pure white. Remember this point -- any pixels at or above the white point (or very near to it) will go to pure white when an image is stretched.
We can stretch further, moving the black point up to 348 and the white point down to 1437, which will show even more detail in the image (producing a 4.2 to 1 pixel mapping of image to display):


Stretching even further shows most of the detail in the image, at the cost of blowing out the bright parts!
With those values for the black and white points, we can now see most of the detail in the nebulous parts of the image, and the zoomed-in histogram looks much closer to our "ideal" histogram above. However, you should notice that now most of the stars in the image, and several of the bright parts of the Flame Nebula, have saturated to pure white. This happened because of the much lower white point -- any pixels in the image now with a brightness of about 1430 or above are being displayed as pure white, and we've lost all of the subtle shading data in the brighter ranges of the picture in order to see the middle values better. Compare the image above to the image above with the arrows...notice how this one has blown out highlights and saturated stars. That's one of the artifacts of linear stretching -- in order to see a better range of middle-brightness values, you generally need to set the white point so low that highlights get blown out.
Despite the blown-out highlights, the image *does* look better than the unstretched version. If we want to make the linear stretch we've been looking at permanent -- burn it into the actual image data instead of just doing a screen stretch, we can do so. In MaximDL/CCD, you do this by performing the Stretch function (which affects the image instead of just the screen display). Choose Stretch from the Process menu, and the dialog box shown below will appear:

Applying a linear stretch to the image data, not just the screen,
using MaximDL's Stretch function
In the stretch dialog box, we've chose "Linear Only" (ignore those other two for now, we'll get to them shortly), "Screen Stretch" (which tells the stretch function to use the black and white points we set on the histogram display for the screen stretch), and "16-bit" (to use the full 16-bits available in the camera's native format). Clicking OK on this dialog box performs the stretch from the selected black/white point range to a full 16-bit range of 0 to 65,535. This is how the image's histogram looks after performing the stretch function:

Histogram of a linearly-stretched 16-bit image
Notice that the histogram's shape *looks* the same as the zoomed-in screen stretch histogram just above -- the only difference is that instead of our selected range being mapped to 8-bit values for display, the stretch function mapped our selected range to full 16-bit values. We've now mapped 1089 possible brightness values (348 to 1437) to 65,535 possible brightness values. The mapping is sort of a reverse of the screen stretch, going from fewer to more values instead of more to fewer. This gives us 60 possible output values for every possible input value. Wait, how can that be...60 output values for every input value? How does the program know which of the 60 values to map the input value to? The answer is that it *doesn't*! The software can't make up numbers for the ones in the 60 output value range, instead it just stretches out the mapping...input pixels with a value of 348 (or lower) get an output of 0, input pixels with a value of 349 get an output value of 60, input pixels with a value of 350 get an output value of 120...you get the picture. But, you might say, doesn't this leave big gaps in the output histogram? Yep, it sure does.

Zooming in on a portion of the stretched histogram shows the "gaps"...
This isn't the ideal situation, but it's really pretty much OK. Despite the gaps in the histogram, your eye still perceives the image as being a continuous tonal range for the most part. Once again, this is a "feature" of linear stretches with integer data, and will happen anytime you map a smaller range of integers to a larger range of integers. As you'll see below there are also ways to stretch to avoid much of the "gapping" of a linear stretch, you just need to be aware of what's really happening to your data when you do stretches.
Gamma and Other Non-Linear Stretches
We've seen that a linear stretch can map the more interesting portions of an image to a wider brightness range, and make them more visible -- this is a good thing. We've also learned that a linear stretch will usually blow out the highlights in order to do that mapping -- this is a bad thing. Wouldn't it be nice if there was a function that would allow us to stretch the range of an image that we really want to stretch, and leave the other parts alone so as not to blow out the bright parts?
As it turns out, there is.
Non-linear stretches.
Let's go back to linear stretching for just a moment, and take a graphical look at how it works.



Linear stretches show graphically, indicating the transfer function
Above are three graphs that show how linear stretches work. On the left is a direct one-to-one mapping of pixel values -- the input pixel value gets mapped directly to the same output pixel value. It's not really a stretch, since the values of the pixels don't change, but it illustrates a reference "transfer function." In the middle, a large range of input pixel values (under the red shaded part of the graph) get mapped to a smaller range of output pixel values (under the green shaded part of the graph). This what happens when you do a screen stretch, and map a large number of input pixel values to a smaller number (say, 8-bit values). Notice the line that defines the transfer function is still a straight line (making this a linear function), it's just at a steeper angle than the reference function. Finally, on the right, is a mapping from a smaller number of input values (again, under the red shaded portion) to a larger number of output values (the green shaded portion). This is the same as a 16-bit stretch from a range of the initial image to a full 16-bit output range. Once again, the line defining the function is straight, indicating a linear function, but less steep than the reference function. In math terms (which I try to avoid as much as possible in this tutorial), only the slope of the transfer function has changed -- but they're all linear functions.
My wish at the beginning of this section was for a function that would stretch out the mid-range values, but leave the ends alone and not blow out the highlights. Looking at the graphs above, can you think of a shape for the transfer function line that would do just that? What if we threw out the requirement for the line to be a *straight* line (a linear function), and instead allowed it be be a *curve*? A non-linear function?

Allowing the transfer function to be a curve...
Take a look at the graph above with a CURVED transfer function, and take a look at the lines that show how a few input values get mapped to output values. Pixels on the input side that are dimmer (near the lower part of the "Input" vertical axis) get mapped as brighter on the output side (further right on the "Output" axis). At the middle of the input range, they're getting mapped to about 80% of the brightest output value, boosting their brightness quite a bit! However, as we move up the input values from there, the rate of change of the mapping slows down, so that by the time we're near the top, the input pixels are getting mapped to nearly the same value in the output. The curve shown boosts low-to-midrange pixel values higher, while leaving the bright input pixels alone. Our perfect stretch function!
Fortunately, most image-processing programs have a function built-in to do this kind of non-linear stretching. It's called "Gamma" stretching. A gamma stretch builds a transfer curve based on several inputs that you provide, and applies the non-linear function to the input data to give new output data. In some programs (notably Adobe PhotoShop), you even get a graphical display of the transfer curve that looks just like the graphs above (which is why I made them that way! ), and you can push, pull, twist, and tweak the curve to get just the effect you want. In many others, like MaximDL/CCD, you specify the shape of the curve with numbers. Gamma values above 1 bend the curve's middle up and to the left in our graphs, meaning that middle input values produce a *smaller* range of output values, darkening the overall midrange of the image. Gamma values below 1 bend the curve as shown in the graph above, down and to the right, brightening mid-range values. Let's look at a couple of examples on a real image.


Same base image gamma-stretched with a gamma value of 0.5 (left),
and linear-stretched (right).
Compare the two images above, gamma-stretched with a gamma value of 0.5 on the left, and linear-stretched from our exercise above. The dimmer parts of the nebula in the image are about the same brightness in the two images -- but the gamma stretched version hasn't blown out the brighter parts of the Flame Nebula, still showing a good range of brightness values there. Also look at Alnitak, the bright star near the middle of the frame...in the linear-stretched version, the star has "grown" in size, since the low white point of the linear stretch took a large range of values right up to pure white, so the brighter pixels surrounding the star itself all went white. The gamma-stretched image, with it's non-linear transfer function, basically left that range of pixel values as they were, and didn't enlarge the star. Look at many of the other star images, and compare the range of star brightnesses in the gamma version with the blown-out-white stars of the linear-stretch version. The gamma stretch did just what we wanted it to do, boosting low and mid-level brightness values while leaving the bright parts of the image alone. Cool!
I usually do a few gamma stretches in a row to an image. I'll first set the black point just below the start of the background pixel values (in the case of this first image, at about 384), leave the white point at the maximum pixel value, and do a gamma stretch of 0.5 or 0.4. This is a fairly extreme curve, and does a good job of bringing up those low and mid-level pixel values. I'll then move the black point closer to the newly-generated histogram's left edge, move the white point down just a bit so that really bright stars *do* saturate but the rest are left alone, and do another gamma stretch of about 0.8 to once again punch up the midrange values. The result of two stretches in a row looks like this:


Gamma stretch of 0.5, adjust white/black points,
then gamma stretch of 0.8 gives good results! Compare to linear-stretch version...
Once again, compare the twice-stretched gamma version to the linear stretched version. There's much more detail in dim areas of the gamma image, without saturating any of the dark areas. Because I moved the white point down just a little, Alnitak has grown a bit over the first gamma image, but it's still not nearly as oversized as the linear version, and most of the other stars still show good brightness variation. This is pretty close to a final "stretch" for the image.
For completeness' sake, let's also look at what gamma stretching does to the image's histogram. Here's the histogram of the twice-stretched gamma image:


Full histogram of doubly-gamma-stretched image, and zoomed in on part of the histogram
The histogram on the left is the full-view of the new image's brightness graph. This shows very close to the "ideal" shape as mentioned at the top of this page, and is in great shape. The histogram on the right is a zoomed-in view of part of the one on the left -- remember the blank spaces we got after linear stretching? That happens with gamma stretching as well (we are, after all, still mapping a smaller number of input values to a larger set of output values), but you can see from the image that the "gaps" aren't equally spaced as they were with the linear stretch. The gaps get fewer and further apart as we move from dimmer to brighter pixels, showing the non-linear nature of the mapping function, and that fact that as we got towards bright pixels the transfer function didn't stretch their values as much when remapping, but mostly left their values as-is. Non-linear stretching is a wonderful thing!
While we're on the subject of non-linear stretches, keep in mind that a gamma stretch is not the only curve in town. Now that we're away from strictly linear brightness mappings, we should be able to specify any shaped curve we want to, right? Right. Some curve shapes (like the gamma curve) are very useful for our purposes, and some aren't. MaximDL/CCD, for example, has a function called "Histogram Specification" that brings up the dialog box below:

Specifying arbitrary curves in MaximDL/CCD
This dialog box lets you specify the shape of the transfer function yourself, and gives starting point curves from a number of mathematical functions. The curve shown above is for the logonormal function, and is actually quite similar to a gamma curve with just a bit more boost in the low end and even more dramatic flattening on the high end (Maxim's curves are sideways compared to mine...). Applying the logonormal curve to our raw image, we get the results below:



logonormal stretch applied to image (left), compared to gamma (middle) and linear (right)
Personally, I like the results of gamma streching the best, and find I have the most control over the results using it. However, the log stretched image has some very good properties, and experimenting with combinations of these two transfer functions could produce some very interesting results. Just remember that, unlike a screen stretch, performing a non-linear stretch on an image changes the data in the image -- so save intermediate results you get with new file names often. That way if you ever want to go back a few steps and try something different, you have a saved version of the image you can revert to and try new things on. Try various curve shapes for the transfer function, play around with the white and black points, and basically mess around with your images to see what kind of results you can get. Some images will respond well to certain curves, while others will do better with other curves. Get to know what's available in your own image processing software so that you can have a good idea ahead of time what stretch functions will do to your images.
Above all, keep in mind that the goal of stretching your image data is to show off the things you want to show off. My tirades against clipping image data while doing capture or calibration in the previous chapters were all designed to get you to stretch operations with the maximum amount of data to be manipulated. Look at those images above -- there is an awful lot of faint detail that's just above background level, and if you had set a too-high blackpoint at some stage before getting to the stretch, you would never be able to pull that data out with non-linear stretching. Same thing goes for the white point -- if you'd set it too low earlier on, the stars (and bright parts of nebulae) would have already been saturated, and you wouldn't have to worry about trying to keep subtle value changes at the bright end of your histogram when stretching, because there wouldn't *be* any subtle value changes, everything would have been pure white.
Stretching the data in your images gives you a chance to manipulate the data in the image to make full use of the dynamic range of the data, to highlight important parts of the image and to include those subtle brightness variations that are the difference between a "harsh" image and a beautiful one. Take your time with it, play around with it, and have fun!
Brightness and Contrast
Astute readers will no doubt notice that I haven't mentioned Brightness or Contrast adjustments until now...that's because we've really been adjusting brightness and contrast all along with our stretches.
Brightness and contrast are actually just linear stretches. Brightness moves both the black point and white point the same amount at the same time. Increase brightness, and both the black and white point sliders on the historgram display move to the left. Decrease brightness, and both sliders move to the right.
Contrast is similar, but instead of moving the black and white point sliders the same direction as with brightness, contrast adjustments move them in opposite directions. Increasing contrast moves the white point to the left, and the black point to the right (shrinking the range of values displayed, and mapping the inputs to fewer outputs). Decreasing contrast moves the black point to the left, and the white point to the right, increasing the range of output values. Most non-astronomical image processing packages have seperate brightness and contrast adjustments -- now you know what they're really doing! Astro packages like MaximDL don't have functions to do those two adjustments, since you can do it yourself using the histogram black and white point sliders. If you want to directly do brightness and contrast adjustments in MaximDL, though, you still can -- on the histogram display, notice the box at the upper right that shows a range of grayscale values...if you click and hold down your mouse button on this box, you can drag the mouse to move the sliders together. Moving left and right adjusts contrast (moving the sliders towards the middle of the histogram or away from it), while moving the mouse up and down adjusts brightness (moving both sliders to either the right or the left at the same time). If you're using non-astro image processing software, be careful with brightness and contrast adjustments -- remember, they're just linear stretches, with the clipping and washout pitfalls of that stretching method. Keep the adjustments small and subtle for best results.
Hopefully now you have a good understanding of how to read histograms, how (and why!) to use various image stretches on your data, and the importance of having a full dynamic range image that highlights the parts of your scene that you want to emphasize. Sometimes, though, after taking, calibrating, stacking, and stretching your images, some things that you do NOT want to have emphasized look like glaring beacons...tracking errors, satellite trails, reflections in your optics (like the reflections around Alnitak in the images we've been using for this chapter), and other nasties. The next chapter will deal with minimizing or eliminating those problems, and put the finishing touches on your hard-won data!
Tutorial Introduction
Chapter 1: Gathering and Preparing the Image Data
Chapter 2: Calibration -- Darks and Flats
Chapter 3: Aligning and Stacking Images
Chapter 4: Maximizing Your Data -- Histograms, Stretching, Contrast, Brightness, Gamma
Chapter 5: Dealing with Imperfections and Artifacts
Chapter 6: Basics of Color Images
Chapter 7: Advanced Color Image Processing
Chapter 8: Advanced Techniques -- Masking and other tricks
Chapter 9: Summary and Final Thoughts
All text and images Copyright (c) 2003, Paul LeFevre
Mail me with comments & criticisms!