Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Recreate HSV color using blend modes

I’m working on an app that creates images whose hue, saturation, and value change according to different parameters. For performance reasons, it would make sense to render the hue, saturation, and value components separately, and then composite them together using Photoshop-style blending modes (multiply, overlay, screen, hue, etc).

I already know how to do this for RGB images: split each channel into its own red, green, or blue image with values ranging from transparent to that channel’s color. Layer them together on top of black and set their blend mode to Screen, and hey presto, you have your color image:

RGB image made from components

How would I do this with an image defined by HSV values? My app often changes one of these channels without changing the other two, and it would speed up my rendering if I could composite existing images on the GPU instead of rendering a completely new image every time something changes.

Here’s an example: an image generated with separate H, S, and V channels

In this example, the hue varies from 0º to 360º around the circumference, the saturation varies from 0% to 100% from the center to the edge, and the brightness (V) varies from 0% to 100% around the circumference. This is typical of the kind of image my app creates. Is there a combination of common blending modes I could use to create these channels separately and composite them in a mathematically perfect way?

like image 976
Zev Eisenberg Avatar asked Oct 03 '12 01:10

Zev Eisenberg


People also ask

How do you change blend mode to color?

Real World Example of the Color Blend Mode Simply add a new blank layer above your image and set the blend mode of the layer to Color. Select your Brush Tool from the Tools palette, choose the color you want to paint with, and begin painting on the layer to add your color.

What is HSV color format?

1. HSV Color Scale: The HSV (which stands for Hue Saturation Value) scale provides a numerical readout of your image that corresponds to the color names contained therein. Hue is measured in degrees from 0 to 360. For instance, cyan falls between 181–240 degrees, and magenta falls between 301–360 degrees.


1 Answers

My app often changes one of these channels without changing the other two, and it would speed up my rendering if I could composite existing images on the GPU instead of rendering a completely new image every time something changes. [OP, @ZevEisenberg]

With regard to fastness and the GPU, I'd just throw a conversion function into a fragment shader (e.g.). This would read HSV stored in a texture or three different textures, do the conversion per-pixel and output RGB. Nice and easy. I can't see any benefit to not changing other layers since either H, S or V will affect all RGB channels. Perhaps storing intermediate RGB results such as hue=hsv2rgb(H,1,1), and updating with final=(hue*S+1-S)*V, caching hue-to-rgb but I don't think it's worth it.

Anyway, each blend mode has a simple formula, and you could string them together for HSV involving an overly complex set of intermediate textures, but it will be much slower primarily because of the unnecessary temporary storage and memory bandwidth. Not to mention, trying to rewrite the formula into blend functions sounds pretty challenging, what with branching, divisions, fract, clamping, absolutes etc...

I am very interested in an a solution for splitting an Image into its HSV components and recreate the original Image using blend modes in Photoshop. [Bounty, @phisch]

With regard to photoshop... I'm not made of money. So in gimp, there's Colours -> Components -> Compose/Decompose which does this for you. I'd be kinda surprised if this doesn't exist in photoshop but then also kinda not. Perhaps there are photoshop scripts/plugins that could do it if not? But you did specifically say blending. Your question might get better attention at https://graphicdesign.stackexchange.com/. Below, I've given an idea of the complexity involved and I doubt photoshop can actually do it. There may be ways around pixel values outside 0 to 1, but then you might run into precision issues, it just shouldn't be done.


Anyway, a challenge is a challenge despite how impractical. The following is just for fun.

I'll start with the following function (from here) and three HSV textures...

vec3 hsv2rgb(vec3 c)
{
    vec4 K = vec4(1.0, 2.0 / 3.0, 1.0 / 3.0, 3.0);
    vec3 p = abs(fract(c.xxx + K.xyz) * 6.0 - K.www);
    return c.z * mix(K.xxx, clamp(p - K.xxx, 0.0, 1.0), c.y);
}

hhh

I only know OpenGL and I'm not sure how I'd do this without floating point textures or some of the extended blending functions, so I'm using them. But I'm only allowed to use blending (no shaders whatsoever). For the constants I'll make textures with (1,1,1), (1,2/3,1/3), (3,3,3), (6,6,6) (1/255,1/255,1/255), (255,255,255), (1/2,1/2,1/2) and (0,0,0) because I couldn't get GL_ZERO to scale with GL_DIFFERENCE_NV.

  1. start with hue texture
  2. use additive blending to add (1,2/3,1/3)
  3. find the fractional part

    1. with subtractive blending, subtract 0.5 (this is for a floor() as I'm assuming GL rounds colours when converting to 8 bits. if not, skip this)
    2. scale down by 1/255. this could be done with regular alpha blending, but I've scaled with a colour texture instead.
    3. pass through a non-floating point texture to round to the nearest 1/255
    4. scale back up by 255 (back into a floating point texture)

      integer

    5. now we have the integer component. subtract this from what we started with

      fractional

  4. scale by 6

  5. with subtractive blending, take 3
  6. take the absolute of the value

    I'm going to simply use GL_DIFFERENCE_NV for this, but without it there might be a way using two separate clamps for the next step. since negatives will be clamped anyway, something along the lines of clamp(p-K.xxx,0,1) + clamp(-p-K.xxx,0,1).

  7. subtract 1

    enter image description here well, that's hue done

  8. could clamp by passing through a non-floating point texture, but just going to use GL_MIN

  9. now I could use alpha blending for mix(), but saturation is loaded as a B/W image without an alpha channel. since it's mixing white, doing it by hand is actually easier...

    scale by the saturation

  10. add 1
  11. subtract the saturation

    enter image description here and saturation has been applied

  12. scale by value

    enter image description here and there's the image

  13. coffee break

All done using

  • glBlendEquation with GL_FUNC_REVERSE_SUBTRACT, GL_MIN and GL_DIFFERENCE_NV
  • glBlendFunc

Here's my code...

//const tex init
constTex[0] = makeTex() with 1, 1, 1...
constTex[1] = makeTex() with 1, 2/3, 1/3...
constTex[2] = makeTex() with 3, 3, 3...
constTex[3] = makeTex() with 6, 6, 6...
constTex[4] = makeTex() with 1/255, 1/255, 1/255...
constTex[5] = makeTex() with 255, 255, 255...
constTex[6] = makeTex() with 1/2, 1/2, 1/2...
constTex[7] = makeTex() with 0, 0, 0...

...

fbo[0] = makeFBO() with GL_RGB
fbo[1] = makeFBO() with GL_RGB32F
fbo[2] = makeFBO() with GL_RGB32F

...


hsv[0] = loadTex() hue
hsv[1] = loadTex() value
hsv[2] = loadTex() saturation

...

fbo[1].bind();
glDisable(GL_BLEND);
draw(hsv[0]); //start with hue
glEnable(GL_BLEND);
glBlendEquation(GL_FUNC_ADD);
glBlendFunc(GL_ONE, GL_ONE); //add
draw(constTex[1]); //(1, 2/3, 1/3)
glBlendFunc(GL_ONE, GL_ONE);
fbo[1].unbind();

//compute integer part
fbo[2].bind();
glDisable(GL_BLEND);
draw(*fbo[1].colour[0]); //copy the last bit
glEnable(GL_BLEND);
glBlendEquation(GL_FUNC_REVERSE_SUBTRACT);
glBlendFunc(GL_ONE, GL_ONE); //subtract
draw(constTex[6]); //0.5
glBlendEquation(GL_FUNC_ADD);
glBlendFunc(GL_ZERO, GL_SRC_COLOR); //scale down
draw(constTex[4]); //1/255
fbo[2].unbind();

fbo[0].bind(); //floor to integer
glDisable(GL_BLEND);
draw(*fbo[2].colour[0]);
fbo[0].unbind();

fbo[2].bind(); //scale back up
glDisable(GL_BLEND);
draw(*fbo[0].colour[0]);
glEnable(GL_BLEND);
glBlendEquation(GL_FUNC_ADD);
glBlendFunc(GL_ZERO, GL_SRC_COLOR); //scale up
draw(constTex[5]); //255
fbo[2].unbind();

//take integer part for fractional
fbo[1].bind();
glEnable(GL_BLEND);
glBlendEquation(GL_FUNC_REVERSE_SUBTRACT);
glBlendFunc(GL_ONE, GL_ONE); //subtract
draw(*fbo[2].colour[0]); //integer part
glBlendEquation(GL_FUNC_ADD);
glBlendFunc(GL_ZERO, GL_SRC_COLOR); //scale
draw(constTex[3]); //6
glBlendEquation(GL_FUNC_REVERSE_SUBTRACT);
glBlendFunc(GL_ONE, GL_ONE); //subtract
draw(constTex[2]); //3
glBlendEquation(GL_DIFFERENCE_NV);
glBlendFunc(GL_ZERO, GL_ONE); //take the absolute
draw(constTex[7]); //0
glBlendEquation(GL_FUNC_REVERSE_SUBTRACT);
glBlendFunc(GL_ONE, GL_ONE); //subtract
draw(constTex[0]); //1
glBlendEquation(GL_MIN);
glBlendFunc(GL_ONE, GL_ONE); //clamp (<0 doesn't matter, >1 use min)
draw(constTex[0]); //1
glBlendEquation(GL_FUNC_ADD);
glBlendFunc(GL_ZERO, GL_SRC_COLOR); //scale
draw(hsv[1]); //saturation
glBlendEquation(GL_FUNC_ADD);
glBlendFunc(GL_ONE, GL_ONE); //add
draw(constTex[0]); //1
glBlendEquation(GL_FUNC_REVERSE_SUBTRACT);
glBlendFunc(GL_ONE, GL_ONE); //subtract
draw(hsv[1]); //saturation
glBlendEquation(GL_FUNC_ADD);
glBlendFunc(GL_ZERO, GL_SRC_COLOR); //scale
draw(hsv[2]); //saturation
fbo[1].unbind();

fbo[1].blit(); //check result
like image 166
jozxyqk Avatar answered Sep 17 '22 01:09

jozxyqk