Add perceptually uniform rainbow color maps.
authorMartin Lambers <marlam@marlam.de>
Thu, 11 Feb 2016 11:48:35 +0000 (12:48 +0100)
committerMartin Lambers <marlam@marlam.de>
Thu, 11 Feb 2016 11:48:35 +0000 (12:48 +0100)
cmdline.cpp
colormap.cpp
colormap.hpp
colormapwidgets.cpp
colormapwidgets.hpp
gui.cpp
gui.hpp

index a3adc77..1eaf0ed 100644 (file)
@@ -34,16 +34,17 @@ extern int optind;
 #include "colormap.hpp"
 
 enum type {
-    brewer_seq = 0,
-    brewer_div = 1,
-    brewer_qual = 2,
-    isolum_seq = 3,
-    isolum_div = 4,
-    isolum_qual = 5,
-    blackbody = 6,
-    cubehelix = 7,
-    moreland = 8,
-    mcnames = 9
+    brewer_seq,
+    brewer_div,
+    brewer_qual,
+    isolum_seq,
+    isolum_div,
+    isolum_qual,
+    unirainbow,
+    blackbody,
+    cubehelix,
+    moreland,
+    mcnames
 };
 
 int main(int argc, char* argv[])
@@ -59,9 +60,9 @@ int main(int argc, char* argv[])
     float brightness = -1.0f;
     float warmth = -1.0f;
     float luminance = -1.0f;
+    float rotations = NAN;
     float temperature = -1.0f;
     float range = -1.0f;
-    float rotations = NAN;
     float gamma = -1.0f;
     bool have_color0 = false;
     unsigned char color0[3];
@@ -80,9 +81,9 @@ int main(int argc, char* argv[])
         { "brightness",  required_argument, 0, 'b' },
         { "warmth",      required_argument, 0, 'w' },
         { "luminance",   required_argument, 0, 'l' },
+        { "rotations",   required_argument, 0, 'r' },
         { "temperature", required_argument, 0, 'T' },
         { "range",       required_argument, 0, 'R' },
-        { "rotations",   required_argument, 0, 'r' },
         { "gamma",       required_argument, 0, 'g' },
         { "color0",      required_argument, 0, 'A' },
         { "color1",      required_argument, 0, 'O' },
@@ -108,6 +109,7 @@ int main(int argc, char* argv[])
                     : strcmp(optarg, "isoluminant-sequential") == 0 ? isolum_seq
                     : strcmp(optarg, "isoluminant-divergent") == 0 ? isolum_div
                     : strcmp(optarg, "isoluminant-qualitative") == 0 ? isolum_qual
+                    : strcmp(optarg, "uniformrainbow") == 0 ? unirainbow
                     : strcmp(optarg, "blackbody") == 0 ? blackbody
                     : strcmp(optarg, "cubehelix") == 0 ? cubehelix
                     : strcmp(optarg, "moreland") == 0 ? moreland
@@ -138,15 +140,15 @@ int main(int argc, char* argv[])
         case 'l':
             luminance = atof(optarg);
             break;
+        case 'r':
+            rotations = atof(optarg);
+            break;
         case 'T':
             temperature = atof(optarg);
             break;
         case 'R':
             range = atof(optarg);
             break;
-        case 'r':
-            rotations = atof(optarg);
-            break;
         case 'g':
             gamma = atof(optarg);
             break;
@@ -197,12 +199,18 @@ int main(int argc, char* argv[])
                 "    [-s|--saturation=S]               Set saturation in [0,1]\n"
                 "    [-h|--hue=H]                      Set default hue in [0,360] degrees\n"
                 "    [-d|--divergence=D]               Set divergence in deg. for div. and qual. maps\n"
+                "  Uniform Rainbow color maps:\n"
+                "    -t|--type=uniformrainbow          Generate a uniform rainbow color map\n"
+                "    [-h|--hue=H]                      Set start hue in [0,360] degrees\n"
+                "    [-r|--rotations=R]                Set number of rotations, in (-infty,infty)\n"
+                "    [-s|--saturation=S]               Set saturation, in [0,1]\n"
                 "  Black Body color maps:\n"
                 "    -t|--type=blackbody               Generate a Black Body color map\n"
                 "    [-T|--temperature=T]              Start temperature of the map in Kelvin\n"
                 "    [-R|--range=R]                    Range of temperatures of the map in Kelvin\n"
                 "  CubeHelix color maps:\n"
                 "    -t|--type=cubehelix               Generate a CubeHelix color map\n"
+                "    [-h|--hue=H]                      Set start hue in [0,180] degrees\n"
                 "    [-r|--rotations=R]                Set number of rotations, in (-infty,infty)\n"
                 "    [-s|--saturation=S]               Set saturation, in [0,1]\n"
                 "    [-g|--gamma=G]                    Set gamma correction, in (0,infty)\n"
@@ -239,6 +247,8 @@ int main(int argc, char* argv[])
             hue = ColorMap::IsoluminantDivergingDefaultHue;
         else if (type == isolum_qual)
             hue = ColorMap::IsoluminantQualitativeDefaultHue;
+        else if (type == unirainbow)
+            hue = ColorMap::UniformRainbowDefaultHue;
         else if (type == cubehelix)
             hue = ColorMap::CubeHelixDefaultHue;
     }
@@ -273,6 +283,8 @@ int main(int argc, char* argv[])
             saturation = ColorMap::IsoluminantDivergingDefaultSaturation;
         else if (type == isolum_qual)
             saturation = ColorMap::IsoluminantQualitativeDefaultSaturation;
+        else if (type == unirainbow)
+            saturation = ColorMap::UniformRainbowDefaultSaturation;
         else if (type == cubehelix)
             saturation = ColorMap::CubeHelixDefaultSaturation;
     }
@@ -298,6 +310,12 @@ int main(int argc, char* argv[])
         else if (type == isolum_qual)
             luminance = ColorMap::IsoluminantQualitativeDefaultLuminance;
     }
+    if (std::isnan(rotations)) {
+        if (type == unirainbow)
+            rotations = ColorMap::UniformRainbowDefaultRotations;
+        else if (type == cubehelix)
+            rotations = ColorMap::CubeHelixDefaultRotations;
+    }
     if (temperature < 0.0f) {
         if (type == blackbody)
             temperature = ColorMap::BlackBodyDefaultTemperature;
@@ -306,10 +324,6 @@ int main(int argc, char* argv[])
         if (type == blackbody)
             range = ColorMap::BlackBodyDefaultRange;
     }
-    if (std::isnan(rotations)) {
-        if (type == cubehelix)
-            rotations = ColorMap::CubeHelixDefaultRotations;
-    }
     if (gamma < 0.0f) {
         if (type == cubehelix)
             gamma = ColorMap::CubeHelixDefaultGamma;
@@ -353,6 +367,9 @@ int main(int argc, char* argv[])
     case isolum_qual:
         ColorMap::IsoluminantQualitative(n, &(colormap[0]), luminance, saturation, hue);
         break;
+    case unirainbow:
+        ColorMap::UniformRainbow(n, &(colormap[0]), hue, rotations, saturation);
+        break;
     case blackbody:
         ColorMap::BlackBody(n, &(colormap[0]), temperature, range);
         break;
index 293d292..ecb89e7 100644 (file)
@@ -596,6 +596,20 @@ void IsoluminantQualitative(int n, unsigned char* colormap,
     }
 }
 
+/* UniformRainbow */
+
+void UniformRainbow(int n, unsigned char* colormap, float hue, float rotations, float saturation)
+{
+    triplet luv, lch;
+    for (int i = 0; i < n; i++) {
+        float t = i / (n - 1.0f);
+        lch.l = t * 100.0f;
+        lch.c = lch_chroma(lch.l, (1.0f - t) * saturation);
+        lch.h = hue + t * rotations * twopi;
+        lch_to_colormap(lch, colormap + 3 * i);
+    }
+}
+
 /* BlackBody */
 
 static float plancks_law(float temperature, float lambda)
index fd09b46..081a38e 100644 (file)
@@ -143,6 +143,22 @@ void IsoluminantQualitative(int n, unsigned char* colormap,
         float hue = IsoluminantQualitativeDefaultHue);
 
 /*
+ * Perceptually uniform rainbow.
+ *
+ * These are similar to CubeHelix, but are constructed in LCH color space
+ * to achieve better perceptual uniformity.
+ */
+
+const float UniformRainbowDefaultHue = 0.0f;
+const float UniformRainbowDefaultRotations = -1.5f;
+const float UniformRainbowDefaultSaturation = 1.2f;
+
+void UniformRainbow(int n, unsigned char* colormap,
+        float hue = UniformRainbowDefaultHue,
+        float rotations = UniformRainbowDefaultRotations,
+        float saturation = UniformRainbowDefaultSaturation);
+
+/*
  * Black Body color maps, based on the chromaticity (hue and saturation) of a black body
  * at increading temperatures. The luminance is linearly increasing.
  *
index e43b3e4..f5f36f8 100644 (file)
@@ -45,6 +45,8 @@ static QString brewerlike_reference = QString("Relevant paper: "
 
 static QString isoluminant_reference = QString("");
 
+static QString uniformrainbow_reference = QString("");
+
 static QString blackbody_reference = QString("");
 
 static QString cubehelix_reference = QString("Relevant paper: "
@@ -817,6 +819,95 @@ void ColorMapIsoluminantQualitativeWidget::update()
         emit colorMapChanged();
 }
 
+/* ColorMapUniformRainbowWidget */
+
+ColorMapUniformRainbowWidget::ColorMapUniformRainbowWidget() :
+    _update_lock(false)
+{
+    QGridLayout *layout = new QGridLayout;
+
+    QLabel* n_label = new QLabel("Colors:");
+    layout->addWidget(n_label, 1, 0);
+    _n_spinbox = new QSpinBox();
+    _n_spinbox->setRange(2, 1024);
+    _n_spinbox->setSingleStep(1);
+    layout->addWidget(_n_spinbox, 1, 1, 1, 3);
+
+    QLabel* hue_label = new QLabel("Hue:");
+    layout->addWidget(hue_label, 2, 0);
+    _hue_changer = new ColorMapCombinedSliderSpinBox(0, 360, 1);
+    layout->addWidget(_hue_changer->slider, 2, 1, 1, 2);
+    layout->addWidget(_hue_changer->spinbox, 2, 3);
+
+    QLabel* rotations_label = new QLabel("Rotations:");
+    layout->addWidget(rotations_label, 3, 0);
+    _rotations_changer = new ColorMapCombinedSliderSpinBox(-5.0f, +5.0f, 0.1f);
+    layout->addWidget(_rotations_changer->slider, 3, 1, 1, 2);
+    layout->addWidget(_rotations_changer->spinbox, 3, 3);
+
+    QLabel* saturation_label = new QLabel("Saturation:");
+    layout->addWidget(saturation_label, 4, 0);
+    _saturation_changer = new ColorMapCombinedSliderSpinBox(0.0f, 5.0f, 0.1f);
+    layout->addWidget(_saturation_changer->slider, 4, 1, 1, 2);
+    layout->addWidget(_saturation_changer->spinbox, 4, 3);
+
+    layout->setColumnStretch(1, 1);
+    layout->addItem(new QSpacerItem(0, 0), 5, 0, 1, 4);
+    layout->setRowStretch(5, 1);
+    setLayout(layout);
+
+    connect(_n_spinbox, SIGNAL(valueChanged(int)), this, SLOT(update()));
+    connect(_hue_changer, SIGNAL(valueChanged(float)), this, SLOT(update()));
+    connect(_rotations_changer, SIGNAL(valueChanged(float)), this, SLOT(update()));
+    connect(_saturation_changer, SIGNAL(valueChanged(float)), this, SLOT(update()));
+    reset();
+}
+
+ColorMapUniformRainbowWidget::~ColorMapUniformRainbowWidget()
+{
+}
+
+void ColorMapUniformRainbowWidget::reset()
+{
+    _update_lock = true;
+    _n_spinbox->setValue(256);
+    _hue_changer->setValue(qRadiansToDegrees(ColorMap::UniformRainbowDefaultHue));
+    _rotations_changer->setValue(ColorMap::UniformRainbowDefaultRotations);
+    _saturation_changer->setValue(ColorMap::UniformRainbowDefaultSaturation);
+    _update_lock = false;
+    update();
+}
+
+QVector<QColor> ColorMapUniformRainbowWidget::colorMap() const
+{
+    int n;
+    float h, r, s;
+    parameters(n, h, r, s);
+    QVector<unsigned char> colormap(3 * n);
+    ColorMap::UniformRainbow(n, colormap.data(), h, r, s);
+    return toQColor(colormap);
+}
+
+QString ColorMapUniformRainbowWidget::reference() const
+{
+    return uniformrainbow_reference;
+}
+
+void ColorMapUniformRainbowWidget::parameters(int& n, float& hue,
+        float& rotations, float& saturation) const
+{
+    n = _n_spinbox->value();
+    hue = qDegreesToRadians(_hue_changer->value());
+    rotations = _rotations_changer->value();
+    saturation = _saturation_changer->value();
+}
+
+void ColorMapUniformRainbowWidget::update()
+{
+    if (!_update_lock)
+        emit colorMapChanged();
+}
+
 /* ColorMapBlackBodyWidget */
 
 ColorMapBlackBodyWidget::ColorMapBlackBodyWidget() :
index 9c7c57d..c7a8490 100644 (file)
@@ -227,6 +227,28 @@ public:
     void parameters(int& n, float& luminance, float& saturation, float& hue) const;
 };
 
+class ColorMapUniformRainbowWidget : public ColorMapWidget
+{
+Q_OBJECT
+private:
+    bool _update_lock;
+    QSpinBox* _n_spinbox;
+    ColorMapCombinedSliderSpinBox* _hue_changer;
+    ColorMapCombinedSliderSpinBox* _rotations_changer;
+    ColorMapCombinedSliderSpinBox* _saturation_changer;
+private slots:
+    void update();
+
+public:
+    ColorMapUniformRainbowWidget();
+    ~ColorMapUniformRainbowWidget();
+
+    void reset() override;
+    QVector<QColor> colorMap() const override;
+    QString reference() const override;
+    void parameters(int& n, float& hue, float& rotations, float& saturation) const;
+};
+
 class ColorMapBlackBodyWidget : public ColorMapWidget
 {
 Q_OBJECT
diff --git a/gui.cpp b/gui.cpp
index 16747f5..a4b2f59 100644 (file)
--- a/gui.cpp
+++ b/gui.cpp
@@ -49,6 +49,7 @@ GUI::GUI()
     _isolumseq_widget = new ColorMapIsoluminantSequentialWidget;
     _isolumdiv_widget = new ColorMapIsoluminantDivergingWidget;
     _isolumqual_widget = new ColorMapIsoluminantQualitativeWidget;
+    _unirainbow_widget = new ColorMapUniformRainbowWidget;
     _blackbody_widget = new ColorMapBlackBodyWidget;
     _cubehelix_widget = new ColorMapCubeHelixWidget;
     _moreland_widget = new ColorMapMorelandWidget;
@@ -59,6 +60,7 @@ GUI::GUI()
     connect(_isolumseq_widget, SIGNAL(colorMapChanged()), this, SLOT(update()));
     connect(_isolumdiv_widget, SIGNAL(colorMapChanged()), this, SLOT(update()));
     connect(_isolumqual_widget, SIGNAL(colorMapChanged()), this, SLOT(update()));
+    connect(_unirainbow_widget, SIGNAL(colorMapChanged()), this, SLOT(update()));
     connect(_blackbody_widget, SIGNAL(colorMapChanged()), this, SLOT(update()));
     connect(_cubehelix_widget, SIGNAL(colorMapChanged()), this, SLOT(update()));
     connect(_moreland_widget, SIGNAL(colorMapChanged()), this, SLOT(update()));
@@ -73,6 +75,7 @@ GUI::GUI()
     _category_seq_widget->addTab(_brewerseq_widget, "Brewer-like");
     _category_seq_widget->addTab(_isolumseq_widget, "Isoluminant");
     //_category_seq_widget->addTab(_blackbody_widget, "Black Body");
+    _category_seq_widget->addTab(_unirainbow_widget, "Uniform Rainbow");
     _category_seq_widget->addTab(_cubehelix_widget, "CubeHelix");
     //_category_seq_widget->addTab(_mcnames_widget, "McNames");
     connect(_category_seq_widget, SIGNAL(currentChanged(int)), this, SLOT(update()));
diff --git a/gui.hpp b/gui.hpp
index e13dec0..c569a0a 100644 (file)
--- a/gui.hpp
+++ b/gui.hpp
@@ -33,6 +33,7 @@ class ColorMapBrewerQualitativeWidget;
 class ColorMapIsoluminantSequentialWidget;
 class ColorMapIsoluminantDivergingWidget;
 class ColorMapIsoluminantQualitativeWidget;
+class ColorMapUniformRainbowWidget;
 class ColorMapBlackBodyWidget;
 class ColorMapCubeHelixWidget;
 class ColorMapMorelandWidget;
@@ -56,6 +57,7 @@ private:
     ColorMapIsoluminantSequentialWidget* _isolumseq_widget;
     ColorMapIsoluminantDivergingWidget* _isolumdiv_widget;
     ColorMapIsoluminantQualitativeWidget* _isolumqual_widget;
+    ColorMapUniformRainbowWidget* _unirainbow_widget;
     ColorMapBlackBodyWidget* _blackbody_widget;
     ColorMapCubeHelixWidget* _cubehelix_widget;
     ColorMapMorelandWidget* _moreland_widget;