Skip to content

Commit 5e46e4d

Browse files
committed
Add wide color gamut (WCG) support
In practice, this means color space conversions between sRGB, DCI-P3, and Rec2020. Input and output color spaces can be toggled on the fly and independent of each other with the hotkeys 'i' and 'o'. This commit constitutes release 1.8.0.
1 parent fa039d3 commit 5e46e4d

File tree

6 files changed

+274
-4
lines changed

6 files changed

+274
-4
lines changed

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
[![Build Status](https://travis-ci.com/toaarnio/glview.svg?branch=master)](https://travis-ci.com/github/toaarnio/glview)
44

5-
Lightning-fast image viewer with synchronized split-screen zooming & panning + HDR exposure control.
5+
Lightning-fast image viewer with synchronized split-screen zooming & panning + HDR & WCG support
66

77
**Installing:**
88
```

glview/glrenderer.py

+2
Original file line numberDiff line numberDiff line change
@@ -84,6 +84,8 @@ def redraw(self):
8484
self.prog['scale'] = self.ui.scale
8585
self.prog['grayscale'] = (texture.components == 1)
8686
self.prog['gamma'] = self.ui.gamma
87+
self.prog['cs_in'] = self.ui.cs_in
88+
self.prog['cs_out'] = self.ui.cs_out
8789
self.prog['maxval'] = maxval if self.ui.normalize else 1.0
8890
self.prog['ev'] = self.ui.ev
8991
self.prog['gamut.compress'] = (self.ui.gamut_fit != 0)

glview/glview.py

+2
Original file line numberDiff line numberDiff line change
@@ -138,6 +138,8 @@ def main():
138138
print(" n toggle exposure normalization on/off")
139139
print(" e slide exposure from -2EV to +2EV & back")
140140
print(" b toggle between HDR/LDR exposure control")
141+
print(" i toggle input color space: sRGB/P3/Rec2020")
142+
print(" o toggle output color space: sRGB/P3/Rec2020")
141143
print(" k toggle gamut compression strength")
142144
print(" x print image information (EXIF)")
143145
print(" d drop the currently shown image")

glview/pygletui.py

+11-1
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,8 @@ def __init__(self, files, debug, verbose=False):
4040
self.renderer = None
4141
self.texture_filter = "NEAREST"
4242
self.img_per_tile = [0, 1, 2, 3]
43+
self.cs_in = 0
44+
self.cs_out = 0
4345
self.gamma = 0
4446
self.normalize = False
4547
self.ev_range = 2
@@ -109,10 +111,12 @@ def _init_pyglet(self):
109111

110112
def _caption(self):
111113
fps = pyglet.clock.get_frequency()
114+
cspaces = ["sRGB", "DCI-P3", "Rec2020"]
115+
csc = f"{cspaces[self.cs_in]} => {cspaces[self.cs_out]}"
112116
norm = "off" if not self.normalize else "on"
113117
gamma = ["off", "sRGB", "HDR"][self.gamma]
114118
gamut = "off" if not self.gamut_fit else f"p = {self.gamut_pow[0]:.1f}"
115-
caption = f"glview [{self.ev:+1.2f}EV | norm {norm} | gamma {gamma} | gamut fit {gamut} | {fps:.1f} fps]"
119+
caption = f"glview [{self.ev:+1.2f}EV | norm {norm} | {csc} | gamma {gamma} | gamut fit {gamut} | {fps:.1f} fps]"
116120
for tileidx in range(self.numtiles):
117121
imgidx = self.img_per_tile[tileidx]
118122
basename = self.files.filespecs[imgidx]
@@ -286,6 +290,12 @@ def on_key_press(symbol, modifiers):
286290
if symbol == keys.G: # gamma
287291
self.gamma = (self.gamma + 1) % 3
288292
self.need_redraw = True
293+
if symbol == keys.I: # input color space
294+
self.cs_in = (self.cs_in + 1) % 3
295+
self.need_redraw = True
296+
if symbol == keys.O: # output color space
297+
self.cs_out = (self.cs_out + 1) % 3
298+
self.need_redraw = True
289299
if symbol == keys.B: # toggle between narrow/wide (LDR/HDR) exposure control
290300
self.ev_range = (self.ev_range + 6) % 12
291301
self.need_redraw = True

glview/texture.fs

+257-1
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,10 @@
1-
#version 130
1+
#version 140
2+
3+
precision highp float;
24

35
uniform int debug;
6+
uniform int cs_in;
7+
uniform int cs_out;
48
uniform bool grayscale;
59
uniform int gamma;
610
uniform float ev;
@@ -39,6 +43,18 @@ float max3(vec3 v) {
3943
}
4044

4145

46+
mat3 mround(mat3 mtx) {
47+
/**
48+
* Rounds the elements of the given matrix to the nearest integer. Annoyingly,
49+
* the built-in round() function does not work with matrices.
50+
*/
51+
mtx[0] = round(mtx[0]);
52+
mtx[1] = round(mtx[1]);
53+
mtx[2] = round(mtx[2]);
54+
return mtx;
55+
}
56+
57+
4258
/**************************************************************************************/
4359
/*
4460
/* G A M M A
@@ -102,6 +118,245 @@ vec3 apply_gamma(vec3 rgb) {
102118
}
103119

104120

121+
/**************************************************************************************/
122+
/*
123+
/* C O L O R S P A C E C O N V E R S I O N S
124+
/*
125+
/**************************************************************************************/
126+
127+
128+
vec3 xy_to_xyz(vec2 xy) {
129+
/**
130+
* Transforms the given color coordinates from CIE xy to CIE XYZ.
131+
*/
132+
vec3 xyz;
133+
xyz.x = xy.x / xy.y;
134+
xyz.y = 1.0;
135+
xyz.z = (1.0 - xy.x - xy.y) / xy.y;
136+
return xyz;
137+
}
138+
139+
140+
mat3 rgb_to_xyz_mtx(vec2 xy_r, vec2 xy_g, vec2 xy_b, vec2 xy_w) {
141+
/**
142+
* Returns a 3 x 3 conversion matrix from RGB to XYZ, given the (x, y) chromaticity
143+
* coordinates of the RGB primaries and the reference white. Conversion formula taken
144+
* from http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html.
145+
*/
146+
vec3 XYZ_r = xy_to_xyz(xy_r);
147+
vec3 XYZ_g = xy_to_xyz(xy_g);
148+
vec3 XYZ_b = xy_to_xyz(xy_b);
149+
vec3 XYZ_w = xy_to_xyz(xy_w);
150+
mat3 M = mat3(XYZ_r, XYZ_g, XYZ_b);
151+
152+
// Scale each column of the RGB-to-XYZ matrix with a scalar such
153+
// that [1, 1, 1] gets transformed to the given whitepoint (XYZ_w);
154+
// for example, M * [1, 1, 1] = [0.9504, 1.0, 1.0888] in case of D65.
155+
156+
vec3 S = inverse(M) * XYZ_w; // whitepoint in RGB
157+
M[0] *= S[0]; // R column vector scale
158+
M[1] *= S[1]; // G column vector scale
159+
M[2] *= S[2]; // B column vector scale
160+
return M;
161+
}
162+
163+
164+
mat3 xyz_to_rgb_mtx(vec2 xy_r, vec2 xy_g, vec2 xy_b, vec2 xy_w) {
165+
/**
166+
* Returns a 3 x 3 conversion matrix from XYZ to RGB, given the (x, y) chromaticity
167+
* coordinates of the RGB primaries and the reference white. Conversion formula taken
168+
* from http://www.brucelindbloom.com/index.html?Eqn_RGB_XYZ_Matrix.html.
169+
*/
170+
mat3 M = rgb_to_xyz_mtx(xy_r, xy_g, xy_b, xy_w);
171+
M = inverse(M);
172+
return M;
173+
}
174+
175+
176+
mat3 srgb_to_xyz_mtx() {
177+
/**
178+
* Returns the exact sRGB to XYZ conversion matrix defined by the sRGB specification.
179+
* Note that the matrix is computed from limited-precision primaries and whitepoint,
180+
* and rounded to four decimals at the end, as per the specification.
181+
*/
182+
vec2 D65_WP = vec2(0.3127, 0.3290);
183+
vec2 xy_r = vec2(0.640, 0.330);
184+
vec2 xy_g = vec2(0.300, 0.600);
185+
vec2 xy_b = vec2(0.150, 0.060);
186+
mat3 M = rgb_to_xyz_mtx(xy_r, xy_g, xy_b, D65_WP);
187+
M = mround(M * 10000.0) / 10000.0;
188+
return M;
189+
}
190+
191+
192+
mat3 xyz_to_srgb_mtx() {
193+
/**
194+
* Returns the exact XYZ to sRGB conversion matrix defined by the sRGB specification.
195+
* Note that the matrix is computed from limited-precision primaries and whitepoint,
196+
* and rounded to four decimals at the end, as per the specification.
197+
*/
198+
mat3 M = inverse(srgb_to_xyz_mtx());
199+
M = mround(M * 10000.0) / 10000.0;
200+
return M;
201+
}
202+
203+
204+
mat3 p3_to_xyz_mtx() {
205+
/**
206+
* Returns the exact DCI-P3 to XYZ conversion matrix defined by the P3 specification.
207+
* Note that the matrix is computed from limited-precision primaries and whitepoint,
208+
* as per the specification.
209+
*/
210+
vec2 D65_WP = vec2(0.3127, 0.3290);
211+
vec2 xy_r = vec2(0.680, 0.320);
212+
vec2 xy_g = vec2(0.265, 0.690);
213+
vec2 xy_b = vec2(0.150, 0.060);
214+
mat3 M = rgb_to_xyz_mtx(xy_r, xy_g, xy_b, D65_WP);
215+
return M;
216+
}
217+
218+
219+
mat3 xyz_to_p3_mtx() {
220+
/**
221+
* Returns the exact XYZ to DCI-P3 conversion matrix defined by the P3 specification.
222+
* Note that the matrix is computed from limited-precision primaries and whitepoint,
223+
* as per the specification.
224+
*/
225+
mat3 M = inverse(p3_to_xyz_mtx());
226+
return M;
227+
}
228+
229+
230+
mat3 rec2020_to_xyz_mtx() {
231+
/**
232+
* Returns the exact Rec2020 to XYZ conversion matrix defined by the Rec2020
233+
* specification. Note that the matrix is computed from limited-precision
234+
* primaries and whitepoint, as per the specification.
235+
*/
236+
vec2 D65_WP = vec2(0.3127, 0.3290);
237+
vec2 xy_r = vec2(0.708, 0.292);
238+
vec2 xy_g = vec2(0.170, 0.797);
239+
vec2 xy_b = vec2(0.131, 0.046);
240+
mat3 M = rgb_to_xyz_mtx(xy_r, xy_g, xy_b, D65_WP);
241+
return M;
242+
}
243+
244+
245+
mat3 xyz_to_rec2020_mtx() {
246+
/**
247+
* Returns the exact XYZ to Rec2020 conversion matrix defined by the Rec2020
248+
* specification. Note that the matrix is computed from limited-precision
249+
* primaries and whitepoint, as per the specification.
250+
*/
251+
mat3 M = inverse(rec2020_to_xyz_mtx());
252+
return M;
253+
}
254+
255+
256+
vec3 srgb_to_xyz(vec3 rgb) {
257+
/**
258+
* Transforms the given sRGB color to CIE XYZ.
259+
*/
260+
vec3 xyz = srgb_to_xyz_mtx() * rgb;
261+
return xyz;
262+
}
263+
264+
265+
vec3 xyz_to_srgb(vec3 xyz) {
266+
/**
267+
* Transforms the given CIE XYZ color to sRGB.
268+
*/
269+
vec3 rgb = xyz_to_srgb_mtx() * xyz;
270+
return rgb;
271+
}
272+
273+
274+
vec3 p3_to_xyz(vec3 rgb) {
275+
/**
276+
* Transforms the given DCI-P3 color to CIE XYZ.
277+
*/
278+
vec3 xyz = p3_to_xyz_mtx() * rgb;
279+
return xyz;
280+
}
281+
282+
283+
vec3 xyz_to_p3(vec3 xyz) {
284+
/**
285+
* Transforms the given CIE XYZ color to DCI-P3.
286+
*/
287+
vec3 rgb = xyz_to_p3_mtx() * xyz;
288+
return rgb;
289+
}
290+
291+
292+
vec3 rec2020_to_xyz(vec3 rgb) {
293+
/**
294+
* Transforms the given Rec2020 color to CIE XYZ.
295+
*/
296+
vec3 xyz = rec2020_to_xyz_mtx() * rgb;
297+
return xyz;
298+
}
299+
300+
301+
vec3 xyz_to_rec2020(vec3 xyz) {
302+
/**
303+
* Transforms the given CIE XYZ color to Rec2020.
304+
*/
305+
vec3 rgb = xyz_to_rec2020_mtx() * xyz;
306+
return rgb;
307+
}
308+
309+
310+
vec3 csconv(vec3 rgb) {
311+
/**
312+
* Transforms the given color from a given input color space to a given output
313+
* color space. If the input and output color spaces are the same, the color is
314+
* unchanged. The following color spaces are available as both input & output:
315+
*
316+
* 0 - sRGB
317+
* 1 - DCI-P3
318+
* 2 - Rec2020
319+
*
320+
* As a concrete example, to display a P3-JPEG captured by a modern smartphone
321+
* on a high-quality wide-gamut monitor, you would set the input color space to
322+
* DCI-P3 and the output to Rec2020, while making sure that the monitor is set
323+
* to Rec2020 mode.
324+
*/
325+
if (cs_in != cs_out) {
326+
vec3 xyz;
327+
switch (cs_in) {
328+
case 0:
329+
xyz = srgb_to_xyz(rgb);
330+
break;
331+
case 1:
332+
xyz = p3_to_xyz(rgb);
333+
break;
334+
case 2:
335+
xyz = rec2020_to_xyz(rgb);
336+
break;
337+
default:
338+
xyz = rgb;
339+
break;
340+
}
341+
switch (cs_out) {
342+
case 0:
343+
rgb = xyz_to_srgb(xyz);
344+
break;
345+
case 1:
346+
rgb = xyz_to_p3(xyz);
347+
break;
348+
case 2:
349+
rgb = xyz_to_rec2020(xyz);
350+
break;
351+
default:
352+
rgb = xyz;
353+
break;
354+
}
355+
}
356+
return rgb;
357+
}
358+
359+
105360
/**************************************************************************************/
106361
/*
107362
/* G A M U T C O M P R E S S I O N
@@ -230,6 +485,7 @@ vec3 debug_indicators(vec3 rgb) {
230485
void main() {
231486
color = texture2D(texture, rotate(texcoords, orientation));
232487
color.rgb = color.rgb / maxval;
488+
color.rgb = csconv(color.rgb);
233489
color.rgb = grayscale ? color.rrr : color.rgb;
234490
color.rgb = gamut.compress ? compress_gamut(color.rgb) : color.rgb;
235491
color.rgb = color.rgb * exp(ev); // exp(x) == 2^x

glview/version.py

+1-1
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
__version__ = "1.7.0"
1+
__version__ = "1.8.0"

0 commit comments

Comments
 (0)