Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

57

58

59

60

61

62

63

64

65

66

67

68

69

70

71

72

73

74

75

76

77

78

79

80

81

82

83

84

85

86

87

88

89

90

91

92

93

94

95

96

97

98

99

100

101

102

103

104

105

106

107

108

109

110

111

112

113

114

115

116

117

118

119

120

121

122

123

124

125

126

127

128

129

130

131

132

133

134

135

136

137

138

139

140

141

142

143

144

145

146

147

148

149

150

151

152

153

154

155

156

157

158

159

160

161

162

163

164

165

166

167

168

169

170

171

172

173

174

175

176

177

178

179

180

181

182

183

184

185

186

187

188

189

190

191

192

193

194

195

196

197

198

199

200

201

202

203

204

205

206

207

208

209

210

211

212

213

214

215

216

217

218

219

220

221

222

223

224

225

226

227

228

229

230

231

232

233

234

235

236

237

238

239

240

241

242

243

244

245

246

247

248

249

250

251

252

253

254

255

256

257

258

259

260

261

262

263

264

265

266

267

268

269

270

271

272

273

274

275

276

277

278

279

280

281

282

283

284

285

286

287

288

289

290

291

292

293

294

295

296

297

298

299

300

301

302

303

304

305

306

307

308

309

310

311

312

313

314

315

316

317

318

319

320

321

322

323

324

325

326

327

328

329

330

331

332

333

334

335

336

337

338

339

340

341

342

343

344

345

346

347

348

349

350

351

352

353

354

355

356

357

358

359

360

361

362

363

364

365

366

367

368

369

370

371

372

373

374

375

376

377

378

379

380

381

382

383

384

385

386

387

388

389

390

391

392

393

394

395

396

397

398

399

400

401

402

403

404

405

406

407

408

409

410

411

412

413

414

415

416

417

418

419

420

421

422

423

424

425

426

427

428

429

430

431

432

433

434

435

436

437

438

439

440

441

442

443

444

445

446

447

448

449

450

451

452

453

454

455

456

457

458

459

460

461

462

463

# -*- coding: utf-8 -*- 

""" 

Color rendering of spectra 

========================== 

 

:author: Tobias Kölling <tobias.koelling@physik.uni-muenchen.de> 

 

This module can convert spectra to RGB. It is inspired by 

John Walkers ``specrend.c`` and the respective python version 

of Andrew Hutchins, but is completely rewritten. 

 

The wavelenth resampling code is from Florian Ewald <florian.ewald@campus.lmu.de>. 

 

Usage:: 

 

>>> spectralimage = ... #some spectral image 

>>> spectralbands = ... #the corresponding wavelengths 

>>> converter = SpecRGBConverter(spectralbands) 

>>> rgbimage = converter.spectrum_to_rgb(spectralimage) 

 

The original specrend.py comment:: 

 

Colour Rendering of Spectra 

 

by John Walker 

http://www.fourmilab.ch/ 

 

Last updated: March 9, 2003 

 

Converted to Python by Andrew Hutchins, sometime in early 

2011. 

 

This program is in the public domain. 

The modifications are also public domain. (AH) 

 

For complete information about the techniques employed in 

this program, see the World-Wide Web document: 

 

http://www.fourmilab.ch/documents/specrend/ 

 

The xyz_to_rgb() function, which was wrong in the original 

version of this program, was corrected by: 

 

Andrew J. S. Hamilton 21 May 1999 

Andrew.Hamilton@Colorado.EDU 

http://casa.colorado.edu/~ajsh/ 

 

who also added the gamma correction facilities and 

modified constrain_rgb() to work by desaturating the 

colour by adding white. 

 

A program which uses these functions to plot CIE 

"tongue" diagrams called "ppmcie" is included in 

the Netpbm graphics toolkit: 

http://netpbm.sourceforge.net/ 

(The program was called cietoppm in earlier 

versions of Netpbm.) 

 

""" 

import numpy as np 

 

from runmacs.spec.util.cietables import CIE1931 

 

class ColorSystem(object): 

""" 

Baseclass for a general color system. 

""" 

name = 'UNDEFINED' 

gamma = None 

def add_gamma(self, rgb): 

""" 

Adds gamma correction to a given rgb image. 

 

:note: The image must be given in float, normalized to 0...1 

""" 

76 ↛ 79line 76 didn't jump to line 79, because the condition on line 76 was never false if callable(self.gamma): 

return self.gamma(rgb) # pylint: disable=not-callable 

else: 

return rgb**(1.0 / self.gamma) 

def __repr__(self): 

return "<%s ColorSystem>"%self.name 

 

class SpecrendColorSystem(ColorSystem): 

""" 

ColorSystem corresponding to the original specrend implementation 

of John Walker. 

""" 

def __init__(self, csdef): 

self.xyz2rgb = self.cs2mat(csdef) 

self.gamma = csdef["gamma"] 

self.name = csdef["name"] 

@classmethod 

def cs2mat(cls, cs): 

""" 

Given an additive tricolour system CS, defined by the CIE x 

and y chromaticities of its three primaries (z is derived 

trivially as 1-(x+y)), and a desired chromaticity (XC, YC, 

ZC) in CIE space, determine the contribution of each 

primary in a linear combination which sums to the desired 

chromaticity. If the requested chromaticity falls outside 

the Maxwell triangle (colour gamut) formed by the three 

primaries, one of the r, g, or b weights will be negative. 

 

Caller can use constrain_rgb() to desaturate an 

outside-gamut colour to the closest representation within 

the available gamut and/or norm_rgb to normalise the RGB 

components so the largest nonzero component has value 1. 

""" 

# pylint: disable=invalid-name,too-many-locals 

xr = cs["xRed"] 

yr = cs["yRed"] 

zr = 1 - (xr + yr) 

xg = cs["xGreen"] 

yg = cs["yGreen"] 

zg = 1 - (xg + yg) 

xb = cs["xBlue"] 

yb = cs["yBlue"] 

zb = 1 - (xb + yb) 

xw = cs["xWhite"] 

yw = cs["yWhite"] 

zw = 1 - (xw + yw) 

 

rx = (yg * zb) - (yb * zg) 

ry = (xb * zg) - (xg * zb) 

rz = (xg * yb) - (xb * yg) 

gx = (yb * zr) - (yr * zb) 

gy = (xr * zb) - (xb * zr) 

gz = (xb * yr) - (xr * yb) 

bx = (yr * zg) - (yg * zr) 

by = (xg * zr) - (xr * zg) 

bz = (xr * yg) - (xg * yr) 

 

rw = ((rx * xw) + (ry * yw) + (rz * zw)) / yw 

gw = ((gx * xw) + (gy * yw) + (gz * zw)) / yw 

bw = ((bx * xw) + (by * yw) + (bz * zw)) / yw 

 

rx /= rw 

ry /= rw 

rz /= rw 

gx /= gw 

gy /= gw 

gz /= gw 

bx /= bw 

by /= bw 

bz /= bw 

 

return np.array(((rx, ry, rz), (gx, gy, gz), (bx, by, bz))) 

 

class MatrixColorSystem(ColorSystem): 

""" 

ColorSystem defined by a linear transformation w.r.t. XYZ space. 

""" 

def __init__(self, name, xyz2rgb, gamma): 

self.name = name 

self.xyz2rgb = xyz2rgb 

self.gamma = gamma 

 

# TODO: check a maybe more efficient gamma correction: 

# http://stackoverflow.com/questions/4401122/how-can-each-element-of-a-numpy-array-be-operated-upon-according-to-its-relative (which is inplace) 

def gamma_rec709(rgb): 

""" 

Compute REC709 Gamma (according to the original specrend.py) 

""" 

cc = 0.018 # pylint: disable=invalid-name 

return np.where(rgb < cc, 

rgb * (((1.099 * (cc**0.45)) - 0.099) / cc), 

(1.099 * (rgb**0.45)) - 0.099) 

 

def gamma_srgb(rgb): 

""" 

Compute sRGB Gamma according to: 

http://www.w3.org/Graphics/Color/sRGB 

 

:param rgb: rgb-array WITHOUT gamma, values 0.0...1.0 (linear data) 

 

:return: rgb-array WITH gamma, values 0.0...1.0 (nonlinear data) 

""" 

return np.where(rgb <= 0.00304, 12.92*rgb, 1.055*(rgb**(1./2.4))-0.055) 

 

def gamma_srgb_reverse(rgb): 

""" 

Compute sRGB Gamma according to: 

http://www.w3.org/Graphics/Color/sRGB 

 

:param rgb: rgb-array WITH gamma, values 0.0...1.0 (nonlinear data) 

 

:return: rgb-array WITHOUT gamma, values 0.0...1.0 (linear data) 

""" 

return np.where(rgb <= 0.03928, rgb/12.92, ((rgb+0.055)/1.055)**2.4) 

 

 

#sRGB table is also from: http://www.w3.org/Graphics/Color/sRGB 

CS_SRGB = MatrixColorSystem("sRGB", 

np.array(((3.2410, -1.5374, -0.4986), 

(-0.9692, 1.8760, 0.0416), 

(0.0556, -0.2040, 1.0570))), 

gamma_srgb) 

 

#the following color systems stem from the original specrend.py 

CS_NTSC = SpecrendColorSystem({"name": "NTSC", 

"xRed": 0.67, "yRed": 0.33, 

"xGreen": 0.21, "yGreen": 0.71, 

"xBlue": 0.14, "yBlue": 0.08, 

"xWhite": 0.3101, "yWhite": 0.3163, 

"gamma": gamma_rec709}) 

 

CS_EBU = SpecrendColorSystem({"name": "SUBU (PAL/SECAM)", 

"xRed": 0.64, "yRed": 0.33, 

"xGreen": 0.29, "yGreen": 0.60, 

"xBlue": 0.15, "yBlue": 0.06, 

"xWhite": 0.3127, "yWhite": 0.3291, 

"gamma": gamma_rec709}) 

 

CS_SMPTE = SpecrendColorSystem({"name": "SMPTE", 

"xRed": 0.63, "yRed": 0.34, 

"xGreen": 0.31, "yGreen": 0.595, 

"xBlue": 0.155, "yBlue": 0.07, 

"xWhite": 0.3127, "yWhite": 0.3291, 

"gamma": gamma_rec709}) 

 

CS_HDTV = SpecrendColorSystem({"name": "HDTV", 

"xRed": 0.67, "yRed": 0.33, 

"xGreen": 0.21, "yGreen": 0.71, 

"xBlue": 0.15, "yBlue": 0.06, 

"xWhite": 0.3127, "yWhite": 0.3291, 

"gamma": gamma_rec709}) 

 

CS_CIE = SpecrendColorSystem({"name": "CIE", 

"xRed": 0.7355, "yRed": 0.2645, 

"xGreen": 0.2658, "yGreen": 0.7243, 

"xBlue": 0.1669, "yBlue": 0.0085, 

"xWhite": 0.3333333333, "yWhite": 0.3333333333, 

"gamma": gamma_rec709}) 

 

CS_REC709 = SpecrendColorSystem({"name": "CIE REC709", 

"xRed": 0.64, "yRed": 0.33, 

"xGreen": 0.30, "yGreen": 0.60, 

"xBlue": 0.15, "yBlue": 0.06, 

"xWhite": 0.3127, "yWhite": 0.3291, 

"gamma": gamma_rec709}) 

 

class CieColorTable(object): 

""" 

Represents a color sensitivity function for given wavelenths. 

:param wvlns: list of wavelengths to interpolate the sensitivity on. 

""" 

table = np.array(zip(*CIE1931)[1]) 

wvlns = np.array(zip(*CIE1931)[0]) 

 

def __init__(self, wvlns): 

compare = (wvlns != self.wvlns) 

try: 

compare = any(compare) 

except TypeError: 

pass 

256 ↛ 259line 256 didn't jump to line 259, because the condition on line 256 was never false if compare: 

self.resample_wvlns(wvlns) 

else: 

self.wvlns_idx = np.arange(0, len(self.wvlns), 1) 

self.nwvlns = len(self.wvlns) 

 

def resample_wvlns(self, wvlns): 

""" 

Resamples the color table to a set of given wavelenths. 

 

:author: Florian Ewald <florian.ewald@campus.lmu.de> 

 

:param wvlns: Wavelengths to resample to. 

""" 

 

self.wvlns_idx, = np.where((self.wvlns.min() < wvlns) & (wvlns < self.wvlns.max())) 

wvlns_common = wvlns[self.wvlns_idx] 

delta_wvlns_common = np.abs(wvlns_common[1:] - wvlns_common[:-1]) 

delta_wvlns_common = np.hstack(([delta_wvlns_common[0],], delta_wvlns_common)) 

 

cie_colour_match = np.zeros((len(wvlns_common), 3), float) 

for i in range(3): 

cie_colour_match[:, i] = np.interp(wvlns_common, self.wvlns, self.table[:, i]) * delta_wvlns_common 

#ensure that the integrals of the colors stay equal 

cie_colour_match[:, i] /= cie_colour_match[:, i].sum() 

 

self.wvlns = wvlns_common 

self.table = cie_colour_match 

 

def norm_array(arr): 

""" 

Returns an array scaled to a maximum value of 1. 

""" 

return arr/arr.max() 

 

def array_max_dynamic_range(arr): 

""" 

Returns an array scaled to a minimum value of 0 and a maximum value of 1. 

""" 

finite_arr = arr[np.isfinite(arr)] 

low = np.nanmin(finite_arr) 

high = np.nanmax(finite_arr) 

return (arr - low)/(high - low) 

 

def float2byte(arr): 

""" 

Convert float [0 ... 1]-valued array to uint byte array. 

""" 

return (arr*255.).astype('uint8') 

 

def set_outliers_to_zero(arr): 

""" 

Set all pixels which are bigger than 100 times the median to 0 

""" 

arr[arr >= np.median(arr)*100.] = 0. 

 

def constrain_rgb(rgb, inplace=False): 

""" 

If the requested RGB shade contains a negative weight for 

one of the primaries, it lies outside the colour gamut 

accessible from the given triple of primaries. Desaturate 

it by adding white, equal quantities of R, G, and B, enough 

to make RGB all positive. 

""" 

white = np.nanmin(rgb) 

321 ↛ 328line 321 didn't jump to line 328, because the condition on line 321 was never false if white < 0: 

322 ↛ 326line 322 didn't jump to line 326, because the condition on line 322 was never false if inplace: 

rgb -= white 

return rgb 

else: 

return rgb - white 

else: 

return rgb 

 

def postProcessRGB(rgb, colorsystem=CS_SRGB): 

""" 

Post process an rgb image. 

 

This is normalize to full dynamic range, apply gamma and convert to 8bit uint. 

 

:note: This function will give errorneous results, if applied only on image slices. 

 

:note: This function operates in-place, so the input rgb will be overwritten! 

 

:param cs: Color system to use 

:param rgb: Preprocessed rgb image 

""" 

constrain_rgb(rgb, inplace=True) 

set_outliers_to_zero(rgb) 

return float2byte(colorsystem.add_gamma(array_max_dynamic_range(rgb))) 

 

 

class SpecRGBConverter(object): 

""" 

Converter for spectra to RGB images. 

 

The default color system is sRGB because it is the most widely adopted 

color system on the web. 

 

:note: Check self.colortable.wvlns_idx for the taken spectral indices. 

The spectra will be contrained to these indices when the 

transformation is applied. 

 

:param wvlns: List of wavelengths of the spectra to convert. 

:param cs: RGB color system to convert to (default: SRGB) 

""" 

def __init__(self, wvlns, colorsystem=CS_SRGB): 

self.colortable = CieColorTable(wvlns) 

self.colorsystem = colorsystem 

self.spec2rgb = np.tensordot(self.colortable.table, 

self.colorsystem.xyz2rgb, 

axes=((1,), (1,))) 

 

def crop_spectrum(self, spectrum): 

""" 

Crop a given spectrum to the used wavelengths, assuming the spectrum 

is either given with the same wavelenths as in ``__init__`` or already 

cropped (-> this function is idempotent) 

 

:param spectrum: over-defined spectrum 

:return: spectrum only on needed wavelengths 

""" 

nwvlns = spectrum.shape[-1] 

379 ↛ 380line 379 didn't jump to line 380, because the condition on line 379 was never true if nwvlns < self.colortable.nwvlns: 

raise ValueError('converter is configured for %d wavelenths, but I got only %d!' \ 

% (self.colortable.nwvlns, nwvlns)) 

382 ↛ 385line 382 didn't jump to line 385, because the condition on line 382 was never false elif nwvlns > self.colortable.nwvlns: 

#assume shape of ``spectrum`` is according to ``wvlns`` in __init__ and cut. 

return spectrum[..., self.colortable.wvlns_idx] 

return spectrum 

 

def xyz_to_rgb(self, xyz): 

""" 

convert xyz to RGB according to the defined color system 

:param xyz: Input image in xyz coordinates 

:return: Image in rgb coordinates (not normalized) 

""" 

return np.tensordot(xyz, self.colorsystem.xyz2rgb, axes=((-1,), (1,))) 

 

def spectrum_to_xyz(self, spectrum): 

""" 

Calculate the CIE X, Y, and Z coordinates corresponding to 

a light source with spectral distribution given by ``spectrum``. 

``spectrum`` must be a ``numpy`` array and the last dimension 

must be the spectral dimension with equally spaced wavelengths 

equal to ``self.wvlns``. 

 

:param spectrum: Input spectrum 

:return: Image in xyz coordinates (not normalized) 

""" 

 

#NOTE: Do NOT normalize XYZ, otherwise the gray information is lost! 

# The image must be normalized globally after color conversion. 

return np.tensordot(self.crop_spectrum(spectrum), self.colortable.table, axes=((-1,), (0,))) 

 

def spectrum_to_rgb(self, source, postprocess=True): 

""" 

Calculate the R, G, B coordinates corresponding to 

a light source with spectral distribution given by ``source``. 

The spectral axis must be the last axis, the spectrum 

must be defined in equidistant steps, either on the wavelenths 

defined during the ``__init__`` call, or already cropped to 

``self.colortable.wvlns``. 

 

:note: Use ``postprocess`` only on full images, not on slices! 

 

:param source: a [...,nwvlns] - shaped input spectral distribution 

:param postprocess: If set to ``True``, a 'ready-to-store' rgb image 

will be returned. 

:return: RGB image 

""" 

rgb = np.tensordot(self.crop_spectrum(source), self.spec2rgb, axes=((-1,), (0,))) 

if postprocess: 

return postProcessRGB(rgb, self.colorsystem) 

else: 

return rgb 

 

# 

#These functions are only for debugging: 

# 

def load(filename): 

""" 

Load calibrated netCDF file and return RGB image 

""" 

from netCDF4 import Dataset 

dataset = Dataset(filename) 

rad = dataset.groups['Data'].variables['radiance'] 

bands = dataset.variables['bands'] 

rend = SpecRGBConverter(bands) 

return rend.spectrum_to_rgb(np.rot90(rad)) 

 

def display(filename): 

""" 

Display RGB image from netCDF file 

""" 

import pylab 

pylab.imshow(load(filename)) 

 

def tojpg(filename): 

""" 

Convert a calibrated netCDF file to RGB JPEG. 

""" 

import Image 

#import StringIO 

img = Image.fromarray(load(filename)) 

#sio = StringIO.StringIO() 

#img.save(sio, 'JPEG') 

img.save(filename + '.jpg', 'JPEG')