Handling colors in new `Makie.voxels` function

The new voxels function was introduced in Makie v0.21 to plot 3D Cartesian grids more efficiently. However, it assumes Array{<:Real,3} for the colors. Can it sill be used with general Colors.jl colors? It is easy to convert Gray to Real, but multi-channel colors (e.g. RGB) cannot be converted to Gray without loss.

As it is now you can’t pass RGB colors directly. You either have to make a colormap and pass float values or pass an array of RGB colors as color and use UInt8 voxel ids to index that array.

The assumption with voxels is that you want to plot large amount of them, so each individual voxel carries as little data as possible. Regardless of the input you use they get compressed to UInt8. If you value color depth over quantity you can always just use a meshscatter with a Rect3.

1 Like

Assuming we have a color vector with colors from Colors.jl how would you call Makie.voxels to get a good approximation of the colors? Any MWE to get started? :slight_smile:

If you have a function that converts an Array{RGB, 3} to an Array{UInt8, 3} of indices (with 0 being invisible) and a vector of (up to 255) colors then you just need to call voxels(indices, color = colors).

I think my question is how to write such function? Do you have a specific transformation of color spaces in mind?

Yea, that’s the hard part. I don’t know any good way to do that, though I’m wondering if the Image and Color libraries have something useful for that. Some older gifs have a heavily compressed color space so I’d assume there is a algorithm for it out there

1 Like

I did some searching. The keywords for this are color palette, color quantization and color depth rather than color space.

One relatively simple option would be to use Clustering:

using Colors
using Clustering
using GLMakie

# Sample input
N = 30
cs = [RGB(r^2, g^2, b^2) for 
    r in range(0.2, 1, length=N), 
    g in range(0, 0.6, length=N), 
    b in range(0.3, 1, length=N)]
        
begin
    X = Matrix{Float32}(undef, 3, length(cs))
    for (i, c) in enumerate(cs)
        lab = Lab(c)
        X[1, i] = lab.l
        X[2, i] = lab.a
        X[3, i] = lab.b
    end

    @time result = kmeans(X, 255)
    colors = [RGB(Lab(result.centers[:, i]...)) for i in 1:255]

    ids = map(cs) do c
        _, idx = findmin(ref -> colordiff(c, ref), colors)
        UInt8(idx)
    end
end;

voxels(ids, color = colors)

image

1 Like

This could be implement as a convert_arguments(::Type{Voxels}, xs, ys, zs, vs::AbstractArray{3, <: Colors.Colorant}) that returns a PlotSpec, if I understand what happened here correctly?

If so, it would be cool to push this to some MakieClusteringExt extension package, so that it’s isolated from the user!

I wouldn’t place it in an extension with Clustering.jl as a dependency. The K-means algorithm is super easy to implement, and a basic implementation can live in Makie.jl itself to provide this fallback.

I don’t think this solution is fast enough to be used in general. It already takes on the order of 1s for 30^3 voxels, 1min for 100^3. Following some crude benchmarks that would end up taking 15h for a 1000^3 voxel plot, which is around the upper limit that can be handled by voxels atm.colordiff is also fairly slow here, but should scale better.
Screenshot 2024-06-11 152851

I also tried a naive Octree approach with RGB colors instead of LAB. It’s much faster (~1s for 100^3, depends on breadth of colors) but doesn’t do as good of a job. With colordiff and a direct euclidean distance:

Screenshot 2024-06-11 153857 Screenshot 2024-06-11 154035

There is also the option to used a predefined colormap, like List of software palettes - Wikipedia. These will look pretty bad if you don’t use a wide color spectrum though.

begin
    rgb676 = [RGB(r/0xff, g/0xff, b/0xff) 
        for r in (0x00, 0x33, 0x66, 0x99, 0xCC, 0xFF)
        for g in (0x00, 0x2A, 0x55, 0x80, 0xAA, 0xD4, 0xFF)
        for b in (0x00, 0x33, 0x66, 0x99, 0xCC, 0xFF)
    ]
    ids = map(cs) do c
        _, idx = findmin(ref -> colordiff(c, ref), rgb676)
        UInt8(idx)
    end
end
voxels(ids, color = rgb676, gap = 0.0)

Screenshot 2024-06-11 154735

1 Like