Skip to content

RGB -> XTerm256 approximation? #473

Description

@jakewilliami

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.

Activity

  1. changed the title [-]RGB -> iTerm256 approximation?[/-] [+]RGB -> XTerm256 approximation?[/+] on Apr 30, 2021
  2. johnnychen94 commented on Apr 30, 2021

    @johnnychen94
    Member

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

  3. kimikage commented on Apr 30, 2021

    @kimikage
    Collaborator

    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.

  4. kimikage commented on Apr 30, 2021

    @kimikage
    Collaborator

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

  5. jakewilliami commented on May 1, 2021

    @jakewilliami
    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.

  6. kimikage commented on May 1, 2021

    @kimikage
    Collaborator

    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
  7. kimikage commented on May 1, 2021

    @kimikage
    Collaborator

    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
  8. kimikage commented on May 1, 2021

    @kimikage
    Collaborator

    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.
  9. kimikage commented on May 2, 2021

    @kimikage
    Collaborator

    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.

  10. kimikage commented on May 2, 2021

    @kimikage
    Collaborator

    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.

  11. johnnychen94 commented on May 2, 2021

    @johnnychen94
    Member

    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.

  12. kimikage commented on May 2, 2021

    @kimikage
    Collaborator

    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.

  13. kimikage commented on May 4, 2021

    @kimikage
    Collaborator

    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.

  14. johnnychen94 commented on May 3, 2022

    @johnnychen94
    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

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions