-
Notifications
You must be signed in to change notification settings - Fork 0
/
color.md
327 lines (230 loc) · 14.4 KB
/
color.md
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
[[<< Back to Index]](../index.md)
---
# Color
Colors in RGB, HSV, HSL and GrayScale Representation, Converting between Representations, Pseudocolor Transform
```cpp
#include <color.hpp>
#include <pseudocolor_mapping.hpp>
```
## Table of Contents
1. [**Introduction**](#1-introduction)<br>
2. [**RGB**](#2-rgb)<br>
3. [**HSV**](#3-hsv)<br>
4. [**HSL**](#4-hsl)<br>
5. [**GrayScale**](#5-grayscale)<br>
6. [**In Summary**](#6-in-summary)<br>
7. [**Pseudo Color**](#7-pseudo-color)<br>
7.1 [crisp::PseudoColor](#71-crisppseudocolor)<br>
7.2 [Multi Range Mapping](#72-multi-range-mapping)<br>
## 1. Introduction
While it's nice to simplify things by using binary or grayscale images, (most) humans see in color and thus colors are a central part of many of ``crisp``s image-related features.
A color in crisp is a implementation of ``crisp::ColorRepresentation``, which is a templated pure virtual class with the following functions:
```cpp
template<size_t N>
struct ColorRepresentation : public Vector<float, N>
{
virtual RGB to_rgb() const = 0;
virtual HSV to_hsv() const = 0;
virtual HSL to_hsl() const = 0;
virtual GrayScale to_grayscale const = 0;
}
```
We note that any color in `crisp` is a vector of 32-bit floats. All values of the components of any color representation are assumed to be in `[0, 1]`. We see that each color must furthermore provide four conversion operators to ``crisp::RGB``, ``crisp::HSV``, ``crisp::HSL`` and ``crisp::GrayScale`` respectively. These are the four representations native to `crisp` and we'll look at them in detail now.
## 2. RGB
Colors in RGB representation have 3 components: red, green and blue. Each component represents a fraction of red, green and blue light respectively, where if all components are 0 the color becomes black, if all components are 1 it becomes white. ``crisp::ColorImage``s pixels are in RGB as it is the most used image format in storing digital images.
![](./.resources/rgb_cube_wiki.png)<br>
(source: wikipedia)
Other than functions inherited from ``crisp::ColorRepresentation<3>``, ``RGB`` offers the following functions:
```cpp
// ctors
RGB(float red, float green, float blue);
RGB(float all);
RGB(Vector<float, 3>);
// expose components
float& red();
float red() const;
float& green();
float green() const;
float& blue();
float blue() const;
```
To access and modify any of the components we can either use ``red()``, ``green()``, ``blue()`` or (as provided by inheriting from ``crisp::Vector``) ``x()``, ``y()`` ``z()``, ``at(size_t)`` and `operator[](size_t)`.
## 3. HSV
HSV stands for "hue, saturation, value" and is an attempt to represent colors in a way humans understand them. When we describe the color of a car to someone, we don't name the fractions of red, green and blue paint. Instead we call it "light red" or "a dark green-ish blue". The hue component is what many would call color *tone*, which can be quantified as a frequency value along the spectrum of visible light. We can visualize what this means:
```cpp
auto color = HSV{0, 1, 1};
auto spectrum = ColorImage(/*...*/);
float step = 1.f / (spectrum.get_size().x() * spectrum.get_size().y());
for (size_t x = 0; x < spectrum.get_size().x(); ++x)
for (size_t y = 0; y < specturm.get_size().y(); ++y)
{
spectrum.at(x, y) = color.to_rgb();
color.hue() += step;
}
```
(Recall that ``crisp::ColorImage`` assumes values are in RGB so we need to convert HSV to RGB before assigning it to the image data)
![](./.resources/hue_spectrum.png)
We note the typical rainbow.
The second component of HSV is *saturation*, it can be conceptualized as the amount of a pigment in a in mixture of paint that uses white as it's base. So, lets say we are mixing red paint, a saturation of 1 would mean there is only red paint, no white paint, a saturation of 0 would mean there is only white paint, no red paint, and a saturation of 0.5 would mean equal parts of red and white paint, resulting in what many would call "light red". We again visualize it like so:
```cpp
auto color = HSV{0, 0, 1};
auto spectrum = ColorImage(/*...*/);
float step = 1.f / (spectrum.get_size().x() * spectrum.get_size().y());
for (size_t x = 0; x < spectrum.get_size().x(); ++x)
for (size_t y = 0; y < specturm.get_size().y(); ++y)
{
spectrum.at(x, y) = color.to_rgb();
color.saturation() += step;
}
```
![](./.resources/saturation_spectrum.png)
Lastly we have *value*, sometimes also called *brightness* in the literature. This component can be conceptualized similarly to saturation, but instead of white, we are now mixing black paint with a colored pigment. Let's again assume we're mixing red, a value of 0 means 100% black paint, no red paint. A value of 1 means all red, no black and a value of 0.5 is equal parts red and black. We again visualize it in the same way as above:
![](./.resources/value_spectrum.png)
Hopefully this made clearer what each component of HSV represents.
``crisp::HSV`` supplies the follow methods (again in addition to inherited functions from ``crisp::ColorRepresentation`` and ``crisp::Vector``):
```cpp
// ctors
HSV(float hue, float saturation, float value);
HSV(float all);
HSV(Vector<float, 3>);
// assignment from vector
HSV& operator=(Vector<float, 3>);
// expose components
float& hue();
float hue() const;
float& saturation();
float saturation() const;
float& value();
float value() const;
```
## 4. HSL
HSL stands for hue, saturation, *lightness* (not to be confused with HSVs *brightness* which is partly why `crisp` and many others call it "*value*" instead). Comparing HSL to HSV, both the hue and saturation components are identical, traveling along their spectra shows that there is no difference in terms of the resulting color:
```cpp
auto color = HSL{0, 1, 1}
auto spectrum = ColorImage(/*...*/);
float step = 1.f / (spectrum.get_size().x() * spectrum.get_size().y());
for (size_t x = 0; x < spectrum.get_size().x(); ++x)
for (size_t y = 0; y < specturm.get_size().y(); ++y)
{
spectrum.at(x, y) = color.to_rgb();
color.hue() += step;
}
```
![](./.resources/hue_spectrum.png)
```cpp
auto color = HSL{0, 0, 1}
auto spectrum = ColorImage(/*...*/);
float step = 1.f / (spectrum.get_size().x() * spectrum.get_size().y());
for (size_t x = 0; x < spectrum.get_size().x(); ++x)
for (size_t y = 0; y < specturm.get_size().y(); ++y)
{
spectrum.at(x, y) = color.to_rgb();
color.saturation() += step;
}
```
![](./.resources/saturation_spectrum.png)
Lightness, however, behaves differently: a lightness value of 0 corresponds to black, a lightness value of 1 corresponds to white and a lightness value of 0.5 corresponds to what would've been in our HSV example no black paint, no white pain, only red paint:
```cpp
auto color = HSL{0, 1, 0}
auto spectrum = ColorImage(/*...*/);
for (size_t x = 0; x < spectrum.get_size().x(); ++x)
for (size_t y = 0; y < specturm.get_size().y(); ++y)
{
spectrum.at(x, y) = color.to_rgb();
color.lightness() += step;
}
```
![](./.resources/lightness_spectrum.png)
# 5. GrayScale
GrayScale is a color representation with only a single value called *intensity*. ``crisp::GrayScaleImage`` uses this representation. While it can be thought of as just a `float`, it can be helpful to keep in mind that all functions from both ``crisp::Vector<float, 1>`` and ``ColorRepresentation<1>`` are of course available.
Other than these inherited functions, ``GrayScale`` provides the following methods:
```cpp
// ctors
GrayScale() = default;
GrayScale(float);
GrayScale(Vector<float, 1>);
// assignment and cast from/to float
GrayScale& operator=(float);
GrayScale& operator=(Vector<float, 1>);
operator float() const;
// expose component
float& intensity();
float intensity() const;
```
We note that grayscale offers both construction from and assignment to float, making it easy to operate on this color representation as if it were a raw number.
We can create a visualization of the effect of varying intensity like so:
```cpp
#include <image.hpp>
#include <color.hpp>
using namespace crisp;
// in main.cpp
// create an image and fill it with grayscale values [0, 1]
GrayScaleImage grayscale_spectrum;
grayscale_spectrum.create(300, 50);
GrayScale intensity = 0.f;
float step = 1.f / (grayscale_spectrum.get_size().x() * grayscale_spectrum.get_size().y());
for (size_t x = 0; x < grayscale_spectrum.get_size().x(); ++x)
for (size_t y = 0; y < grayscale_spectrum.get_size().y(); ++y)
{
grayscale_spectrum.at(x, y) = intensity;
intensity += step;
}
```
![](./.resources/grayscale_spectrum.png)
GrayScale is attractive for it's computational simplicity and the fact it's easy to convert to and from other color representations. For RGB, we would simply average all color components, while for HSV and HSL ,the GrayScale intensity is equivalent to the `value` and `lightness` component respectively.
## 6. In Summary
Differentiating between some color representations can be difficult, especially HSV and HSL have enough similarities to get hung up on, but hopefully this has cleared up what exactly each component of each representation means. It's important to keep in mind that in ``crisp``, converting from any color representation to any other color representation is easy and quick, we simply call ``.to_xyz()`` and we're done.
## 7. Pseudo Color
"Pseudo Color" is a term describing a transform function that maps grayscale values onto a different color representation, often to aid humans in visually parsing images easier. Let's consider an example first:
![](./.resources/infrared_deer.png)<br>
(source: texasoutdoors)
Here we have a noisy, low-resolution infrared image of deer. Infrared cameras only record light in a spectrum band not visible to humans, thus it is customary to translate the equipment's response into grayscale instead. Human brains don't do very well at parsing different shades of gray, so to aid in visual confirmation, we can do the following:
![](./.resources/final_deer.png)
No segmentation algorithm was performed here, all we did was map higher (lighter) intensity values to a range of color values. Because the deer are much warmer than their surrounding, the infrared response to their bodies will be higher. With the new color image, it's much easier to differentiate the deer from surrounding foliage.
Pseudocolor has many applications in medicine and any field where 1-plane images are meant for human inspections. This is why ``crisp`` offer a comprehensive, flexible interface for transforming images in this way.
## 7.1 ``crisp::PseudoColor``
In ``crisp``, pseudocolor transformations are handled by functions of [``crisp::PseudoColor``](../../include/pseudocolor_mapping.hpp). This is a class with only static members, so it behaves exactly like a namespace. For each function (or *linear map*, in more exact terms) we want to specify a range of floating point intensity values ``[g_min, g_max]`` to be mapped onto a range of hue values ``[h_min, h_max]`` in a way that is unambigous. The following functions for this are provided:
+ ``identity()`` maps all gray values onto themselves, resulting in a color image that is visually identical to the grayscale image<br>
![](./.resources/infrared_deer.png)<br><br>
+ ``value_to_hue(float g, flat h)`` maps a single gray value onto a single hue values, that is ``{g} -> {h}``<br>
![](./.resources/gray_value_to_hue_value.png)<br><br>
+ ``value_range_to_hue(float g_min, float g_max, float h)`` maps a range of gray values onto a single hue, that is ``[g_min, g_max] -> {h}``<br>
![](./.resources/gray_range_to_hue_value.png)<br><br>
+ ``value_range_to_hue_range(float g_min, float g_max, float h_min, float h_max)`` maps a range of gray values onto a range of hue values, that is ``[g_min, g_max] -> [h_min, h_max]``<br>
![](./.resources/gray_range_to_hue_range.png)<br><br>
+ ``value_range_to_inverse_hue_range(float g_min, float g_max, float h_min, float h_max)`` maps a range of gray values onto an *inverted* range of hue values, that is ``[g_min, g_max] -> [h_max, h_min]``<br>
![](./.resources/gray_range_to_inverse_hue_range.png)<br><br>
To illustrate the usage in actual code, we try to recreate the image shown at the start of this chapter (after noting that all objects with an intensity > 0.6 are likely to be warmblooded):
```cpp
using namespace crisp;
auto deer = load_grayscale_image(/*...*/ + "/crips/docs/color/infrared_deer.png");
ColorImage as_color = PseudoColor::value_range_to_hue_range(0.6, 1, 0, 1, deer);
// save to disk or render here
```
![](./.resources/final_deer.png)
## 7.2 Multi Range Mapping
`crisp` offers an even more flexible way of mapping intensities to colors: Let's say we want to map the deer onto an easily recognizable solid color, while mapping all other foliage and such onto a darker hue range. Humans tend to do a lot better at differentiating color from color than color from gray. With the functions mentioned so far, this is not possible. To ameliorate this, ``crisp`` offers
``PseudoColor::RangeMapping``. This object stores multiple ranges, which are initialized similarly to the functions in ``PseudoColor`` itself:
```cpp
// members of PseudoColor::Mapping
void add_value_to_hue(float g, float h);
void add_value_range_to_hue(float g_min, float g_max, float h);
void add_value_range_to_hue_range(float g_min, float g_max, float h_min, float h_max);
void add_value_range_to_inverse_hue_range(float g_min, float g_max, float h_min, float h_max);
```
We can then use the ``RangeMapping`` with ``PseudoColor::value_ranges_to_hue_ranges(RangeMapping&)``.
If the user specifies ranges that overlap, only one of them will be applied. Which, is undefined.
To illustrate the functionality of ``RangeMapping``, we implement our goal from earlier: We want to map the deer (intensity > 0.6) onto a solid color, let's say red, and map the foliage (intensity < 0.6) onto a range of darker (in terms of perceived luminosity) colors, let's say blue to purple:
```cpp
auto deer = load_grayscale_image(/*...*/ + "/crips/docs/color/infrared_deer.png");
auto ranges = PseudoColor::RangeMapping();
ranges.add_value_range_to_hue(0.601, 1, 0);
ranges.add_value_range_to_hue_range(0, 0.6, 0.2, 0.75);
color_deer = PseudoColor::value_ranges_to_hue_ranges(ranges, deer);
// save to disk or render
```
![](./.resources/gray_ranges_to_hue_ranges.png)
As we can see, the deer are even easier to spot now, while the tree and grass can be made out much better than in just grayscale.
We note some artifacting in the top left corner. This happens because of a low dynamic range, we can pre-process the image by normalizing the intensity values first, however this step was omitted here.
---
[[<< Back to Index]](../index.md)