|
| 1 | +#= |
| 2 | +Visualize colored matrices using the Julia Images ecosystem. |
| 3 | +Colors.jl is nearly the most light-weight dependency to achieve this. |
| 4 | +
|
| 5 | +This code is written prioritizing maintainability over performance |
| 6 | +
|
| 7 | +How it works |
| 8 | +≡≡≡≡≡≡≡≡≡≡≡≡ |
| 9 | +- First, the outer `show_colors` function gets called, which |
| 10 | + - handles argument errors |
| 11 | + - eagerly promotes color types in `promote_colors` to support transparency |
| 12 | + - allocates an output buffer in `allocate_output` |
| 13 | +- The allocated output is a matrix filled with the background color |
| 14 | +- An internal `show_color!` function is called on the allocated output |
| 15 | + - for each non-zero entry in the coloring, the output is filled in |
| 16 | +=# |
| 17 | +module SparseMatrixColoringsColorsExt |
| 18 | + |
| 19 | +using SparseMatrixColorings: |
| 20 | + SparseMatrixColorings, |
| 21 | + AbstractColoringResult, |
| 22 | + sparsity_pattern, |
| 23 | + column_colors, |
| 24 | + row_colors |
| 25 | +using Colors: Colorant, RGB, RGBA, distinguishable_colors |
| 26 | + |
| 27 | +const DEFAULT_BACKGROUND = RGBA(0, 0, 0, 0) |
| 28 | +const DEFAULT_SCALE = 1 # update docstring in src/images.jl when changing this default |
| 29 | +const DEFAULT_PAD = 0 # update docstring in src/images.jl when changing this default |
| 30 | + |
| 31 | +# Sample n distinguishable colors, excluding the background color |
| 32 | +default_colorscheme(n, background) = distinguishable_colors(n, background; dropseed=true) |
| 33 | + |
| 34 | +ncolors(res::AbstractColoringResult{s,:column}) where {s} = maximum(column_colors(res)) |
| 35 | +ncolors(res::AbstractColoringResult{s,:row}) where {s} = maximum(row_colors(res)) |
| 36 | + |
| 37 | +## Top-level function that handles argument errors, eagerly promotes types and allocates output buffer |
| 38 | + |
| 39 | +function SparseMatrixColorings.show_colors( |
| 40 | + res::AbstractColoringResult; |
| 41 | + colorscheme=nothing, |
| 42 | + background::Colorant=DEFAULT_BACKGROUND, # color used for zero matrix entries and pad |
| 43 | + scale::Int=DEFAULT_SCALE, # scale size of matrix entries to `scale × scale` pixels |
| 44 | + pad::Int=DEFAULT_PAD, # pad between matrix entries |
| 45 | + warn::Bool=true, |
| 46 | +) |
| 47 | + scale < 1 && throw(ArgumentError("`scale` has to be ≥ 1.")) |
| 48 | + pad < 0 && throw(ArgumentError("`pad` has to be ≥ 0.")) |
| 49 | + |
| 50 | + if !isnothing(colorscheme) |
| 51 | + if warn && ncolors(res) > length(colorscheme) |
| 52 | + @warn "`show_colors` will reuse colors since the provided `colorscheme` has $(length(colorscheme)) colors and the matrix needs $(ncolors(res)). You can turn off this warning via the keyword argument `warn = false`, or choose a larger `colorscheme` from ColorSchemes.jl." |
| 53 | + end |
| 54 | + colorscheme, background = promote_colors(colorscheme, background) |
| 55 | + else |
| 56 | + colorscheme = default_colorscheme(ncolors(res), convert(RGB, background)) |
| 57 | + end |
| 58 | + out = allocate_output(res, background, scale, pad) |
| 59 | + return show_colors!(out, res, colorscheme, scale, pad) |
| 60 | +end |
| 61 | + |
| 62 | +function promote_colors(colorscheme, background) |
| 63 | + # eagerly promote colors to same type |
| 64 | + T = promote_type(eltype(colorscheme), typeof(background)) |
| 65 | + colorscheme = convert.(T, colorscheme) |
| 66 | + background = convert(T, background) |
| 67 | + return colorscheme, background |
| 68 | +end |
| 69 | + |
| 70 | +function allocate_output( |
| 71 | + res::AbstractColoringResult, background::Colorant, scale::Int, pad::Int |
| 72 | +) |
| 73 | + A = sparsity_pattern(res) |
| 74 | + Base.require_one_based_indexing(A) |
| 75 | + hi, wi = size(A) |
| 76 | + h = hi * (scale + pad) + pad |
| 77 | + w = wi * (scale + pad) + pad |
| 78 | + return fill(background, h, w) |
| 79 | +end |
| 80 | + |
| 81 | +# Given a CartesianIndex I of an entry in the original matrix, |
| 82 | +# this function returns the corresponding area in the output image as CartesianIndices. |
| 83 | +function matrix_entry_area(I::CartesianIndex, scale, pad) |
| 84 | + stencil = CartesianIndices((1:scale, 1:scale)) |
| 85 | + return CartesianIndex(pad, pad) + (I - CartesianIndex(1, 1)) * (scale + pad) .+ stencil |
| 86 | +end |
| 87 | + |
| 88 | +## Implementations for different AbstractColoringResult types start here |
| 89 | + |
| 90 | +function show_colors!( |
| 91 | + out, res::AbstractColoringResult{s,:column}, colorscheme, scale, pad |
| 92 | +) where {s} |
| 93 | + color_indices = mod1.(column_colors(res), length(colorscheme)) # cycle color indices if necessary |
| 94 | + colors = colorscheme[color_indices] |
| 95 | + pattern = sparsity_pattern(res) |
| 96 | + for I in CartesianIndices(pattern) |
| 97 | + if !iszero(pattern[I]) |
| 98 | + r, c = Tuple(I) |
| 99 | + area = matrix_entry_area(I, scale, pad) |
| 100 | + out[area] .= colors[c] |
| 101 | + end |
| 102 | + end |
| 103 | + return out |
| 104 | +end |
| 105 | + |
| 106 | +function show_colors!( |
| 107 | + out, res::AbstractColoringResult{s,:row}, colorscheme, scale, pad |
| 108 | +) where {s} |
| 109 | + color_indices = mod1.(row_colors(res), length(colorscheme)) # cycle color indices if necessary |
| 110 | + colors = colorscheme[color_indices] |
| 111 | + pattern = sparsity_pattern(res) |
| 112 | + for I in CartesianIndices(pattern) |
| 113 | + if !iszero(pattern[I]) |
| 114 | + r, c = Tuple(I) |
| 115 | + area = matrix_entry_area(I, scale, pad) |
| 116 | + out[area] .= colors[r] |
| 117 | + end |
| 118 | + end |
| 119 | + return out |
| 120 | +end |
| 121 | + |
| 122 | +end # module |
0 commit comments