User Tools

Differences

This shows you the differences between two versions of the page.

Link to this comparison view

Both sides previous revisionPrevious revision
Next revision
Previous revision
isp:vibrancy [2018/09/25 19:46] – [Possible optimization] Igor Yefmovisp:vibrancy [2023/09/05 06:22] (current) – [RGB color space] Igor Yefmov
Line 3: Line 3:
 A vibrancy of a color is a perceived quality that is somewhat similar to Saturation. One could think of it as pure, bright, high chroma color. It is not an absolute measure (like Saturation) and there's no agreed upon definition of what it means to change the vibrancy of a color. A vibrancy of a color is a perceived quality that is somewhat similar to Saturation. One could think of it as pure, bright, high chroma color. It is not an absolute measure (like Saturation) and there's no agreed upon definition of what it means to change the vibrancy of a color.
  
-Having said that we still want to make a distinction between "changing color's Saturation" and "changing color's Vibrancy" when talking about color corrections of a given image. Both increase the color's [[https://en.wikipedia.org/wiki/Colorfulness#Saturation|Saturation]] but one behaves like a linear Gain increase, applied uniformly to any color. And the one uses a sliding scale that adjusts the magnitude of change based on the current Saturation level, where the highest multiplication is done for low Saturation colors and highly vibrant (highly saturated, vivid) colors are adjusted on a progressively smaller scale.+Having said that we still want to make a distinction between "changing color's Saturation" and "changing color's Vibrancy" when talking about color corrections of a given image. Both increase the color's [[https://en.wikipedia.org/wiki/Colorfulness#Saturation|Saturation]] but one behaves like a linear Gain increase, applied uniformly to any color. And the other one uses a sliding scale that adjusts the magnitude of change based on the current Saturation level, where the highest multiplication is done for low Saturation colors and highly vibrant (highly saturated, vivid) colors are adjusted on a progressively smaller scale.
  
-===== Implementation overview =====+===== RGB color space ===== 
 +To adjust the saturation of a pixel in RGB space we will follow this general approach: 
 +  - create a desaturated version of the pixel by converting it to grayscale 
 +  - interpolate/extrapolate between the original and desaturated pixel
  
-The goal is to increase the Saturation of each pixel based on its current Saturation level, where the increase factor is inversely proportional to the current value. Given the range of values for ''U'' and ''V'' to be [''-127''..''+128''] the highest increase factor will be around values of ''0'' and as the value gradually nears either end of the spectrum the multiplication coefficient approaches ''1.0''.+FIXME 
 +===== YUV color space ===== 
 + 
 +The goal is to increase the Saturation of each pixel based on its current Saturation level, where the increase factor is inversely proportional to the current value. Given the range of values for ''U'' and ''V'' \(\in[-127..+128]\), the highest increase factor will be around values of ''0'' and as the value gradually nears either end of the spectrum the multiplication coefficient approaches \(1.0\).
  
 There are a few ways to implement this, listed below. There are a few ways to implement this, listed below.
Line 13: Line 19:
 ==== Linear estimation ==== ==== Linear estimation ====
  
-Let's remember that the YUV color space is using the pair of ''U'' and ''V'' values to represent all the colors on a 2D cartesian plane, where the center ''(0, 0)'' represents a pure grey color (or white, or black - depending on the luminosity ''Y''). Both ''U'' and ''V'' are bound to [''-127''..''+128''] range and we can just use the one "closest" to the outer edge as our baseline for calculating the multiplication coefficient:+Let's remember that the YUV color space is using the pair of ''U'' and ''V'' values to represent all the colors on a 2D cartesian plane, where the center \((0, 0)\) represents a pure grey color (or white, or black - depending on the luminosity ''Y''). Both ''U'' and ''V'' are bound to \([-127..+128]\) range and we can just use the one "closest" to the outer edge as our baseline for calculating the multiplication coefficient:
 <code c++>double scale(int _u, int _v, double _vib){ // both _u and _v are in range [0..255] <code c++>double scale(int _u, int _v, double _vib){ // both _u and _v are in range [0..255]
   // center-normalize the U and V   // center-normalize the U and V
Line 19: Line 25:
   const unsigned int cv = abs(128 - _v);   const unsigned int cv = abs(128 - _v);
   // use the one that is "further from the center"   // use the one that is "further from the center"
-  return 1. + (cu > cv ? cu : cv) / 128. * _vib;+  return 1. + (128 - max(cucv)) / 128. * _vib;
 }</code> }</code>
  
Line 28: Line 34:
     const auto k = scale(pixel.u, pixel.v, _vib);     const auto k = scale(pixel.u, pixel.v, _vib);
     // of course both new values have to be properly capped (omitted here for readability)     // of course both new values have to be properly capped (omitted here for readability)
-    pixel.u = (128 - pixel.u) * k + 128; +    pixel.u = (pixel.u - 128) * k + 128; 
-    pixel.v = (128 - pixel.v) * k + 128;+    pixel.v = (pixel.v - 128) * k + 128;
   }   }
 }</code> }</code>
Line 35: Line 41:
 ==== Vector's magnitude ==== ==== Vector's magnitude ====
  
-Another way to see "how saturated is the color" would be to calculate the magnitude of the color vector, represented by its ''U'' and ''V'' components with the origin at ''(0, 0)'' using a well-known Pythagorean formula.+Another way to see "how saturated is the color" would be to calculate the magnitude of the color vector, represented by its ''U'' and ''V'' components with the origin at \((0, 0)\) using a well-known Pythagorean formula.
 <code c++>double scale(int _u, int _v, double _vib){ <code c++>double scale(int _u, int _v, double _vib){
   // center-normalize the U and V   // center-normalize the U and V
Line 58: Line 64:
 ===== Possible optimization ===== ===== Possible optimization =====
  
-(This is valid for CPU-type architecture and is not very applicable to FPGAs where general memory is a big limiting factor)+(This is valid for CPU-type architecture and is not very applicable to FPGAs where general purpose RAM is a big limiting factor)
  
 There are over 8 million pixels in a 4K image and only 65K (256*256) possible transformations to either ''U'' or ''V'' component of the YUV triplet so it makes sense to pre-calculate those transformations and use the result as a LUT (provided it is faster to index the memory than to re-calculate the value, of course). There are over 8 million pixels in a 4K image and only 65K (256*256) possible transformations to either ''U'' or ''V'' component of the YUV triplet so it makes sense to pre-calculate those transformations and use the result as a LUT (provided it is faster to index the memory than to re-calculate the value, of course).
Line 79: Line 85:
   }   }
 </code> </code>
 +
 +===== HSL color space =====
 +
 +Moving to HSL for image processing has a wonderful advantage of "separation of concerns" where the 3 visual components are treated independently of each other. Correcting the Saturation is just one of such areas where moving into HSL color space provides a notable simplification in the approach. As the \(S\) component of the HSL is "just" a linear scalar value we don't need to concern ourselves with the ''UV'' square or an ''RGB'' cube and just directly approach the subject of calculating only the \(S\).
 +
 +As was described before the main idea of the Vibrancy is that the lowest saturated colors get the most relative boost in Saturation while highly saturated colors get progressively lower boost, up to a "no boost" for the 100% saturated colors.
 +
 +The scale (boost) multiplier therefore depends on the vibrancy factor and the pixel's saturation:
 +\[
 +scale = 1 + \frac{100 - saturation}{100} * (vibrancy-1) \\
 +vibrancy \in [0..1]\\
 +saturation \in [0..100]\%
 +\]
 +<code c++>double scale(int _s, double _vib){ // _s is in range [0..100]%
 +  return 1. + (100 - _s) / 100. * (_vib - 1);
 +}</code>
 +Once the scale (boost) value is calculated - just apply it to the pixels:
 +<code c++>// pseudo-code
 +void vibrancy(/*array of pixels*/image, double _vib){
 +  for(const & pixel: image){
 +    pixel.sat *= scale(pixel.sat, _vib);
 +  }
 +}</code>
 +

This website uses cookies. By using the website, you agree with storing cookies on your computer. Also, you acknowledge that you have read and understand our Privacy Policy. If you do not agree, please leave the website.

More information