Hue Shift (like photoshop) Effect

Blitz3D Forums/Blitz3D Programming/Hue Shift (like photoshop) Effect

Roland(Posted 2008) [#1]
Hey everybody...

this is a really weird thing, but I'm hoping that someone can help me out with it. Kinda hard to explain, but I'll try.

I'm trying to accomplish a "hue shift", like in photoshop, but in realtime on an image. Doing read / write pixelfast would not be fast enough.

You can see what i'm trying to do if you take an image in photoshop, go to image > adjust > Hue / Saturation and move the hue slider. On a rainbow-image, the image will appear to move right to left, as green becomes blue, red becomes orange, etc.

Here's my approach. It's ALMOST WORKING. but not quite. I'm splitting my image into 3 grayscale channels, R, G, & B. Then placing them on top of each other additively, so they combine to form the image. Next, I'm doing an entitycolor on each of them to change how much red green and blue is in them. But it's not quite right.

I'm attaching a zip file so you can see what i'm talking about. right now the rainbow kind of moves left to right, but it's not smooth. Any ideas??

[a] http://www.prayercore.com/roland/hueshift.zip [/a]

thanks!
roland


Roland(Posted 2008) [#2]
man, couldn't get the URL to work... what's the syntax?


mtnhome3d(Posted 2008) [#3]
slash goes the other direction like this /
[edit] nevermind i forgot its "a" not url.
the link


Roland(Posted 2008) [#4]
Thanks mayaman!


jfk EO-11110(Posted 2008) [#5]
You may have to play with additive blending since EntityColor is only Subtractive. Working with both may allow to get any color.

At the other hand, to alter the Hue you should convert RGB to HSB in the first place, then you only need to edit the Hue value, or Saturation or Brightness. Tho, convertion is slow:
http://www.blitzbasic.com/codearcs/codearcs.php?code=1380
http://www.blitzbasic.com/codearcs/codearcs.php?code=2072


Roland(Posted 2008) [#6]
Hey jfk, thanks for the reply!

I'll look at using the HSV conversion, but I'm pretty sure this will be too slow to do in realtime. I'll let you know how it works out, though.


As to the other way, I'm not sure I know what you mean by saying that EntityColor is only subtractive... What I'm doing right now is I've split my image into 3 different textures which correspond to the greyscale R, G, and B channels out of photoshop. Then I am doing entitycolor on each of these individually and combining them additively. Does that make sense? Download the zip and check out the code if you get a sec and you'll see what i mean.

Do you have any suggestions for why this isn't working correctly? I feel like i've tried every possible way of shifting their colors and still can't get it to work right.

The only thing I can think at this point is that not having the Saturation and Brightness separate from the color is preventing the hue shift from being continuous. But what's weird is that the shift on a single color seems to work perfectly, and the shift on all three works correctly at some points, just not all.

I really appreciate your help and hope we can sort this out. It would be an awesome realtime effect in the project that i'm working on!

cheers,
roland


Roland(Posted 2008) [#7]
hmm... well, i integrated the HSL into the code and made it calculate the way to color each sprite using the hue instead, and it is better. But the colors still get blown out when they are cycled instead of staying at the correct luminance. Strange.

This is definitely simpler and closer to working right, but it's not quite there. any ideas?

 
Graphics3D 640,480,0,2

pivot = CreatePivot()
cam = CreateCamera(pivot)
PositionEntity cam,0,0,30
PointEntity cam,pivot

AmbientLight 0,0,0

Global result_r#, result_g#, result_b#		
Global result_h#, result_s#, result_l#

Type col
	Field image
	Field red
	Field green
	Field blue
	Field state
	
	Field h#
	Field s#
	Field l#
End Type

Function makecol(file$,red,green,blue,state)
	c.col = New col
	c\image = LoadSprite(file)
	ScaleSprite c\image,20,20
	EntityFX c\image,1
	EntityBlend c\image,3

	c\red = red
	c\green = green
	c\blue = blue
	c\state = state
	
	rgb_to_hsl(c\red,c\green,c\blue)
	c\h = result_h
	c\s = result_s
	c\l = result_l
	
End Function

makecol("red.jpg",255,0,0,1)
makecol("green.jpg",0,255,0,3)
makecol("blue.jpg",0,0,255,5)

speed = 5

While Not KeyHit(1)


	For c.col = Each col
	
		hsl_to_rgb(c\h,c\s,c\l)
		c\red = result_r
		c\green = result_g
		c\blue = result_b
		
		If c\h < 360
			c\h = c\h+speed
		Else
			c\h = 0
		EndIf
		
		EntityColor c\image,c\red,c\green,c\blue	
	
	
	Next

RenderWorld()

Flip

Wend
End



; RGB/HSL function return values.


;
; Converts an RGB color to HSL.
;
; Params:
; ir,ig,ib - Color's red, green and blue components (0-255).
;
; Returns:
;	The converted color's HSL values via the following globals:
; result_h - Hue component (0-360).
; result_s - Saturation component (0-1).
; result_l - Luminance component (0-1).
;
Function rgb_to_hsl(ir%, ig%, ib%)

	; Scale RGB down to unit range (0-1).
	r# = ir/255.0
	g# = ig/255.0
	b# = ib/255.0
	
	If r > g
		max_is = 0
		max_color# = r
		min_color# = g
	Else
		max_is = 1
		max_color# = g
		min_color# = r
	EndIf
	
	If b > max_color
		max_is = 2
		max_color = b
	ElseIf b < min_color
		min_color = b
	EndIf
	
	; Luminance.
	result_l = (max_color + min_color) / 2

	If max_color = min_color
		; Color is grey.
		result_s = 0
		;result_h = 0
	Else
		delta# = (max_color - min_color)

		; Saturation.
		If result_l < 0.5
			result_s = delta / (max_color + min_color)
		Else
			result_s = delta / (2.0 - max_color - min_color)
		EndIf
	
		; Hue.
		Select max_is
		Case 0
			; Red.
			result_h = (g - b) / delta
		Case 1
			; Green.
			result_h = 2.0 + (b - r) / delta
		Case 2
			; Blue.
			result_h = 4.0 + (r - g) / delta
		End Select

		result_h = result_h * 60.0
		If result_h < 0 Then result_h = result_h + 360.0
	EndIf

End Function


;
; Converts an HSL color to RGB.
;
; Params:
; h,s,l - Color's hue(0-360), luminance(0-1) and saturation(0-1) components.
;
; Returns:
;	The converted color's RGB values via the following globals:
; result_r - Hue component (0-255).
; result_g - Saturation component (0-255).
; result_b - Luminance component (0-255).
;
Function hsl_to_rgb(h#, s#, l#)

	If s = 0
		result_r# = l
		result_g# = l
		result_b# = l
	Else
		If l < 0.5
			temp2# = l * (1.0 + s)
		Else
			temp2# = (l + s) - (l * s)
		EndIf
		temp1# = 2.0 * l - temp2
		
		h = h / 360.0
		
		rtemp3# = h + 1.0 / 3.0
		If rtemp3 < 0
			rtemp3 = rtemp3 + 1.0
		ElseIf rtemp3 > 1
			rtemp3 = rtemp3 - 1.0
		EndIf

		gtemp3# = h
		If gtemp3 < 0
			gtemp3 = gtemp3 + 1.0
		ElseIf gtemp3 > 1
			gtemp3 = gtemp3 - 1.0
		EndIf

		btemp3# = h - 1.0 / 3.0
		If btemp3 < 0
			btemp3 = btemp3 + 1.0
		ElseIf btemp3 > 1
			btemp3 = btemp3 - 1.0
		EndIf
		
		; Set red.
		If (6.0 * rtemp3) < 1
			result_r# = temp1 + (temp2 - temp1) * 6.0 * rtemp3
		ElseIf (2.0 * rtemp3) < 1
			result_r# = temp2
		ElseIf (3.0 * rtemp3) < 2
			result_r# = temp1 + (temp2 - temp1) * ((2.0 / 3.0) - rtemp3) * 6.0
		Else
			result_r# = temp1
		EndIf

		; Set green.
		If (6.0 * gtemp3) < 1
			result_g# = temp1 + (temp2 - temp1) * 6.0 * gtemp3
		ElseIf (2.0 * gtemp3) < 1
			result_g# = temp2
		ElseIf (3.0 * gtemp3) < 2
			result_g# = temp1 + (temp2 - temp1) * ((2.0 / 3.0) - gtemp3) * 6.0
		Else
			result_g# = temp1
		EndIf

		; Set blue.
		If (6.0 * btemp3) < 1
			result_b# = temp1 + (temp2 - temp1) * 6.0 * btemp3
		ElseIf (2.0 * btemp3) < 1
			result_b# = temp2
		ElseIf (3.0 * btemp3) < 2
			result_b# = temp1 + (temp2 - temp1) * ((2.0 / 3.0) - btemp3) * 6.0
		Else
			result_b# = temp1
		EndIf
	EndIf

	; Scale RGB back to 0-255 range.
	; Remove this if you want to keep RGB in the range 0-1!
	result_r = result_r * 255
	result_g = result_g * 255
	result_b = result_b * 255





sswift(Posted 2008) [#8]
Hm...

Your code starts with:
A*(255,0,0) + B*(0,255,0) + C*(0,0,255)

Then the way you increment and decrement stuff, when you're halfway through one state it should be:
A*(127,0,127) + B*(127,127,0) + C*(0,127,127)

So if your initial image was red, then A would be (255, 255, 255), and B and C would be (0,0,0).

So:

(255, 255, 255)*(127,0,127) + (0,0,0)*(127,127,0) + (0,0,0)*(0,127,127)

= (127,0,127)

There's your problem. You need to increment the green until it reaches 255 and THEN start to decrement the red until it is 0, leaving the green at 255, not increment one while decrementing the other simulaneously. The total value of the three channels for each grayscale image should oscillate between 255 and 510.

But due to the way your code works, and a bug you have somewhere, it doesn't do that.

Try printing this:
Text 0,0,c\red+c\green+c\blue

It starts a 255, then jumps to 260 suddenly and stays there/ That's obviously wrong, even if 255 was the right value to always have.


Roland(Posted 2008) [#9]
Hey Sswift, thanks for jumping in!

I'm afraid I'd already tried that approach and found that it didn't work either (though I suspect it's <more> right than the one that i posted). In fact, it looks identical to the HSL version that I posted based on jfk's suggestion.

I still don't understand what I can do to eliminate the intermediate stages where the colors add together to be too bright. Please let me know if you have any other ideas!

thanks again,
roland

here's a version of it based on that approach. It adds to the next color while the previous color is at 255, then decrements the previous color, etc. A 6-step approach instead of 3.


Graphics3D 640,480,0,2

pivot = CreatePivot()
cam = CreateCamera(pivot)
PositionEntity cam,0,0,30
PointEntity cam,pivot

Type col
	Field image
	Field red
	Field green
	Field blue
	Field state
End Type

Function makecol(file$,red,green,blue,state)
	c.col = New col
	c\image = LoadSprite(file)
	ScaleSprite c\image,20,20
	EntityFX c\image,1
	EntityBlend c\image,3

	c\red = red
	c\green = green
	c\blue = blue
	c\state = state
	
	EntityColor c\image,c\red,c\green,c\blue
	
End Function


makecol("red.jpg",255,0,0,1)
makecol("green.jpg",0,255,0,3)
makecol("blue.jpg",0,0,255,5)

speed = 10

While Not KeyHit(1)

	For c.col= Each col
	
		Select c\state
		
			Case 1
			c\green = c\green+speed
			If c\green>255 Then c\state = 2
			
			Case 2
			c\red = c\red-speed
			If c\red < 0  Then c\state = 3
			
			Case 3
			c\blue = c\blue+speed
			If c\blue > 255 Then c\state = 4
			
			Case 4
			c\green = c\green-speed
			If c\green < 0 Then c\state = 5	
			
			Case 5
			c\red = c\red+speed
			If c\red > 255 Then c\state = 6				

			Case 6
			c\blue = c\blue -speed
			If c\blue < 0 Then c\state = 1	
				
		
		End Select	
	
		EntityColor c\image,c\red,c\green,c\blue
	
		If c\red < 0 Then c\red = 0
		If c\green < 0 Then c\green = 0
		If c\blue < 0 Then c\blue = 0
	
	
	Next

RenderWorld()

c.col = First col

Text 0,0,c\red
Text 0,14,c\green
Text 0,28,c\blue

Text 0,50,c\red+c\green+c\blue

Flip

Wend
 



sswift(Posted 2008) [#10]


I noticed you're still going up to 260 on your color channels, so I modified your code to not only make sure it doesn't go over 255, but so that it goes slower so you can see what's going on, and so it displays the original image and waits for you to hit a key before starting so you can get a better idea of where the bright spots are occuring.

And it looks like the bright spots are occuring where your secondary colors, magenta, yellow, and cyan are, which is not surprising.

Hm...

Well, I know what's happening, but I don't know how to solve it.

Let's say the color you're looking at in the image is yellow. You get yellow where both the red and green images are white.

Now you start sliding the hue. The red layer starts becoming yellow, the green layer starts becoming cyan, and the blue layer starts becoming magenta.

Yellow and cyan together contain all three primary colors, so you end up with white wherever your original image had both red and greem or green and blue, or blue and red.


Roland(Posted 2008) [#11]
Yeah, that's exactly what's happening, sswift. You put it into words nicely... I'm stumped as to how to fix it as well.

It makes me wonder why this behavior isn't seen in photoshop when you adjust the Hue of an image. It seems like there must be a bound on how high the additive values of a pixel can be based on its initial luminosity. So if you were evaluating the color of just a single pixel at a time you could prevent it from becoming too bright based on that initial measurement.

Here, we don't have the ability to do that because we're modifying the entire image all the time. I wonder if it's not possible to fix it without doing read / write pixels on the whole image.

if you have any other ideas, please let me know. I'm really motivated to get this working, but feel pretty much stumped right now.

thanks again for your help!

roland


sswift(Posted 2008) [#12]
Well this behavior isn't seen in photoshop because they can do stuff like convert from RGB to HSV and back. So they can adjust the hue, without affecting the saturation or the value components of the image.

If you could find some way to split an image into hue saturation and value channels, and alter the hue channel as desired, and then recombine them, then maybe you could do what you're trying to do.

Value is easy enough, that's a black and white version of your image. Saturation... That one's not so easy to define. You can have dark pixels which are less saturated and light pixels which are less saturated. Maybe if you laid down the hue layer first, then added a saturation layer to that where white is least saturated, and then applied the value on top of that with multiply? The hue layer would be pixels that are full brightness hues, so (255,0,0)..(255,255,0)..(0,255,0) and so on.

But how do you then adjust the hue layer? Once again you seem to have the same problem with either white showing up, or ending up with half-bright primary colors.



I played around with photoshop a bit to see if I could figure out a way to get the effect you want with the color channels like you have now, but I failed there. I used the channel mixer to create a version of the image that looks like a hue adjustment of -120. So I moved the red channel to the green, the green to the blue, and the blue to the red. I had that on a layer over the original. I could then experiment with layer blending styles to try to find a way to blend between the two that would create a result that looks like a hue adjustment of only -60.

The closest I got was to set the modified layer to 50% transparency, and normal blend with the original. The result was the right hues, and the right brightness level, but they were desaturated by 50%.

Nothing else I tried worked.

I tried cutting the brightness of both layers by half and adding them and that didn't look right at all. Desaturated and super contrasty. And simply adding them didn't work of course. Nice saturation, but everythng overbrightened.

There was no point in playing around with most of the blending modes offered, since the only ones you have to play with in Blitz are multiply, add, and alpha. So those are the ones I tried to get to do the job. But no dice.