|
24 | 24 | pass |
25 | 25 |
|
26 | 26 | from pycam02ucs import ViewingConditions |
27 | | -from pycam02ucs.cam02ucs import deltaEp_sRGB, UCS_space |
| 27 | +from pycam02ucs.cam02ucs import UCS_space, SCD_space, LCD_space |
28 | 28 | from pycam02ucs.srgb import sRGB_to_XYZ, XYZ_to_sRGB |
29 | 29 | from pycam02ucs.cm.minimvc import Trigger |
30 | 30 |
|
| 31 | +# Our preferred space (mostly here so we can easily tweak it when curious) |
| 32 | +UNIFORM_SPACE = UCS_space |
31 | 33 |
|
32 | 34 | def _sRGB_to_CIECAM02(RGB): |
33 | 35 | XYZ = sRGB_to_XYZ(RGB) |
34 | 36 | return ViewingConditions.sRGB.XYZ_to_CIECAM02(XYZ) |
35 | 37 |
|
36 | | - |
37 | 38 | def _CIECAM02_to_JKapbp(ciecam02): |
38 | 39 | JMh = np.column_stack((ciecam02.J, ciecam02.M, ciecam02.h)) |
39 | | - return UCS_space.JMh_to_JKapbp(JMh) |
| 40 | + return UNIFORM_SPACE.JMh_to_JKapbp(JMh) |
40 | 41 |
|
41 | 42 |
|
42 | 43 | def _JKapbp_to_JMh(JKapbp): |
43 | | - return UCS_space.JKapbp_to_JMh(JKapbp) |
| 44 | + return UNIFORM_SPACE.JKapbp_to_JMh(JKapbp) |
44 | 45 |
|
45 | 46 |
|
46 | 47 | def _JMh_to_sRGB(JMh): |
@@ -148,160 +149,191 @@ def is_gray(self): |
148 | 149 | return False |
149 | 150 |
|
150 | 151 | def _vis_axes(): |
151 | | - grid = GridSpec(10, 8, |
| 152 | + grid = GridSpec(10, 4, |
152 | 153 | left=0.02, |
153 | 154 | right=0.98, |
154 | 155 | bottom=0.02, |
155 | | - width_ratios=[1, 1, 1, 1, 1, 1, 2, 2], |
| 156 | + width_ratios=[1] * 4, |
156 | 157 | height_ratios=[1] * 10) |
157 | | - axes = {'cmap': grid[0, :3], |
158 | | - 'deltas': grid[1:4, :3], |
159 | | - 'deuteranomaly': grid[0, 3:6], |
160 | | - 'deuteranopia': grid[1, 3:6], |
161 | | - 'protanomaly': grid[2, 3:6], |
162 | | - 'protanopia': grid[3, 3:6], |
163 | | - 'lightness': grid[4:6, :2], |
164 | | - 'colourfulness': grid[4:6, 2:4], |
165 | | - 'hue': grid[4:6, 4:6], |
166 | | - |
167 | | - 'image0': grid[0:2, 6], |
168 | | - 'image0-cb': grid[0:2, 7], |
169 | | - 'image1': grid[2:4, 6], |
170 | | - 'image1-cb': grid[2:4, 7], |
171 | | - 'image2': grid[4:6, 6], |
172 | | - 'image2-cb': grid[4:6, 7], |
| 158 | + axes = {'cmap': grid[0, 0], |
| 159 | + 'deltas': grid[1:4, 0], |
| 160 | + |
| 161 | + 'cmap-greyscale': grid[0, 1], |
| 162 | + 'lightness-deltas': grid[1:4, 1], |
| 163 | + |
| 164 | + 'deuteranomaly': grid[4, 0], |
| 165 | + 'deuteranopia': grid[5, 0], |
| 166 | + 'protanomaly': grid[4, 1], |
| 167 | + 'protanopia': grid[5, 1], |
| 168 | + |
| 169 | + # 'lightness': grid[4:6, 1], |
| 170 | + # 'colourfulness': grid[4:6, 2], |
| 171 | + # 'hue': grid[4:6, 3], |
| 172 | + |
| 173 | + 'image0': grid[0:3, 2], |
| 174 | + 'image0-cb': grid[0:3, 3], |
| 175 | + 'image1': grid[3:6, 2], |
| 176 | + 'image1-cb': grid[3:6, 3], |
| 177 | + 'image2': grid[6:9, 2], |
| 178 | + 'image2-cb': grid[6:9, 3], |
| 179 | + |
| 180 | + 'gamut-checkbox': grid[9, 2], |
173 | 181 | } |
174 | 182 |
|
175 | 183 | axes = {key: plt.subplot(value) for (key, value) in axes.items()} |
176 | | - axes['gamut'] = plt.subplot(grid[6:, :6], projection='3d') |
| 184 | + axes['gamut'] = plt.subplot(grid[6:, :2], projection='3d') |
177 | 185 |
|
178 | 186 | return axes |
179 | 187 |
|
180 | 188 |
|
181 | 189 | # N=256 matches the default quantization for LinearSegmentedColormap, which |
182 | 190 | # reduces quantization/aliasing artifacts (esp. in the perceptual deltas |
183 | 191 | # plot). |
184 | | -def viscm(cm, name=None, N=256, N_dots=50, show_gamut=False): |
185 | | - if isinstance(cm, str): |
186 | | - cm = plt.get_cmap(cm) |
187 | | - if name is None: |
188 | | - name = cm.name |
189 | | - |
190 | | - fig = plt.figure() |
191 | | - fig.suptitle("Colormap evaluation: %s" % (name,), fontsize=24) |
192 | | - axes = _vis_axes() |
193 | | - |
194 | | - x = np.linspace(0, 1, N) |
195 | | - x_dots = np.linspace(0, 1, N_dots) |
196 | | - RGB = cm(x)[:, :3] |
197 | | - RGB_dots = cm(x_dots)[:, :3] |
198 | | - |
199 | | - ax = axes['cmap'] |
200 | | - _show_cmap(ax, RGB) |
201 | | - ax.set_title("The colormap in its glory") |
202 | | - ax.get_xaxis().set_visible(False) |
203 | | - ax.get_yaxis().set_visible(False) |
204 | | - |
205 | | - def label(ax, s): |
206 | | - ax.text(0.95, 0.05, s, |
207 | | - horizontalalignment="right", |
208 | | - verticalalignment="bottom", |
209 | | - transform=ax.transAxes) |
210 | | - |
211 | | - ax = axes['deltas'] |
212 | | - local_deltas = N * deltaEp_sRGB(RGB[:-1, :], RGB[1:, :]) |
213 | | - ax.plot(x[1:], local_deltas) |
214 | | - arclength = np.sum(local_deltas) / N |
215 | | - label(ax, "Perceptual deltas (total: %0.2f)" % (arclength,)) |
216 | | - ax.set_ylim(0, ax.get_ylim()[1]) |
217 | | - # ax.text(0.05, 0.9, "Total length: %0.2f" % (arclength,), |
218 | | - # horizontalalignment="left", |
219 | | - # verticalalignment="top", |
220 | | - # transform=ax.transAxes) |
221 | | - |
222 | | - def anom(ax, mat, name): |
223 | | - _show_cmap(ax, _apply_rgb_mat(mat, RGB)) |
224 | | - label(ax, name) |
| 192 | +class viscm(object): |
| 193 | + def __init__(self, cm, name=None, N=256, N_dots=50, show_gamut=False): |
| 194 | + if isinstance(cm, str): |
| 195 | + cm = plt.get_cmap(cm) |
| 196 | + if name is None: |
| 197 | + name = cm.name |
| 198 | + |
| 199 | + fig = plt.figure() |
| 200 | + fig.suptitle("Colormap evaluation: %s" % (name,), fontsize=24) |
| 201 | + axes = _vis_axes() |
| 202 | + |
| 203 | + x = np.linspace(0, 1, N) |
| 204 | + x_dots = np.linspace(0, 1, N_dots) |
| 205 | + RGB = cm(x)[:, :3] |
| 206 | + RGB_dots = cm(x_dots)[:, :3] |
| 207 | + |
| 208 | + ax = axes['cmap'] |
| 209 | + _show_cmap(ax, RGB) |
| 210 | + ax.set_title("The colormap in its glory") |
225 | 211 | ax.get_xaxis().set_visible(False) |
226 | 212 | ax.get_yaxis().set_visible(False) |
227 | 213 |
|
228 | | - anom(axes['deuteranomaly'], DEUTERANOMALY_05, "Moderate deuteranomaly") |
229 | | - anom(axes['deuteranopia'], DEUTERANOMALY_10, "Complete deuteranopia") |
230 | | - |
231 | | - anom(axes['protanomaly'], PROTANOMALY_05, "Moderate protanomaly") |
232 | | - anom(axes['protanopia'], PROTANOMALY_10, "Complete protanopia") |
233 | | - |
234 | | - ciecam02 = _sRGB_to_CIECAM02(RGB) |
235 | | - ax = axes['lightness'] |
236 | | - ax.plot(x, ciecam02.J, label="Lightness (J)") |
237 | | - label(ax, "Lightness (J)") |
238 | | - ax.set_ylim(0, 105) |
239 | | - |
240 | | - ax = axes['colourfulness'] |
241 | | - ax.plot(x, ciecam02.M, label="Colourfulness (M)") |
242 | | - label(ax, "Colourfulness (M)") |
243 | | - |
244 | | - ax = axes['hue'] |
245 | | - ax.plot(x, ciecam02.h, label="Hue angle (h)") |
246 | | - label(ax, "Hue angle (h)") |
247 | | - ax.set_ylim(0, 360) |
248 | | - |
249 | | - JKapbp = _CIECAM02_to_JKapbp(ciecam02) |
250 | | - ax = axes['gamut'] |
251 | | - ax.plot(JKapbp[:, 1], JKapbp[:, 2], JKapbp[:, 0]) |
252 | | - JKapbp_dots = _CIECAM02_to_JKapbp(_sRGB_to_CIECAM02(RGB_dots)) |
253 | | - ax.scatter(JKapbp_dots[:, 1], |
254 | | - JKapbp_dots[:, 2], |
255 | | - JKapbp_dots[:, 0], |
256 | | - c=RGB_dots[:, :], |
257 | | - s=80) |
258 | | - |
259 | | - # Draw a wireframe indicating the sRGB gamut |
260 | | - if show_gamut: |
261 | | - gamut_patch = sRGB_gamut_patch() |
262 | | - # That function returns a patch where each face is colored to match |
263 | | - # the represented colors. For present purposes we want something |
264 | | - # less... colorful. |
265 | | - gamut_patch.set_facecolor([0.5, 0.5, 0.5, 0.1]) |
266 | | - gamut_patch.set_edgecolor([0.2, 0.2, 0.2, 0.1]) |
267 | | - ax.add_collection3d(gamut_patch) |
268 | | - |
269 | | - _setup_JKapbp_axis(ax) |
270 | | - |
271 | | - images = [] |
272 | | - image_args = [] |
273 | | - example_dir = os.path.dirname(__file__) + "/examples/" |
274 | | - |
275 | | - images.append(np.loadtxt(example_dir + "hist2d.txt")) |
276 | | - image_args.append({"aspect": "equal", |
277 | | - "origin": "lower", |
278 | | - "interpolation": "nearest", |
279 | | - "vmin": 0}) |
280 | | - |
281 | | - images.append(np.loadtxt(example_dir + "st-helens_before-modified.txt.gz")) |
282 | | - image_args.append({}) |
283 | | - |
284 | | - # Adapted from http://matplotlib.org/mpl_examples/images_contours_and_fields/pcolormesh_levels.py |
285 | | - dx = dy = 0.05 |
286 | | - y, x = np.mgrid[-5 : 5 + dy : dy, -5 : 10 + dx : dx] |
287 | | - z = np.sin(x) ** 10 + np.cos(10 + y * x) + np.cos(x) + 0.2 * y + 0.1 * x |
288 | | - images.append(z) |
289 | | - image_args.append({}) |
290 | | - |
291 | | - deuter_cm = TransformedCMap(DEUTERANOMALY_05, cm) |
292 | | - for i, (image, args) in enumerate(zip(images, image_args)): |
293 | | - ax = axes['image%i' % (i,)] |
294 | | - ax.imshow(image, cmap=cm, **args) |
| 214 | + def label(ax, s): |
| 215 | + ax.text(0.95, 0.05, s, |
| 216 | + horizontalalignment="right", |
| 217 | + verticalalignment="bottom", |
| 218 | + transform=ax.transAxes) |
| 219 | + |
| 220 | + ax = axes['deltas'] |
| 221 | + local_deltas = N * UNIFORM_SPACE.deltaEp_sRGB(RGB[:-1, :], RGB[1:, :]) |
| 222 | + ax.plot(x[1:], local_deltas) |
| 223 | + arclength = np.sum(local_deltas) / N |
| 224 | + label(ax, "Perceptual deltas (total: %0.2f)" % (arclength,)) |
| 225 | + ax.set_ylim(0, ax.get_ylim()[1]) |
| 226 | + ax.get_xaxis().set_visible(False) |
| 227 | + |
| 228 | + ciecam02 = _sRGB_to_CIECAM02(RGB) |
| 229 | + JKapbp = _CIECAM02_to_JKapbp(ciecam02) |
| 230 | + |
| 231 | + ax = axes['cmap-greyscale'] |
| 232 | + grey_RGB = _JMh_to_sRGB(np.column_stack((ciecam02.J, |
| 233 | + np.zeros_like(ciecam02.M), |
| 234 | + ciecam02.h))) |
| 235 | + _show_cmap(ax, grey_RGB) |
| 236 | + ax.set_title("Black-and-white printed") |
295 | 237 | ax.get_xaxis().set_visible(False) |
296 | 238 | ax.get_yaxis().set_visible(False) |
297 | 239 |
|
298 | | - ax_cb = axes['image%i-cb' % (i,)] |
299 | | - ax_cb.imshow(image, cmap=deuter_cm, **args) |
300 | | - ax_cb.get_xaxis().set_visible(False) |
301 | | - ax_cb.get_yaxis().set_visible(False) |
| 240 | + ax = axes['lightness-deltas'] |
| 241 | + ax.plot(x[1:], N * np.diff(JKapbp[:, 0])) |
| 242 | + label(ax, |
| 243 | + "Perceptual lightness deltas (total: %0.2f)" |
| 244 | + % (JKapbp[-1, 0] - JKapbp[0, 0])) |
| 245 | + ax.set_ylim(0, ax.get_ylim()[1]) |
| 246 | + ax.get_xaxis().set_visible(False) |
302 | 247 |
|
303 | | - axes['image0'].set_title("Sample images") |
304 | | - axes['image0-cb'].set_title("Moderate deuter.") |
| 248 | + # ax = axes['lightness'] |
| 249 | + # ax.plot(x, ciecam02.J) |
| 250 | + # label(ax, "Lightness (J)") |
| 251 | + # ax.set_ylim(0, 105) |
| 252 | + |
| 253 | + # ax = axes['colourfulness'] |
| 254 | + # ax.plot(x, ciecam02.M) |
| 255 | + # label(ax, "Colourfulness (M)") |
| 256 | + |
| 257 | + # ax = axes['hue'] |
| 258 | + # ax.plot(x, ciecam02.h) |
| 259 | + # label(ax, "Hue angle (h)") |
| 260 | + # ax.set_ylim(0, 360) |
| 261 | + |
| 262 | + def anom(ax, mat, name): |
| 263 | + _show_cmap(ax, _apply_rgb_mat(mat, RGB)) |
| 264 | + label(ax, name) |
| 265 | + ax.get_xaxis().set_visible(False) |
| 266 | + ax.get_yaxis().set_visible(False) |
| 267 | + |
| 268 | + anom(axes['deuteranomaly'], DEUTERANOMALY_05, "Moderate deuteranomaly") |
| 269 | + anom(axes['deuteranopia'], DEUTERANOMALY_10, "Complete deuteranopia") |
| 270 | + |
| 271 | + anom(axes['protanomaly'], PROTANOMALY_05, "Moderate protanomaly") |
| 272 | + anom(axes['protanopia'], PROTANOMALY_10, "Complete protanopia") |
| 273 | + |
| 274 | + ax = axes['gamut'] |
| 275 | + ax.plot(JKapbp[:, 1], JKapbp[:, 2], JKapbp[:, 0]) |
| 276 | + JKapbp_dots = _CIECAM02_to_JKapbp(_sRGB_to_CIECAM02(RGB_dots)) |
| 277 | + ax.scatter(JKapbp_dots[:, 1], |
| 278 | + JKapbp_dots[:, 2], |
| 279 | + JKapbp_dots[:, 0], |
| 280 | + c=RGB_dots[:, :], |
| 281 | + s=80) |
| 282 | + |
| 283 | + # Draw a wireframe indicating the sRGB gamut |
| 284 | + self.gamut_patch = sRGB_gamut_patch() |
| 285 | + # That function returns a patch where each face is colored to match |
| 286 | + # the represented colors. For present purposes we want something |
| 287 | + # less... colorful. |
| 288 | + self.gamut_patch.set_facecolor([0.5, 0.5, 0.5, 0.1]) |
| 289 | + self.gamut_patch.set_edgecolor([0.2, 0.2, 0.2, 0.1]) |
| 290 | + ax.add_collection3d(self.gamut_patch) |
| 291 | + self.gamut_patch.set_visible(show_gamut) |
| 292 | + |
| 293 | + self.gamut_patch_toggle = Button(axes['gamut-checkbox'], |
| 294 | + "<- Toggle gamut display") |
| 295 | + def toggle(*args): |
| 296 | + self.gamut_patch.set_visible(not self.gamut_patch.get_visible()) |
| 297 | + plt.draw() |
| 298 | + self.gamut_patch_toggle.on_clicked(toggle) |
| 299 | + |
| 300 | + _setup_JKapbp_axis(ax) |
| 301 | + |
| 302 | + images = [] |
| 303 | + image_args = [] |
| 304 | + example_dir = os.path.dirname(__file__) + "/examples/" |
| 305 | + |
| 306 | + images.append(np.loadtxt(example_dir + "hist2d.txt")) |
| 307 | + image_args.append({"aspect": "equal", |
| 308 | + "origin": "lower", |
| 309 | + "interpolation": "nearest", |
| 310 | + "vmin": 0}) |
| 311 | + |
| 312 | + images.append(np.loadtxt(example_dir |
| 313 | + + "st-helens_before-modified.txt.gz")) |
| 314 | + image_args.append({}) |
| 315 | + |
| 316 | + # Adapted from http://matplotlib.org/mpl_examples/images_contours_and_fields/pcolormesh_levels.py |
| 317 | + dx = dy = 0.05 |
| 318 | + y, x = np.mgrid[-5 : 5 + dy : dy, -5 : 10 + dx : dx] |
| 319 | + z = np.sin(x) ** 10 + np.cos(10 + y * x) + np.cos(x) + 0.2 * y + 0.1 * x |
| 320 | + images.append(z) |
| 321 | + image_args.append({}) |
| 322 | + |
| 323 | + deuter_cm = TransformedCMap(DEUTERANOMALY_05, cm) |
| 324 | + for i, (image, args) in enumerate(zip(images, image_args)): |
| 325 | + ax = axes['image%i' % (i,)] |
| 326 | + ax.imshow(image, cmap=cm, **args) |
| 327 | + ax.get_xaxis().set_visible(False) |
| 328 | + ax.get_yaxis().set_visible(False) |
| 329 | + |
| 330 | + ax_cb = axes['image%i-cb' % (i,)] |
| 331 | + ax_cb.imshow(image, cmap=deuter_cm, **args) |
| 332 | + ax_cb.get_xaxis().set_visible(False) |
| 333 | + ax_cb.get_yaxis().set_visible(False) |
| 334 | + |
| 335 | + axes['image0'].set_title("Sample images") |
| 336 | + axes['image0-cb'].set_title("Moderate deuter.") |
305 | 337 |
|
306 | 338 | def sRGB_gamut_patch(resolution=20): |
307 | 339 | step = 1.0 / resolution |
|
0 commit comments