├── .gitignore ├── settings.py ├── LICENSE ├── plotting.py ├── tools.py ├── settings.py.p3 ├── settings.py.srgb ├── main.py ├── README.md └── solver.py /.gitignore: -------------------------------------------------------------------------------- 1 | __pycache__ 2 | -------------------------------------------------------------------------------- /settings.py: -------------------------------------------------------------------------------- 1 | settings.py.p3 -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | This is free and unencumbered software released into the public domain. 2 | 3 | Anyone is free to copy, modify, publish, use, compile, sell, or 4 | distribute this software, either in source code form or as a compiled 5 | binary, for any purpose, commercial or non-commercial, and by any 6 | means. 7 | 8 | In jurisdictions that recognize copyright laws, the author or authors 9 | of this software dedicate any and all copyright interest in the 10 | software to the public domain. We make this dedication for the benefit 11 | of the public at large and to the detriment of our heirs and 12 | successors. We intend this dedication to be an overt act of 13 | relinquishment in perpetuity of all present and future rights to this 14 | software under copyright law. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. 19 | IN NO EVENT SHALL THE AUTHORS BE LIABLE FOR ANY CLAIM, DAMAGES OR 20 | OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, 21 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 22 | OTHER DEALINGS IN THE SOFTWARE. 23 | 24 | For more information, please refer to 25 | -------------------------------------------------------------------------------- /plotting.py: -------------------------------------------------------------------------------- 1 | import matplotlib.pyplot as plt 2 | import colour 3 | import numpy as np 4 | import matplotlib 5 | import matplotlib.pyplot as plt 6 | import random 7 | from colour.plotting import * 8 | colour_style() 9 | 10 | from settings import * 11 | from tools import * 12 | 13 | 14 | 15 | def plotSDS(spds, illuminant_SPD): 16 | colour.plotting.plot_multi_sds([spds[0], spds[1], spds[2], illuminant_SPD / illuminant_SPD.values.max()], use_sds_colours=True, normalise_sds_colours=False) 17 | 18 | def draw_colors(color_target, T_MATRIX_XYZ, T_MATRIX_DEVICE, primarySDs): 19 | # generate additional columns with 25% and 10% intensity 20 | colors = np.concatenate((colorset, colorset *0.18, colorset * 0.09), axis=0) 21 | 22 | # init destination image array 23 | srgb_colors = np.zeros([51,len(colors) * 3, 3]) 24 | 25 | # fill the image with columns of color mixes 26 | for column, color in enumerate(colors): 27 | i = 0 28 | #print ("column", column *3 + 1) 29 | if column == 0: 30 | colour.plotting.plot_RGB_colourspaces_in_chromaticity_diagram_CIE1976UCS(colourspaces=colorspace, standalone=False) 31 | uv_list = [] 32 | for i in range(0, 51): 33 | 34 | ratio = i / 50. 35 | # mix with linear RGB 36 | srgb_colors[i][column*3 + 0] = colour.RGB_to_RGB(np.array(color) * ratio + (1. - ratio) * np.array(color_target), colorspace, colorspaceTargetDevice, apply_cctf_decoding=False,apply_cctf_encoding=True) 37 | # mix with pigment/spectral upsampling and weighted geometric mean 38 | pigment_color = spectral_Mix_WGM((rgb_to_Spectral(color_target, primarySDs)), (rgb_to_Spectral(color, primarySDs)), ratio) 39 | pigment_color_rgb = np.array(spectral_to_RGB(pigment_color, T_MATRIX_DEVICE)) 40 | if column < len(colorset): 41 | pigment_xyz = spectral_to_XYZ(pigment_color, T_MATRIX_XYZ) 42 | xy = colour.XYZ_to_xy(pigment_xyz) 43 | uv = colour.xy_to_Luv_uv(xy) 44 | uv_list.append(uv) 45 | 46 | srgb_colors[i][column*3 + 1] = colour.RGB_to_RGB(pigment_color_rgb, colorspaceTargetDevice, colorspaceTargetDevice, apply_cctf_decoding=False, apply_cctf_encoding=True) 47 | # mix with perceptual RGB (OETF encoded before mixing) 48 | srgb_colors[i][column*3 + 2] = colour.RGB_to_RGB(colorspace.cctf_encoding(np.array(color)) * ratio + (1. - ratio) * colorspace.cctf_encoding(np.array(color_target)), colorspace, colorspaceTargetDevice, apply_cctf_decoding=True, apply_cctf_encoding=True) 49 | matplotlib.pyplot.plot(*zip(*uv_list)) 50 | 51 | render( 52 | standalone=True) 53 | 54 | # print the image and make it bigger 55 | plt.rcParams["axes.grid"] = False 56 | plt.figure(figsize = (25,10)) 57 | plt.imshow(srgb_colors) 58 | render( 59 | standalone=True) 60 | 61 | 62 | def plotColorMixes(T_MATRIX_XYZ, T_MATRIX_DEVICE, primarySDs): 63 | print("plot shows pigment mixes only") 64 | print("\ncolumns order goes linear rgb, spectral weighted geometric mean (pigment), then non-linear rgb (perceptual rgb?)") 65 | for i in colorset: 66 | draw_colors(i, T_MATRIX_XYZ, T_MATRIX_DEVICE, primarySDs) -------------------------------------------------------------------------------- /tools.py: -------------------------------------------------------------------------------- 1 | import numpy as np 2 | from settings import * 3 | 4 | 5 | 6 | def spectral_to_RGB(spd, T_MATRIX): 7 | """Converts n segments spectral power distribution curve to RGB. 8 | Undoes the offset applies during upsampling 9 | Based on work by Scott Allen Burns. 10 | """ 11 | offset = 1.0 - WGM_EPSILON 12 | r, g, b = np.sum(spd * T_MATRIX, axis=1) 13 | r = (r - WGM_EPSILON) / offset 14 | g = (g - WGM_EPSILON) / offset 15 | b = (b - WGM_EPSILON) / offset 16 | return r, g, b 17 | 18 | def spectral_to_XYZ(spd, T_MATRIX): 19 | """Converts n segments spectral power distribution curve to XYZ. 20 | """ 21 | XYZ = np.sum(spd * T_MATRIX, axis=1) 22 | return XYZ 23 | 24 | 25 | def spectral_Mix_WGM(spd_a, spd_b, ratio): 26 | """Mixes two SPDs via weighted geomtric mean and returns an SPD. 27 | Based on work by Scott Allen Burns. 28 | """ 29 | return np.exp(np.log(spd_a)*(1.0 - ratio) + np.log(spd_b)*ratio) 30 | 31 | def rgb_to_Spectral(rgb, spds): 32 | """Converts RGB to n segments spectral power distribution curve. 33 | Upsamples to spectral primaries and sums them together into one SPD 34 | Applies an offset to avoid 0.0 35 | """ 36 | offset = 1.0 - WGM_EPSILON 37 | r, g, b = rgb 38 | r = r * offset + WGM_EPSILON 39 | g = g * offset + WGM_EPSILON 40 | b = b * offset + WGM_EPSILON 41 | # Spectral primaries derived by an optimization routine devised by 42 | # Allen Burns. Smooth curves <= 1.0 to match XYZ 43 | 44 | red = r 45 | green = g 46 | blue = b 47 | 48 | whiteSpectrum = np.repeat(1.0, numwaves) 49 | redSpectrum = spds[0].values 50 | greenSpectrum = spds[1].values 51 | blueSpectrum = spds[2].values 52 | cyanSpectrum = spds[2].values + spds[1].values 53 | magentaSpectrum = spds[2].values + spds[0].values 54 | yellowSpectrum = spds[0].values + spds[1].values 55 | 56 | 57 | ret = np.repeat(0.0, numwaves) 58 | 59 | # use a technique like Brian Smits uses upsample 60 | if red <= green and red <= blue: 61 | ret += red * whiteSpectrum 62 | if green <= blue: 63 | ret += (green - red) * cyanSpectrum 64 | ret += (blue - green) * blueSpectrum 65 | else: 66 | ret += (blue - red) * cyanSpectrum 67 | ret += (green - blue) * greenSpectrum 68 | elif green <= red and green <= blue: 69 | ret += green * whiteSpectrum 70 | if red <= blue: 71 | ret += (red - green) * magentaSpectrum 72 | ret += (blue - red) * blueSpectrum 73 | else: 74 | ret += (blue - green) * magentaSpectrum 75 | ret += (red - blue) * redSpectrum 76 | elif blue <= red and blue <= green: 77 | ret += blue * whiteSpectrum 78 | if red <= green: 79 | ret += (red - blue) * yellowSpectrum 80 | ret += (green - red) * greenSpectrum 81 | else: 82 | ret += (green - blue) * yellowSpectrum 83 | ret += (red - green) * redSpectrum 84 | 85 | return ret 86 | # spectral_r = red * spds[0].values 87 | 88 | # spectral_g = green * spds[1].values 89 | 90 | # spectral_b = blue * spds[2].values 91 | 92 | 93 | # return np.sum([spectral_r, spectral_g, spectral_b], axis=0) 94 | 95 | 96 | def generateT_MATRIX_RGB(cmfs, illuminant, xyzMatrix): 97 | cmfs_ = cmfs.transpose() 98 | T_MATRIX = np.matmul(xyzMatrix, np.matmul(cmfs_, np.diag(illuminant)) # weight for whitepoint 99 | / np.matmul(cmfs_[1], illuminant)) 100 | return T_MATRIX 101 | 102 | 103 | def generateT_MATRIX_XYZ(cmfs, illuminant): 104 | cmfs_ = cmfs.transpose() 105 | T_MATRIX = (np.matmul(cmfs_, np.diag(illuminant)) # weight for whitepoint 106 | / np.matmul(cmfs_[1], illuminant)) 107 | return T_MATRIX -------------------------------------------------------------------------------- /settings.py.p3: -------------------------------------------------------------------------------- 1 | from enum import unique 2 | import colour 3 | import numpy as np 4 | 5 | 6 | # this is our model colorspace that was want to be multispectral 7 | # it's much easier to use a smaller gamut like sRGB, than wider gamutes 8 | colorspace = colour.models.RGB_COLOURSPACE_P3_D65 9 | #colorspace = colour.models.RGB_COLOURSPACE_sRGB 10 | 11 | # This is our target RGB colorspace 12 | # probably the same as our model 13 | #colorspacetarget = colour.models.RGB_COLOURSPACE_sRGB 14 | colorspacetarget = colour.models.RGB_COLOURSPACE_P3_D65 15 | 16 | # iOS/UIkit Metal uses sRGB primaries even if it is wide color 17 | # use this for transforming to your device's screen if it needs to be different 18 | # from the model colorspace 19 | colorspaceTargetDevice = colour.models.RGB_COLOURSPACE_sRGB 20 | 21 | 22 | # wavelength range to solve for 23 | # maybe makes sense to make this as narrow as possible 24 | # maybe not 25 | begin = 430.0 26 | end = 700.0 27 | 28 | # number of wavelengths/channels to solve for 29 | # wavelengths will be non-uniformly spaced 30 | # and will penalize solutions with less than waveVariance 31 | # distance between channels 32 | numwaves = 12 33 | #numwaves = 8 34 | waveVariance = 2.0 35 | 36 | # max iterations for solver 37 | maxiter = 100000 38 | # population size for diffev 39 | npop = 20 40 | # tolerance 41 | tol = 0.00001 42 | # cpu cores to use, -1 is all cores 43 | workers = -1 44 | 45 | # solve additional colors? see munsell.py 46 | # and add your own XYZ colors 47 | # solveAdditionalXYZs = True 48 | solveAdditionalXYZs = True 49 | 50 | additionalXYZs = ([ 0.46780336, 0.23689442, 0.07897962], [ 0.60375823, 0.48586636, 0.08183366], [ 0.69141481, 0.72890368, 0.03672838], [ 0.53874774, 0.74048729, 0.04405483], [ 0.36800563, 0.72124238, 0.52510832], [ 0.45262124, 0.75488848, 0.9837921 ], [ 0.38936903, 0.52007146, 1.16368056], [ 0.47838485, 0.48171774, 1.15655669], [ 0.58214621, 0.39101099, 1.1441827 ], [ 0.59798203, 0.31675163, 0.46063757]) 51 | 52 | # plot color mixes and spectral curves 53 | # (matplotlib) 54 | plotMixes = True 55 | 56 | illuminant_xy = colour.CCS_ILLUMINANTS['cie_2_1931']['D65'].copy() 57 | illuminant_XYZ = colour.xy_to_XYZ(illuminant_xy) 58 | illuminant_SPD = colour.SDS_ILLUMINANTS['D65'].copy() 59 | 60 | # list of colors to plot mixes with 61 | colorset = np.array([[0.01, 0.01, 0.01], # black 62 | [1., 1., 1.], # white 63 | [0., 0., 1.], # blue 64 | [0., 1., 0.], # green 65 | [1., 0., 0.], # red 66 | [0., 1., 1.], # cyan 67 | [1., 1., 0.], # yellow 68 | [1., 0., 1.], # magenta 69 | [1., 0.18, 0.], # orange 70 | [1., 0., 0.18], # fuscia 71 | [0.18, 1., 0.], # lime 72 | [0.18, 0., 1.], #purple 73 | [0., .18, 1.], #sky blue 74 | [0., 1., 0.18]])#sea foam 75 | 76 | 77 | # should not have to edit below 78 | colorspace.use_derived_transformation_matrices(True) 79 | colorspacetarget.use_derived_transformation_matrices(True) 80 | colorspaceTargetDevice.use_derived_transformation_matrices(True) 81 | 82 | RGB_to_XYZ_m = colorspace.matrix_RGB_to_XYZ 83 | XYZ_to_RGB_m = colorspacetarget.matrix_XYZ_to_RGB 84 | 85 | XYZ_to_RGB_Device_m = colorspaceTargetDevice.matrix_XYZ_to_RGB 86 | 87 | # conversions from light emission to reflectance must avoid absolute zero 88 | # because log(0.0) is undefined 89 | WGM_EPSILON = .0001 90 | MAX_REFLECTANCE = 0.999 91 | MIN_REFLECTANCE = WGM_EPSILON 92 | 93 | # color matching functions to use when converting from spectral to XYZ 94 | CMFS = colour.MSDS_CMFS['CIE 2015 10 Degree Standard Observer'].copy() 95 | 96 | 97 | # weights for differential evolution cost functions 98 | # adjust these if necessary, for instance if your blue primary is not matching, bump it up x 10 or something 99 | 100 | weight_minslope = 0.001 # how important smooth reflectance curvers are 101 | weight_red = 100000. # how important matching the XYZ of red primary 102 | weight_green = 100000. # how important matching the XYZ of green primary 103 | weight_blue = 1000000. # how important matching the XYZ of blue primary 104 | weight_illuminant_white = 1000000. # how important matching the xy chromaticity of the illuminant when reflectance is 1.0 105 | weight_variance = 10. # how important it is to have gaps between wavelengths 500nm, 505nm, vs 500.1nm, 500.2nm, etc 106 | weight_uniqueWaves = 1. # don't bother fiddling this is 0 or inf. We must not have duplicates 107 | weight_illuminant_shape = 0.0001 # how important it is to keep the new illuminant the same shape as the canonical SD 108 | weight_ill_slope = 0.001 # how important it is for the new illuminant to be smooth 109 | weight_mixtest1 = 100. # how important it is for blue + yellow to make green 110 | weight_mixtest2 = 10. # how important it is for blue + white to be more cyan instead of purple 111 | weight_mixtest3 = 10. # how important it is for blue + red to be purple. Yeah. 112 | # weight_mixtest4 = 10. # how important it is for dark purple + white to go cyanish 113 | weight_lum_drop_rg = 1. # how important to avoid drop in luminance when mixing red and green 114 | weight_lum_drop_rb = 100. # how important to avoid drop in luminance when mixing red and blue 115 | weight_lum_drop_gb = 1. # how important to avoid drop in luminance when mixing green and blue 116 | weight_visual_efficiency = 1. # how import to maximize visual efficiency of the chosen wavelengths 117 | weight_sum_to_one = 1000. # how important to sum to MAX_REFLECTANCE for conservation of energy 118 | -------------------------------------------------------------------------------- /settings.py.srgb: -------------------------------------------------------------------------------- 1 | from enum import unique 2 | import colour 3 | import numpy as np 4 | 5 | 6 | # this is our model colorspace that was want to be multispectral 7 | # it's much easier to use a smaller gamut like sRGB, than wider gamutes 8 | #colorspace = colour.models.RGB_COLOURSPACE_P3_D65 9 | colorspace = colour.models.RGB_COLOURSPACE_sRGB 10 | 11 | # This is our target RGB colorspace 12 | # probably the same as our model 13 | colorspacetarget = colour.models.RGB_COLOURSPACE_sRGB 14 | #colorspacetarget = colour.models.RGB_COLOURSPACE_P3_D65 15 | 16 | # iOS/UIkit Metal uses sRGB primaries even if it is wide color 17 | # use this for transforming to your device's screen if it needs to be different 18 | # from the model colorspace 19 | colorspaceTargetDevice = colour.models.RGB_COLOURSPACE_sRGB 20 | 21 | 22 | # wavelength range to solve for 23 | # maybe makes sense to make this as narrow as possible 24 | # maybe not 25 | begin = 430.0 26 | end = 700.0 27 | 28 | # number of wavelengths/channels to solve for 29 | # wavelengths will be non-uniformly spaced 30 | # and will penalize solutions with less than waveVariance 31 | # distance between channels 32 | numwaves = 12 33 | # numwaves = 8 34 | waveVariance = 2.0 35 | 36 | # max iterations for solver 37 | maxiter = 100000 38 | # population size for diffev 39 | npop = 20 40 | # tolerance 41 | tol = 0.00001 42 | # cpu cores to use, -1 is all cores 43 | workers = -1 44 | 45 | # solve additional colors? see munsell.py 46 | # and add your own XYZ colors 47 | # solveAdditionalXYZs = True 48 | solveAdditionalXYZs = False 49 | 50 | additionalXYZs = ([ 0.46780336, 0.23689442, 0.07897962], [ 0.60375823, 0.48586636, 0.08183366], [ 0.69141481, 0.72890368, 0.03672838], [ 0.53874774, 0.74048729, 0.04405483], [ 0.36800563, 0.72124238, 0.52510832], [ 0.45262124, 0.75488848, 0.9837921 ], [ 0.38936903, 0.52007146, 1.16368056], [ 0.47838485, 0.48171774, 1.15655669], [ 0.58214621, 0.39101099, 1.1441827 ], [ 0.59798203, 0.31675163, 0.46063757], [.0469, .0215, .2383]) 51 | 52 | # plot color mixes and spectral curves 53 | # (matplotlib) 54 | plotMixes = True 55 | 56 | illuminant_xy = colour.CCS_ILLUMINANTS['cie_2_1931']['D65'].copy() 57 | illuminant_XYZ = colour.xy_to_XYZ(illuminant_xy) 58 | illuminant_SPD = colour.SDS_ILLUMINANTS['D65'].copy() 59 | 60 | # list of colors to plot mixes with 61 | colorset = np.array([[0.01, 0.01, 0.01], # black 62 | [1., 1., 1.], # white 63 | [0., 0., 1.], # blue 64 | [0., 1., 0.], # green 65 | [1., 0., 0.], # red 66 | [0., 1., 1.], # cyan 67 | [1., 1., 0.], # yellow 68 | [1., 0., 1.], # magenta 69 | [1., 0.18, 0.], # orange 70 | [1., 0., 0.18], # fuscia 71 | [0.18, 1., 0.], # lime 72 | [0.18, 0., 1.], #purple 73 | [0., .18, 1.], #sky blue 74 | [0., 1., 0.18]])#sea foam 75 | 76 | 77 | # should not have to edit below 78 | colorspace.use_derived_transformation_matrices(True) 79 | colorspacetarget.use_derived_transformation_matrices(True) 80 | colorspaceTargetDevice.use_derived_transformation_matrices(True) 81 | 82 | RGB_to_XYZ_m = colorspace.matrix_RGB_to_XYZ 83 | XYZ_to_RGB_m = colorspacetarget.matrix_XYZ_to_RGB 84 | 85 | XYZ_to_RGB_Device_m = colorspaceTargetDevice.matrix_XYZ_to_RGB 86 | 87 | # conversions from light emission to reflectance must avoid absolute zero 88 | # because log(0.0) is undefined 89 | WGM_EPSILON = .0001 90 | MAX_REFLECTANCE = 0.999 91 | MIN_REFLECTANCE = WGM_EPSILON 92 | 93 | # color matching functions to use when converting from spectral to XYZ 94 | CMFS = colour.MSDS_CMFS['CIE 2015 10 Degree Standard Observer'].copy() 95 | 96 | 97 | # weights for differential evolution cost functions 98 | # adjust these if necessary, for instance if your blue primary is not matching, bump it up x 10 or something 99 | 100 | weight_minslope = 0.001 # how important smooth reflectance curvers are 101 | weight_red = 100000. # how important matching the XYZ of red primary 102 | weight_green = 100000. # how important matching the XYZ of green primary 103 | weight_blue = 1000000. # how important matching the XYZ of blue primary 104 | weight_illuminant_white = 1000000. # how important matching the xy chromaticity of the illuminant when reflectance is 1.0 105 | weight_variance = 10. # how important it is to have gaps between wavelengths 500nm, 505nm, vs 500.1nm, 500.2nm, etc 106 | weight_uniqueWaves = 1. # don't bother fiddling this is 0 or inf. We must not have duplicates 107 | weight_illuminant_shape = 0.0001 # how important it is to keep the new illuminant the same shape as the canonical SD 108 | weight_ill_slope = 0.001 # how important it is for the new illuminant to be smooth 109 | weight_mixtest1 = 100. # how important it is for blue + yellow to make green 110 | weight_mixtest2 = 10. # how important it is for blue + white to be more cyan instead of purple 111 | weight_mixtest3 = 10. # how important it is for blue + red to be purple. Yeah. 112 | # weight_mixtest4 = 10. # how important it is for dark purple + white to go cyanish 113 | weight_lum_drop_rg = 1. # how important to avoid drop in luminance when mixing red and green 114 | weight_lum_drop_rb = 100. # how important to avoid drop in luminance when mixing red and blue 115 | weight_lum_drop_gb = 1. # how important to avoid drop in luminance when mixing green and blue 116 | weight_visual_efficiency = 1. # how import to maximize visual efficiency of the chosen wavelengths 117 | weight_sum_to_one = 1000. # how important to sum to MAX_REFLECTANCE for conservation of energy 118 | -------------------------------------------------------------------------------- /main.py: -------------------------------------------------------------------------------- 1 | import colour 2 | import numpy as np 3 | import sys 4 | 5 | from colour.plotting import * 6 | 7 | from scipy.optimize import differential_evolution, basinhopping 8 | from solver import * 9 | from plotting import plotSDS, plotColorMixes 10 | from tools import generateT_MATRIX_RGB 11 | from itertools import repeat 12 | from settings import * 13 | 14 | np.set_printoptions(formatter={"float": "{:0.15f}".format}, threshold=sys.maxsize) 15 | 16 | 17 | def func(a): 18 | return 0.0 19 | sd = np.repeat(0.0, numwaves) 20 | whiteSpectrum = np.repeat(1.0, numwaves) 21 | 22 | 23 | def objectiveFunction(a): 24 | 25 | sds, cmfs, tmat = extractDataFromParameter(a) 26 | 27 | result = minimize_slopes(sds) * weight_minslope 28 | result += match_XYZ(sds[0], XYZ[0], tmat) ** 2.0 * weight_red 29 | result += match_XYZ(sds[1], XYZ[1], tmat) ** 2.0 * weight_green 30 | result += match_XYZ(sds[2], XYZ[2], tmat) ** 2.0 * weight_blue 31 | result += match_XYZ(whiteSpectrum, illuminant_XYZ, tmat) ** 2.0 * weight_illuminant_white 32 | result += varianceWaves(a) * weight_variance 33 | result += uniqueWaves(a) * weight_uniqueWaves 34 | 35 | # nudge b+y = green 36 | yellow = sds[0] + sds[1] 37 | result += mix_test(sds[2], yellow, sds[1], 0.5, tmat) ** 2.0 * weight_mixtest1 38 | # nudge b+w towards desaturated cyan 39 | cyan = sds[1] + sds[2] + (sds[0] * 0.05) 40 | result += mix_test(sds[2], np.repeat(1.0, numwaves), cyan, 0.5, tmat) ** 2.0 * weight_mixtest2 41 | # nudge b+r should be purple 42 | purple = sds[0] + sds[2] 43 | result += mix_test(sds[0], sds[2], purple, 0.5, tmat) ** 2.0 * weight_mixtest3 44 | 45 | # penalize large drop in luminance when mixing primaries 46 | result += luminance_drop(sds[0], sds[1], 0.5, tmat) ** 2.0 * weight_lum_drop_rg 47 | result += luminance_drop(sds[0], sds[2], 0.5, tmat) ** 2.0 * weight_lum_drop_rb 48 | result += luminance_drop(sds[1], sds[2], 0.5, tmat) ** 2.0 * weight_lum_drop_gb 49 | 50 | # encourage maximal visual efficiency ( high Y ) 51 | result += -np.sum(cmfs, axis=0)[1] ** 2.0 * weight_visual_efficiency 52 | 53 | # sum to one 54 | result += ((np.sum([sds[0], sds[1], sds[2]],axis=0) - MAX_REFLECTANCE) ** 2.0).sum() * weight_sum_to_one 55 | return result 56 | 57 | 58 | def objectiveFunctionSingle(a, targetXYZ, spectral_to_XYZ_m): 59 | result = minimize_slope(a) 60 | result += match_XYZ(a, targetXYZ, spectral_to_XYZ_m) * 10000. 61 | return result 62 | 63 | if __name__ == '__main__': 64 | spdBounds = (MIN_REFLECTANCE, MAX_REFLECTANCE) 65 | spdBoundsIlluminant = (0.50 * MAX_REFLECTANCE, MAX_REFLECTANCE * 2.0) 66 | waveBounds = (begin, end) 67 | #illuminantModifierBounds = (0.75, 2.0) 68 | from itertools import repeat 69 | bounds = (tuple(repeat(spdBounds, 3 * numwaves)) + tuple(repeat(spdBoundsIlluminant, numwaves)) + 70 | tuple(repeat(waveBounds, numwaves)) 71 | ) 72 | # format: 3 spectral primaries spd, 1 illum spd + wavelength indices in nm 73 | initialGuess = np.concatenate((np.repeat((MAX_REFLECTANCE), 74 | (numwaves * 4)), np.linspace(begin, end, num=numwaves, endpoint=True))) 75 | print("initial guess is", initialGuess) 76 | 77 | result = differential_evolution( 78 | objectiveFunction, 79 | x0=initialGuess, 80 | callback=plotProgress, 81 | bounds=bounds, 82 | workers=workers, 83 | mutation=(0.1, 1.99), 84 | maxiter=maxiter, 85 | tol=tol, 86 | popsize=npop, 87 | polish=True, 88 | disp=True 89 | ).x 90 | 91 | (waves, spectral_to_XYZ_m, spectral_to_RGB_m, Spectral_to_Device_RGB_m, red_xyz, green_xyz, blue_xyz, 92 | illuminant_xyz, red_sd, green_sd, blue_sd, illuminant_sd, cmfs, tmat) = processResult(result) 93 | 94 | mspds = [] 95 | if solveAdditionalXYZs: 96 | mspds = [] 97 | for targetXYZ in additionalXYZs: 98 | boundsSingle = tuple(repeat(spdBounds, numwaves)) 99 | initialGuess = np.repeat(MAX_REFLECTANCE, numwaves) 100 | result = differential_evolution( 101 | objectiveFunctionSingle, 102 | x0=initialGuess, 103 | bounds=boundsSingle, 104 | args=(targetXYZ, spectral_to_XYZ_m), 105 | workers=workers, 106 | mutation=(0.1, 1.99), 107 | maxiter=maxiter, 108 | popsize=npop, 109 | disp=True).x 110 | mspd = SpectralDistribution((result), waves) 111 | mspd.name = str(targetXYZ) 112 | mspds.append(result) 113 | 114 | print("optimal (maybe) wavelengths:", np.array2string(waves, separator=', ')) 115 | 116 | print("Spectral red is") 117 | print(np.array2string(red_sd.values, separator=', ')) 118 | 119 | print("Spectral green is") 120 | print(np.array2string(green_sd.values, separator=', ')) 121 | 122 | print("Spectral blue is") 123 | print(np.array2string(blue_sd.values, separator=', ')) 124 | 125 | print("spectral_to_XYZ_m is") 126 | print(np.array2string(spectral_to_XYZ_m, separator=', ')) 127 | 128 | print("spectral_to_RGB_m is") 129 | print(np.array2string(spectral_to_RGB_m, separator=', ')) 130 | 131 | print("Spectral_to_Device_RGB_m is") 132 | print(np.array2string(Spectral_to_Device_RGB_m, separator=', ')) 133 | 134 | if solveAdditionalXYZs: 135 | print("munsell/additional colors are") 136 | for mspd in mspds: 137 | print(np.array2string(mspd, separator=', ')) 138 | 139 | 140 | if plotMixes: 141 | plotSDS([red_sd, green_sd, blue_sd], illuminant_sd) 142 | plotColorMixes(spectral_to_XYZ_m, Spectral_to_Device_RGB_m, [red_sd, green_sd, blue_sd]) 143 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # RGB to/from Spectral Colorspace 2 | 3 | Largley based on ideas from Scott Allen Burns 4 | 5 | http://scottburns.us/color-science-projects/ 6 | 7 | Generates 3 spectral reflectance distributions in range > 0.0 and < 1.0, along with matrices to convert to/from RGB. Also produces a set of 10 Munsell colors from 5R thru 5RP 8 | 9 | 10 | ``` 11 | python3 -m pip install git+https://github.com/colour-science/colour@develop 12 | python3 -m pip install matplotlib 13 | python3 -m pip install scipy 14 | ``` 15 | 16 | edit settings.py to adjust colorspace and parameters (if necessary). The default is DisplayP3 and 12 spectral channels. Look in tools.py to see how you can convert to/from and mix pigment colors in log. 17 | 18 | run the solver 19 | 20 | ``` 21 | python3 main.py 22 | ``` 23 | 24 | Output will spit out the primary spectral distrubtions and some matrices. The `spectral_to_XYZ_m` is the only one you really need, you can combine with any XYZ to RGB matrix to get to RGB from there. `spectral_to_RGB_m` will get you back to the original color space, while `Spectral_to_Device_RGB_m` is intended for weird scenarios like iPad where you need to use sRGB primaries even if your colorspace was originally DisplayP3. 25 | 26 | ``` 27 | differential_evolution step 478: f(x)= 2.32856 28 | cost metric, weighted delta cost, actual delta value 29 | red delta: 0.0497544804228 0.00705368559144 30 | green delta: 0.0295454927939 0.00543557658339 31 | blue delta: 0.114556104878 0.0107030885672 32 | illum xy delta: 0.0144509163821 0.00380143609471 33 | bumpiness: 0.00351695069735 3.51695069735 34 | wave variance 0.0 0.0 35 | illum shape diff 0.038609011688 386.09011688 36 | illum bumpiness 0.0806991056501 80.6991056501 37 | lum drop rg 0.0864492314365 0.294022501582 38 | lum drop rb 1.03911693386 0.101937085198 39 | lum drop gb 0.0451388565592 0.212459070315 40 | mix green delta: 0.260163418226 0.161295820847 41 | mix bl/wh delta: 0.365444285811 0.191165971295 42 | mix purple delta: 0.201112616365 0.141814179956 43 | `touch halt` to exit early with this solution. 44 | --- 45 | differential_evolution step 479: f(x)= 2.32856 46 | cost metric, weighted delta cost, actual delta value 47 | red delta: 0.0497544804228 0.00705368559144 48 | green delta: 0.0295454927939 0.00543557658339 49 | blue delta: 0.114556104878 0.0107030885672 50 | illum xy delta: 0.0144509163821 0.00380143609471 51 | bumpiness: 0.00351695069735 3.51695069735 52 | wave variance 0.0 0.0 53 | illum shape diff 0.038609011688 386.09011688 54 | illum bumpiness 0.0806991056501 80.6991056501 55 | lum drop rg 0.0864492314365 0.294022501582 56 | lum drop rb 1.03911693386 0.101937085198 57 | lum drop gb 0.0451388565592 0.212459070315 58 | mix green delta: 0.260163418226 0.161295820847 59 | mix bl/wh delta: 0.365444285811 0.191165971295 60 | mix purple delta: 0.201112616365 0.141814179956 61 | `touch halt` to exit early with this solution. 62 | --- 63 | 64 | original XYZ targets: [array([0.412444605778738, 0.212717741970923, 0.019437791408796]), array([0.357643626542645, 0.715197161899879, 0.119291766091722]), array([0.180557785915699, 0.072285096129198, 0.950546004809512])] 65 | final XYZ results: [0.411653956837962 0.214051160858206 0.019937672179341] [0.358549073445655 0.713950369077184 0.118765350156111] [0.183746287138646 0.075267324938449 0.951310403978453] [87.849601534623886 99.597360042767519 118.354854233593628] 66 | optimal (maybe) wavelengths: [462.234773543109952, 513.060176647041089, 565.452706695094548, 67 | 617.968872719492197, 702.024717054742609, 716.710927736806184, 68 | 723.207695305741822, 729.991155510753174] 69 | Spectral red is 70 | [0.016839219848991, 0.056297605942981, 0.034211916841505, 71 | 0.996855372514724, 0.999990000050000, 0.999952101050293, 72 | 0.970044712764112, 0.999990000050000] 73 | Spectral green is 74 | [0.051925270155586, 0.999883682663797, 0.784699164495622, 75 | 0.179017334640088, 0.999990000050000, 0.963949717360944, 76 | 0.941471776849435, 0.957484656296654] 77 | Spectral blue is 78 | [0.999990000050000, 0.069412518874038, 0.018702055417155, 79 | 0.027715468817236, 0.793292459419939, 0.881241745509140, 80 | 0.750431552708948, 0.999990000050000] 81 | spectral_to_XYZ_m is 82 | [[0.160911702209152, 0.010536082264362, 0.335863234341330, 83 | 0.392091779907267, 0.003577037160754, 0.001199280922560, 84 | 0.000744951625635, 0.000502994513244], 85 | [0.038578653590621, 0.302725534072098, 0.478125520092737, 86 | 0.178394820214319, 0.001291733514223, 0.000433082262815, 87 | 0.000269015687274, 0.000181640565914], 88 | [0.946532443606047, 0.068580748489464, 0.001308663025418, 89 | 0.000093394132679, 0.000000000000000, 0.000000000000000, 90 | 0.000000000000000, 0.000000000000000]] 91 | spectral_to_RGB_m is 92 | [[-0.009751444231376, -0.465453116724931, 0.352808002267490, 93 | 0.996449910264658, 0.009607180544409, 0.003221020036560, 94 | 0.002000785634738, 0.001350938947967], 95 | [-0.044257132949613, 0.560541110050266, 0.571469016356550, 96 | -0.045365696355241, -0.001043770411510, -0.000349947151784, 97 | -0.000217374935762, -0.000146772432437], 98 | [1.001540204578923, 0.011324986856812, -0.077459271522419, 99 | -0.014477621006246, -0.000064493011594, -0.000021622709617, 100 | -0.000013431283479, -0.000009068865387]] 101 | Spectral_to_Device_RGB_m is 102 | [[-0.009751444231376, -0.465453116724931, 0.352808002267490, 103 | 0.996449910264658, 0.009607180544409, 0.003221020036560, 104 | 0.002000785634738, 0.001350938947967], 105 | [-0.044257132949613, 0.560541110050266, 0.571469016356550, 106 | -0.045365696355241, -0.001043770411510, -0.000349947151784, 107 | -0.000217374935762, -0.000146772432437], 108 | [1.001540204578923, 0.011324986856812, -0.077459271522419, 109 | -0.014477621006246, -0.000064493011594, -0.000021622709617, 110 | -0.000013431283479, -0.000009068865387]] 111 | ``` 112 | 113 | 114 | It will also plot color mixes. The columular output order is linear sRGB, weighted geometric mean spectral, then perceptual sRGB: 115 | 116 | ![Figure_1](https://user-images.githubusercontent.com/6015639/150627303-476c9959-cd6c-4a2e-8090-1374c4e2859c.png) 117 | ![Figure_2](https://user-images.githubusercontent.com/6015639/150627306-16b4896f-90cc-492c-aba1-5eda4e7dc500.png) 118 | ![Figure_3](https://user-images.githubusercontent.com/6015639/150627309-50f58cd4-7323-42cf-9755-07512b7e877e.png) 119 | ![Figure_4](https://user-images.githubusercontent.com/6015639/150627329-3d1cecc1-c058-4f0c-aeba-4624d415f4fe.png) 120 | ![Figure_5](https://user-images.githubusercontent.com/6015639/150627334-6800c07e-9640-4a5a-a967-75957459084b.png) 121 | ![Figure_6](https://user-images.githubusercontent.com/6015639/150627339-3843a048-8bd5-462c-a56d-6c51d6683684.png) 122 | ![Figure_7](https://user-images.githubusercontent.com/6015639/150627343-be564c25-b760-4b2e-9974-3e9907fc82ec.png) 123 | ![Figure_8](https://user-images.githubusercontent.com/6015639/150627346-b768b1ac-15e8-4499-86be-eeb032909e8c.png) 124 | ![Figure_9](https://user-images.githubusercontent.com/6015639/150627349-3ae7dac9-567d-4518-be19-cff3d7a47fc6.png) 125 | ![Figure_10](https://user-images.githubusercontent.com/6015639/150627353-faace08e-90b7-42f3-ab27-2c00d48f8f04.png) 126 | ![Figure_11](https://user-images.githubusercontent.com/6015639/150627358-800a449d-e480-4f0f-90c3-8d7b2954d9b1.png) 127 | ![Figure_12](https://user-images.githubusercontent.com/6015639/150627366-59345b5a-b69b-4ba6-b7d9-ef49cd23011c.png) 128 | -------------------------------------------------------------------------------- /solver.py: -------------------------------------------------------------------------------- 1 | from settings import * 2 | from tools import * 3 | from colour import (XYZ_to_xy, SpectralDistribution) 4 | from os.path import exists 5 | from os import remove 6 | 7 | # layout of the minimization parameter vector: 8 | # wavelengths(nm) 9 | 10 | 11 | red_XYZ = colour.RGB_to_XYZ([1.0,0.0,0.0], illuminant_xy, illuminant_xy, RGB_to_XYZ_m) 12 | green_XYZ = colour.RGB_to_XYZ([0.0,1.0,0.0], illuminant_xy, illuminant_xy, RGB_to_XYZ_m) 13 | blue_XYZ = colour.RGB_to_XYZ([0.0,0.0,1.0], illuminant_xy, illuminant_xy, RGB_to_XYZ_m) 14 | 15 | XYZ = [red_XYZ, green_XYZ, blue_XYZ] 16 | 17 | def extractSPDS(a, numwaves): 18 | sds = np.asarray(a)[:4 * numwaves].reshape((4, numwaves)) 19 | return sds 20 | 21 | def extractCMFS(a, numwaves): 22 | cmfs = np.array([[]]) 23 | waves = np.sort(np.asarray(a)[4 * numwaves:5 * numwaves]) 24 | for idx, wave in enumerate(waves): 25 | if idx == 0: 26 | cmfs = np.append(cmfs, [CMFS[wave]], axis=1) 27 | else: 28 | cmfs = np.append(cmfs, [CMFS[wave]], axis=0) 29 | return cmfs 30 | 31 | def extractDataFromParameter(a): 32 | sds = extractSPDS(a, numwaves) 33 | cmfs = extractCMFS(a, numwaves) 34 | illuminant = sds[3] 35 | tmat = generateT_MATRIX_XYZ(cmfs, illuminant) 36 | 37 | return (sds, cmfs, tmat) 38 | 39 | 40 | 41 | def processResult(a): 42 | sds, cmfs, tmat = extractDataFromParameter(a) 43 | spectral_to_XYZ_m = generateT_MATRIX_XYZ(cmfs, sds[3]) 44 | spectral_to_RGB_m = generateT_MATRIX_RGB(cmfs, sds[3], XYZ_to_RGB_m) 45 | Spectral_to_Device_RGB_m = generateT_MATRIX_RGB(cmfs, sds[3], XYZ_to_RGB_Device_m) 46 | waves = np.sort(np.asarray(a)[4 * numwaves:5 * numwaves]) 47 | red_xyz = spectral_to_XYZ(sds[0], spectral_to_XYZ_m) 48 | green_xyz = spectral_to_XYZ(sds[1], spectral_to_XYZ_m) 49 | blue_xyz = spectral_to_XYZ(sds[2], spectral_to_XYZ_m) 50 | illuminant_xyz = spectral_to_XYZ(sds[3], spectral_to_XYZ_m) 51 | red_sd = SpectralDistribution( 52 | (sds[0]), 53 | waves) 54 | red_sd.name = str(red_xyz) 55 | green_sd = SpectralDistribution( 56 | (sds[1]), 57 | waves) 58 | green_sd.name = str(green_xyz) 59 | blue_sd = SpectralDistribution( 60 | (sds[2]), 61 | waves) 62 | blue_sd.name = str(blue_xyz) 63 | illuminant_sd = SpectralDistribution( 64 | (sds[3]), 65 | waves) 66 | illuminant_sd.name = str(illuminant_xyz) 67 | return (waves, spectral_to_XYZ_m, spectral_to_RGB_m, Spectral_to_Device_RGB_m, red_xyz, green_xyz, blue_xyz, 68 | illuminant_xyz, red_sd, green_sd, blue_sd, illuminant_sd, cmfs, tmat) 69 | 70 | 71 | def mix_test(sda, sdb, targetsd, ratio, tmat): 72 | mixed = spectral_Mix_WGM(sda, sdb, ratio) 73 | mixedXYZ = spectral_to_XYZ(mixed, tmat) 74 | mixedxy = XYZ_to_xy(mixedXYZ) 75 | targetXYZ = spectral_to_XYZ(targetsd, tmat) 76 | targetxy = XYZ_to_xy(targetXYZ) 77 | 78 | diff = np.linalg.norm(mixedxy - targetxy) 79 | return diff 80 | 81 | def luminance_drop(sda, sdb, ratio, tmat): 82 | mixed = spectral_Mix_WGM(sda, sdb, ratio) 83 | mixedXYZ = spectral_to_XYZ(mixed, tmat) 84 | xyzA = spectral_to_XYZ(sda, tmat) 85 | xyzB = spectral_to_XYZ(sdb, tmat) 86 | lumAvg = np.mean((xyzA, xyzB), axis=0)[1] 87 | 88 | diff = lumAvg - mixedXYZ[1] 89 | return diff 90 | 91 | def match_XYZ(a, targetXYZ, spectral_to_XYZ_m): 92 | """ 93 | match one XYZ 94 | """ 95 | spec = np.asarray(a) 96 | xyz = spectral_to_XYZ(spec, spectral_to_XYZ_m) 97 | diff = np.linalg.norm(xyz - targetXYZ) 98 | return diff 99 | 100 | def match_xy(a, targetXYZ, spectral_to_XYZ_m): 101 | """ 102 | match one xy 103 | """ 104 | spec = np.asarray(a) 105 | xyz = spectral_to_XYZ(spec, spectral_to_XYZ_m) 106 | xy = XYZ_to_xy(xyz) 107 | 108 | targetxy = XYZ_to_xy(targetXYZ) 109 | 110 | diff = np.linalg.norm(xy - targetxy) 111 | return diff 112 | 113 | def minimize_slope(a): 114 | """ 115 | minimize a slope 116 | """ 117 | diff = np.sum(np.diff(np.asarray(a)) ** 2) 118 | return diff 119 | 120 | def minimize_slopes(sds): 121 | """ 122 | minimize multiple slopes 123 | """ 124 | red_diff = np.sum(np.diff(sds[0]) ** 2) 125 | green_diff = np.sum(np.diff(sds[1]) ** 2) 126 | blue_diff = np.sum(np.diff(sds[2]) ** 2) 127 | diff = red_diff + green_diff + blue_diff 128 | return diff 129 | 130 | # having duplicate wavelengths is an error for Colour library 131 | # (and probably doesn't make sense) 132 | def uniqueWaves(a): 133 | waves = np.sort(np.asarray(a)[4 * numwaves:5 * numwaves]) 134 | _, counts = np.unique(waves, return_counts=True) 135 | if np.any(counts > 1): 136 | return np.inf 137 | else: 138 | return 0.0 139 | 140 | # try to have at least some difference between each wavelength 141 | # penalize less than waveVariance 142 | def varianceWaves(a): 143 | waves = np.sort(np.asarray(a)[4 * numwaves:5 * numwaves]) 144 | variance = np.min(np.diff(np.sort(waves))) 145 | if variance < waveVariance: 146 | return (waveVariance - variance) 147 | else: 148 | return 0.0 149 | 150 | def plotProgress(xk, convergence): 151 | (waves, spectral_to_XYZ_m, spectral_to_RGB_m, Spectral_to_Device_RGB_m, red_xyz, green_xyz, blue_xyz, 152 | illuminant_xyz, red_sd, green_sd, blue_sd, illuminant_sd, cmfs, tmat) = processResult(xk) 153 | red_delta = np.linalg.norm(red_xyz - XYZ[0]) 154 | green_delta = np.linalg.norm(green_xyz - XYZ[1]) 155 | blue_delta = np.linalg.norm(blue_xyz - XYZ[2]) 156 | ilum_delta = np.linalg.norm(illuminant_xy - colour.XYZ_to_xy(illuminant_xyz)) 157 | bumpiness = minimize_slopes([red_sd.values, green_sd.values, blue_sd.values]) 158 | variance = varianceWaves(xk) 159 | illum_bumpiness = minimize_slope(illuminant_sd.values) 160 | yellow = red_sd.values + green_sd.values 161 | mixtest1 = mix_test(blue_sd.values, yellow, green_sd.values, 0.5, spectral_to_XYZ_m) 162 | cyan = blue_sd.values + green_sd.values + red_sd.values * 0.05 163 | mixtest2 = mix_test(blue_sd.values, np.repeat(1.0, numwaves), cyan, 0.5, spectral_to_XYZ_m) 164 | purple = red_sd.values + blue_sd.values 165 | mixtest3 = mix_test(red_sd.values, blue_sd.values, purple, 0.5, spectral_to_XYZ_m) 166 | # darkp = red_sd.values* 0.298 + green_sd.values * 0.18 + blue_sd.values * 0.551 167 | # lightcy = red_sd.values * 0.502 + green_sd.values * 0.723 + blue_sd.values * 0.861 168 | # mixtest4 = mix_test(darkp, np.repeat(1.0, numwaves), lightcy, 0.5, spectral_to_XYZ_m) 169 | lum_drop_rg = luminance_drop(red_sd.values, green_sd.values, 0.5, spectral_to_XYZ_m) 170 | lum_drop_rb= luminance_drop(red_sd.values, blue_sd.values, 0.5, spectral_to_XYZ_m) 171 | lum_drop_gb = luminance_drop(green_sd.values, blue_sd.values, 0.5, spectral_to_XYZ_m) 172 | vis_efficiency = np.sum(cmfs, axis=0)[1] 173 | sums = ((np.sum([red_sd.values, green_sd.values, blue_sd.values],axis=0) - 1.0) ** 2.0).sum() 174 | 175 | print("cost metric (smaller = better), weighted cost, actual cost value") 176 | print("red delta: ", red_delta ** 2.0 * weight_red, red_delta) 177 | print("green delta: ", green_delta ** 2.0 * weight_green, green_delta) 178 | print("blue delta: ", blue_delta ** 2.0 * weight_blue, blue_delta) 179 | print("illum xy delta: ", ilum_delta ** 2.0 * weight_illuminant_white, ilum_delta) 180 | print("bumpiness: ", bumpiness * weight_minslope, bumpiness) 181 | print("wave variance ", variance * weight_variance, variance) 182 | print("illum bumpiness ", illum_bumpiness * weight_ill_slope, illum_bumpiness) 183 | print("lum drop rg ", lum_drop_rg ** 2.0 * weight_lum_drop_rg, lum_drop_rg) 184 | print("lum drop rb ", lum_drop_rb ** 2.0 * weight_lum_drop_rb, lum_drop_rb) 185 | print("lum drop gb ", lum_drop_gb ** 2.0 * weight_lum_drop_gb, lum_drop_gb) 186 | print("visual effic ", -(vis_efficiency ** 2.0) * weight_visual_efficiency, -vis_efficiency) 187 | print("sd sums to one ", sums * weight_sum_to_one, sums) 188 | 189 | 190 | print("mix green delta: ", mixtest1 ** 2.0 * weight_mixtest1, mixtest1) 191 | # nudge b+w towards desaturated cyan 192 | 193 | print("mix bl/wh delta: ", mixtest2 ** 2.0 * weight_mixtest2, mixtest2) 194 | print("mix prple delta: ", mixtest3 ** 2.0 * weight_mixtest3, mixtest3) 195 | # print("mix dprp/w delta ", mixtest4 ** 2.0 * weight_mixtest4, mixtest4) 196 | print("selected wavelengths: ", waves) 197 | print("`touch halt` to exit early with this solution.") 198 | print("---") 199 | 200 | if exists("halt"): 201 | print("halting early. . .") 202 | remove("halt") 203 | return True 204 | else: 205 | return False --------------------------------------------------------------------------------