Skip to content

Commit ea196bb

Browse files
committed
Improve HSV/HSL/HSI conversions
This adds the clamping and hue normalization for sources to HSx-->RGB conversions. This also adds the clamping for destinations to HSI-->RGB conversion.
1 parent 9c08ccb commit ea196bb

File tree

3 files changed

+152
-105
lines changed

3 files changed

+152
-105
lines changed

src/conversions.jl

Lines changed: 111 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,8 @@ end
7171
# -----------------
7272

7373
correct_gamut(c::CV) where {CV<:AbstractRGB} = CV(clamp01(red(c)), clamp01(green(c)), clamp01(blue(c)))
74-
clamp01(v::T) where {T<:Fractional} = ifelse(v < zero(T), zero(T), ifelse(v > one(T), one(T), v))
74+
correct_gamut(c::CV) where {T<:Union{N0f8,N0f16,N0f32,N0f64},
75+
CV<:Union{AbstractRGB{T},TransparentRGB{T}}} = c
7576

7677
function srgb_compand(v::Fractional)
7778
# the following is an optimization technique for `1.055v^(1/2.4) - 0.055`.
@@ -81,58 +82,79 @@ end
8182

8283
cnvt(::Type{CV}, c::AbstractRGB) where {CV<:AbstractRGB} = CV(red(c), green(c), blue(c))
8384

84-
function cnvt(::Type{CV}, c::HSV) where CV<:AbstractRGB
85-
h = c.h / 60
86-
i = floor(Int, h)
87-
f = h - i
88-
if i & 1 == 0
89-
f = 1 - f
90-
end
91-
m = c.v * (1 - c.s)
92-
n = c.v * (1 - c.s * f)
93-
if i == 6 || i == 0; CV(c.v, n, m)
94-
elseif i == 1; CV(n, c.v, m)
95-
elseif i == 2; CV(m, c.v, n)
96-
elseif i == 3; CV(m, n, c.v)
97-
elseif i == 4; CV(n, m, c.v)
98-
else; CV(c.v, m, n)
99-
end
100-
end
101-
102-
function qtrans(u, v, hue)
103-
hue = normalize_hue(hue)
104-
105-
if hue < 60; u + (v - u) * hue / 60
106-
elseif hue < 180; v
107-
elseif hue < 240; u + (v - u) * (240 - hue) / 60
108-
else; u
109-
end
110-
end
111-
112-
function cnvt(::Type{CV}, c::HSL) where CV<:AbstractRGB
113-
v = c.l <= 0.5 ? c.l * (1 + c.s) : c.l + c.s - (c.l * c.s)
114-
u = 2 * c.l - v
115-
116-
if c.s == 0; CV(c.l, c.l, c.l)
117-
else; CV(qtrans(u, v, c.h + 120),
118-
qtrans(u, v, c.h),
119-
qtrans(u, v, c.h - 120))
120-
end
121-
end
12285

123-
function cnvt(::Type{CV}, c::HSI) where CV<:AbstractRGB
124-
h, s, i = normalize_hue(c.h), c.s, c.i
125-
is = i*s
126-
if h < 120
127-
cosr = cosd(h) / cosd(60-h)
128-
CV(i+is*cosr, i+is*(1-cosr), i-is)
129-
elseif h < 240
130-
cosr = cosd(h-120) / cosd(180-h)
131-
CV(i-is, i+is*cosr, i+is*(1-cosr))
86+
function _hsx_to_rgb(im::UInt8, v, n, m)
87+
#=
88+
if hue < 60; im = 0b000001 # ---------+
89+
elseif hue < 120; im = 0b000010 # --------+|
90+
elseif hue < 180; im = 0b000100 # -------+||
91+
elseif hue < 240; im = 0b001000 # ------+|||
92+
elseif hue < 300; im = 0b010000 # -----+||||
93+
else ; im = 0b100000 # ----+|||||
94+
end # ||||||
95+
(hue < 60 || hue >= 300) === ((im & 0b100001) != 0x0)
96+
=#
97+
r = ifelse((im & 0b100001) == 0x0, ifelse((im & 0b010010) == 0x0, m, n), v)
98+
g = ifelse((im & 0b000110) == 0x0, ifelse((im & 0b001001) == 0x0, m, n), v)
99+
b = ifelse((im & 0b011000) == 0x0, ifelse((im & 0b100100) == 0x0, m, n), v)
100+
return (r, g, b)
101+
end
102+
function _hsx_to_rgb(im::UInt8, v::T, n::T, m::T) where T <:Union{Float16, Float32, Float64}
103+
vu, nu, mu = reinterpret.(Unsigned, (v, n, m)) # prompt the compiler to use conditional moves
104+
r = ifelse((im & 0b100001) == 0x0, ifelse((im & 0b010010) == 0x0, mu, nu), vu)
105+
g = ifelse((im & 0b000110) == 0x0, ifelse((im & 0b001001) == 0x0, mu, nu), vu)
106+
b = ifelse((im & 0b011000) == 0x0, ifelse((im & 0b100100) == 0x0, mu, nu), vu)
107+
return reinterpret.(T, (r, g, b))
108+
end
109+
110+
function cnvt(::Type{CV}, c::HSV) where {T, CV<:AbstractRGB{T}}
111+
F = promote_type(T, eltype(c))
112+
h, s, v = div60(F(c.h)), clamp01(F(c.s)), clamp01(F(c.v))
113+
hi = unsafe_trunc(Int32, h) # instead of floor
114+
i = h < 0 ? hi - one(hi) : hi
115+
f = i & one(i) == zero(i) ? 1 - (h - i) : h - i
116+
im = 0x1 << (mod6(UInt8, i) & 0x07)
117+
# use `@fastmath` just to reduce the estimated costs for inlining
118+
@fastmath m = v * (1 - s)
119+
@fastmath n = v * (1 - s * f)
120+
121+
r, g, b = _hsx_to_rgb(im, v, n, m)
122+
T <: FixedPoint && typemax(T) >= 1 ? CV(r % T, g % T, b % T) : CV(r, g, b)
123+
end
124+
125+
function cnvt(::Type{CV}, c::HSL) where {T, CV<:AbstractRGB{T}}
126+
F = promote_type(T, eltype(c))
127+
h, s, l = div60(F(c.h)), clamp01(F(c.s)), clamp01(F(c.l))
128+
a = @fastmath min(l, 1 - l) * s
129+
v = l + a
130+
hi = unsafe_trunc(Int32, h) # instead of floor
131+
i = h < 0 ? hi - one(hi) : hi
132+
f = i & one(i) == zero(i) ? 1 - (h - i) : h - i
133+
im = 0x1 << (mod6(UInt8, i) & 0x07)
134+
# use `@fastmath` just to reduce the estimated costs for inlining
135+
@fastmath m = l - a # v - 2 * a
136+
@fastmath n = v - 2 * a * f
137+
138+
r, g, b = _hsx_to_rgb(im, v, n, m)
139+
T <: FixedPoint && typemax(T) >= 1 ? CV(r % T, g % T, b % T) : CV(r, g, b)
140+
end
141+
142+
function cnvt(::Type{CV}, c::HSI) where {T, CV<:AbstractRGB{T}}
143+
F = promote_type(T, eltype(c))
144+
h, s, i = deg2rad(normalize_hue(F(c.h))), clamp01(F(c.s)), clamp01(F(c.i))
145+
is = i * s
146+
if h < F(2π/3)
147+
@fastmath cosr = cos(h) / cos(F/3)-h)
148+
r0, g0, b0 = muladd(is, cosr, i), muladd(is, 1-cosr, i), i - is
149+
elseif h < F(4π/3)
150+
@fastmath cosr = cos(h-F(2π/3)) / cos(F(π)-h)
151+
r0, g0, b0 = i - is, muladd(is, cosr, i), muladd(is, 1-cosr, i)
132152
else
133-
cosr = cosd(h-240) / cosd(300-h)
134-
CV(i+is*(1-cosr), i-is, i+is*cosr)
153+
@fastmath cosr = cos(h-F(4π/3)) / cos(F(5π/3)-h)
154+
r0, g0, b0 = muladd(is, 1-cosr, i), i - is, muladd(is, cosr, i)
135155
end
156+
r, g, b = min(r0, oneunit(F)), min(g0, oneunit(F)), min(b0, oneunit(F))
157+
T <: FixedPoint && typemax(T) >= 1 ? CV(r % T, g % T, b % T) : CV(r, g, b)
136158
end
137159

138160
function cnvt(::Type{CV}, c::XYZ) where CV<:AbstractRGB
@@ -175,26 +197,20 @@ end
175197
# -----------------
176198

177199
function cnvt(::Type{HSV{T}}, c::AbstractRGB) where T
178-
c_min = Float64(min(red(c), green(c), blue(c)))
179-
c_max = Float64(max(red(c), green(c), blue(c)))
180-
if c_min == c_max
181-
return HSV{T}(zero(T), zero(T), c_max)
182-
end
200+
F = promote_type(T, eltype(c))
201+
r, g, b = F.((red(c), green(c), blue(c)))
202+
c_min = @fastmath min(min(r, g), b)
203+
c_max = @fastmath max(max(r, g), b)
204+
s0 = c_max - c_min
205+
s0 == zero(F) && return HSV{T}(zero(T), zero(T), T(c_max))
206+
s = @fastmath s0 / c_max
183207

184-
if c_min == red(c)
185-
f = Float64(green(c)) - Float64(blue(c))
186-
i = 3
187-
elseif c_min == green(c)
188-
f = Float64(blue(c)) - Float64(red(c))
189-
i = 5
190-
else
191-
f = Float64(red(c)) - Float64(green(c))
192-
i = 1
193-
end
208+
# In general, it is dangerous to compare floating point numbers with `===`.
209+
diff = ifelse(c_max === r, g - b, ifelse(c_max === g, b - r, r - g))
210+
ofs = ifelse(c_max === r, (g < b)*F(360), ifelse(c_max === g, F(120), F(240)))
211+
h0 = @fastmath diff * F(60) / s0
194212

195-
HSV{T}(60 * (i - f / (c_max - c_min)),
196-
(c_max - c_min) / c_max,
197-
c_max)
213+
HSV{T}(h0 + ofs, s, c_max)
198214
end
199215

200216

@@ -205,28 +221,22 @@ cnvt(::Type{HSV{T}}, c::Color3) where {T} = cnvt(HSV{T}, convert(RGB{T}, c))
205221
# -----------------
206222

207223
function cnvt(::Type{HSL{T}}, c::AbstractRGB) where T
208-
r, g, b = T(red(c)), T(green(c)), T(blue(c))
209-
c_min = min(r, g, b)
210-
c_max = max(r, g, b)
211-
l = (c_max + c_min) / 2
212-
213-
if c_max == c_min
214-
return HSL(zero(T), zero(T), l)
215-
end
224+
F = promote_type(T, eltype(c))
225+
r, g, b = F(red(c)), F(green(c)), F(blue(c))
226+
c_min = @fastmath min(min(r, g), b)
227+
c_max = @fastmath max(max(r, g), b)
228+
l0 = c_max + c_min
229+
s0 = c_max - c_min
230+
l = l0 * F(0.5)
231+
s0 == zero(F) && return HSL{T}(zero(T), zero(T), T(l))
232+
s = @fastmath s0 / min(l0, F(2) - l0)
216233

217-
if l < 0.5; s = (c_max - c_min) / (c_max + c_min)
218-
else; s = (c_max - c_min) / (convert(T, 2) - c_max - c_min)
219-
end
234+
# In general, it is dangerous to compare floating point numbers with `===`.
235+
diff = ifelse(c_max === r, g - b, ifelse(c_max === g, b - r, r - g))
236+
ofs = ifelse(c_max === r, (g < b)*F(360), ifelse(c_max === g, F(120), F(240)))
237+
h0 = @fastmath diff * F(60) / s0
220238

221-
if c_max == red(c)
222-
h = (g - b) / (c_max - c_min)
223-
elseif c_max == green(c)
224-
h = convert(T, 2) + (b - r) / (c_max - c_min)
225-
else
226-
h = convert(T, 4) + (r - g) / (c_max - c_min)
227-
end
228-
229-
HSL{T}(normalize_hue(h * 60), s, l)
239+
HSL{T}(h0 + ofs, s, l)
230240
end
231241

232242

@@ -236,22 +246,20 @@ cnvt(::Type{HSL{T}}, c::Color3) where {T} = cnvt(HSL{T}, convert(RGB{T}, c))
236246
# Everything to HSI
237247
# -----------------
238248

239-
function cnvt(::Type{HSI{T}}, c::AbstractRGB) where T
249+
# Since acosd() is slow, the following is "inline-worthy".
250+
@inline function cnvt(::Type{HSI{T}}, c::AbstractRGB) where T
240251
rgb = correct_gamut(c)
241-
r, g, b = float(red(rgb)), float(green(rgb)), float(blue(rgb))
242-
isum = r+g+b
243-
dnorm = sqrt(((r-g)^2 + (r-b)^2 + (g-b)^2)/2)
244-
dnorm = dnorm == 0 ? oftype(dnorm, 1) : dnorm
245-
i = isum/3
246-
m = min(r, g, b)
247-
s = i > 0 ? 1-m/i : zero(1 - m/i)
248-
val = (r-(g+b)/2)/dnorm
249-
val = clamp(val, -oneunit(val), oneunit(val))
250-
h = acosd(val)
251-
if b > g
252-
h = 360-h
253-
end
254-
HSI{T}(h, s, i)
252+
F = promote_type(T, eltype(c))
253+
r, g, b = F(red(rgb)), F(green(rgb)), F(blue(rgb))
254+
dnorm = @fastmath sqrt(((r-g)^2 + (r-b)^2 + (g-b)^2) * F(0.5))
255+
isum = r + g + b
256+
i = isum / F(3)
257+
dnorm == zero(F) && return HSI{T}(T(90), zero(T), T(i))
258+
val = muladd(g + b, F(-0.5), r) / dnorm
259+
h = @fastmath acosd(clamp(val, -oneunit(F), oneunit(F)))
260+
m = @fastmath min(min(r, g), b)
261+
s = oneunit(F) - m/i
262+
HSI{T}(b > g ? F(360) - h : h, s, i)
255263
end
256264

257265
cnvt(::Type{HSI{T}}, c::Color3) where {T} = cnvt(HSI{T}, convert(RGB{T}, c))

src/utilities.jl

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,28 @@
11
# Helper data for CIE observer functions
22
include("cie_data.jl")
33

4+
# for optimization
5+
div60(x) = x / 60
6+
_div60(x::T) where T = muladd(x, T(1/960), x * T(0x1p-6))
7+
if reduce(max, _div60.((90.0f0,))) == 1.5f0
8+
div60(x::T) where T <: Union{Float32, Float64} = _div60(x)
9+
else
10+
# force two-step multiplication
11+
div60(x::T) where T <: Union{Float32, Float64} = x * T(0x1p-6) + x * T(1/960)
12+
end
13+
14+
# mod6 supports the input `x` in [-2^28, 2^29]
15+
mod6(::Type{T}, x::Int32) where T = unsafe_trunc(T, x - 6 * ((widemul(x, 0x2aaaaaaa) + Int64(0x20000000)) >> 0x20))
416

517
# Linear interpolation in [a, b] where x is in [0,1],
618
# or coerced to be if not.
719
function lerp(x, a, b)
820
a + (b - a) * max(min(x, one(x)), zero(x))
921
end
1022

23+
clamp01(v::T) where {T<:Fractional} = ifelse(v < zero(T), zero(T), ifelse(v > oneunit(T), oneunit(T), v))
24+
clamp01(v::T) where {T<:Union{N0f8,N0f16,N0f32,N0f64}} = v
25+
1126
"""
1227
HexNotation{C, A, N}
1328
@@ -148,7 +163,7 @@ Returns a normalized (wrapped-around) hue angle, or a color with the normalized
148163
hue, in degrees, in [0, 360]. The normalization is essentially equivalent to
149164
`mod(h, 360)`, but is faster at the expense of some accuracy.
150165
"""
151-
@fastmath normalize_hue(h::Real) = max(fma(floor(h / 360), -360, h), zero(h))
166+
@fastmath normalize_hue(h::Real) = max(muladd(floor(h / 360), -360, h), zero(h))
152167
@fastmath normalize_hue(h::Float16) = Float16(normalize_hue(Float32(h)))
153168
normalize_hue(c::C) where {C <: Union{HSV, HSL, HSI}} = C(normalize_hue(c.h), c.s, comp3(c))
154169
normalize_hue(c::C) where {Cb <: Union{HSV, HSL, HSI}, C <: Union{AlphaColor{Cb}, ColorAlpha{Cb}}} =

test/conversion.jl

Lines changed: 25 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -117,7 +117,7 @@ using ColorTypes: eltype_default, parametric3
117117

118118
@test convert(RGB{N0f8}, red24) == RGB{N0f8}(1,0,0)
119119
@test convert(RGBA{N0f8}, red32) == RGBA{N0f8}(1,0,0,1)
120-
@test convert(HSVA{Float64}, red32) == HSVA{Float64}(360, 1, 1, 1)
120+
@test convert(HSVA{Float64}, red32) == HSVA{Float64}(0, 1, 1, 1)
121121

122122
@test_throws MethodError AlphaColor(RGB(1,0,0), r8(0xff))
123123

@@ -202,6 +202,30 @@ using ColorTypes: eltype_default, parametric3
202202
hsi = convert(HSI, c)
203203
@test hsi.i > 0.96 && hsi.h 210
204204

205+
# {HSV, HSL, HSI} --> RGB (issue #379)
206+
@testset "HSx --> RGB" begin
207+
@test convert(RGB, HSV{Float32}( 780, 1, 1)) === RGB{Float32}(1,1,0)
208+
@test convert(RGB, HSV{Float32}( 0, 1, 1)) === RGB{Float32}(1,0,0)
209+
@test convert(RGB, HSV{Float32}(-780, 1, 1)) === RGB{Float32}(1,0,1)
210+
@test convert(RGB, HSV{Float32}(30, 2, .5)) === RGB{Float32}(.5,.25,0)
211+
@test convert(RGB, HSV{Float32}(30, .5, -1)) === RGB{Float32}(0,0,0)
212+
@test convert(RGB{Float64}, HSV{BigFloat}(-360120, 2, 1)) === RGB{Float64}(0,0,1)
213+
214+
@test convert(RGB, HSL{Float32}( 780, 1, .5)) === RGB{Float32}(1,1,0)
215+
@test convert(RGB, HSL{Float32}( 0, 1, .5)) === RGB{Float32}(1,0,0)
216+
@test convert(RGB, HSL{Float32}(-780, 1, .5)) === RGB{Float32}(1,0,1)
217+
@test convert(RGB, HSL{Float32}(30, 2, .25)) === RGB{Float32}(.5,.25,0)
218+
@test convert(RGB, HSL{Float32}(30, .5, -1)) === RGB{Float32}(0,0,0)
219+
@test convert(RGB{Float64}, HSL{BigFloat}(-360120, 2, .5)) === RGB{Float64}(0,0,1)
220+
221+
@test convert(RGB, HSI{Float32}( 780, .5, .5)) RGB{Float32}(.625,.625,.25)
222+
@test convert(RGB, HSI{Float32}( 0, .5, .5)) RGB{Float32}(1,.25,.25)
223+
@test convert(RGB, HSI{Float32}(-780, .5, .5)) RGB{Float32}(.625,.25,.625)
224+
@test convert(RGB, HSI{Float32}(30, 2, .25)) RGB{Float32}(.5,.25,0)
225+
@test convert(RGB, HSI{Float32}(30, .5, -1)) RGB{Float32}(0,0,0)
226+
@test convert(RGB{Float64}, HSI{BigFloat}(-360120, .5, .5)) RGB{Float64}(.25,.25,1)
227+
end
228+
205229
# Test accuracy of conversion
206230
include("test_conversions.jl")
207231

0 commit comments

Comments
 (0)