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.
- astroclr-1.0.0/.gitignore +3 -0
- astroclr-1.0.0/LICENSE +21 -0
- astroclr-1.0.0/PKG-INFO +196 -0
- astroclr-1.0.0/README.md +177 -0
- astroclr-1.0.0/pyproject.toml +31 -0
- astroclr-1.0.0/scripts/black_and_white.py +47 -0
- astroclr-1.0.0/scripts/blackbody_scale.py +41 -0
- astroclr-1.0.0/scripts/clr_wheel.py +56 -0
- astroclr-1.0.0/scripts/data/WDJ1011+2845.dat +4703 -0
- astroclr-1.0.0/scripts/data/WDJ1011+2845_mu.dat +4704 -0
- astroclr-1.0.0/scripts/data/WDJ1330+6435.dat +4703 -0
- astroclr-1.0.0/scripts/data/WDJ1330+6435_mu.dat +4704 -0
- astroclr-1.0.0/scripts/data/clr_wheel.jpeg +0 -0
- astroclr-1.0.0/scripts/data/solar_model.dat +384198 -0
- astroclr-1.0.0/scripts/figures.py +3 -0
- astroclr-1.0.0/scripts/illuminants_CCT.py +21 -0
- astroclr-1.0.0/scripts/luminous_efficacy_BB.py +39 -0
- astroclr-1.0.0/scripts/rainbow.py +69 -0
- astroclr-1.0.0/scripts/solar_spectrum.py +21 -0
- astroclr-1.0.0/scripts/spec_to_rgb.py +51 -0
- astroclr-1.0.0/scripts/spotlight.py +75 -0
- astroclr-1.0.0/scripts/starry_sky.py +65 -0
- astroclr-1.0.0/scripts/stellar_disc.py +79 -0
- astroclr-1.0.0/scripts/the_sun.py +69 -0
- astroclr-1.0.0/src/astroclr/__init__.py +101 -0
- astroclr-1.0.0/src/astroclr/_colormodels_core.py +276 -0
- astroclr-1.0.0/src/astroclr/cmfs.py +87 -0
- astroclr-1.0.0/src/astroclr/colormodels.py +423 -0
- astroclr-1.0.0/src/astroclr/data/CIE_cfb_stv_10deg.csv +441 -0
- astroclr-1.0.0/src/astroclr/data/CIE_cfb_stv_2deg.csv +441 -0
- astroclr-1.0.0/src/astroclr/data/CIE_illum_Dxx_comp.csv +107 -0
- astroclr-1.0.0/src/astroclr/data/CIE_xyz_1964_10deg.csv +472 -0
- astroclr-1.0.0/src/astroclr/data/Illuminanta.txt +531 -0
- astroclr-1.0.0/src/astroclr/data/Illuminantd65.txt +531 -0
- astroclr-1.0.0/src/astroclr/data/alpha_lyr_stis_011.fits +668 -5
- astroclr-1.0.0/src/astroclr/data/ciexyz31_1.txt +471 -0
- astroclr-1.0.0/src/astroclr/data/ciexyzj.csv +41 -0
- astroclr-1.0.0/src/astroclr/data/ciexyzjv.csv +90 -0
- astroclr-1.0.0/src/astroclr/delta_color.py +78 -0
- astroclr-1.0.0/src/astroclr/illuminants.py +145 -0
- astroclr-1.0.0/src/astroclr/images/CIE_xy.png +0 -0
- astroclr-1.0.0/src/astroclr/plots.py +235 -0
- astroclr-1.0.0/src/astroclr/rgb_color_spaces.py +185 -0
- astroclr-1.0.0/src/astroclr/spectral.py +314 -0
- astroclr-1.0.0/src/astroclr/transfer_functions.py +110 -0
- astroclr-1.0.0/unittests.py +192 -0
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.
|
astroclr-1.0.0/PKG-INFO
ADDED
|
@@ -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
|
+

|
|
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.
|
astroclr-1.0.0/README.md
ADDED
|
@@ -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
|
+

|
|
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()
|