Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

RGB -> XTerm256 approximation? #473

Open
jakewilliami opened this issue Apr 30, 2021 · 13 comments
Open

RGB -> XTerm256 approximation? #473

jakewilliami opened this issue Apr 30, 2021 · 13 comments

Comments

@jakewilliami
Copy link

jakewilliami commented Apr 30, 2021

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 jakewilliami changed the title RGB -> iTerm256 approximation? RGB -> XTerm256 approximation? Apr 30, 2021
@johnnychen94
Copy link
Member

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

@kimikage
Copy link
Collaborator

kimikage commented Apr 30, 2021

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. JuliaDocs/ANSIColoredPrinters.jl#8) FWIW, ImageInTerminal.jl has the same issue.

@kimikage kimikage reopened this Apr 30, 2021
@kimikage
Copy link
Collaborator

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

@jakewilliami
Copy link
Author

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.

@kimikage
Copy link
Collaborator

kimikage commented May 1, 2021

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
Copy link
Collaborator

kimikage commented May 1, 2021

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
Copy link
Collaborator

kimikage commented May 1, 2021

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
Copy link
Collaborator

kimikage commented May 2, 2021

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
Copy link
Collaborator

kimikage commented May 2, 2021

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.

@johnnychen94
Copy link
Member

johnnychen94 commented May 2, 2021

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.

@kimikage
Copy link
Collaborator

kimikage commented May 2, 2021

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
Copy link
Collaborator

kimikage commented May 4, 2021

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.

@johnnychen94
Copy link
Member

I think I've supported this feature in JuliaImages/ImageInTerminal.jl#62... Maybe I should just port the codes to Colors?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

3 participants