Astrophotography post-processing involves several steps to reveal the full detail and color of an image. A typical workflow after star alignment and stacking includes setting a black point, image stretching , and restoring color lost during the stretch. These steps are commonly performed using software such as Photoshop, PixInsight, or Siril – often in combination. While all of these programs are capable of producing excellent results, they typically require a series of manual adjustments to get to a final image.
There is a solution, however, that can accomplish all of these post-processing operations on a stacked, star-aligned image automatically in one step. The open-source script rnc-color-stretch, developed by Roger Clark, performs this workflow using the davinci data manipulation program from Arizona State University. It runs in a terminal window and delivers remarkably good results. Full details and documentation for the program can be found here.
Running rnc-color-stretch does require a little patience and some underlying technical computer skills. Since all the program parameters must be entered via a command line, it can be a bit finicky to get running at first. The biggest issue I have had, though, is execution time. Even on fast machines, run times can be upwards of 10 to 20 minutes or more depending on image size. While that may be a modest amount of time compared to manually editing an image, it does make optimization over multiple runs with different parameters quite time consuming.
Program astro-color-stretch
Fortunately, there is a solution in the form of astro-color-stretch. It is an upgraded Python version of rnc-color-stretch that performs all the same functions but runs significantly faster. It also does not require a command-line interface. Instead, it can be run easily in an IDE like PyCharm. With astro-color-stretch, images like those shown above can be generated from a stacked image file all in one step in typically under a minute! The only additional post-processing work that I typically do after running astro-color-stretch is some minor cropping, curves adjustment, and noise reduction.
I originally developed the first versions of astro-color-stretch several years ago, and it has been what I have been using for my post-processing ever since. It leverages OpenCV and NumPy for high-performance image array manipulation. To optimize efficiency, I implemented a variety of programming techniques and utilization of built-in functions to streamline computations. One of the most significant improvements came from using array masking in the color correction algorithm, which significantly reduced execution times compared to traditional pixel-by-pixel processing. Overall, astro-color-stretch can process identical files with the same input parameters more than an order of magnitude faster than rnc-color-stretch.
Some features have also been added to astro-color-stretch such as options for an asinh stretch, HSV adjustment, vignette, and gradient correction. Additionally, output file names can optionally automatically include the main input parameter values for easy identification after multiple runs. The program also does not overwrite previous runs. It keeps each version with incremental integer suffixes added to newer filenames.
astro-color-stretch vs. rnc-color-stretch
An example demonstrating the results of astro-color-stretch, along with a comparison to rnc-color-stretch, is shown below. Figure 1 (right) displays the star-aligned, stacked input image of the Andromeda Galaxy. The source images were captured using a 45.7 MP Nikon Z7II paired with a 600mm f/6.3 lens at ISO 3200. After a slight crop, the final stacked image has a resolution of approximately 32 megapixels.
The stacked image was then processed using identical parameters in both astro-color-stretch and rnc-color-stretch. The resulting outputs are shown below in Figures 2a and 2b. The results are virtually indistinguishable. Even upon close inspection at full resolution, I could find no meaningful differences between the two images.
The one major difference, though, was in execution time. On a MacBook Pro M4, the image processed with rnc-color-stretch took about 13.5 minutes compared to just 21 seconds with astro-color-stretch!
Stretching and Color Correction with astro-color-stretch
Another example showing the output results of astro-color-stretch is of a test image of the Lagoon and Triffid nebulae available from Roger Clark’s advanced image stretching page. Figure 3a is the original stacked input test image. Figure 3b is the result using astro-color-stretch with a root-power (RTP) stretch and no color correction. As expected it shows the typical loss of color due to the stretching operation. Figure 3c is processed from the same input image but with color correction enabled. This clearly shows the amazing results of the rnc-color-stretch color correction algorithm as implemented in astro-color-stretch.
Even on this small approximately 1/3 MP image the difference in execution times is notable. On a MacBook Pro M4, execution time with rnc-color-stretch was 40 seconds compared to less than one second on astro-color-stretch.
The images below are the same sequence of images except with an asinh stretch applied. Figure 4a is the same input image file as figure3a. Figure 4b is the asinh stretched image without any color correction. As expected, it similarly shows the loss of color caused by image stretching. Figure 4c shows image 4a stretched with color correction.
Operation of astro-color-stretch
Program astro-color-stretch necessarily varies from rnc-color-stretch in a number of ways since it is coded in Python rather than the C-based scripted language of davinci. However, program flow and the fundamental image processing algorithms are virtually identical. Even the majority of the variable names have been retained so as to make it easy for anyone familiar with rnc-color-stretch to use astro-color-stretch.
Full credit goes to Roger Clark at clarkvision.com for the development of the original algorithms in rnc-color-stretch. They are the fundamental basis for much of the image processing implemented in astro-color-stretch. What follows is a description of the operation and relevant input parameters for astro-color-stretch. Some of the descriptions below also borrow from Roger Clark’s original writeup for rnc-color-stretch. Again, credit goes to any relevant areas of his original work.
Starting with what program astro-color-stretch fundamentally does, like rnc-color-stretch, it first analyzes the histograms to set a black point. It then does an image stretch, followed by optional curves, and color correction while maintaining black points for each operation.
Input Parameters for astro-color-stretch
The following are the user modifiable input variables and their descriptions for operation of astro-color-stretch.
Filename and Path:
dirpath – String specifying the OS path location of the input file as well as where the output files will be written.
infile – String specifying the name of the input file (filename.ext) to be processed. File types that can be processed are tif, jpg, and png. Supported data types for these file formats are uint8, uint16, and float32.
Unsigned 8-bit (uint8) files are scaled up to 16 bits (0-65535) for processing. Uint8 is, however, the least desirable data type for processing since this only scales up the limited 8-bit per channel information. Some color shift, especially in compressed jpg files, is likely. Tif files encoded in uint16 or float32 are the best choice. Stacked output files like those from Deep Sky Stacker (DSS) are normalized float32 and are scaled to 0-65535. All image arrays are processed using float64 math.
Output File:
ftype – Output file type specifier: 0 – tif, 1 – jpg, 2 – png
write_full – Boolean specifying whether to write the input parameters in the output file name or just a shortened version (infile-color-stretched.ext). Multiple runs do not overwrite previous versions. A -1, -2, -3, etc. suffix is added to successive identical file names.
Vignetting and Gradient Correction:
vn_correct – Radial Vignette Correction enabled when set to True. Radial vignette correction corrects brightness falloff toward the edges using a least squares fit of intensity vs. radius. It assumes falloff is radially symmetric and smoothly varying.
lg_correct – Linear Gradient Correction enabled when set to True. Linear gradient correction corrects for linear gradients across the frame using a least squares fit. Gradients that can be fixed are horizontal linear, vertical linear, diagonal linear, and planar tilt (brighter in one corner, darker in the opposite).
Either vignette correction, linear gradient correction, or both can be applied, depending on the specific characteristics of the image. In some cases correction is either unnecessary or may even produce negative effects. Experimentation is needed to find what is optimal.
Vignette correction is most effective for images that are un-cropped or only lightly cropped. Without correction, stretched images often appear with a bright center and darker corners, making it difficult to establish a consistent post-stretch zero sky background level. Heavily cropped images typically exhibit little to no vignetting and likely will not benefit from this correction.
Linear gradient correction is recommended for images captured under conditions with significant sky glow or light pollution. These often exhibit a brightness gradient across the frame, typically from one edge to the other. Gradient correction can effectively mitigate this effect.
Note that both corrections are computationally intensive and will increase processing time. For reference, a full-size uncorrected image typically processes in 30–40 seconds on a MacBook M4 Pro. Applying vignette correction increases this to just over one minute.
clip_mode – 1 – Clip output, 2 – Scale output. Methods for handling high data after either vignette or linear gradient correction that might fall outside of the 0-65535 range. Clipping hard clips any data outside of 0-65535, while scaling scales the data according to:
65535 * (corrected – corrected.min) / (corrected.max – corrected.min) (1)
Image Stretching Options:
stretch_type – Determines the type of stretch to perform. 0 – no stretch, 1 – root-power stretch, 2 – asinh stretch.
Root-Power-Stretching:
The root-power-stretch is done according to the following:
output = (input)x where x = 1 / root-power (2)
rootiter – Set to 1 for a single pass stretch using the value specified in rootpower. Set to 2 for an added second pass using the value specified in rootpower2.
rootpower – Value for a single pass root-power stretch or for the first pass of two.
rootpower2 – Value used for the second pass root-power stretch.
Asinh Stretching:
The asinh stretch option is done according to the following:
output = asinh(input / Kf) / asinh(Kf) (3)
asinhiter – Set to 1 for a single pass stretch using the value specified in K1. Set to 2 to add a second pass using the value specified in K2.
K1 – Kf value used for a single pass asinh stretch or for the first pass of two.
K2 – Kf Value used for the second pass asinh stretch.
Figure 5 below shows output vs. input plots of both the root-power and asinh stretch types for representative values of both single and two pass stretches.
Choosing the amount of stretch with either method is largely dependent on the quality of the input image data. Images with good SNR can work well with root-powers or Kf values of 100 or more. Much higher values in a single pass stretch start to have a diminishing effect. Consider a two pass stretch if more stretch is needed and can be tolerated. A two-pass stretch with lower first pass root-power values of 6-10 or Kf values of 30-50 followed by a second pass of either in the 2-5 range is a good place to start.
On brighter images or those with a lot of sky glow, consider starting with a single pass stretch using much smaller root-powers or Kf values and working up from there. Such images may not tolerate a lot of stretching at all. Images with large gradients or vignetting should be fixed prior to stretching for best results.
As to which stretch type to use, it depends upon the image and personal preference. The curves of both the root-power and asinh stretches are similar. With the right respective parameters, resulting images from each stretch type can be made to be virtually identical. The asinh curves do have a slightly ‘lazier’ slope in the lower input values which can have a slightly different effect depending on the stretch parameters and the image.
s-curves:
scurve – Set for the type of curve to apply: 0 – no curves 1 – scurve1 2 – a sequence of scurve1, scurve2
3 – a sequence of scurve1, scurve2, scurve1 4 – a sequence of scurve1, scurve2, scurve1, scurve2.
The input vs. output plots of scurve 1 and scurve 2 are shown in figure 6 below. They are based on the same curve equations in rnc-color-stretch. Curve 1 crosses from below linear in the lower input range, which will have the effect of adding contrast to the lower mid-tones. Curve 2 will brighten the overall image with slightly more bias on the upper brighter parts of the image .
Choosing the right curve or curve sequence (if any) depends on the image data, amount of initial RTP/asinh stretch, and personal preference. I usually go with the more conservative curve 1 on my first run. Curve options 2, 3, and 4 progressively add more stretch if desired or tolerated. It can take some experimentation to find the right combination of initial RTP/asinh stretch and curve sequence.
Color Correction:
colorcorrect – Boolean, set to True to perform color correction.
colorenhance – Color enhancement factor when colorcorrect is True. Values greater than 1 add saturation, less than 1 decreases it.
HSV Adjustment:
HSVadjust – Set to True to do additional HSV adjustments according to the values below. These optional HSV adjustments can also be done in a post-processor. However, I have sometimes found them to be convenient for quickly comparing slightly different hue shifts or saturation levels on multiple runs.
hue_adjust – Adjusts hue from -120 to +120 degrees (0 to 360 degrees according on the standard HSV model). Zero degrees is no hue adjust.
sat_adjust – Set between 0 and 2 to reduce or increase saturation. Has a similar but stronger effect as colorenhance in colorcorrection.
val_adjust – Set between 0.1 and 2.0 to darken or lighten.
Sky Level and Minimum Sky Values:
skylevelfactor – Sky level relative to histogram peak. Default of 6% (0.06) seems to work well in most cases. Lowering can help improve zero sky color shift at the risk of not finding zero sky levels for one or more colors. Higher levels might be needed when zero sky levels are not found.
zeroskyred, zeroskygreen, zeroskyblue – Zero sky levels for respective rgb channels (0 – 25000). These values are set to 4096 as default, which seems to work well.
setmin – Set to true to perform a set minimum in the rgb data. This brings up the base level, assuring there are no really dark pixels.
setminr, setming, setminb – Minimum values used for respective rgb channels.
Running astro-color-stretch
Program astro-color-stretch is a single Python file and is available in the astrophotography software downloads page. It is provided here free and open source according to the license agreement in the downloads page. Since this program is an adaptation of Roger Clark’s original rnc-color-stretch program, all previous copyright and license terms in his original license also apply.
The program was developed using PyCharm. I highly recommend this IDE to load and run astro-color-stretch for first time and even experienced users. It is reliable and easy to use. In order to run astro-color-stretch, a number of imports are required. Modules os, math, sys, and time are built-in so they should already be included in the current version of Python. CV2 and NumPy, however, must be installed if not already done so by the user. This can be done either with pip install via a console window or from within PyCharm by going to PyCharm “settings…”. Under your project name, find “Python Interpreter” and then click the “+” (install) symbol. This will pull up a window where you can search for CV2 and NumPy to install. There are a number of internet resources that can help with these installations or debug any issues.
As I have not yet written a GUI or implemented an input file method for parameter input, the program parameters must be modified directly in the “user modifiable variables” section near the top of the program code. Comments in the code and the information provided above should easily guide the user. Beyond that, no programming knowledge of Python is required to run astro-color-stretch.
I have only run astro-color-stretch on Mac OS but it should run fine in Windows provided that the dirpath variable is set for the correct Windows path syntax. I am working on a Win 11 VMware virtual machine but have so far found it to be far too unstable to load and verify astro-color-stretch in Windows. Any feedback on issues or modifications needed with running this code on Windows are welcome via an email or in the comments.
Download
astro-color-stretch can be downloaded here on the Astrophotography Software Page.
Additional Astrophotography Resources
Main Astrophotography Page

This is awesome, thanks for getting this on python. I have two questions – hopefully of use;
A: Have you used this script in Siril as it is open source? I imagine the benefit would be you maintain color space throughout the whole process of registration, stacking and stretching.
B: What version of rnc-color-stretch is this based on? I believe 1.02 has histogrambox1 so you can choose the histogrambox you want to set the black point and adjust as needed. I use it often now.
Hi Daniel, I have not yet run this as an open source script in Siril. Sounds like a great idea, though, as it would make for a nice integrated process. I might give it a go sometime when I have a chance.
When I wrote the original astro-color-stretch program, it was based on the first version of rnc-color-stretch, so the black point is set using the entire image histogram. Now that I look at it, adding a specified area for setting a zero sky would be a good add. Certainly easy enough to implement. Out of curiosity – where to you usually “look” for the best area to specify in your images and how large is your typical box?
In practice, I typically either do a rough stretch to see where the “dark patches” are — if there are any — or I look at an example image and see what is a near black are. A lot of the background may have reddish hues that are not apparent (m31 for example, Ha is everywhere), but it is close enough to get reasonably good black point (caveat I guess is that any error in blackpoint magnifies low end color shift). The “newest” rnc-color-stretch allows one to change the rgbskyzero points in that histrogrambox to adjust if there are no true black points in the image. The program also allows one to see the graph/charts of the histogrambox to see if there are any low end shifts in color and whether any rgbskyzero adjustments are needed (or if a lower skylevelfactor would help). At that point, it does become a guessing game, however, given that the speed here is so much faster, I think there’s some benefit one could get it nailed down pretty quick. I believe the histogrambox1 will expand to include gradients, but I have not seen any updates on that yet.
The size of the box depends on the overall image size, but I typically will make it big enough so it doesn’t result in a splotchy histogram, so maybe a 700×700+ pixels on a standard photo size. I think in this case, you’d want to avoid the box being too small as it might fail. The user typically fills out the coordinates using the stacked image from gimp/photoshop/siril.
R. Clark has an article on the newish histogrambox1 bit, Article 3f5 on his website as I’m sure you are aware to use as a guide if needed, and software page should have the updated version listed as well.
Siril is supposed to expand support for python on 1.5; so if it doesn’t work now, it should in the near future. They are on 1.4 beta 3 (color managed workflow now). It’s a little bit of a learning curve on how to use siril, but not too bad (and really can’t complain given a lot of this stuff is free or open source).
Thanks for the great info. I already have some thoughts on implementation. Lots on the plate right now, so It may be a little while. I will let you know if there is an update.
Hi David,
Thank you for your efforts and work to make rnc-colorstretch possible in Python.
It works perfectly and what a performance improvement!
I am also pleased with the naming of the output file, which includes the values used in the stretch.
With rnc-colourstretch, I always change the name of the output file myself to include the power factors, the Sky level used, the RGB values of the desired sky level, the minimum values used, and the scurve, for example, ‘output-filename_ (RP4_2P3_Sky0.01_RGB4096_Min2028_S1_Enh1.3).tif’. In this case, the RGB and Min values for the 3 channels are the same. When I use different values for the RGB channels, it becomes, for example, “RGB-(4096_4096_3456)”.
Perhaps it would be a good idea to include the values of Sky-level, Sky-RGB and minimum in the naming convention? Possibly in combination with an option to include this in the file name or not?
An additional advantage of using PyCharm is that after running the script, I can easily save the console messages as a log file by printing the console messages to a PDF file.
Perhaps you could ask Roger Clark to mention the development of your Python script on his website, including a link to your website.
Kind regards,
Lex
Hi Lex, thank you for the feedback on astro-color-stretch. So glad to hear that it is working well for you. I will look into adding the zero sky parameters. Perhaps on the next revision. In the interim, since you are using PyCharm, you can easily modify to add the zerosky parameters to your output filenames by adding the following line in the subroutine def build_output_basename():
obase += f”-RGB-({zeroskyred}-{zeroskygreen}-{zeroskyblue})”
I recommend putting it immediately after the statement:
if not write_full:
return obase
This will put it first in the output filename after the file name.
I have reached out to Roger Clark a couple of times but have not received a reply. Perhaps my emails did not get through. Not sure.
Hi David,
Thank you for the Python statement to change the naming.
I used the following statement:
if zeroskyred == zeroskygreen and zeroskyred==zeroskyblue:
obase+=f”-RGB-({zeroskyred})”
else: obase+=f”-RGB-({zeroskyred}-{zeroskygreen}-{zeroskyblue})”
I see that Roger Clark is active on the Reddit channel for astrophotography under the username https://www.reddit.com/user/rnclark/. Perhaps you can reach him there.
Hi Lex, Excellent – that looks like a good implementation for the zerosky parameters in the output filename. Are you using astro-color-stretch version 1.1 with the box histogram feature to adjust the rgbskyzero parametes?
Thanks for the Reddit link. I will give it a try.
Hi David,
I am using version 1.1 of your colour-stretch script.
I often use the rnc-colour-stretch with the histogram box, although I find it difficult to select a suitable area.
With the rnc-colour-stretch, I have occasionally used the histogram box in combination with different values for the R, G and B channels. For example, with a photograph of the West Veil nebula. In this case, I was unsure whether the light pollution had been removed properly.
I will now compare the result of this rnc-colour-stretch with your astro-colour-stretch using the same values and with or without the histogram box.
Hi David,
I compared the results of using different values for the RGB sky zero level with and without the histogram box.
Background: when photographing the West Veil, I had to contend with a rising moon (last quarter). As a result, the light pollution was also more concentrated in the blue channel. I reduced it somewhat before stacking and performed a background subtraction after stacking.
But apparently this was not enough, because when stretching (rnc-colour-stretch) with skyzerolevel = 4096 for all channels, the black point of the blue channel is not correct. That is why I used a different value for the skyzerolevel of the blue channel in my original processing, in combination with the histogram box.
When processing my photo of the West Veil with the astro-colour-stretch in combination with a different value for the black point of the B channel, there is a slight difference in the histogram of the result between using the histogram-box and the full version. And it makes sense to use skyzero in combination with the histogram box.
However, I also notice a difference between the results of the astro-colour stretch and the rnc-colour stretch, both when using the (same position of the) histogram box and the full version. These are minor differences. Perhaps the cause lies in rounding differences and the fact that the result of the rnc-colour-stretch needs to be adjusted from a 15 to a 16-bit scale.
With your astro-colour-stretch, I could only plot a histogram when I enabled the histogram box.
I find it useful to plot a histogram of the input file and the output file.
I therefore made a small adjustment to the script to make this possible. I also changed the naming of the histogram plots to include the name of the input file and the name of the output file (including the stretch factors, et cetera).
I am not a programmer and do not know Python well enough, but I have created the following:
In the ‘end-user modifiable variables’ section an option to plot the histograms:
plot_hist = True
In the ‘Read in Input File’ section I’ve changed:
if rgbskyzero_method == restricted_area:
plot_rgb_histograms(im, “Histogram – Input Image File”)
into:
if plot_hist:
histname: str
histname = “Histogram – (” + infile[:-len(ext)] + “)”
plot_rgb_histograms(im, histname)
And in the ‘Write Output File’section I’ve added:
if plot_hist:
histname: str
histname = “Histogram – (” + obase + “)”
plot_rgb_histograms(im, histname)
I’ve also added the values of de ‘setmin’ variables to the output filename:
if setmin:
if setminr == setming and setminr == setminb:
obase += f”-setmin=({setminr})”
else: obase += f”-setmin=({setminr}-{setming}-{setminb})”
Hi Lex, thank you for the feedback on the box histogram comparisons. I am glad to hear they are pretty close between the two programs. That has been my experience as well. There are definitely some differences in how CV2 and DaVinci read/write image data. As you point out, it is probably where the majority of any differences lie. I have not looked too deeply into the specifics of CV2 vs. DaVinci since the histogram data and, more importantly, the actual image results are always really close.
Since you have some light pollution from rising moonlight, I am guessing you may also have some gradients across your image. Unless you are already correcting for it before processing, have you tried doing some gradient correction using the gradient correction option? Even if you optimize for a better blue skyzero value in the darkest region it still might not be optimal for the sky variation across the rest of your image. Even a small amount of the adjustable gradient correction amount may help. Just a thought.
Your coding additions definitely look good. I am glad you are able to quickly customize to your needs.
-David