Color3.fromHSV and Color3.toHSV do not map to each other

When using both Color3.fromHSV and Color3.toHSV I noticed that the domain and the codomain do not map to each other when they should since these functions are inverses of each other.

For example:

local color = Color3.fromHex("cc7245")
local sameColor = Color3.fromHSV(color:ToHSV())

print(color:ToHex()) -- cc7245
print(sameColor:ToHex()) -- cc7244 ????

In fact, if you iterate over the colors on the RGB color block you’ll find that the two functions properly map to each other only ~42% of the time:

local colorCount = 0
local matchedHSVCount = 0

for r = 0, 255 do
	for g = 0, 255 do
		for b = 0, 255 do
			local color = Color3.fromRGB(r, g, b)
			
			local h, s, v = color:ToHSV()
			local hsvColor = Color3.fromHSV(h, s, v)
			
			if color:ToHex() == hsvColor:ToHex() then
				matchedHSVCount = matchedHSVCount + 1
			end
			
			colorCount = colorCount + 1
			
			if colorCount % 1000000 == 0 then
				task.wait()
			end
		end
	end
end

print("Total Colors:", colorCount) -- 16777216
print("Matched Count:", matchedHSVCount) -- 7118997
print("Unmatched Count:", colorCount - matchedHSVCount) -- 9658219
print("Percentage matched: %" .. (matchedHSVCount / colorCount) * 100) -- %42.43252873420715

These functions can map to each other correctly, but for some reason the built in functions do not. I ran the above code with my own replacement functions for Color3.fromHSV and Color3.toHSV and I was able to get a 100% match rate.

local function toHSV(color: Color3)
	-- https://en.wikipedia.org/wiki/HSL_and_HSV#From_RGB
	local r, g, b = color.R, color.G, color.B
	local max, min = math.max(r, g, b), math.min(r, g, b)

	local d = max - min
	local s = max == 0 and 0 or d / max

	local h = 0
	if max ~= min then
		if max == r then
			h = (g - b) / d + (g < b and 6 or 0)
		elseif max == g then
			h = (b - r) / d + 2
		elseif max == b then
			h = (r - g) / d + 4
		end
		h = h / 6
	end

	return h, s, max
end

local function fromHSV(h: number, s: number, v: number): Color3
	-- https://en.wikipedia.org/wiki/HSL_and_HSV#HSV_to_RGB
	local r, g, b

	local i = math.floor(h * 6)
	local f = h * 6 - i
	local p = v * (1 - s)
	local q = v * (1 - f * s)
	local t = v * (1 - (1 - f) * s)

	local j = i % 6
	if j == 0 then
		r, g, b = v, t, p
	elseif j == 1 then
		r, g, b = q, v, p
	elseif j == 2 then
		r, g, b = p, v, t
	elseif j == 3 then
		r, g, b = p, q, v
	elseif j == 4 then
		r, g, b = t, p, v
	elseif j == 5 then
		r, g, b = v, p, q
	end

	return Color3.new(r, g, b)
end

Expected behavior

I would expect the two functions to map to each other. If I get the HSV values from Color3.toHSV and then plug them back into Color3.fromHSV I expect to get the exact same color.

16 Likes

Thanks for the report! We’ll follow up when we have an update for you.

5 Likes