Help with Macro addition to Color-Multi-Tool

Hello Termplate Guru’s,

I’ve recieved some help from the community for this project before, now I’m looking to build a new feature requested by the community.

I’m sure if I spent a couple of days on it I’d figure it out, but maybe we can shortcut that a bit with help from someone that sees curly braces in their sleep.

I have a function in my custom macro
GitHub - SirGoodenough/Color-Multi-Tool: Toolbox to work with and convert colors rgb, hs, xy, and Color Name. It will also provide random colors in any format, attempt to match your RGB value to an official color, and test if the color is valid (numbers or name). (The name map is in the download for this, avail thru HACS as well)

That accepts an rgb color code and returns the HA color name for it. It also accepts a range so you don’t have to guess exact match. This means if you ask rgb2name([100,100,100],5) it will return a list of the color names in HA that range from [95,95,95] to [105,105,105] and all with numbers within that range.
That all works.
The request is to return the one color closest, which would be a more useful function indeed.

I was thinking of adding a bool to the option list and have it do this. That works and I got that.

I am also thinking of having 2 sections in the jinja and re-use some of ths code with yaml anchors to shorten it up. Done that before so I have that as well.

My problem is trying to envision the process. Maybe iterating the search with progressively larger range, then as soon as something is in the list, grab the first one and return it? That whole looping stuff in jinja gets me every time… Maybe there’s a better way as well. I’m not sure.

Maybe this could be the closest brighter color or the closest darker color, not sure. Returning 1 close color would be the goal and more useful that what I came up with originally.

Here’s the current code.
Thanks so much for reading this over.
I hope I provided a fun Sunday Afternoon puzzle for someone. Loops make me loopy…


{% macro rgb2name(_range, rgbl) %}
{#-
  This template will accept a list representing an RGB value plus a range
    value representing the window size of the 'close enough' color name.
    It will return the Home Assistant Color name that matches it or is close
    to it within a +/- range you specify.

    > If you want only an exact match, then set the range to 0.
    > If you think that +10/-10 is close enough, then set the range to 10.

    For default the color is set to black [0,0,0] and the range is 0, so it will
    by give you black for invalid input.

  SAMPLE USAGE:
    {% from 'color_multi_tool.jinja' import rgb2name %}
    {{ rgb2name(10,rgbl) }}

  REMEMBER:
    Everything returned from a macro template is a string.

  #### 🗿License Notice:
    * I & my license require attribution as a link back to the original should
      you use this code in your own creation.
    * Here is a link to my license & the original github post
      https://github.com/SirGoodenough/Color-Multi-Tool?tab=License-1-ov-file
      expected to be followed & referenced
      as attribution should you use this code elsewhere.
#}
  {#- First a test to make sure input is valid #}
{%- if chkRGB(rgbl) | default(false) | bool %}
  {#- Set defaults #}
{%- set out = ["black"] | list %}
  {#- Pull in the data #}
{%- set _rnge = _range | default(0) | round(0,0) %}
{%- set iR = rgbl[0] | default(0) | int(0) %}
{%- set iG = rgbl[1] | default(0) | int(0) %}
{%- set iB = rgbl[2] | default(0) | int(0) %}
  {#- Define Ranges for each color #}
{%- set _Rl = rgbl[0] + _rnge %}
{%- set _Gl = rgbl[1] + _rnge %}
{%- set _Bl = rgbl[2] + _rnge %}
{%- set _Rg = rgbl[0] - _rnge %}
{%- set _Gg = rgbl[1] - _rnge %}
{%- set _Bg = rgbl[2] - _rnge %}
  {#- Grab matching colors out of the list. #}
{%- set out = _nameMap['_rgb'] |
  selectattr('r', 'ge', _Rg) | 
  selectattr('r', 'le', _Rl) | 
  selectattr('g', 'ge', _Gg) |
  selectattr('g', 'le', _Gl) |
  selectattr('b', 'ge', _Bg) |
  selectattr('b', 'le', _Bl) |
  map(attribute='color') | list | default(['black']) %}
{{- out -}}
{%- else %}
  {#- Input was not valid #}
{{-['black'] | list -}}
{%- endif %}
{% endmacro %}

I’ve since found this code researching another problem:
courtesy: Kelvin to RGB in python · GitHub

    unsigned minDist = 0xFFFFFF;
    unsigned minTemp = 0;
    for (unsigned i = 0; i < HEIGHT; i++) {
        unsigned rT = colorTable[i][0];
        unsigned gT = colorTable[i][1];
        unsigned bT = colorTable[i][2];
        unsigned dist = (r-rT)*(r-rT) + (g-gT)*(g-gT) + (b-bT)*(b-bT);
        if (dist < minDist) {
            minDist = dist;
            minTemp = colorTable[i][3];
        }
        }
    }
    return minTemp;

I don’t know the theory behind this, math wise, but it looks interesting…

I looked into getting closest named color from rgb a while back and set it aside because it was more than I wanted to deal with, and it wasn’t something I really had a reoccurring need for.

Here’s where I got with yours… Instead of taking a range input, it loops through increasing ranges until it gets at least one hit:

{%- set out_ns = namespace(final=[]) %}
{%- for val in range(10,151,5)%}
  {% if out_ns.final|count >= 1 %}
    {%break%}
  {% else %}
    {%- set _rnge = val | default(10) | round(0,0) %}
    {%- set d ={
    "_Rl": (rgbl[0] + _rnge),
    "_Gl": (rgbl[1] + _rnge),
    "_Bl": (rgbl[2] + _rnge),
    "_Rg": (rgbl[0] - _rnge),
    "_Gg": (rgbl[1] - _rnge),
    "_Bg": (rgbl[2] - _rnge) } %}    
  
    {%- set ns = namespace(selected=[]) %}
    {%- for x in ['r','g','b'] %}
      {%- set full = _nameMap['_rgb'] %}
      {%- set comp = [d.get('_'~x|upper~'l'), d.get('_'~x|upper~'g')] %}
      {%- set ns.selected = ns.selected + [{x: full | selectattr(x, 'le', comp[0]) | selectattr(x, 'ge', comp[1]) | list}] %}
    {%- endfor %}
    
    {%- set r_col = ns.selected[0].get('r')|map(attribute='color')|list%}
    {%- set g_col = ns.selected[1].get('g')|map(attribute='color')|list%}
    {%- set b_col = ns.selected[2].get('b')|map(attribute='color')|list%}
    
    {%- set rb = intersect(r_col,b_col) %}
    {%- set bg = intersect(b_col,g_col) %}
    {%- set rg = intersect(r_col,g_col) %}
    {%- set out_ns.final = intersect(intersect(rb,bg),intersect(rg,bg)) %}
  {% endif %}
{%- endfor %}
{{out_ns.final}}

It’s definitely not sophisticated and sometimes, when it returns more than one color name, one of them will be way off.

I think the better answer is probably a K-D Tree. But that’s outside my skills and attention span for today… and may be outside of what’s possible in Jinja.

1 Like

I’ve been slugging with this all day and got this working like this…

    {#- Set defaults #}
{%- set M = namespace(dist = 0, minDist = 200000, out = 'black', r = 0, g = 0, b = 0) %}
    {#- First a test to make sure input is valid #}
{%- if chkRGB(rgbl) | bool %}
  {%- set M.r = rgbl[0] | int(0) %}
  {%- set M.g = rgbl[1] | int(0) %}
  {%- set M.b = rgbl[2] | int(0) %}
  {%- if M.r + M.g + M.b != 0 %}
    {#- Find the best matching color in the list. #}
    {%- for best in _nameMap['_rgb'] %}
    {#- Sum of the square of the differences of r, g, & b. Lowest is best match. #}
      {%- set M.dist = (M.r-best.r)*(M.r-best.r)+(M.g-best.g)*(M.g-best.g)+(M.b-best.b)*(M.b-best.b) %}
      {%- if M.dist < M.minDist %}
        {%- set M.minDist = M.dist  %}
        {%- set M.out = best.color %}
      {%- endif %}
    {%- endfor %}
  {%- endif %}
{%- else %}
  {%- set M.out = 'black' %}
{%- endif %}
{{- M.out -}}

Based it on the code I found for getting rgb from kelvin above…

At least I actually understand namespace now…

That sum of the differences squared seems to work pretty good.

I pushed this up to a branch GitHub - SirGoodenough/Color-Multi-Tool at rgb_2_match_name

K-D Tree looks like harder math for me than reversing the magic formula you found for kelvin.

I’m going to use this same code basically and another table and reverse kelvin that way.
I found 2 people that said use the formulas you found to build the table one way, then save the table for looking up the reverse… I guess it’s the way to go.

Thanks for looking at it. I started this 12 hours ago…

1 Like

That will require scipy and it would require a custom integration. Or a custom jinja function built by a custom integration.

https://docs.scipy.org/doc//scipy-1.9.1/reference/generated/scipy.spatial.KDTree.html