Initial version.
authorMartin Lambers <marlam@marlam.de>
Wed, 14 Oct 2015 13:37:47 +0000 (15:37 +0200)
committerMartin Lambers <marlam@marlam.de>
Wed, 14 Oct 2015 13:37:47 +0000 (15:37 +0200)
CMakeLists.txt [new file with mode: 0644]
LICENSE
README.md
cg-logo.png [new file with mode: 0644]
cmdline.cpp [new file with mode: 0644]
colormap.cpp [new file with mode: 0644]
colormap.hpp [new file with mode: 0644]
gui.cpp [new file with mode: 0644]
gui.hpp [new file with mode: 0644]
gui.qrc [new file with mode: 0644]
screenshot.png [new file with mode: 0644]

diff --git a/CMakeLists.txt b/CMakeLists.txt
new file mode 100644 (file)
index 0000000..0681a0d
--- /dev/null
@@ -0,0 +1,27 @@
+# Copyright (C) 2015
+# Computer Graphics Group, University of Siegen
+# Written by Martin Lambers <martin.lambers@uni-siegen.de>
+#
+# Copying and distribution of this file, with or without modification, are
+# permitted in any medium without royalty provided the copyright notice and this
+# notice are preserved. This file is offered as-is, without any warranty.
+
+cmake_minimum_required(VERSION 2.8)
+set(CMAKE_AUTOMOC ON)
+set(CMAKE_INCLUDE_CURRENT_DIR ON)
+
+if(CMAKE_COMPILER_IS_GNUCXX)
+       set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -std=c++11 -Wall -Wextra")
+endif()
+
+find_package(Qt5Widgets QUIET)
+
+add_executable(gencolormap cmdline.cpp colormap.hpp colormap.cpp)
+install(TARGETS gencolormap RUNTIME DESTINATION bin)
+
+if(Qt5Widgets_FOUND)
+        qt5_add_resources(GUI_RESOURCES gui.qrc)
+       add_executable(gencolormap-gui gui.cpp colormap.hpp colormap.cpp ${GUI_RESOURCES})
+       target_link_libraries(gencolormap-gui Qt5::Widgets)
+       install(TARGETS gencolormap-gui RUNTIME DESTINATION bin)
+endif()
diff --git a/LICENSE b/LICENSE
index f2c1d8d..0103530 100644 (file)
--- a/LICENSE
+++ b/LICENSE
@@ -1,6 +1,6 @@
 The MIT License (MIT)
 
-Copyright (c) 2015 Martin Lambers
+Copyright (c) 2015 Computer Graphics Group, University of Siegen, Germany.
 
 Permission is hereby granted, free of charge, to any person obtaining a copy
 of this software and associated documentation files (the "Software"), to deal
index 43d1eb8..0ba64d2 100644 (file)
--- a/README.md
+++ b/README.md
@@ -1,2 +1,14 @@
 # gencolormap
-Generate Brewer-like color maps for visualization from intuitive parameters.
+
+This tool generates Brewer-like color maps for visualization using intuitive parameters.
+It implements the paper M. Wijffelaars, R. Vliegen, J.J. van Wijk, E.-J. van
+der Linden. Generating color palettes using intuitive parameters. In Computer
+Graphics Forum, vol. 27, no. 3, pp. 743-750, 2008.
+
+The color map generation itself is contained in just two C++ files
+(`colormap.hpp` and `colormap.cpp`) and requires no additional libraries.
+
+Two frontends are included: a command line tool (for scripting) and a Qt-based
+GUI (for interactive use).
+
+![GUI screen shot](https://raw.githubusercontent.com/marlam/gencolormap/master/screenshot.png)
diff --git a/cg-logo.png b/cg-logo.png
new file mode 100644 (file)
index 0000000..2495b94
Binary files /dev/null and b/cg-logo.png differ
diff --git a/cmdline.cpp b/cmdline.cpp
new file mode 100644 (file)
index 0000000..243e048
--- /dev/null
@@ -0,0 +1,199 @@
+/*
+ * Copyright (C) 2015 Computer Graphics Group, University of Siegen
+ * Written by Martin Lambers <martin.lambers@uni-siegen.de>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+#include <vector>
+#include <cstdio>
+#include <cstdlib>
+#include <cstring>
+#include <cmath>
+
+#include <getopt.h>
+extern char *optarg;
+extern int optind;
+
+#include "colormap.hpp"
+
+
+int main(int argc, char* argv[])
+{
+    bool print_version = false;
+    bool print_help = false;
+    int type = -1;
+    int n = -1;
+    float hue = -1.0f;
+    float divergence = -1.0f;
+    float contrast = -1.0f;
+    float saturation = -1.0f;
+    float brightness = -1.0f;
+    float warmth = -1.0f;
+    struct option options[] = {
+        { "version",    no_argument,       0, 'v' },
+        { "help",       no_argument,       0, 'H' },
+        { "type",       required_argument, 0, 't' },
+        { "n",          required_argument, 0, 'n' },
+        { "hue",        required_argument, 0, 'h' },
+        { "divergence", required_argument, 0, 'd' },
+        { "contrast",   required_argument, 0, 'c' },
+        { "saturation", required_argument, 0, 's' },
+        { "brightness", required_argument, 0, 'b' },
+        { "warmth",     required_argument, 0, 'w' },
+        { 0, 0, 0, 0 }
+    };
+    int retval = 0;
+
+    for (;;) {
+        int c = getopt_long(argc, argv, "vHt:n:h:d:c:s:b:w:", options, NULL);
+        if (c == -1)
+            break;
+        switch (c) {
+        case 'v':
+            print_version = true;
+            break;
+        case 'H':
+            print_help = true;
+            break;
+        case 't':
+            type = (strcmp(optarg, "sequential") == 0 ? 0
+                    : strcmp(optarg, "diverging") == 0 ? 1
+                    : strcmp(optarg, "qualitative") == 0 ? 2
+                    : -2);
+            break;
+        case 'n':
+            n = atoi(optarg);
+            break;
+        case 'h':
+            hue = atof(optarg) * M_PI / 180.0;
+            break;
+        case 'd':
+            divergence = atof(optarg) * M_PI / 180.0;
+            break;
+        case 'c':
+            contrast = atof(optarg);
+            break;
+        case 's':
+            saturation = atof(optarg);
+            break;
+        case 'b':
+            brightness = atof(optarg);
+            break;
+        case 'w':
+            warmth = atof(optarg);
+            break;
+        default:
+            retval = 1;
+            break;
+        }
+    }
+    if (retval != 0)
+        return retval;
+
+    if (print_version) {
+        printf("gencolormap version 0.1\n"
+                "Copyright (C) 2015 Computer Graphics Group, University of Siegen.\n"
+                "Written by Martin Lambers <martin.lambers@uni-siegen.de>.\n"
+                "This is free software under the terms of the MIT/Expat License.\n"
+                "There is NO WARRANTY, to the extent permitted by law.\n");
+        return retval;
+    }
+
+    if (type < 0) {
+        fprintf(stderr, "Invalid or missing option -t|--type.\n");
+        print_help = true;
+        retval = 1;
+    }
+    if (n < 2) {
+        fprintf(stderr, "Invalid or missing option -n|--n.\n");
+        print_help = true;
+        retval = 1;
+    }
+    if (hue < 0.0f) {
+        fprintf(stderr, "Invalid or missing option -h|--hue.\n");
+        print_help = true;
+        retval = 1;
+    }
+    if (divergence < 0.0f) {
+        if (type <= 1)
+            divergence = 2.0f / 3.0f * static_cast<float>(M_PI);
+        else
+            divergence = ColorMap::DefaultQualitativeDivergence;
+    }
+    if (contrast < 0.0f) {
+        if (type <= 1)
+            if (n > 0 && n < 9)
+                contrast = ColorMap::DefaultContrastForSmallN(n);
+            else
+                contrast = ColorMap::DefaultContrast;
+        else
+            contrast = ColorMap::DefaultQualitativeContrast;
+    }
+    if (saturation < 0.0f) {
+        if (type <= 1)
+            saturation = ColorMap::DefaultSaturation;
+        else
+            saturation = ColorMap::DefaultQualitativeSaturation;
+    }
+    if (brightness < 0.0f) {
+        if (type <= 1)
+            brightness = ColorMap::DefaultBrightness;
+        else
+            brightness = ColorMap::DefaultQualitativeBrightness;
+    }
+    if (warmth < 0.0f) {
+        warmth = ColorMap::DefaultWarmth;
+    }
+
+    if (print_help) {
+        printf("Usage: %s\n"
+                "  -t|--type=sequential   Generate a sequential color map\n"
+                "  -t|--type=diverging    Generate a diverging color map\n"
+                "  -t|--type=qualitative  Generate a sequential color map\n"
+                "  -n|--n=N               Set number of colors in the map\n"
+                "  -h|--hue=H             Set hue in [0,360] degrees\n"
+                "  [-d|--divergence=D]    Set divergence for div. and qual. maps\n"
+                "  [-c|--contrast=C]      Set contrast in [0,1]\n"
+                "  [-s|--saturation=S]    Set saturation in [0,1]\n"
+                "  [-b|--brightness=B]    Set brightness in [0,1]\n"
+                "  [-w|--warmth=W]        Set color warmth in [0,1] for seq. and div. maps\n"
+                "Generates a color map and prints it to standard output as sRGB triplets.\n"
+                "Report bugs to <martin.lambers@uni-siegen.de>.\n", argv[0]);
+        return retval;
+    }
+
+    std::vector<unsigned char> colormap(3 * n);
+    if (type == 0) {
+        ColorMap::Sequential(n, &(colormap[0]), hue,
+                contrast, saturation, brightness, warmth);
+    } else if (type == 1) {
+        ColorMap::Diverging(n, &(colormap[0]), hue, divergence,
+                contrast, saturation, brightness, warmth);
+    } else {
+        ColorMap::Qualitative(n, &(colormap[0]), hue, divergence,
+                contrast, saturation, brightness);
+    }
+
+    for (int i = 0; i < n; i++) {
+        printf("%d, %d, %d\n", colormap[3 * i + 0], colormap[3 * i + 1], colormap[3 * i + 2]);
+    }
+
+    return retval;
+}
diff --git a/colormap.cpp b/colormap.cpp
new file mode 100644 (file)
index 0000000..9a45fd4
--- /dev/null
@@ -0,0 +1,492 @@
+/*
+ * Copyright (C) 2015 Computer Graphics Group, University of Siegen
+ * Written by Martin Lambers <martin.lambers@uni-siegen.de>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+#include <algorithm>
+#include <vector>
+#include <limits>
+#include <cmath>
+#include <cstring>
+
+#include "colormap.hpp"
+
+/* Notes about the color spaces used internally:
+ *
+ * - We use D65 white everywhere
+ * - RGB means linear RGB; we also have sRGB
+ * - RGB and sRGB values are in [0,1]
+ * - XYZ, LUV, and similar values are in the original range (not normalized);
+ *   often this is [0,100]
+ * - All angles (for hue) are measured in radians
+ */
+
+namespace ColorMap {
+
+/* Generic helpers */
+
+static const float pi = M_PI;
+static const float twopi = 2.0 * M_PI;
+
+static float clamp(float x, float lo, float hi)
+{
+    return std::min(std::max(x, lo), hi);
+}
+
+/* XYZ and related color spaces helper functions and values */
+
+static float u_prime(float x, float y, float z)
+{
+    return 4.0f * x / (x + 15.0f * y + 3.0f * z);
+}
+
+static float v_prime(float x, float y, float z)
+{
+    return 9.0f * y / (x + 15.0f * y + 3.0f * z);
+}
+
+static const float d65_x =  95.047f;
+static const float d65_y = 100.000f;
+static const float d65_z = 108.883f;
+static const float d65_u_prime = u_prime(d65_x, d65_y, d65_z);
+static const float d65_v_prime = v_prime(d65_x, d65_y, d65_z);
+
+/* Color space conversion: LCH <-> LUV */
+
+static float lch_saturation(float l, float c)
+{
+    return c / std::max(l, 1e-8f);
+}
+
+static float lch_chroma(float l, float s)
+{
+    return s * l;
+}
+
+static void lch_to_luv(float c, float h, float* u, float* v)
+{
+    *u = c * std::cos(h);
+    *v = c * std::sin(h);
+}
+
+static void luv_to_lch(float u, float v, float* c, float* h)
+{
+    *c = std::hypot(u, v);
+    *h = std::atan2(v, u);
+    if (*h < 0.0f)
+        *h += twopi;
+}
+
+static float luv_saturation(float l, float u, float v)
+{
+    return lch_saturation(l, std::hypot(u, v));
+}
+
+/* Color space conversion: LUV <-> XYZ */
+
+static void luv_to_xyz(float l, float u, float v, float* x, float* y, float* z)
+{
+    float u_prime = u / (13.0f * l) + d65_u_prime;
+    float v_prime = v / (13.0f * l) + d65_v_prime;
+    if (l <= 8.0f) {
+        *y = d65_y * l * (3.0f * 3.0f * 3.0f / (29.0f * 29.0f * 29.0f));
+    } else {
+        float tmp = (l + 16.0f) / 116.0f;
+        *y = d65_y * tmp * tmp * tmp;
+    }
+    *x = (*y) * (9.0f * u_prime) / (4.0f * v_prime);
+    *z = (*y) * (12.0f - 3.0f * u_prime - 20.0f * v_prime) / (4.0f * v_prime);
+}
+
+static void xyz_to_luv(float x, float y, float z, float* l, float* u, float* v)
+{
+    float y_ratio = y / d65_y;
+    if (y_ratio <= (6.0f * 6.0f * 6.0f) / (29.0f * 29.0f * 29.0f)) {
+        *l = (29.0f * 29.0f * 29.0f) / (3.0f * 3.0f * 3.0f) * y_ratio;
+    } else {
+        *l = 116.0f * std::cbrt(y_ratio) - 16.0f;
+    }
+    *u = 13.0f * (*l) * (u_prime(x, y, z) - d65_u_prime);
+    *v = 13.0f * (*l) * (v_prime(x, y, z) - d65_v_prime);
+}
+
+/* Color space conversion: RGB <-> XYZ */
+
+static void rgb_to_xyz(float r, float g, float b, float* x, float* y, float *z)
+{
+    *x = (0.4124f * r + 0.3576f * g + 0.1805f * b) * 100.0f;
+    *y = (0.2126f * r + 0.7152f * g + 0.0722f * b) * 100.0f;
+    *z = (0.0193f * r + 0.1192f * g + 0.9505f * b) * 100.0f;
+}
+
+static void xyz_to_rgb(float x, float y, float z, float* r, float* g, float* b)
+{
+    *r = clamp((+3.2406255f * x - 1.5372080f * y - 0.4986286f * z) / 100.0f, 0.0f, 1.0f);
+    *g = clamp((-0.9689307f * x + 1.8757561f * y + 0.0415175f * z) / 100.0f, 0.0f, 1.0f);
+    *b = clamp((+0.0557101f * x - 0.2040211f * y + 1.0569959f * z) / 100.0f, 0.0f, 1.0f);
+}
+
+/* Color space conversion: RGB <-> sRGB */
+
+static float rgb_to_srgb_helper(float x)
+{
+    return (x <= 0.0031308f ? (x * 12.92f) : (1.055f * std::pow(x, 1.0f / 2.4f) - 0.055f));
+}
+
+static void rgb_to_srgb(float r, float g, float b, float* sr, float* sg, float* sb)
+{
+    *sr = rgb_to_srgb_helper(r);
+    *sg = rgb_to_srgb_helper(g);
+    *sb = rgb_to_srgb_helper(b);
+}
+
+static float srgb_to_rgb_helper(float x)
+{
+    return (x <= 0.04045f ? (x / 12.92f) : std::pow((x + 0.055f) / 1.055f, 2.4f));
+}
+
+static void srgb_to_rgb(float sr, float sg, float sb, float* r, float* g, float* b)
+{
+    *r = srgb_to_rgb_helper(sr);
+    *g = srgb_to_rgb_helper(sg);
+    *b = srgb_to_rgb_helper(sb);
+}
+
+/* Helpers for LUV colors */
+
+typedef struct {
+    float l;
+    float u;
+    float v;
+} LUVColor;
+
+LUVColor operator+(LUVColor a, LUVColor b)
+{
+    LUVColor c = { .l = a.l + b.l, .u = a.u + b.u, .v = a.v + b.v };
+    return c;
+}
+
+LUVColor operator*(float a, LUVColor b)
+{
+    LUVColor c = { .l = a * b.l, .u = a * b.u, .v = a * b.v };
+    return c;
+}
+
+static float srgb_to_lch_hue(float sr, float sg, float sb)
+{
+    float r, g, b;
+    srgb_to_rgb(sr, sg, sb, &r, &g, &b);
+    float x, y, z;
+    rgb_to_xyz(r, g, b, &x, &y, &z);
+    float l, u, v;
+    xyz_to_luv(x, y, z, &l, &u, &v);
+    float c, h;
+    luv_to_lch(u, v, &c, &h);
+    return h;
+}
+
+// Compute most saturated color that fits into the sRGB
+// cube for the given LCH hue value. This is the core
+// of the paper.
+static LUVColor most_saturated_in_srgb(float hue)
+{
+    /* Static values, only computed once */
+    static float h[] = {
+        srgb_to_lch_hue(1, 0, 0),
+        srgb_to_lch_hue(1, 1, 0),
+        srgb_to_lch_hue(0, 1, 0),
+        srgb_to_lch_hue(0, 1, 1),
+        srgb_to_lch_hue(0, 0, 1),
+        srgb_to_lch_hue(1, 0, 1)
+    };
+
+    /* RGB values and variable pointers to them */
+    int i, j, k;
+    if (hue < h[0]) {
+        i = 2;
+        j = 1;
+        k = 0;
+    } else if (hue < h[1]) {
+        i = 1;
+        j = 2;
+        k = 0;
+    } else if (hue < h[2]) {
+        i = 0;
+        j = 2;
+        k = 1;
+    } else if (hue < h[3]) {
+        i = 2;
+        j = 0;
+        k = 1;
+    } else if (hue < h[4]) {
+        i = 1;
+        j = 0;
+        k = 2;
+    } else if (hue < h[5]) {
+        i = 0;
+        j = 1;
+        k = 2;
+    } else {
+        i = 2;
+        j = 1;
+        k = 0;
+    }
+
+    /* Compute the missing component */
+    float srgb[3];
+    float M[3][3] = {
+        { 0.4124f, 0.3576f, 0.1805f },
+        { 0.2126f, 0.7152f, 0.0722f },
+        { 0.0193f, 0.1192f, 0.9505f }
+    };
+    float alpha = -std::sin(hue);
+    float beta = std::cos(hue);
+    float T = alpha * d65_u_prime + beta * d65_v_prime;
+    srgb[j] = 0.0f;
+    srgb[k] = 1.0f;
+    float q0 = T * (M[0][k] + 15.0f * M[1][k] + 3.0f * M[2][k]) - (4.0f * alpha * M[0][k] + 9.0f * beta * M[1][k]);
+    float q1 = T * (M[0][i] + 15.0f * M[1][i] + 3.0f * M[2][i]) - (4.0f * alpha * M[0][i] + 9.0f * beta * M[1][i]);
+    srgb[i] = rgb_to_srgb_helper(clamp(- q0 / q1, 0.0f, 1.0f));
+
+    /* Convert back to LUV */
+    float r, g, b;
+    srgb_to_rgb(srgb[0], srgb[1], srgb[2], &r, &g, &b);
+    float x, y, z;
+    rgb_to_xyz(r, g, b, &x, &y, &z);
+    float l, u, v;
+    xyz_to_luv(x, y, z, &l, &u, &v);
+    LUVColor color = { .l = l, .u = u, .v = v };
+    return color;
+}
+
+static float Smax(float l, float h)
+{
+    LUVColor pmid = most_saturated_in_srgb(h);
+    LUVColor pend = { .l = 0.0f, .u = 0.0f, .v = 0.0f };
+    if (l > pmid.l)
+        pend.l = 100.0f;
+    float alpha = (pend.l - l) / (pend.l - pmid.l);
+    float pmids = luv_saturation(pmid.l, pmid.u, pmid.v);
+    float pends = luv_saturation(pend.l, pend.u, pend.v);
+    return alpha * (pmids - pends) + pends;
+}
+
+static LUVColor get_bright_point()
+{
+    static LUVColor pb = { .l = -1.0f, .u = -1.0f, .v = -1.0f };
+    if (pb.l < 0.0f) {
+        float x, y, z;
+        rgb_to_xyz(1, 1, 0, &x, &y, &z);
+        float l, u, v;
+        xyz_to_luv(x, y, z, &l, &u, &v);
+        pb.l = l;
+        pb.u = u;
+        pb.v = v;
+    }
+    return pb;
+}
+
+static float mix_hue(float alpha, float h0, float h1)
+{
+    float M = std::fmod(pi + h1 - h0, twopi) - pi;
+    return std::fmod(h0 + alpha * M, twopi);
+}
+
+static void get_color_points(float hue, float saturation, float warmth,
+        LUVColor pb, float pb_hue, float pb_saturation,
+        LUVColor* p0, LUVColor* p1, LUVColor* p2,
+        LUVColor* q0, LUVColor* q1, LUVColor* q2)
+{
+    p0->l = 0.0f;
+    lch_to_luv(0.0f, hue, &(p0->u), &(p0->v));
+    *p1 = most_saturated_in_srgb(hue);
+    float p2l = (1.0f - warmth) * 100.0f + warmth * pb.l;
+    float p2h = mix_hue(warmth, hue, pb_hue);
+    float p2c = lch_chroma(p2l, std::min(Smax(p2l, p2h), warmth * saturation * pb_saturation));
+    p2->l = p2l;
+    lch_to_luv(p2c, p2h, &(p2->u), &(p2->v));
+    *q0 = (1.0f - saturation) * (*p0) + saturation * (*p1);
+    *q2 = (1.0f - saturation) * (*p2) + saturation * (*p1);
+    *q1 = 0.5f * ((*q0) + (*q2));
+}
+
+static LUVColor B(LUVColor b0, LUVColor b1, LUVColor b2, float t)
+{
+    float a = (1.0f - t) * (1.0f - t);
+    float b = 2.0f * (1.0f - t) * t;
+    float c = t * t;
+    LUVColor color = a * b0 + b * b1 + c * b2;
+    return color;
+}
+
+static float invB(float b0, float b1, float b2, float v)
+{
+    return (b0 - b1 + std::sqrt(std::max(b1 * b1 - b0 * b2 + (b0 - 2.0f * b1 + b2) * v, 0.0f)))
+        / (b0 - 2.0f * b1 + b2);
+}
+
+static LUVColor get_colormap_entry(float t,
+        LUVColor p0, LUVColor p2,
+        LUVColor q0, LUVColor q1, LUVColor q2,
+        float contrast, float brightness)
+{
+    float l = 125 - 125 * std::pow(0.2f, (1.0f - contrast) * brightness + t * contrast);
+    float T = (l <= q1.l ? 0.5f * invB(p0.l, q0.l, q1.l, l) : 0.5f * invB(q1.l, q2.l, p2.l, l) + 0.5f);
+    return (T <= 0.5f ? B(p0, q0, q1, 2.0f * T) : B(q1, q2, p2, 2.0f * (T - 0.5f)));
+}
+
+static void convert_colormap_entry(LUVColor color, unsigned char* srgb)
+{
+    float x, y, z;
+    luv_to_xyz(color.l, color.u, color.v, &x, &y, &z);
+    float r, g, b;
+    xyz_to_rgb(x, y, z, &r, &g, &b);
+    float sr, sg, sb;
+    rgb_to_srgb(r, g, b, &sr, &sg, &sb);
+    srgb[0] = std::round(sr * 255.0f);
+    srgb[1] = std::round(sg * 255.0f);
+    srgb[2] = std::round(sb * 255.0f);
+}
+
+/* The public functions */
+
+float DefaultContrastForSmallN(int n)
+{
+    return std::min(0.88f, 0.34f + 0.06f * n);
+}
+
+void Sequential(int n, unsigned char* colormap, float hue,
+        float contrast, float saturation, float brightness, float warmth)
+{
+    LUVColor pb, p0, p1, p2, q0, q1, q2;
+    pb = get_bright_point();
+    float pbc, pbh, pbs;
+    luv_to_lch(pb.u, pb.v, &pbc, &pbh);
+    pbs = lch_saturation(pb.l, pbc);
+    get_color_points(hue, saturation, warmth, pb, pbh, pbs, &p0, &p1, &p2, &q0, &q1, &q2);
+
+    for (int i = 0; i < n; i++) {
+        float t = (n - 1 - i) / (n - 1.0f);
+        LUVColor c = get_colormap_entry(t, p0, p2, q0, q1, q2, contrast, brightness);
+        convert_colormap_entry(c, colormap + 3 * i);
+    }
+}
+
+void Diverging(int n, unsigned char* colormap, float hue0, float divergence,
+        float contrast, float saturation, float brightness, float warmth)
+{
+    float hue1 = hue0 + divergence;
+    if (hue1 >= twopi)
+        hue1 -= twopi;
+
+    LUVColor pb;
+    LUVColor p00, p01, p02, q00, q01, q02;
+    LUVColor p10, p11, p12, q10, q11, q12;
+    pb = get_bright_point();
+    float pbc, pbh, pbs;
+    luv_to_lch(pb.u, pb.v, &pbc, &pbh);
+    pbs = lch_saturation(pb.l, pbc);
+    get_color_points(hue0, saturation, warmth, pb, pbh, pbs, &p00, &p01, &p02, &q00, &q01, &q02);
+    get_color_points(hue1, saturation, warmth, pb, pbh, pbs, &p10, &p11, &p12, &q10, &q11, &q12);
+
+    for (int i = 0; i < n; i++) {
+        LUVColor c;
+        if (n % 2 == 1 && i == n / 2) {
+            // compute neutral color in the middle of the map
+            LUVColor c0 = get_colormap_entry(1.0f, p00, p02, q00, q01, q02, contrast, brightness);
+            LUVColor c1 = get_colormap_entry(1.0f, p10, p12, q10, q11, q12, contrast, brightness);
+            if (n <= 9) {
+                // for discrete color maps, use an extra neutral color
+                float c0s = luv_saturation(c0.l, c0.u, c0.v);
+                float c1s = luv_saturation(c1.l, c1.u, c1.v);
+                float sn = 0.5f * (c0s + c1s) * warmth;
+                c.l = 0.5f * (c0.l + c1.l);
+                float cc = lch_chroma(c.l, std::min(Smax(c.l, pbh), sn));
+                lch_to_luv(cc, pbh, &(c.u), &(c.v));
+            } else {
+                // for continuous color maps, use an average, since the extra neutral color looks bad
+                c = 0.5f * (c0 + c1);
+            }
+        } else {
+            float t = i / (n - 1.0f);
+            if (i < n / 2) {
+                float tt = 2.0f * t;
+                c = get_colormap_entry(tt, p00, p02, q00, q01, q02, contrast, brightness);
+            } else {
+                float tt = 2.0f * (1.0f - t);
+                c = get_colormap_entry(tt, p10, p12, q10, q11, q12, contrast, brightness);
+            }
+        }
+        convert_colormap_entry(c, colormap + 3 * i);
+    }
+}
+
+static float HueDiff(float h0, float h1)
+{
+    float t = std::fabs(h1 - h0);
+    return (t < pi ? t : twopi - t);
+}
+
+void Qualitative(int n, unsigned char* colormap, float hue0, float divergence,
+        float contrast, float saturation, float brightness)
+{
+    // Get all information about yellow
+    static float yl = -1.0f, yh = -1.0f;
+    if (yl < 0.0f) {
+        float yx, yy, yz;
+        rgb_to_xyz(1, 1, 0, &yx, &yy, &yz);
+        float yu, yv;
+        xyz_to_luv(yx, yy, yz, &yl, &yu, &yv);
+        float yc;
+        luv_to_lch(yu, yv, &yc, &yh);
+    }
+
+    // Get saturation of red (maximum possible saturation)
+    static float rs = -1.0f;
+    if (rs < 0.0f) {
+        float rx, ry, rz;
+        rgb_to_xyz(1, 0, 0, &rx, &ry, &rz);
+        float rl, ru, rv;
+        xyz_to_luv(rx, ry, rz, &rl, &ru, &rv);
+        rs = luv_saturation(rl, ru, rv);
+    }
+
+    // Derive parameters of the method
+    float eps = hue0 / twopi;
+    float r = divergence / twopi;
+    float l0 = brightness * yl;
+    float l1 = (1.0f - contrast) * l0;
+
+    // Generate colors
+    for (int i = 0; i < n; i++) {
+        float t = i / (n - 1.0f);
+        float ch = std::fmod(twopi * (eps + t * r), twopi);
+        float alpha = HueDiff(ch, yh) / pi;
+        float cl = (1.0f - alpha) * l0 + alpha * l1;
+        float cs = std::min(Smax(cl, ch), saturation * rs);
+        LUVColor c;
+        c.l = cl;
+        lch_to_luv(lch_chroma(cl, cs), ch, &(c.u), &(c.v));
+        convert_colormap_entry(c, colormap + 3 * i);
+    }
+}
+
+}
diff --git a/colormap.hpp b/colormap.hpp
new file mode 100644 (file)
index 0000000..688f78f
--- /dev/null
@@ -0,0 +1,92 @@
+/*
+ * Copyright (C) 2015 Computer Graphics Group, University of Siegen
+ * Written by Martin Lambers <martin.lambers@uni-siegen.de>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+#ifndef COLORMAP_HPP
+#define COLORMAP_HPP
+
+/* Generate color maps for scientific visualization purposes.
+ *
+ * This implements the methods described in
+ * "Generating Color Palettes using Intuitive Parameters" by
+ * Martijn Wijffelaars, Roel Vliegen, Jarke J. van Wijk and Erik-Jan van der
+ * Linden, Eurographics/IEEE-VGTC Symposium on Visualization 2008
+ *
+ * Usage:
+ *
+ * Decide which type of color map you need and how many colors the map should
+ * contain:
+ * - For a continuous range of values (e.g. temperature, size, ...):
+ *   Sequential map with a single hue, at least 200 colors
+ * - For a continuous of values around a neutral middle (e.g. deviation from an
+ *   ideal value):
+ *   Diverging map, composed of two sequential maps with a neutral color in the
+ *   middle, at least 200 colors
+ *
+ * Allocate memory for your color map, and call the function that generates your
+ * map. All colors are represented as unsigned char sRGB triplets, with each
+ * value in [0,255].
+ */
+
+namespace ColorMap {
+
+const float DefaultContrast = 0.88f;
+float DefaultContrastForSmallN(int n); // only for discrete color maps, i.e. n <= 9
+const float DefaultSaturation = 0.6f;
+const float DefaultBrightness = 0.75f;
+const float DefaultWarmth = 0.15f;
+
+// Create a sequential color map with n colors of the given hue in [0,2*PI].
+void Sequential(int n, unsigned char* srgb_colormap, float hue,
+        float contrast = DefaultContrast,
+        float saturation = DefaultSaturation,
+        float brightness = DefaultBrightness,
+        float warmth = DefaultWarmth);
+
+// Create a diverging color map with n colors. Half of them will have hue0 (in
+// [0,2*PI]), the other half will have a hue that has the distance given by
+// divergence (in [0,2*PI]) to hue0, and they will meet in the middle at a
+// neutral color.
+void Diverging(int n, unsigned char* srgb_colormap, float hue0, float divergence,
+        float contrast = DefaultContrast,
+        float saturation = DefaultSaturation,
+        float brightness = DefaultBrightness,
+        float warmth = DefaultWarmth);
+
+const float DefaultQualitativeDivergence = 6.2831853071795864769; // 2*PI
+const float DefaultQualitativeContrast = 0.5f;
+const float DefaultQualitativeSaturation = 0.5f;
+const float DefaultQualitativeBrightness = 1.0f;
+
+// Create a qualitative color map with n colors. The colors will have the same
+// saturation; lightness and hue will differ. The parameter hue0 sets the hue of
+// the first color, and the parameter divergence defines the hue range starting
+// from hue0 that can be used for the colors.
+void Qualitative(int n, unsigned char* colormap, float hue0,
+        float divergence = DefaultQualitativeDivergence,
+        float contrast = DefaultQualitativeContrast,
+        float saturation = DefaultQualitativeSaturation,
+        float brightness = DefaultQualitativeBrightness);
+
+}
+
+#endif
diff --git a/gui.cpp b/gui.cpp
new file mode 100644 (file)
index 0000000..77f55ac
--- /dev/null
+++ b/gui.cpp
@@ -0,0 +1,405 @@
+/*
+ * Copyright (C) 2015 Computer Graphics Group, University of Siegen
+ * Written by Martin Lambers <martin.lambers@uni-siegen.de>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+#include <cmath>
+
+#include "gui.hpp"
+
+#include <QApplication>
+#include <QGridLayout>
+#include <QLabel>
+#include <QRadioButton>
+#include <QSlider>
+#include <QDoubleSpinBox>
+#include <QMenu>
+#include <QMenuBar>
+#include <QImage>
+#include <QPixmap>
+#include <QFileDialog>
+#include <QClipboard>
+#include <QTextStream>
+#include <QMessageBox>
+
+#include "colormap.hpp"
+
+
+CombinedSliderSpinBox::CombinedSliderSpinBox(float minval, float maxval, float step) :
+    _update_lock(false),
+    minval(minval), maxval(maxval), step(step)
+{
+    slider = new QSlider(Qt::Horizontal);
+    slider->setMinimum(0);
+    slider->setMaximum((maxval - minval) / step);
+    slider->setSingleStep(step);
+
+    spinbox = new QDoubleSpinBox();
+    spinbox->setRange(minval, maxval);
+    spinbox->setSingleStep(step);
+    spinbox->setDecimals(std::log10(1.0f / step));
+
+    connect(slider, SIGNAL(valueChanged(int)), this, SLOT(slider_changed()));
+    connect(spinbox, SIGNAL(valueChanged(double)), this, SLOT(spinbox_changed()));
+}
+
+float CombinedSliderSpinBox::value() const
+{
+    return spinbox->value();
+}
+
+void CombinedSliderSpinBox::setValue(float v)
+{
+    _update_lock = true;
+    spinbox->setValue(v);
+    slider->setValue((v - minval) / step);
+    _update_lock = false;
+}
+
+void CombinedSliderSpinBox::slider_changed()
+{
+    if (!_update_lock) {
+        _update_lock = true;
+        int i = slider->value();
+        float v = i * step + minval;
+        spinbox->setValue(v);
+        _update_lock = false;
+        emit valueChanged(value());
+    }
+}
+
+void CombinedSliderSpinBox::spinbox_changed()
+{
+    if (!_update_lock) {
+        _update_lock = true;
+        float v = spinbox->value();
+        int i = (v - minval) / step;
+        slider->setValue(i);
+        _update_lock = false;
+        emit valueChanged(value());
+    }
+}
+
+GUI::GUI()
+{
+    setWindowTitle("Generate Color Map");
+    setWindowIcon(QIcon(":cg-logo.png"));
+    QWidget *widget = new QWidget;
+    QGridLayout *layout = new QGridLayout;
+
+    QLabel* type_label = new QLabel("Type:");
+    layout->addWidget(type_label, 0, 0);
+    type_seq_btn = new QRadioButton("Sequential");
+    layout->addWidget(type_seq_btn, 0, 1);
+    type_div_btn = new QRadioButton("Diverging");
+    layout->addWidget(type_div_btn, 0, 2);
+    QRadioButton* type_qual_btn = new QRadioButton("Qualitative");
+    layout->addWidget(type_qual_btn, 0, 3);
+
+    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 CombinedSliderSpinBox(0, 360, 1);
+    layout->addWidget(hue_changer->slider, 2, 1, 1, 2);
+    layout->addWidget(hue_changer->spinbox, 2, 3);
+
+    divergence_label = new QLabel("Divergence:");
+    layout->addWidget(divergence_label, 3, 0);
+    divergence_changer = new CombinedSliderSpinBox(0, 360, 1);
+    layout->addWidget(divergence_changer->slider, 3, 1, 1, 2);
+    layout->addWidget(divergence_changer->spinbox, 3, 3);
+
+    warmth_label = new QLabel("Warmth:");
+    layout->addWidget(warmth_label, 4, 0);
+    warmth_changer = new CombinedSliderSpinBox(0, 1, 0.01f);
+    layout->addWidget(warmth_changer->slider, 4, 1, 1, 2);
+    layout->addWidget(warmth_changer->spinbox, 4, 3);
+
+    QLabel* contrast_label = new QLabel("Contrast:");
+    layout->addWidget(contrast_label, 5, 0);
+    contrast_changer = new CombinedSliderSpinBox(0, 1, 0.01f);
+    layout->addWidget(contrast_changer->slider, 5, 1, 1, 2);
+    layout->addWidget(contrast_changer->spinbox, 5, 3);
+
+    QLabel* saturation_label = new QLabel("Saturation:");
+    layout->addWidget(saturation_label, 6, 0);
+    saturation_changer = new CombinedSliderSpinBox(0, 1, 0.01f);
+    layout->addWidget(saturation_changer->slider, 6, 1, 1, 2);
+    layout->addWidget(saturation_changer->spinbox, 6, 3);
+
+    QLabel* brightness_label = new QLabel("Brightness:");
+    layout->addWidget(brightness_label, 7, 0);
+    brightness_changer = new CombinedSliderSpinBox(0, 1, 0.01f);
+    layout->addWidget(brightness_changer->slider, 7, 1, 1, 2);
+    layout->addWidget(brightness_changer->spinbox, 7, 3);
+
+    connect(type_seq_btn, SIGNAL(toggled(bool)), this, SLOT(update()));
+    connect(n_spinbox, SIGNAL(valueChanged(int)), this, SLOT(update()));
+    connect(type_div_btn, SIGNAL(toggled(bool)), this, SLOT(update()));
+    connect(hue_changer, SIGNAL(valueChanged(float)), this, SLOT(update()));
+    connect(divergence_changer, SIGNAL(valueChanged(float)), this, SLOT(update()));
+    connect(warmth_changer, SIGNAL(valueChanged(float)), this, SLOT(update()));
+    connect(contrast_changer, SIGNAL(valueChanged(float)), this, SLOT(update()));
+    connect(saturation_changer, SIGNAL(valueChanged(float)), this, SLOT(update()));
+    connect(brightness_changer, SIGNAL(valueChanged(float)), this, SLOT(update()));
+
+    colormap_label = new QLabel();
+    colormap_label->setScaledContents(true);
+    layout->addWidget(colormap_label, 0, 4, 8, 1);
+
+    layout->setColumnStretch(4, 1);
+    widget->setLayout(layout);
+    setCentralWidget(widget);
+
+    QMenu* file_menu = menuBar()->addMenu("&File");
+    QAction* file_export_png_act = new QAction("Export as &PNG...", this);
+    connect(file_export_png_act, SIGNAL(triggered()), this, SLOT(file_export_png()));
+    file_menu->addAction(file_export_png_act);
+    QAction* file_export_csv_act = new QAction("Export as &CSV...", this);
+    connect(file_export_csv_act, SIGNAL(triggered()), this, SLOT(file_export_csv()));
+    file_menu->addAction(file_export_csv_act);
+    file_menu->addSeparator();
+    QAction* quit_act = new QAction("&Quit...", this);
+    quit_act->setShortcut(QKeySequence::Quit);
+    connect(quit_act, SIGNAL(triggered()), this, SLOT(close()));
+    file_menu->addAction(quit_act);
+    QMenu* edit_menu = menuBar()->addMenu("&Edit");
+    QAction* edit_reset_act = new QAction("&Reset", this);
+    connect(edit_reset_act, SIGNAL(triggered()), this, SLOT(edit_reset()));
+    edit_menu->addAction(edit_reset_act);
+    QAction* edit_copy_as_img_act = new QAction("Copy as &image", this);
+    connect(edit_copy_as_img_act, SIGNAL(triggered()), this, SLOT(edit_copy_as_img()));
+    edit_menu->addAction(edit_copy_as_img_act);
+    QAction* edit_copy_as_txt_act = new QAction("Copy as &text", this);
+    connect(edit_copy_as_txt_act, SIGNAL(triggered()), this, SLOT(edit_copy_as_txt()));
+    edit_copy_as_txt_act->setShortcut(QKeySequence::Copy);
+    edit_menu->addAction(edit_copy_as_txt_act);
+    QMenu* help_menu = menuBar()->addMenu("&Help");
+    QAction* help_about_act = new QAction("&About", this);
+    connect(help_about_act, SIGNAL(triggered()), this, SLOT(help_about()));
+    help_menu->addAction(help_about_act);
+
+    show();
+    edit_reset();
+}
+
+GUI::~GUI()
+{
+}
+
+void GUI::get_params(int& type, int& n, float& hue, float& divergence,
+        float& contrast, float& saturation, float& brightness,
+        float& warmth)
+{
+    type = type_seq_btn->isChecked() ? 0 : type_div_btn->isChecked() ? 1 : 2;
+    n = n_spinbox->value();
+    hue = hue_changer->value() / 180.0f * static_cast<float>(M_PI);
+    divergence = divergence_changer->value() / 180.0f * static_cast<float>(M_PI);
+    contrast = contrast_changer->value();
+    saturation = saturation_changer->value();
+    brightness = brightness_changer->value();
+    warmth = warmth_changer->value();
+}
+
+std::vector<unsigned char> GUI::get_map(int type, int n, float hue, float divergence,
+        float contrast, float saturation, float brightness,
+        float warmth)
+{
+    std::vector<unsigned char> colormap(3 * n);
+    if (type == 0) {
+        ColorMap::Sequential(n, &(colormap[0]), hue,
+                contrast, saturation, brightness, warmth);
+    } else if (type == 1) {
+        ColorMap::Diverging(n, &(colormap[0]), hue, divergence,
+                contrast, saturation, brightness, warmth);
+    } else {
+        ColorMap::Qualitative(n, &(colormap[0]), hue, divergence,
+                contrast, saturation, brightness);
+    }
+    return colormap;
+}
+
+void GUI::update()
+{
+    if (update_lock)
+        return;
+
+    int type, n;
+    float hue, divergence, contrast, saturation, brightness, warmth;
+    get_params(type, n, hue, divergence, contrast, saturation, brightness, warmth);
+
+    divergence_label->setEnabled(type >= 1);
+    divergence_changer->slider->setEnabled(type >= 1);
+    divergence_changer->spinbox->setEnabled(type >= 1);
+    warmth_label->setEnabled(type <= 1);
+    warmth_changer->slider->setEnabled(type <= 1);
+    warmth_changer->spinbox->setEnabled(type <= 1);
+
+    const int img_width = 32;
+    const int img_height = colormap_label->height();
+    if (img_height < n)
+        n = img_height;
+    QImage img(img_width, img_height, QImage::Format_RGB32);
+
+    std::vector<unsigned char> colormap(3 * n);
+    if (type == 0) {
+        ColorMap::Sequential(n, &(colormap[0]), hue,
+                contrast, saturation, brightness, warmth);
+    } else if (type == 1) {
+        ColorMap::Diverging(n, &(colormap[0]), hue, divergence,
+                contrast, saturation, brightness, warmth);
+    } else {
+        ColorMap::Qualitative(n, &(colormap[0]), hue, divergence,
+                contrast, saturation, brightness);
+    }
+    for (int y = 0; y < img_height; y++) {
+        float entry_height = img_height / static_cast<float>(n);
+        int i = y / entry_height;
+        QRgb rgb = QColor(colormap[3 * i + 0], colormap[3 * i + 1], colormap[3 * i + 2]).rgb();
+        QRgb* scanline = reinterpret_cast<QRgb*>(img.scanLine(y));
+        for (int x = 0; x < img_width; x++)
+            scanline[x] = rgb;
+    }
+    colormap_label->setPixmap(QPixmap::fromImage(img));
+}
+
+void GUI::file_export_png()
+{
+    QString name = QFileDialog::getSaveFileName();
+    if (!name.isEmpty()) {
+        QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));
+        int type, n;
+        float hue, divergence, contrast, saturation, brightness, warmth;
+        get_params(type, n, hue, divergence, contrast, saturation, brightness, warmth);
+        std::vector<unsigned char> colormap = get_map(type, n, hue, divergence,
+                contrast, saturation, brightness, warmth);
+        QImage img(n, 1, QImage::Format_RGB32);
+        QRgb* scanline = reinterpret_cast<QRgb*>(img.scanLine(0));
+        for (int i = 0; i < n; i++) {
+            scanline[i] = QColor(colormap[3 * i + 0], colormap[3 * i + 1], colormap[3 * i + 2]).rgb();
+        }
+        img.save(name, "png");
+        QApplication::restoreOverrideCursor();
+    }
+}
+
+void GUI::file_export_csv()
+{
+    QString name = QFileDialog::getSaveFileName();
+    if (!name.isEmpty()) {
+        QApplication::setOverrideCursor(QCursor(Qt::WaitCursor));
+        QFile file(name);
+        if (file.open(QIODevice::ReadWrite)) {
+            int type, n;
+            float hue, divergence, contrast, saturation, brightness, warmth;
+            get_params(type, n, hue, divergence, contrast, saturation, brightness, warmth);
+            std::vector<unsigned char> colormap = get_map(type, n, hue, divergence,
+                    contrast, saturation, brightness, warmth);
+            QTextStream stream(&file);
+            for (int i = 0; i < n; i++) {
+                stream << colormap[3 * i + 0] << ", "
+                       << colormap[3 * i + 1] << ", "
+                       << colormap[3 * i + 2] << endl;
+            }
+        }
+        QApplication::restoreOverrideCursor();
+    }
+}
+
+void GUI::edit_reset()
+{
+    update_lock = true;
+    type_seq_btn->setChecked(true);
+    n_spinbox->setValue(301);
+    hue_changer->setValue(0);
+    divergence_changer->setValue(240);
+    warmth_changer->setValue(ColorMap::DefaultWarmth);
+    contrast_changer->setValue(ColorMap::DefaultContrast);
+    saturation_changer->setValue(ColorMap::DefaultSaturation);
+    brightness_changer->setValue(ColorMap::DefaultBrightness);
+    update_lock = false;
+    update();
+}
+
+void GUI::edit_copy_as_img()
+{
+    int type, n;
+    float hue, divergence, contrast, saturation, brightness, warmth;
+    get_params(type, n, hue, divergence, contrast, saturation, brightness, warmth);
+    std::vector<unsigned char> colormap = get_map(type, n, hue, divergence,
+            contrast, saturation, brightness, warmth);
+    QImage img(n, 1, QImage::Format_RGB32);
+    QRgb* scanline = reinterpret_cast<QRgb*>(img.scanLine(0));
+    for (int i = 0; i < n; i++) {
+        scanline[i] = QColor(colormap[3 * i + 0], colormap[3 * i + 1], colormap[3 * i + 2]).rgb();
+    }
+    QApplication::clipboard()->setImage(img);
+}
+
+void GUI::edit_copy_as_txt()
+{
+    int type, n;
+    float hue, divergence, contrast, saturation, brightness, warmth;
+    get_params(type, n, hue, divergence, contrast, saturation, brightness, warmth);
+    std::vector<unsigned char> colormap = get_map(type, n, hue, divergence,
+            contrast, saturation, brightness, warmth);
+    QString string;
+    QTextStream stream(&string);
+    for (int i = 0; i < n; i++) {
+        stream << colormap[3 * i + 0] << ", "
+               << colormap[3 * i + 1] << ", "
+               << colormap[3 * i + 2] << endl;
+    }
+    QApplication::clipboard()->setText(string);
+}
+
+void GUI::help_about()
+{
+    QMessageBox::about(this, "About",
+                "<p>gencolormap version 0.1</p>"
+                "<p>Copyright (C) 2015<br>"
+                "   <a href=\"http://www.cg.informatik.uni-siegen.de/\">"
+                "   Computer Graphics Group, University of Siegen</a>.<br>"
+                "   Written by <a href=\"http://www.cg.informatik.uni-siegen.de/lambers-martin\">Martin Lambers</a>.<br>"
+                "   This is free software under the terms of the "
+                    "<a href=\"https://www.debian.org/legal/licenses/mit\">MIT/Expat License</a>. "
+                "   There is NO WARRANTY, to the extent permitted by law."
+                "</p>"
+                "<p>This program implements the color map generation techniques described in<br>"
+                "   M. Wijffelaars, R. Vliegen, J.J. van Wijk, E.-J. van der Linden."
+                "   Generating color palettes using intuitive parameters. "
+                "   In Computer Graphics Forum, vol. 27, no. 3, pp. 743-750, 2008."
+                "</p>");
+}
+
+int main(int argc, char* argv[])
+{
+    QApplication app(argc, argv);
+    GUI gui;
+    gui.show();
+    return app.exec();
+}
diff --git a/gui.hpp b/gui.hpp
new file mode 100644 (file)
index 0000000..8f136d9
--- /dev/null
+++ b/gui.hpp
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2015 Computer Graphics Group, University of Siegen
+ * Written by Martin Lambers <martin.lambers@uni-siegen.de>
+ *
+ * Permission is hereby granted, free of charge, to any person obtaining a copy
+ * of this software and associated documentation files (the "Software"), to deal
+ * in the Software without restriction, including without limitation the rights
+ * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
+ * copies of the Software, and to permit persons to whom the Software is
+ * furnished to do so, subject to the following conditions:
+ *
+ * The above copyright notice and this permission notice shall be included in
+ * all copies or substantial portions of the Software.
+ *
+ * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+ * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
+ * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.  IN NO EVENT SHALL THE
+ * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
+ * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
+ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
+ * SOFTWARE.
+ */
+
+#ifndef GUI_HPP
+#define GUI_HPP
+
+#include <vector>
+
+#include <QMainWindow>
+
+class QLabel;
+class QRadioButton;
+class QSpinBox;
+class QSlider;
+class QDoubleSpinBox;
+
+
+class CombinedSliderSpinBox : public QObject
+{
+Q_OBJECT
+private:
+    bool _update_lock;
+public:
+    float minval, maxval, step;
+    QSlider* slider;
+    QDoubleSpinBox* spinbox;
+
+    CombinedSliderSpinBox(float minval, float maxval, float step);
+    float value() const;
+    void setValue(float v);
+
+private slots:
+    void slider_changed();
+    void spinbox_changed();
+
+signals:
+    void valueChanged(float);
+};
+
+class GUI : public QMainWindow
+{
+Q_OBJECT
+
+public:
+    GUI();
+    ~GUI();
+
+private:
+    bool update_lock;
+    QRadioButton* type_seq_btn;
+    QRadioButton* type_div_btn;
+    QSpinBox* n_spinbox;
+    CombinedSliderSpinBox* hue_changer;
+    QLabel* divergence_label;
+    CombinedSliderSpinBox* divergence_changer;
+    QLabel* warmth_label;
+    CombinedSliderSpinBox* warmth_changer;
+    CombinedSliderSpinBox* contrast_changer;
+    CombinedSliderSpinBox* saturation_changer;
+    CombinedSliderSpinBox* brightness_changer;
+    QLabel* colormap_label;
+
+    void get_params(int& type, int& n, float& hue, float& divergence,
+            float& contrast, float& saturation, float& brightness,
+            float& warmth);
+    std::vector<unsigned char> get_map(int type, int n, float hue, float divergence,
+            float contrast, float saturation, float brightness,
+            float warmth);
+
+
+private slots:
+    void update();
+
+    void file_export_png();
+    void file_export_csv();
+    void edit_reset();
+    void edit_copy_as_img();
+    void edit_copy_as_txt();
+    void help_about();
+};
+
+#endif
diff --git a/gui.qrc b/gui.qrc
new file mode 100644 (file)
index 0000000..8cf5f08
--- /dev/null
+++ b/gui.qrc
@@ -0,0 +1,5 @@
+<!DOCTYPE RCC><RCC version="1.0">
+<qresource>
+  <file>cg-logo.png</file>
+</qresource>
+</RCC>
diff --git a/screenshot.png b/screenshot.png
new file mode 100644 (file)
index 0000000..a891354
Binary files /dev/null and b/screenshot.png differ