ColourMapper.cpp
Go to the documentation of this file.
1 /* -*- c-basic-offset: 4 indent-tabs-mode: nil -*- vi:set ts=8 sts=4 sw=4: */
2 
3 /*
4  Sonic Visualiser
5  An audio file viewer and annotation editor.
6  Centre for Digital Music, Queen Mary, University of London.
7  This file copyright 2006-2016 Chris Cannam and QMUL.
8 
9  This program is free software; you can redistribute it and/or
10  modify it under the terms of the GNU General Public License as
11  published by the Free Software Foundation; either version 2 of the
12  License, or (at your option) any later version. See the file
13  COPYING included with this distribution for more information.
14 */
15 
16 #include "ColourMapper.h"
17 
18 #include <iostream>
19 
20 #include <cmath>
21 
22 #include "base/Debug.h"
23 
24 #include <vector>
25 
26 #include <QPainter>
27 
28 using namespace std;
29 
30 static vector<QColor> convertStrings(const vector<QString> &strs,
31  bool reversed)
32 {
33  vector<QColor> converted;
34  for (const auto &s: strs) converted.push_back(QColor(s));
35  if (reversed) {
36  reverse(converted.begin(), converted.end());
37  }
38  return converted;
39 }
40 
41 static vector<QColor> ice = convertStrings({
42  // Based on ColorBrewer ylGnBu
43  "#ffffff", "#ffff00", "#f7fcf0", "#e0f3db", "#ccebc5", "#a8ddb5",
44  "#7bccc4", "#4eb3d3", "#2b8cbe", "#0868ac", "#084081", "#042040"
45  },
46  true);
47 
48 static vector<QColor> cherry = convertStrings({
49  "#f7f7f7", "#fddbc7", "#f4a582", "#d6604d", "#b2182b", "#dd3497",
50  "#ae017e", "#7a0177", "#49006a"
51  },
52  true);
53 
54 static vector<QColor> magma = convertStrings({
55  "#FCFFB2", "#FCDF96", "#FBC17D", "#FBA368", "#FA8657", "#F66B4D",
56  "#ED504A", "#E03B50", "#C92D59", "#B02363", "#981D69", "#81176D",
57  "#6B116F", "#57096E", "#43006A", "#300060", "#1E0848", "#110B2D",
58  "#080616", "#000005"
59  },
60  true);
61 
62 static vector<QColor> cividis = convertStrings({
63  "#00204c", "#00204e", "#002150", "#002251", "#002353", "#002355",
64  "#002456", "#002558", "#00265a", "#00265b", "#00275d", "#00285f",
65  "#002861", "#002963", "#002a64", "#002a66", "#002b68", "#002c6a",
66  "#002d6c", "#002d6d", "#002e6e", "#002e6f", "#002f6f", "#002f6f",
67  "#00306f", "#00316f", "#00316f", "#00326e", "#00336e", "#00346e",
68  "#00346e", "#01356e", "#06366e", "#0a376d", "#0e376d", "#12386d",
69  "#15396d", "#17396d", "#1a3a6c", "#1c3b6c", "#1e3c6c", "#203c6c",
70  "#223d6c", "#243e6c", "#263e6c", "#273f6c", "#29406b", "#2b416b",
71  "#2c416b", "#2e426b", "#2f436b", "#31446b", "#32446b", "#33456b",
72  "#35466b", "#36466b", "#37476b", "#38486b", "#3a496b", "#3b496b",
73  "#3c4a6b", "#3d4b6b", "#3e4b6b", "#404c6b", "#414d6b", "#424e6b",
74  "#434e6b", "#444f6b", "#45506b", "#46506b", "#47516b", "#48526b",
75  "#49536b", "#4a536b", "#4b546b", "#4c556b", "#4d556b", "#4e566b",
76  "#4f576c", "#50586c", "#51586c", "#52596c", "#535a6c", "#545a6c",
77  "#555b6c", "#565c6c", "#575d6d", "#585d6d", "#595e6d", "#5a5f6d",
78  "#5b5f6d", "#5c606d", "#5d616e", "#5e626e", "#5f626e", "#5f636e",
79  "#60646e", "#61656f", "#62656f", "#63666f", "#64676f", "#65676f",
80  "#666870", "#676970", "#686a70", "#686a70", "#696b71", "#6a6c71",
81  "#6b6d71", "#6c6d72", "#6d6e72", "#6e6f72", "#6f6f72", "#6f7073",
82  "#707173", "#717273", "#727274", "#737374", "#747475", "#757575",
83  "#757575", "#767676", "#777776", "#787876", "#797877", "#7a7977",
84  "#7b7a77", "#7b7b78", "#7c7b78", "#7d7c78", "#7e7d78", "#7f7e78",
85  "#807e78", "#817f78", "#828078", "#838178", "#848178", "#858278",
86  "#868378", "#878478", "#888578", "#898578", "#8a8678", "#8b8778",
87  "#8c8878", "#8d8878", "#8e8978", "#8f8a78", "#908b78", "#918c78",
88  "#928c78", "#938d78", "#948e78", "#958f78", "#968f77", "#979077",
89  "#989177", "#999277", "#9a9377", "#9b9377", "#9c9477", "#9d9577",
90  "#9e9676", "#9f9776", "#a09876", "#a19876", "#a29976", "#a39a75",
91  "#a49b75", "#a59c75", "#a69c75", "#a79d75", "#a89e74", "#a99f74",
92  "#aaa074", "#aba174", "#aca173", "#ada273", "#aea373", "#afa473",
93  "#b0a572", "#b1a672", "#b2a672", "#b4a771", "#b5a871", "#b6a971",
94  "#b7aa70", "#b8ab70", "#b9ab70", "#baac6f", "#bbad6f", "#bcae6e",
95  "#bdaf6e", "#beb06e", "#bfb16d", "#c0b16d", "#c1b26c", "#c2b36c",
96  "#c3b46c", "#c5b56b", "#c6b66b", "#c7b76a", "#c8b86a", "#c9b869",
97  "#cab969", "#cbba68", "#ccbb68", "#cdbc67", "#cebd67", "#d0be66",
98  "#d1bf66", "#d2c065", "#d3c065", "#d4c164", "#d5c263", "#d6c363",
99  "#d7c462", "#d8c561", "#d9c661", "#dbc760", "#dcc860", "#ddc95f",
100  "#deca5e", "#dfcb5d", "#e0cb5d", "#e1cc5c", "#e3cd5b", "#e4ce5b",
101  "#e5cf5a", "#e6d059", "#e7d158", "#e8d257", "#e9d356", "#ebd456",
102  "#ecd555", "#edd654", "#eed753", "#efd852", "#f0d951", "#f1da50",
103  "#f3db4f", "#f4dc4e", "#f5dd4d", "#f6de4c", "#f7df4b", "#f9e049",
104  "#fae048", "#fbe147", "#fce246", "#fde345", "#ffe443", "#ffe542",
105  "#ffe642", "#ffe743", "#ffe844", "#ffe945"
106  },
107  false);
108 
109 static void
110 mapDiscrete(double norm, vector<QColor> &colours, double &r, double &g, double &b)
111 {
112  int n = int(colours.size());
113  double m = norm * (n-1);
114  if (m >= n-1) { colours[n-1].getRgbF(&r, &g, &b, nullptr); return; }
115  if (m <= 0) { colours[0].getRgbF(&r, &g, &b, nullptr); return; }
116  int base(int(floor(m)));
117  double prop0 = (base + 1.0) - m, prop1 = m - base;
118  QColor c0(colours[base]), c1(colours[base+1]);
119  r = c0.redF() * prop0 + c1.redF() * prop1;
120  g = c0.greenF() * prop0 + c1.greenF() * prop1;
121  b = c0.blueF() * prop0 + c1.blueF() * prop1;
122 }
123 
124 ColourMapper::ColourMapper(int map, bool inverted, double min, double max) :
125  m_map(map),
126  m_inverted(inverted),
127  m_min(min),
128  m_max(max)
129 {
130  if (m_min == m_max) {
131  SVCERR << "WARNING: ColourMapper: min == max (== " << m_min
132  << "), adjusting" << endl;
133  m_max = m_min + 1;
134  }
135 }
136 
138 {
139 }
140 
141 int
143 {
144  return 15;
145 }
146 
147 QString
149 {
150  // When adding a map, be sure to also update getColourMapCount()
151 
152  if (n >= getColourMapCount()) return QObject::tr("<unknown>");
153  ColourMap map = (ColourMap)n;
154 
155  switch (map) {
156  case Green: return QObject::tr("Green");
157  case WhiteOnBlack: return QObject::tr("White on Black");
158  case BlackOnWhite: return QObject::tr("Black on White");
159  case Cherry: return QObject::tr("Cherry");
160  case Wasp: return QObject::tr("Wasp");
161  case Ice: return QObject::tr("Ice");
162  case Sunset: return QObject::tr("Sunset");
163  case FruitSalad: return QObject::tr("Fruit Salad");
164  case Banded: return QObject::tr("Banded");
165  case Highlight: return QObject::tr("Highlight");
166  case Printer: return QObject::tr("Printer");
167  case HighGain: return QObject::tr("High Gain");
168  case BlueOnBlack: return QObject::tr("Blue on Black");
169  case Cividis: return QObject::tr("Cividis");
170  case Magma: return QObject::tr("Magma");
171  }
172 
173  return QObject::tr("<unknown>");
174 }
175 
176 QString
178 {
179  if (n >= getColourMapCount()) return "<unknown>";
180  ColourMap map = (ColourMap)n;
181 
182  switch (map) {
183  case Green: return "Green";
184  case WhiteOnBlack: return "White on Black";
185  case BlackOnWhite: return "Black on White";
186  case Cherry: return "Cherry";
187  case Wasp: return "Wasp";
188  case Ice: return "Ice";
189  case Sunset: return "Sunset";
190  case FruitSalad: return "Fruit Salad";
191  case Banded: return "Banded";
192  case Highlight: return "Highlight";
193  case Printer: return "Printer";
194  case HighGain: return "High Gain";
195  case BlueOnBlack: return "Blue on Black";
196  case Cividis: return "Cividis";
197  case Magma: return "Magma";
198  }
199 
200  return "<unknown>";
201 }
202 
203 int
205 {
207 
208  if (id == "Green") { map = Green; }
209  else if (id == "White on Black") { map = WhiteOnBlack; }
210  else if (id == "Black on White") { map = BlackOnWhite; }
211  else if (id == "Cherry") { map = Cherry; }
212  else if (id == "Wasp") { map = Wasp; }
213  else if (id == "Ice") { map = Ice; }
214  else if (id == "Sunset") { map = Sunset; }
215  else if (id == "Fruit Salad") { map = FruitSalad; }
216  else if (id == "Banded") { map = Banded; }
217  else if (id == "Highlight") { map = Highlight; }
218  else if (id == "Printer") { map = Printer; }
219  else if (id == "High Gain") { map = HighGain; }
220  else if (id == "Blue on Black") { map = BlueOnBlack; }
221  else if (id == "Cividis") { map = Cividis; }
222  else if (id == "Magma") { map = Magma; }
223 
224  if (map == (ColourMap)getColourMapCount()) {
225  return -1;
226  } else {
227  return int(map);
228  }
229 }
230 
231 int
233 {
234  /* Returned value should be an index into the series
235  * (Default/Green, Sunset, WhiteOnBlack, BlackOnWhite, RedOnBlue,
236  * YellowOnBlack, BlueOnBlack, FruitSalad, Banded, Highlight,
237  * Printer, HighGain). Minimum 0, maximum 11.
238  */
239 
240  if (n >= getColourMapCount()) return 0;
241  ColourMap map = (ColourMap)n;
242 
243  switch (map) {
244  case Green: return 0;
245  case WhiteOnBlack: return 2;
246  case BlackOnWhite: return 3;
247  case Cherry: return 4;
248  case Wasp: return 5;
249  case Ice: return 6;
250  case Sunset: return 1;
251  case FruitSalad: return 7;
252  case Banded: return 8;
253  case Highlight: return 9;
254  case Printer: return 10;
255  case HighGain: return 11;
256  case BlueOnBlack: return 6;
257  case Cividis: return 6;
258  case Magma: return 1;
259  }
260 
261  return 0;
262 }
263 
264 QColor
265 ColourMapper::map(double value) const
266 {
267  double norm = (value - m_min) / (m_max - m_min);
268  if (norm < 0.0) norm = 0.0;
269  if (norm > 1.0) norm = 1.0;
270 
271  if (m_inverted) {
272  norm = 1.0 - norm;
273  }
274 
275  double h = 0.0, s = 0.0, v = 0.0, r = 0.0, g = 0.0, b = 0.0;
276  bool hsv = true;
277 
278  double blue = 0.6666, pieslice = 0.3333;
279 
280  if (m_map >= getColourMapCount()) return Qt::black;
282 
283  switch (map) {
284 
285  case Green:
286  h = blue - norm * 2.0 * pieslice;
287  s = 0.5f + norm/2.0;
288  v = norm;
289  break;
290 
291  case WhiteOnBlack:
292  r = g = b = norm;
293  hsv = false;
294  break;
295 
296  case BlackOnWhite:
297  r = g = b = 1.0 - norm;
298  hsv = false;
299  break;
300 
301  case Cherry:
302  hsv = false;
303  mapDiscrete(norm, cherry, r, g, b);
304  break;
305 
306  case Wasp:
307  h = 0.15;
308  s = 1.0;
309  v = norm;
310  break;
311 
312  case BlueOnBlack:
313  h = blue;
314  s = 1.0;
315  v = norm * 2.0;
316  if (v > 1.0) {
317  v = 1.0;
318  s = 1.0 - (sqrt(norm) - 0.707) * 3.413;
319  if (s < 0.0) s = 0.0;
320  if (s > 1.0) s = 1.0;
321  }
322  break;
323 
324  case Sunset:
325  r = (norm - 0.24) * 2.38;
326  if (r > 1.0) r = 1.0;
327  if (r < 0.0) r = 0.0;
328  g = (norm - 0.64) * 2.777;
329  if (g > 1.0) g = 1.0;
330  if (g < 0.0) g = 0.0;
331  b = (3.6f * norm);
332  if (norm > 0.277) b = 2.0 - b;
333  if (b > 1.0) b = 1.0;
334  if (b < 0.0) b = 0.0;
335  hsv = false;
336  break;
337 
338  case FruitSalad:
339  h = blue + (pieslice/6.0) - norm;
340  if (h < 0.0) h += 1.0;
341  s = 1.0;
342  v = 1.0;
343  break;
344 
345  case Banded:
346  if (norm < 0.125) return Qt::darkGreen;
347  else if (norm < 0.25) return Qt::green;
348  else if (norm < 0.375) return Qt::darkBlue;
349  else if (norm < 0.5) return Qt::blue;
350  else if (norm < 0.625) return Qt::darkYellow;
351  else if (norm < 0.75) return Qt::yellow;
352  else if (norm < 0.875) return Qt::darkRed;
353  else return Qt::red;
354  break;
355 
356  case Highlight:
357  if (norm > 0.99) return Qt::white;
358  else return Qt::darkBlue;
359 
360  case Printer:
361  if (norm > 0.8) {
362  r = 1.0;
363  } else if (norm > 0.7) {
364  r = 0.9;
365  } else if (norm > 0.6) {
366  r = 0.8;
367  } else if (norm > 0.5) {
368  r = 0.7;
369  } else if (norm > 0.4) {
370  r = 0.6;
371  } else if (norm > 0.3) {
372  r = 0.5;
373  } else if (norm > 0.2) {
374  r = 0.4;
375  } else {
376  r = 0.0;
377  }
378  r = g = b = 1.0 - r;
379  hsv = false;
380  break;
381 
382  case HighGain:
383  if (norm <= 1.0 / 256.0) {
384  norm = 0.0;
385  } else {
386  norm = 0.1f + (pow(((norm - 0.5) * 2.0), 3.0) + 1.0) / 2.081;
387  }
388  // now as for Sunset
389  r = (norm - 0.24) * 2.38;
390  if (r > 1.0) r = 1.0;
391  if (r < 0.0) r = 0.0;
392  g = (norm - 0.64) * 2.777;
393  if (g > 1.0) g = 1.0;
394  if (g < 0.0) g = 0.0;
395  b = (3.6f * norm);
396  if (norm > 0.277) b = 2.0 - b;
397  if (b > 1.0) b = 1.0;
398  if (b < 0.0) b = 0.0;
399  hsv = false;
400 /*
401  if (r > 1.0) r = 1.0;
402  r = g = b = 1.0 - r;
403  hsv = false;
404 */
405  break;
406 
407  case Ice:
408  hsv = false;
409  mapDiscrete(norm, ice, r, g, b);
410  break;
411 
412  case Cividis:
413  hsv = false;
414  mapDiscrete(norm, cividis, r, g, b);
415  break;
416 
417  case Magma:
418  hsv = false;
419  mapDiscrete(norm, magma, r, g, b);
420  break;
421  }
422 
423  if (hsv) {
424  return QColor::fromHsvF(h, s, v);
425  } else {
426  return QColor::fromRgbF(r, g, b);
427  }
428 }
429 
430 QColor
432 {
433  if (m_map >= getColourMapCount()) return Qt::white;
435 
436  switch (map) {
437 
438  case Green:
439  return QColor(255, 150, 50);
440 
441  case WhiteOnBlack:
442  return Qt::red;
443 
444  case BlackOnWhite:
445  return Qt::darkGreen;
446 
447  case Cherry:
448  return Qt::green;
449 
450  case Wasp:
451  return QColor::fromHsv(240, 255, 255);
452 
453  case Ice:
454  return Qt::red;
455 
456  case Sunset:
457  return Qt::white;
458 
459  case FruitSalad:
460  return Qt::white;
461 
462  case Banded:
463  return Qt::cyan;
464 
465  case Highlight:
466  return Qt::red;
467 
468  case Printer:
469  return Qt::red;
470 
471  case HighGain:
472  return Qt::red;
473 
474  case BlueOnBlack:
475  return Qt::red;
476 
477  case Cividis:
478  return Qt::white;
479 
480  case Magma:
481  return Qt::white;
482  }
483 
484  return Qt::white;
485 }
486 
487 bool
489 {
490  if (m_map >= getColourMapCount()) return false;
492 
493  switch (map) {
494 
495  case BlackOnWhite:
496  case Printer:
497  case HighGain:
498  return true;
499 
500  case Green:
501  case Sunset:
502  case WhiteOnBlack:
503  case Cherry:
504  case Wasp:
505  case Ice:
506  case FruitSalad:
507  case Banded:
508  case Highlight:
509  case BlueOnBlack:
510  case Cividis:
511  case Magma:
512 
513  default:
514  return false;
515  }
516 }
517 
518 QPixmap
520 {
521  QPixmap pmap(size);
522  pmap.fill(Qt::white);
523  QPainter paint(&pmap);
524 
525  int w = size.width(), h = size.height();
526 
527  int margin = 2;
528  if (w < 4 || h < 4) margin = 0;
529  else if (w < 8 || h < 8) margin = 1;
530 
531  int n = w - margin*2;
532 
533  for (int x = 0; x < n; ++x) {
534  double value = m_min + ((m_max - m_min) * x) / (n-1);
535  QColor colour(map(value));
536  paint.setPen(colour);
537  paint.drawLine(x + margin, margin, x + margin, h - margin);
538  }
539 
540  return pmap;
541 }
542 
543 
static vector< QColor > ice
static vector< QColor > convertStrings(const vector< QString > &strs, bool reversed)
static int getColourMapCount()
Return the number of known colour maps.
static vector< QColor > magma
static int getBackwardCompatibilityColourMap(int n)
Older versions of colour-handling code save and reload colour maps by numerical index and can&#39;t prope...
static QString getColourMapLabel(int n)
Return a human-readable label for the colour map with the given index.
QPixmap getExamplePixmap(QSize size) const
Return a pixmap of the given size containing a preview swatch for the colour map. ...
bool hasLightBackground() const
Return true if the colour map is intended to be placed over a light background, false otherwise...
static int getColourMapById(QString id)
Return the index for the colour map with the given machine-readable id string, or -1 if the id is not...
QColor map(double value) const
Map the given value to a colour.
static void mapDiscrete(double norm, vector< QColor > &colours, double &r, double &g, double &b)
static vector< QColor > cividis
static QString getColourMapId(int n)
Return a machine-readable id string for the colour map with the given index.
ColourMapper(int map, bool inverted, double minValue, double maxValue)
QColor getContrastingColour() const
Return a colour that contrasts somewhat with the colours in the map, so as to be used for cursors etc...
static vector< QColor > cherry