Colors.jl icon indicating copy to clipboard operation
Colors.jl copied to clipboard

RGB -> XTerm256 approximation?

Open jakewilliami opened this issue 3 years ago • 13 comments

Hi there! A few months back, I wrote a port of this gist to convert RGB to XTerm256 colours (or their nearest approximation).

I don't know much about colours. This might have been done in this Colors.jl package, but obfuscated by the technical names of the colour spaces. However, if this hasn't been done, I can add this code to here in a PR. If I found it useful, maybe someone else will.

Is this the best place for such a function?

P.S., in the process of getting permission from Micah Elliott (the original author of the code) to publish the port. Will credit them at the top of the script.

jakewilliami avatar Apr 30 '21 02:04 jakewilliami

I think this already exists. See https://github.com/KristofferC/Crayons.jl

johnnychen94 avatar Apr 30 '21 02:04 johnnychen94

Crayons.jl has the conversion feature, but I don't think there is a public API just for the conversion. At least, Crayons.jl does not provide an interface to the types defined in ColorTypes.jl. Another point of concern is that Crayons.jl's implementation is not compatible with XTerm256. (cf. https://github.com/JuliaDocs/ANSIColoredPrinters.jl/pull/8) FWIW, ImageInTerminal.jl has the same issue.

kimikage avatar Apr 30 '21 03:04 kimikage

Note that I have considered this issue before, and I realized that finding an approximate color is not so easy.

kimikage avatar Apr 30 '21 09:04 kimikage

Micah gave me permission to make public the ported code. You can find it in this gist, though it will probably need a lot of clean-up (see original gist for more detail and notes in comments).

I also contacted Kristoffer, who wrote Crayons.jl. This is what he wrote. This is very reminiscent of this comment in the original gist. Perhaps someone that knows more about colours can shed some light on which method might be better.

Let me know what you think @kimikage.

jakewilliami avatar May 01 '21 08:05 jakewilliami

At least, as I mentioned above, the current implementations of Crayons.jl and ImageInTerminal.jl assume so-called web-safe colors, not so-called terminal colors.

Therefore, the gist version is better WRT the palette. The problem is how to find the closest color.

On the other hand, converting from a terminal color (index) to RGB is easy.

using Colors, FixedPointNumbers

_to_term_tone256(v::Integer) = v * 0x28 + (v === zero(v) ? 0x00 : 0x37)
function from_term256(i::Integer)
    i8 = i % UInt8
    if i8 < 0x10
        term16 = RGB.(reinterpret.(RGB24, (0x000000, 0x800000, 0x008000, 0x808000, 
                                           0x000080, 0x800080, 0x008080, 0xc0c0c0,
                                           0x808080, 0xff0000, 0x00ff00, 0xffff00,
                                           0x0000ff, 0xff00ff, 0x00ffff, 0xffffff)))
        return @inbounds term16[i8 + 1]
    elseif i8 < 0xe8
        c = i8 - 0x10
        r = _to_term_tone256(c ÷ 0x24) % UInt8
        g = _to_term_tone256((c ÷ 0x6) % 0x6) % UInt8
        b = _to_term_tone256(c % 0x6) % UInt8
        return RGB(reinterpret.(N0f8, (r, g, b))...)
    else
        c = i8 - 0xe8
        return RGB(reinterpret(N0f8, c * 0xa + 0x8))
    end
end

kimikage avatar May 01 '21 09:05 kimikage

I'm not happy with it's performance, but the following just works.

_to_term_tone6(v::UInt8) = (((max(v, 0x4a) + 0x0005) * 0x00cd) >> 0xd) - 0x1

to_term256(@nospecialize(c::Color)) = to_term256(RGB24(c))

function to_term256(c::C) where C <: AbstractRGB
    rgb = RGB24(c)
    r0 = reinterpret(red(rgb))
    g0 = reinterpret(green(rgb))
    b0 = reinterpret(blue(rgb))
    rt = _to_term_tone6(r0)
    gt = _to_term_tone6(g0)
    bt = _to_term_tone6(b0)
    d = max(r0, g0, b0) - min(r0, g0, b0)
    if (rt === gt === bt) || d < 0x15
        gr16 = 0x003d * r0 + 0x0079 * g0 + 0x0017 * b0
        ths = (0x04, 0x5c, 0x61, 0x84, 0x89, 0xac, 0xb1, 0xd4, 0xd9, 0xf7) .* 0x00cd
        if mapreduce(th -> gr16 < th, ⊻, ths; init=false)
            return Int(min((gr16 - 0x0334) >> 0xb, 0x17)) + 232
        end
    end
    return Int(rt * 0x24 + gt * 0x6 + bt) + 16
end

kimikage avatar May 01 '21 13:05 kimikage

term256 G - R - B (time-y-x)

The left shows the nearest neighbor colors based on DE_2000(1, 1, 1), and the right shows the channel-wise rounded colors. The result on the left is not necessarily the best (e.g. there should be no sharp edge corners), but the difference is certainly significant.

There are several possible approaches to improve the conversion, but there are the following problems.

  • Searching with a loop is slow.
  • Lookup tables tend to be huge.
  • Converting to a color space such as Lab/Luv is expensive.

kimikage avatar May 01 '21 23:05 kimikage

term256_yrb Y (Rec.601) - R - B (time-y-x)

term256_ygb Y (Rec.601) - G - B (time-y-x)

In the figures above, you can see something like a checker pattern. In other words, this does not seem to be a problem that can be solved by converting to a perceptually uniform color space (e.g. Lab/Luv). This is because converting to a perceptually uniform color space will result in a discontinuity (i.e. zigzag) in the color index space.

Perhaps it is better to diffuse the rounding error in RGB space.

kimikage avatar May 02 '21 02:05 kimikage

As long as round-trip compatibility is ensured for 240 colors, there is no need to maintain compatibility for the rest. So, I think improvements can be left as a future task.

The other thing we need to decide is the API design. One option is to provide conversion methods like to_term256/from_term256. Another option is to define a single-component color like Term256Color <: Color{UInt8, 1} and use the convert scheme for its conversion. However, currently, Color{T,1} is interpreted as grayscale, which can cause trouble.

kimikage avatar May 02 '21 05:05 kimikage

I know very little about this. My understanding is that you're going to add this into Colors.jl. Just curious to ask, is there any reason that we can't add/extend this functionality inside Crayons?

The other thing we need to decide is the API design.

Both to_term256/from_term256 and Term256Color are good to me.

johnnychen94 avatar May 02 '21 06:05 johnnychen94

Crayons.jl is not dependent on ColorTypes.jl. That's both an advantage and a disadvantage. I am not in a position to decide its dependency.

kimikage avatar May 02 '21 06:05 kimikage

This is off-topic, but is there a package for color quantization or color reduction in the JuliaImages ecosystem? The feature for mapping pixel colors to a specific palette (with high quality but high cost) should be there.

kimikage avatar May 04 '21 12:05 kimikage

I think I've supported this feature in https://github.com/JuliaImages/ImageInTerminal.jl/pull/62... Maybe I should just port the codes to Colors?

johnnychen94 avatar May 03 '22 16:05 johnnychen94