astroclr 1.0.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (46) hide show
  1. astroclr-1.0.0/.gitignore +3 -0
  2. astroclr-1.0.0/LICENSE +21 -0
  3. astroclr-1.0.0/PKG-INFO +196 -0
  4. astroclr-1.0.0/README.md +177 -0
  5. astroclr-1.0.0/pyproject.toml +31 -0
  6. astroclr-1.0.0/scripts/black_and_white.py +47 -0
  7. astroclr-1.0.0/scripts/blackbody_scale.py +41 -0
  8. astroclr-1.0.0/scripts/clr_wheel.py +56 -0
  9. astroclr-1.0.0/scripts/data/WDJ1011+2845.dat +4703 -0
  10. astroclr-1.0.0/scripts/data/WDJ1011+2845_mu.dat +4704 -0
  11. astroclr-1.0.0/scripts/data/WDJ1330+6435.dat +4703 -0
  12. astroclr-1.0.0/scripts/data/WDJ1330+6435_mu.dat +4704 -0
  13. astroclr-1.0.0/scripts/data/clr_wheel.jpeg +0 -0
  14. astroclr-1.0.0/scripts/data/solar_model.dat +384198 -0
  15. astroclr-1.0.0/scripts/figures.py +3 -0
  16. astroclr-1.0.0/scripts/illuminants_CCT.py +21 -0
  17. astroclr-1.0.0/scripts/luminous_efficacy_BB.py +39 -0
  18. astroclr-1.0.0/scripts/rainbow.py +69 -0
  19. astroclr-1.0.0/scripts/solar_spectrum.py +21 -0
  20. astroclr-1.0.0/scripts/spec_to_rgb.py +51 -0
  21. astroclr-1.0.0/scripts/spotlight.py +75 -0
  22. astroclr-1.0.0/scripts/starry_sky.py +65 -0
  23. astroclr-1.0.0/scripts/stellar_disc.py +79 -0
  24. astroclr-1.0.0/scripts/the_sun.py +69 -0
  25. astroclr-1.0.0/src/astroclr/__init__.py +101 -0
  26. astroclr-1.0.0/src/astroclr/_colormodels_core.py +276 -0
  27. astroclr-1.0.0/src/astroclr/cmfs.py +87 -0
  28. astroclr-1.0.0/src/astroclr/colormodels.py +423 -0
  29. astroclr-1.0.0/src/astroclr/data/CIE_cfb_stv_10deg.csv +441 -0
  30. astroclr-1.0.0/src/astroclr/data/CIE_cfb_stv_2deg.csv +441 -0
  31. astroclr-1.0.0/src/astroclr/data/CIE_illum_Dxx_comp.csv +107 -0
  32. astroclr-1.0.0/src/astroclr/data/CIE_xyz_1964_10deg.csv +472 -0
  33. astroclr-1.0.0/src/astroclr/data/Illuminanta.txt +531 -0
  34. astroclr-1.0.0/src/astroclr/data/Illuminantd65.txt +531 -0
  35. astroclr-1.0.0/src/astroclr/data/alpha_lyr_stis_011.fits +668 -5
  36. astroclr-1.0.0/src/astroclr/data/ciexyz31_1.txt +471 -0
  37. astroclr-1.0.0/src/astroclr/data/ciexyzj.csv +41 -0
  38. astroclr-1.0.0/src/astroclr/data/ciexyzjv.csv +90 -0
  39. astroclr-1.0.0/src/astroclr/delta_color.py +78 -0
  40. astroclr-1.0.0/src/astroclr/illuminants.py +145 -0
  41. astroclr-1.0.0/src/astroclr/images/CIE_xy.png +0 -0
  42. astroclr-1.0.0/src/astroclr/plots.py +235 -0
  43. astroclr-1.0.0/src/astroclr/rgb_color_spaces.py +185 -0
  44. astroclr-1.0.0/src/astroclr/spectral.py +314 -0
  45. astroclr-1.0.0/src/astroclr/transfer_functions.py +110 -0
  46. astroclr-1.0.0/unittests.py +192 -0
@@ -0,0 +1,3 @@
1
+ __pycache__
2
+ scripts/__pycache__/
3
+ scripts/*.png
astroclr-1.0.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mark Hollands
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,196 @@
1
+ Metadata-Version: 2.5
2
+ Name: astroclr
3
+ Version: 1.0.0
4
+ Summary: Calculate the perceived colours of astrophysical spectra
5
+ Project-URL: Homepage, https://github.com/mahollands/astroclr
6
+ Project-URL: Issues, https://github.com/mahollands/astroclr/issues
7
+ Author-email: Mark Hollands <mhollands14@gmail.com>
8
+ License-Expression: MIT
9
+ License-File: LICENSE
10
+ Classifier: Intended Audience :: Science/Research
11
+ Classifier: Operating System :: OS Independent
12
+ Classifier: Programming Language :: Python :: 3
13
+ Requires-Python: >=3.9
14
+ Requires-Dist: astropy>=6.0.1
15
+ Requires-Dist: matplotlib>=3.8.4
16
+ Requires-Dist: numpy>=2.0
17
+ Requires-Dist: scipy>=1.13
18
+ Description-Content-Type: text/markdown
19
+
20
+ # astroclr
21
+
22
+ Astroclr allows perceived colours of light sources to be calculated from the
23
+ physical spectra that emit them, and conversions between various colour spaces.
24
+ This allows spectra radiated from (for example) stars to be converted into
25
+ colours that can be replicated on a computer display.
26
+
27
+ Astroclr is heavily inspired by [Colorpy](https://github.com/markkness/ColorPy)
28
+ by Mark Kness, which was an invaluable resource for me when calculating colours
29
+ of white dwarfs, and developing my own interest in colour science. While
30
+ Colorpy is well documented and referenced, it is no longer actively maintained,
31
+ and requires several tweaks for compatibility with python3. Astroclr was
32
+ written with the express aim of making colour calculations from spectra as
33
+ simple as possible, usng a modern python interface, and with a particular focus
34
+ on observational astronomy.
35
+
36
+ ## Features
37
+ * A modern interface with a variety of colour classes and methods to convert
38
+ between them (all supporting numpy arrays for multiple objects or images)
39
+ - XYZ_Color
40
+ - RGB_Color
41
+ - IRGB_Color
42
+ - UCS_Color
43
+ - LUV_Color
44
+ - LAB_Color
45
+ * A bespoke Spectrum class with easy conversion to XYZ colours
46
+ - XYZ integrations are performed trapezoidally, so wavelengths can have
47
+ arbitrary spacing.
48
+ - Works with astropy units for calculating luminance, illuminance, and
49
+ visual magnitudes
50
+ - Easy generation of black body spectra
51
+ * Support for a variety of RGB colour spaces
52
+ - sRGB
53
+ - Rec709
54
+ - Rec2020/2100
55
+ - DCI P3
56
+ * A suite of example scripts for interesting things an astronomer might want to
57
+ do, and demonstrations of various colour science concepts
58
+
59
+ Note: All documentation uses the British spelling of "colour" (since I am
60
+ British). For code, however, I have opted to use "color", for consistency with
61
+ Colorpy, and other similar software (and to save on typing). The module name
62
+ was chosen to be neutral towards spelling.
63
+
64
+ ## Installation:
65
+
66
+ ```bash
67
+ pip install astroclr
68
+ ```
69
+
70
+ ## Examples
71
+
72
+ Calculating colours with astroclr has been made as easy as possible, with the
73
+ basic recipe looking like:
74
+ * create a Spectrum object
75
+ * convert to the CIE 1931 XYZ colour space
76
+ * convert from XYZ to a linear RGB colour space (sRGB by default)
77
+ * convert linear RGB to the non-linear display RGB (IRGB) space by applying a
78
+ transfer function
79
+
80
+ Lets say we want to determine the sRGB colour of a 10,000 K black body
81
+ (something like the A-type star Sirius A) as a hexstring. In Astroclr we can
82
+ follow the above recipe as follows:
83
+
84
+ ```python
85
+ >>> from astropy import units as u
86
+ >>> from astroclr import Spectrum, RGB_Color
87
+
88
+ >>> lam = np.arange(300, 900, 0.1) * u.nm
89
+ >>> S = Spectrum.BlackBody(temperature=10000*u.K, wavelength=lam)
90
+ >>> XYZ = S.to_XYZ()
91
+ >>> RGB = RGB_Color.from_XYZ(XYZ, brightest=True)
92
+ >>> IRGB = RGB.to_IRGB()
93
+ >>> print(IRGB.to_string())
94
+ '#CDD9FF'
95
+ ```
96
+
97
+ The `brightest` keyword, ensures the brightest RGB channel is scaled to 1.0, so
98
+ observed spectra (typically faint) can be shown as bright as possible, and
99
+ bright spectra (such as models) can be displayed without colour clipping. The
100
+ above code can be actually be performed even more succinctly:
101
+
102
+ ```python
103
+ >>> S = Spectrum.BlackBody(temperature=10000*u.K, wavelength=lam)
104
+ >>> print(S.to_IRGB_string(brightest=True))
105
+ '#CDD9FF'
106
+ ```
107
+
108
+ You may have noticed that most conversion methods are prefixed with 'to', but
109
+ to convert to RGB we need to use a 'from' method. This is because RGB colour
110
+ spaces (the default is sRGB) are defined in terms of XYZ chromaticities (see
111
+ below), and so all colour spaces are defined in terms of the more fundamental
112
+ `XYZ_Color`. This can be seen by inspecting the sRGB colour space:
113
+
114
+ ```python
115
+ >>> from astroclr import sRGB
116
+ >>> print(sRGB)
117
+ RGB_ColorSpace(name='sRGB', R=XYZ_Color(0.64, 0.33), G=XYZ_Color(0.3, 0.6), ...)
118
+ ```
119
+
120
+ If you have your own spectrum, calculating colours is just as simple:
121
+
122
+ ```python
123
+
124
+ >>> wavelength, flux = np.loadtxt("my_spectrum.dat", unpack=True)
125
+ >>> wavelength *= u.AA
126
+ >>> flux *= u.erg / u.s / u.cm**2 / u.AA
127
+ >>> S = Spectrum(wavelength, flux)
128
+ >>> print(S.to_IRGB_string(brightest=True))
129
+ '#DDFFB8'
130
+ ```
131
+
132
+ Note that fluxes must be in terms of spectral radiance/irradiance (per unit
133
+ wavelength), i.e. equivalent units to W/m²/nm/sr or W/m²/nm, which is necessary
134
+ for calculating the luminance/illuminance using the `to_luminance()` method. If
135
+ no units are provided, default units of nm and W/m²/nm/sr are assumed for
136
+ wavelengths and fluxes.
137
+
138
+ ## Colour science basics
139
+ If we have a spectrum $F_\lambda(\lambda)$ and want to calculate its displayable RGB colour.
140
+ Mathematically we do something akin to synthetic photometry in astronomy:
141
+ ```math
142
+ X = \int F_\lambda(\lambda) \bar{x}(\lambda) \,\mathrm{d}\lambda \qquad
143
+ Y = \int F_\lambda(\lambda) \bar{y}(\lambda) \,\mathrm{d}\lambda \qquad
144
+ Z = \int F_\lambda(\lambda) \bar{z}(\lambda) \,\mathrm{d}\lambda
145
+ ```
146
+
147
+ where $\bar{x}(\lambda)$ etc, are the CIE 1931 colour matching functions, and
148
+ $X,Y,Z$ are the integrated fluxes in the XYZ colour space. But what if we want
149
+ to consider the colour independent of its brightness/intensity/luminance? Then
150
+ we can normalise the $X, Y, Z$ by scaling by their sum:
151
+
152
+ ```math
153
+ x = \frac{X}{X+Y+Z} \qquad y = \frac{Y}{X+Y+Z} \qquad z = \frac{Z}{X+Y+Z}
154
+ ```
155
+
156
+ Having lost a degree of freedom (the intensity), $x$ and $y$ now uniquely
157
+ define the brightness-independent component of colour and is called the
158
+ chromaticity. The CIE 1931 chromaticity diagram is shown below, where the
159
+ shark-fin shape corresponds to monochromactic colours and bounds all perceivable
160
+ colours, the triangle shows the sRGB gamut (colours outside the triangle are
161
+ approximated by clipping negative values), and the black curve starting in the red
162
+ corner is the blackbody locus (ticks from 2000K to 10000K in 1000K steps).
163
+ ![image](https://github.com/mahollands/astroclr/blob/main/src/astroclr/images/CIE_xy.png
164
+ "CIE 1931 chromaticity diagram")
165
+
166
+ The sRGB colour space specifies the chromaticities of the red/green/blue
167
+ primaries (and also the white point), which uniquely define a linear
168
+ transformation between XYZ and sRGB.
169
+
170
+ ```math
171
+ \left(\begin{array}{c}
172
+ R \\
173
+ G \\
174
+ B
175
+ \end{array}\right) =
176
+ \left(\begin{array}{rrr}
177
+ 3.2410 & -1.5374 & -0.4986 \\
178
+ -0.9692 & 1.8760 & 0.0416 \\
179
+ 0.0556 & -0.2040 & 1.0570
180
+ \end{array}\right) \cdot
181
+ \left(\begin{array}{c}
182
+ X \\
183
+ Y \\
184
+ Z
185
+ \end{array}\right)
186
+ ```
187
+
188
+ These give linear intensities in the R/G/B channels in the range 0 to 1, where
189
+ (R,G,B)=(1,1,1) represents full white. To convert these to display RGB values
190
+ (IRGB in astroclr), we cannot simply multiply by 255 and round. As the display
191
+ scale is non-linear. We therefore have to apply an opto-electrical transfer
192
+ function (OETF). In some colour-spaces this is just a power-law (gamma curve), though
193
+ in sRGB a piecewise function is used that is linear at low-brightness and a
194
+ shifted plus scaled power-law at higher brightness, but closely resembles a
195
+ power-law with exponent 1/2.2. After applying a OETF, RGB values can be multiplied
196
+ by 255 and rounded to determine the 8-bit per channel display values.
@@ -0,0 +1,177 @@
1
+ # astroclr
2
+
3
+ Astroclr allows perceived colours of light sources to be calculated from the
4
+ physical spectra that emit them, and conversions between various colour spaces.
5
+ This allows spectra radiated from (for example) stars to be converted into
6
+ colours that can be replicated on a computer display.
7
+
8
+ Astroclr is heavily inspired by [Colorpy](https://github.com/markkness/ColorPy)
9
+ by Mark Kness, which was an invaluable resource for me when calculating colours
10
+ of white dwarfs, and developing my own interest in colour science. While
11
+ Colorpy is well documented and referenced, it is no longer actively maintained,
12
+ and requires several tweaks for compatibility with python3. Astroclr was
13
+ written with the express aim of making colour calculations from spectra as
14
+ simple as possible, usng a modern python interface, and with a particular focus
15
+ on observational astronomy.
16
+
17
+ ## Features
18
+ * A modern interface with a variety of colour classes and methods to convert
19
+ between them (all supporting numpy arrays for multiple objects or images)
20
+ - XYZ_Color
21
+ - RGB_Color
22
+ - IRGB_Color
23
+ - UCS_Color
24
+ - LUV_Color
25
+ - LAB_Color
26
+ * A bespoke Spectrum class with easy conversion to XYZ colours
27
+ - XYZ integrations are performed trapezoidally, so wavelengths can have
28
+ arbitrary spacing.
29
+ - Works with astropy units for calculating luminance, illuminance, and
30
+ visual magnitudes
31
+ - Easy generation of black body spectra
32
+ * Support for a variety of RGB colour spaces
33
+ - sRGB
34
+ - Rec709
35
+ - Rec2020/2100
36
+ - DCI P3
37
+ * A suite of example scripts for interesting things an astronomer might want to
38
+ do, and demonstrations of various colour science concepts
39
+
40
+ Note: All documentation uses the British spelling of "colour" (since I am
41
+ British). For code, however, I have opted to use "color", for consistency with
42
+ Colorpy, and other similar software (and to save on typing). The module name
43
+ was chosen to be neutral towards spelling.
44
+
45
+ ## Installation:
46
+
47
+ ```bash
48
+ pip install astroclr
49
+ ```
50
+
51
+ ## Examples
52
+
53
+ Calculating colours with astroclr has been made as easy as possible, with the
54
+ basic recipe looking like:
55
+ * create a Spectrum object
56
+ * convert to the CIE 1931 XYZ colour space
57
+ * convert from XYZ to a linear RGB colour space (sRGB by default)
58
+ * convert linear RGB to the non-linear display RGB (IRGB) space by applying a
59
+ transfer function
60
+
61
+ Lets say we want to determine the sRGB colour of a 10,000 K black body
62
+ (something like the A-type star Sirius A) as a hexstring. In Astroclr we can
63
+ follow the above recipe as follows:
64
+
65
+ ```python
66
+ >>> from astropy import units as u
67
+ >>> from astroclr import Spectrum, RGB_Color
68
+
69
+ >>> lam = np.arange(300, 900, 0.1) * u.nm
70
+ >>> S = Spectrum.BlackBody(temperature=10000*u.K, wavelength=lam)
71
+ >>> XYZ = S.to_XYZ()
72
+ >>> RGB = RGB_Color.from_XYZ(XYZ, brightest=True)
73
+ >>> IRGB = RGB.to_IRGB()
74
+ >>> print(IRGB.to_string())
75
+ '#CDD9FF'
76
+ ```
77
+
78
+ The `brightest` keyword, ensures the brightest RGB channel is scaled to 1.0, so
79
+ observed spectra (typically faint) can be shown as bright as possible, and
80
+ bright spectra (such as models) can be displayed without colour clipping. The
81
+ above code can be actually be performed even more succinctly:
82
+
83
+ ```python
84
+ >>> S = Spectrum.BlackBody(temperature=10000*u.K, wavelength=lam)
85
+ >>> print(S.to_IRGB_string(brightest=True))
86
+ '#CDD9FF'
87
+ ```
88
+
89
+ You may have noticed that most conversion methods are prefixed with 'to', but
90
+ to convert to RGB we need to use a 'from' method. This is because RGB colour
91
+ spaces (the default is sRGB) are defined in terms of XYZ chromaticities (see
92
+ below), and so all colour spaces are defined in terms of the more fundamental
93
+ `XYZ_Color`. This can be seen by inspecting the sRGB colour space:
94
+
95
+ ```python
96
+ >>> from astroclr import sRGB
97
+ >>> print(sRGB)
98
+ RGB_ColorSpace(name='sRGB', R=XYZ_Color(0.64, 0.33), G=XYZ_Color(0.3, 0.6), ...)
99
+ ```
100
+
101
+ If you have your own spectrum, calculating colours is just as simple:
102
+
103
+ ```python
104
+
105
+ >>> wavelength, flux = np.loadtxt("my_spectrum.dat", unpack=True)
106
+ >>> wavelength *= u.AA
107
+ >>> flux *= u.erg / u.s / u.cm**2 / u.AA
108
+ >>> S = Spectrum(wavelength, flux)
109
+ >>> print(S.to_IRGB_string(brightest=True))
110
+ '#DDFFB8'
111
+ ```
112
+
113
+ Note that fluxes must be in terms of spectral radiance/irradiance (per unit
114
+ wavelength), i.e. equivalent units to W/m²/nm/sr or W/m²/nm, which is necessary
115
+ for calculating the luminance/illuminance using the `to_luminance()` method. If
116
+ no units are provided, default units of nm and W/m²/nm/sr are assumed for
117
+ wavelengths and fluxes.
118
+
119
+ ## Colour science basics
120
+ If we have a spectrum $F_\lambda(\lambda)$ and want to calculate its displayable RGB colour.
121
+ Mathematically we do something akin to synthetic photometry in astronomy:
122
+ ```math
123
+ X = \int F_\lambda(\lambda) \bar{x}(\lambda) \,\mathrm{d}\lambda \qquad
124
+ Y = \int F_\lambda(\lambda) \bar{y}(\lambda) \,\mathrm{d}\lambda \qquad
125
+ Z = \int F_\lambda(\lambda) \bar{z}(\lambda) \,\mathrm{d}\lambda
126
+ ```
127
+
128
+ where $\bar{x}(\lambda)$ etc, are the CIE 1931 colour matching functions, and
129
+ $X,Y,Z$ are the integrated fluxes in the XYZ colour space. But what if we want
130
+ to consider the colour independent of its brightness/intensity/luminance? Then
131
+ we can normalise the $X, Y, Z$ by scaling by their sum:
132
+
133
+ ```math
134
+ x = \frac{X}{X+Y+Z} \qquad y = \frac{Y}{X+Y+Z} \qquad z = \frac{Z}{X+Y+Z}
135
+ ```
136
+
137
+ Having lost a degree of freedom (the intensity), $x$ and $y$ now uniquely
138
+ define the brightness-independent component of colour and is called the
139
+ chromaticity. The CIE 1931 chromaticity diagram is shown below, where the
140
+ shark-fin shape corresponds to monochromactic colours and bounds all perceivable
141
+ colours, the triangle shows the sRGB gamut (colours outside the triangle are
142
+ approximated by clipping negative values), and the black curve starting in the red
143
+ corner is the blackbody locus (ticks from 2000K to 10000K in 1000K steps).
144
+ ![image](https://github.com/mahollands/astroclr/blob/main/src/astroclr/images/CIE_xy.png
145
+ "CIE 1931 chromaticity diagram")
146
+
147
+ The sRGB colour space specifies the chromaticities of the red/green/blue
148
+ primaries (and also the white point), which uniquely define a linear
149
+ transformation between XYZ and sRGB.
150
+
151
+ ```math
152
+ \left(\begin{array}{c}
153
+ R \\
154
+ G \\
155
+ B
156
+ \end{array}\right) =
157
+ \left(\begin{array}{rrr}
158
+ 3.2410 & -1.5374 & -0.4986 \\
159
+ -0.9692 & 1.8760 & 0.0416 \\
160
+ 0.0556 & -0.2040 & 1.0570
161
+ \end{array}\right) \cdot
162
+ \left(\begin{array}{c}
163
+ X \\
164
+ Y \\
165
+ Z
166
+ \end{array}\right)
167
+ ```
168
+
169
+ These give linear intensities in the R/G/B channels in the range 0 to 1, where
170
+ (R,G,B)=(1,1,1) represents full white. To convert these to display RGB values
171
+ (IRGB in astroclr), we cannot simply multiply by 255 and round. As the display
172
+ scale is non-linear. We therefore have to apply an opto-electrical transfer
173
+ function (OETF). In some colour-spaces this is just a power-law (gamma curve), though
174
+ in sRGB a piecewise function is used that is linear at low-brightness and a
175
+ shifted plus scaled power-law at higher brightness, but closely resembles a
176
+ power-law with exponent 1/2.2. After applying a OETF, RGB values can be multiplied
177
+ by 255 and rounded to determine the 8-bit per channel display values.
@@ -0,0 +1,31 @@
1
+ [project]
2
+ name = "astroclr"
3
+ version = "1.0.0"
4
+ authors = [
5
+ { name="Mark Hollands", email="mhollands14@gmail.com" },
6
+ ]
7
+ description = "Calculate the perceived colours of astrophysical spectra"
8
+ readme = "README.md"
9
+ requires-python = ">=3.9"
10
+ classifiers = [
11
+ "Intended Audience :: Science/Research",
12
+ "Programming Language :: Python :: 3",
13
+ "Operating System :: OS Independent",
14
+ ]
15
+ license = "MIT"
16
+ license-files = ["LICEN[CS]E*"]
17
+
18
+ dependencies = [
19
+ "astropy>=6.0.1",
20
+ "matplotlib>=3.8.4",
21
+ "numpy>=2.0",
22
+ "scipy>=1.13",
23
+ ]
24
+
25
+ [project.urls]
26
+ Homepage = "https://github.com/mahollands/astroclr"
27
+ Issues = "https://github.com/mahollands/astroclr/issues"
28
+
29
+ [build-system]
30
+ requires = ["hatchling >= 1.26"]
31
+ build-backend = "hatchling.build"
@@ -0,0 +1,47 @@
1
+ """
2
+ Demonstration of 'correctly' converting an image to black and white. Naively
3
+ one might think to simply average the R,G,B channels. That will produce
4
+ something that looks plausible correct, but the relative brightnesses of some
5
+ colours will not be preserved. For instance full green is perceptually much
6
+ brighter than full blue.
7
+ """
8
+ import numpy as np
9
+ import matplotlib.pyplot as plt
10
+ from astroclr import RGB_Color, IRGB_Color
11
+
12
+ #Load Test Card F with dimensions NxMx3, with data in range 0--225.
13
+ img_clr = plt.imread("data/clr_wheel.jpeg")
14
+
15
+ #The wrong way to make a black and white image by averaging RGB channels
16
+ img_bw_avg = img_clr.mean(axis=2, keepdims=True, dtype=np.int32)
17
+ img_bw_avg = np.repeat(img_bw_avg, 3, axis=2)
18
+
19
+ #The correct way to make a black and white image from the luminance
20
+ # Step 1: Convert image to diplay RGB object
21
+ # Step 2: Convert to linear RGB via EOTF
22
+ # Step 3: Transform to linear XYZ object
23
+ # Step 4: Create new RGB object using XYZ luminance for all channels (Y)
24
+ # Step 5: Convert back to display RGB
25
+ # Step 6: Extract as numpy array to use with plt.imshow
26
+ IRGB = IRGB_Color.from_numpy_image(img_clr)
27
+ RGB = IRGB.to_RGB()
28
+ XYZ = RGB.to_XYZ()
29
+ RGB_bw = RGB_Color.from_grey(XYZ.Y)
30
+ IRGB_bw = RGB_bw.to_IRGB()
31
+ img_bw_lum = IRGB_bw.values_image
32
+
33
+ imgs = [
34
+ (img_clr, "RGB"),
35
+ (img_bw_avg, "B&W from averaging"),
36
+ (img_bw_lum, "B&W from luminance"),
37
+ ]
38
+
39
+ plt.figure(figsize=(11, 3))
40
+ for panel, (img, title) in enumerate(imgs, 1):
41
+ plt.subplot(1, 3, panel)
42
+ plt.imshow(img)
43
+ plt.xticks([])
44
+ plt.yticks([])
45
+ plt.title(title)
46
+ plt.tight_layout()
47
+ plt.show()
@@ -0,0 +1,41 @@
1
+ """
2
+ Plots a blackbody scale from 800K (roughly the Draper point), up
3
+ to 30000K. For low temperatures, where a blackbody may be less luminous
4
+ than a typical monitor luminance of 200 cd/m2, the displayed intensity
5
+ is approximately correct. To avoid hard clipping at 200 cd/m2, a
6
+ basic form of tone mapping adds some amount of roll-off, giving
7
+ a smoother luminance scale.
8
+ """
9
+
10
+ import numpy as np
11
+ import matplotlib.pyplot as plt
12
+ from astroclr import Spectrum, RGB_Color
13
+
14
+ REF_LUM = 200 #Display peak-luminance in cd/m2
15
+ T_LIMITS = 800, 30000
16
+
17
+ Ts = np.geomspace(*T_LIMITS, 256)
18
+ dlogT = np.diff(np.log(Ts)).mean()
19
+
20
+ y = [0, 0, 1, 1]
21
+ dlogx = np.array([-1, 1, 1, -1])
22
+ dx = np.exp(dlogT * dlogx/2)
23
+
24
+ for T in Ts:
25
+ BB = Spectrum.BlackBody(T)
26
+ XYZ = BB.to_XYZ() * 683 / REF_LUM
27
+ RGB = RGB_Color.from_XYZ(XYZ)
28
+ # tonemapping to avoid hard edge from luminance clipping
29
+ RGB /= 1 + np.max(RGB.values)
30
+
31
+ clr = RGB.to_IRGB().to_string()
32
+ plt.fill(T * dx, y, c=clr)
33
+
34
+ plt.semilogx()
35
+ plt.xlim(*T_LIMITS)
36
+ plt.ylim(0, 1)
37
+ plt.xticks([1000, 2000, 5000, 10000, 20000], [1, 2, 5, 10, 20])
38
+ plt.yticks(visible=False)
39
+ plt.xlabel("$T$ [K]")
40
+ plt.title("Blackbody scale")
41
+ plt.show()
@@ -0,0 +1,56 @@
1
+ import numpy as np
2
+ import matplotlib.pyplot as plt
3
+ from astroclr import IRGB_Color
4
+
5
+ xim, yim = np.meshgrid(
6
+ np.linspace(-1.2, 1.2, 500),
7
+ np.linspace(-1.2, 1.2, 500),
8
+ )
9
+
10
+ rim = np.hypot(xim, yim)
11
+
12
+ V = 1.0
13
+ S = rim
14
+ C = V*S
15
+
16
+ H = np.degrees(np.arctan2(yim, xim))
17
+ H_ = H / 60 % 6
18
+ X = C * (1 - np.abs(H_ % 2 - 1))
19
+
20
+ condlist = [
21
+ (S <= 1) & (H_ < 1),
22
+ (S <= 1) & (H_ < 2),
23
+ (S <= 1) & (H_ < 3),
24
+ (S <= 1) & (H_ < 4),
25
+ (S <= 1) & (H_ < 5),
26
+ (S <= 1) & (H_ < 6),
27
+ ]
28
+
29
+ R1 = np.select(
30
+ condlist = condlist,
31
+ choicelist = [C, X, 0, 0, X, C],
32
+ default = np.zeros_like(X),
33
+ )
34
+
35
+ G1 = np.select(
36
+ condlist = condlist,
37
+ choicelist = [X, C, C, X, 0, 0],
38
+ default = np.zeros_like(X),
39
+ )
40
+
41
+ B1 = np.select(
42
+ condlist = condlist,
43
+ choicelist = [0, 0, X, C, C, X],
44
+ default = np.zeros_like(X),
45
+ )
46
+
47
+ m = V - C
48
+ R, G, B = [(255*np.clip(c + m, 0, 1)).astype(int) for c in (R1, G1, B1)]
49
+ IRGB = IRGB_Color(R, G, B)
50
+
51
+ plt.figure(figsize=(5, 5))
52
+ plt.imshow(IRGB.values_image)
53
+ plt.xticks([])
54
+ plt.yticks([])
55
+ plt.subplots_adjust(left=0, right=1, bottom=0, top=1)
56
+ plt.show()