Macro snippet to calculate the Euclidean distance between filament colors

I’m using an X-in-1-out hotend with a purge bucket. Since I don’t have a purge block, I can reduce waste by only purging exactly the amount of filament needed to ensure that the new color/material is fully primed. I have noticed that in general, the more “dissimilar” the filament colors are, the more purging is necessary to avoid visible color bleeding. So I thought, what if it were possible to quantify the difference between the filament colors and use that figure to determine the purge volume during a filament change?

Turns out there’s a fairly straightforward equation to determine the Euclidean distance between two colors in the RGB colorspace. I use SuperSlicer, which has the ability to export as a gcode variable the hex color code that the user assigned to the filament. So I worked up some Klipper macros to use this information to calculate the Euclidean distance between the two filament colors and increase the purge volume the further apart the colors are.

I’ll share commented snippets rather than my entire macro, since my LOAD_FILAMENT macro is otherwise pretty specific to my setup.

First though, some explanation of some of the underlying implementation:

  • I have a GLOBALS macro that is used solely to store certain variables and their values so that they are accessible to other macros during the print. This is the “globals” namespace in the snippet below, and it’s where the information about the new (incoming) filament is temporarily stored and accessed.
  • I also use “save_variables” to save to disk data about the filament that is successfully loaded into the hotend. This data persists between prints (and reboots) so Klipper can always know what filament (or remnants of filament) is in the nozzle. This is the “svv” namespace in the snippet below.
  • Later in the macro (not shown in the snippet below) once the purge is complete, the filament data in “globals” is saved to “svv,” replacing the data that was there before.
  • SuperSlicer exports the hex color code with a leading #, which is technically correct but causes problems because in gcode, everything after # is considered a comment. So far I haven’t figured out how (or if it’s even possible) to escape the # character, so as a workaround, I’m using a postprocessing script in the slicer to excise the # character from the filament color lines.
  • The maximum Euclidean distance (between black and white) is about 441. For no particular reason, I divided this 0-441 range roughly into 6 segments, adding filament purge in multiples of 10mm depending on which segment the color difference falls into. So very similar colors will only have an additional 10mm of purge while very dissimilar colors will have 60mm of additional purge.
  • I have not actually tested yet the specific purge volumes generated by the snippet below. 60mm for very dissimilar colors may be too much, too little, or just right. This is mostly a proof of concept at this point, but I plan to follow up after some real-world testing.

So here’s the snippet. I’m interested to hear any comments from anyone who has tried something similar to this:

    {% if globals.filament_id == svv.FILAMENT_ID %}
        ; If filament IDs match, skip secondary purge because it's the same filament as before.
        {% set SECONDARY_PURGE_VOLUME = 0 %}
    {% else %}
        {% set color1_hex = globals.filament_color_hex %}
        {% set color2_hex = svv.filament_color_hex %}
        {% set c1 = [] %}
        {% set c2 = [] %}
        ; Get RGB values from hex
        {% for i in range(0,6,2) %}
            { c1.append(color1_hex[i:i+2]|int(base=16)) }
            { c2.append(color2_hex[i:i+2]|int(base=16)) }
        {% endfor %}
        ; Perform the Euclidean distance calculation
        {% set sums = [] %}
        {% for n in range(3) %}
            {sums.append((c2[n] - c1[n])**2)}
        {% endfor %}
        {% set diff = (sums|sum())**(1/2) %}
        { action_respond_info('Color difference between new filament (%s) and last filament (%s): %.2f'|
                               format(globals.filament_id, svv.filament_id, diff))}
        {% if diff <= 74 %}
            {% set SECONDARY_PURGE_VOLUME = 10|int %}
        {% elif 74 < diff <= 147 %}
            {% set SECONDARY_PURGE_VOLUME = 20|int %}
        {% elif 147 < diff <= 220 %}
            {% set SECONDARY_PURGE_VOLUME = 30|int %}
        {% elif 220 < diff <= 294 %}
            {% set SECONDARY_PURGE_VOLUME = 40|int %}
        {% elif 294 < diff <= 368 %}
            {% set SECONDARY_PURGE_VOLUME = 50|int %}
        {% elif 368 < diff %}
            {% set SECONDARY_PURGE_VOLUME = 60|int %}
        {% endif %}
        ; Additional purge needed when changing to different filament type
        {% if globals.filament_type != svv.filament_type %}
            { action_respond_info('Different filament type detected. Adding 30mm to secondary purge...') }
            {% set SECONDARY_PURGE_VOLUME = SECONDARY_PURGE_VOLUME + 30 %}
        {% endif %}
        { action_respond_info('Setting secondary purge to %i mm...'|format(SECONDARY_PURGE_VOLUME))}
    {% endif %}
1 Like

It’s actually more simple than that, because it goes mainly one way. Switching dark-to-light requires lots of purging, while switching light-to-dark requires very little purging. Even switching white to black, which I suppose is a very long “euclidean distance”, requires practically no purging at all.

So perhaps you could calculate the amount of purging based on how light the second color is, and if the first color was darker then add purging according to how much darker it was.

Good point. White-to-black definitely requires more purge than black-to-white even though the distance between the two is the same. I think it has something to do with the amount of pigment needed to produce darker colors.

Maybe the thing to do is assign each color a “weight” based on its distance from white. A greater purge volume would be needed when a “heavier” color is being purged by a “lighter” color, and a lesser purge volume would be needed when going the other way around. Similarly, a “heavy” color might not require as much purge volume if being purged by an equally “heavy” color.

Even then though, it would probably still be optimal to factor in the distance between the actual colors themselves, because the closer the colors are, the less likely there is to be perceptual bleed. For example, red (255,0,0) and green (0,255,0) are both the same Euclidean distance from white (255), so might have the same “weight.” But red (255,0,0) and red (254,0,0) are also almost the same Euclidean distance from white, yet are virtually the same color. Purging red-to-green or green-to-red clearly would require more purge volume than purging red to red.

Hmm…

You can do the way I suggested, but calculate the additional purge per channel.

Like this pseudocode when switching from color c1 to color c2:

amountToPurge( c1, c2 ) =
    baseAmount +
    (c2.r + c2.g + c2.b) * lightnessMultiplier +
    (max(c2.r - c1.r, 0) + max(c2.g - c1.g, 0) + max(c2.b - c1.b, 0)) * lighteningMultiplier

For the 3 constants, baseAmount should be the amount to purge even in the best case, maybe 4 mm, and lightnessMultiplier and lighteningMultiplier could be something like 0.1 mm.

Thanks @marcus-in-3d , I think this is very much on the right track. I’ve been doing some spreadsheet simulations of this approach. In many cases it produces results that seem consistent with expectations. For instance, switching from white to black would result in a purge of only 4mm (the base constant), while switching from black to white would result in a purge of 157mm. That’s probably more than necessary in real-world application but it’s good for proof-of-concept.

Some other interesting calculations from this approach:

  • White to Red: 29.5mm
  • Red to White: 131.5mm
  • Blue to Purple: 66.4 mm
  • Blue to Aqua: 80.5 mm
  • Yellow 2 (255,255,0) to Yellow 1 (255,255,153): 85.6mm
  • Red 1 (255,0,0) to Red 2 (254,0,0): 29.4 mm

The last two results are particularly interesting because they are a change between two shades of the same color, but reflect a purge volume that is much greater than expected. I think this is where the difference between the two colors may become relevant.

To try to account for this, I took the “lightnessMultiplier” constant and made it a variable based on the Euclidean distance (“dist”) between c1 and c2:

0.1 * (dist/442)

Leaving the other constants the same, the modified calculation produced the following results:

  • White to Red: 24.8mm
  • Red to White: 117.5mm
  • Blue to Purple: 39.2 mm
  • Blue to Aqua: 58.9.5 mm
  • Yellow 2 (255,255,0) to Yellow 1 (255,255,153): 42.3mm
  • Red 1 (255,0,0) to Red 2 (254,0,0): 4.1 mm

So for the Red<->White changes, there is a reduction in purge volume but it’s only about 10-15%. Meanwhile, the purge reduction in Yellow 2->Yellow 1 is 50%, and the reduction in Red 1->Red 2 is 86%.

I think I’m going to try to set up a rig to test a handful of filament pairs and empirically measure how much purge is necessary to eliminate bleed and then see if I can tune this equation to those values.

The lightnessMultiplier is like “additional base purge amount based on how light it is”, so you could just set it to much smaller, like, 0.005 (or even smaller) and that should make it more suitable to your situation, particularly when the shades are close to each other.

And if you also want to take darkening into account then add this at the end:

+ (max(c1.r - c2.r, 0) + max(c1.g - c2.g, 0) + max(c1.b - c2.b, 0)) * darkeningMultiplier

where darkeningMultiplier would be something small, like, 0.01 or even smaller.