Skip to content

Commit bb695fa

Browse files
authored
viz: split row and column colors in bicoloring visualization (#195)
* viz: split row and column colors in bicoloring visualization * Add border in docs * Typo * Min diff
1 parent 1ce9fb8 commit bb695fa

4 files changed

Lines changed: 146 additions & 72 deletions

File tree

docs/src/vis.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -77,8 +77,9 @@ result_bi = coloring(S, problem_bi, algo_bi)
7777
A_img, Br_img, Bc_img = show_colors(
7878
result_bi;
7979
colorscheme=ColorSchemes.progress,
80-
background=RGB(1, 1, 1), # white
80+
background_color=RGB(1, 1, 1), # white
8181
scale=10,
82+
border=1,
8283
pad=2
8384
)
8485
```

ext/SparseMatrixColoringsColorsExt.jl

Lines changed: 124 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -26,88 +26,153 @@ using SparseMatrixColorings:
2626
compress
2727
using Colors: Colorant, RGB, RGBA, distinguishable_colors
2828

29-
const DEFAULT_BACKGROUND = RGBA(0, 0, 0, 0)
30-
const DEFAULT_SCALE = 1 # update docstring in src/images.jl when changing this default
31-
const DEFAULT_PAD = 0 # update docstring in src/images.jl when changing this default
32-
33-
# Sample n distinguishable colors, excluding the background color
34-
default_colorscheme(n, background) = distinguishable_colors(n, background; dropseed=true)
29+
# update docstring in src/images.jl when changing this default
30+
const DEFAULT_BACKGROUND_COLOR = RGBA(0, 0, 0, 0)
31+
const DEFAULT_BORDER_COLOR = RGB(0, 0, 0)
32+
const DEFAULT_SCALE = 1
33+
const DEFAULT_BORDER = 0
34+
const DEFAULT_PAD = 0
3535

3636
## Top-level function that handles argument errors, eagerly promotes types and allocates output buffer
3737

3838
function SparseMatrixColorings.show_colors(
3939
res::AbstractColoringResult;
4040
colorscheme=nothing,
41-
background::Colorant=DEFAULT_BACKGROUND, # color used for zero matrix entries and pad
41+
background_color::Colorant=DEFAULT_BACKGROUND_COLOR, # color used for zero matrix entries and pad
42+
border_color::Colorant=DEFAULT_BORDER_COLOR, # color used for zero matrix entries and pad
4243
scale::Int=DEFAULT_SCALE, # scale size of matrix entries to `scale × scale` pixels
44+
border::Int=DEFAULT_BORDER, # border around matrix entries
4345
pad::Int=DEFAULT_PAD, # pad between matrix entries
4446
warn::Bool=true,
4547
)
4648
scale < 1 && throw(ArgumentError("`scale` has to be ≥ 1."))
49+
border < 0 && throw(ArgumentError("`border` has to be ≥ 0."))
4750
pad < 0 && throw(ArgumentError("`pad` has to be ≥ 0."))
4851

4952
if !isnothing(colorscheme)
5053
if warn && ncolors(res) > length(colorscheme)
5154
@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."
5255
end
53-
colorscheme, background = promote_colors(colorscheme, background)
56+
colorscheme, background_color, border_color = promote_colors(
57+
colorscheme, background_color, border_color
58+
)
5459
else
55-
colorscheme = default_colorscheme(ncolors(res), convert(RGB, background))
60+
# Sample n distinguishable colors, excluding the background and border color
61+
colorscheme = distinguishable_colors(
62+
ncolors(res),
63+
[convert(RGB, background_color), convert(RGB, border_color)];
64+
dropseed=true,
65+
)
5666
end
57-
outs = allocate_outputs(res, background, scale, pad)
58-
return show_colors!(outs..., res, colorscheme, scale, pad)
67+
outs = allocate_outputs(res, background_color, border_color, scale, border, pad)
68+
return show_colors!(outs..., res, colorscheme, scale, border, pad)
5969
end
6070

61-
function promote_colors(colorscheme, background)
71+
function promote_colors(colorscheme, background_color, border_color)
6272
# eagerly promote colors to same type
63-
T = promote_type(eltype(colorscheme), typeof(background))
73+
T = promote_type(eltype(colorscheme), typeof(background_color), typeof(border_color))
6474
colorscheme = convert.(T, colorscheme)
65-
background = convert(T, background)
66-
return colorscheme, background
75+
background_color = convert(T, background_color)
76+
border_color = convert(T, border_color)
77+
return colorscheme, background_color, border_color
78+
end
79+
80+
# Given a CartesianIndex I of an entry in the original matrix,
81+
# this function returns the corresponding area in the output image as CartesianIndices.
82+
function matrix_entry_area(I::CartesianIndex, scale, border, pad)
83+
stencil = CartesianIndices((1:scale, 1:scale))
84+
return CartesianIndex(1, 1) * (border + pad) +
85+
(I - CartesianIndex(1, 1)) * (scale + 2border + pad) .+ stencil
86+
end
87+
88+
function matrix_entry_plus_border_area(I::CartesianIndex, scale, border, pad)
89+
stencil = CartesianIndices((1:(scale + 2border), 1:(scale + 2border)))
90+
return CartesianIndex(1, 1) * pad +
91+
(I - CartesianIndex(1, 1)) * (scale + 2border + pad) .+ stencil
6792
end
6893

6994
function allocate_outputs(
7095
res::Union{AbstractColoringResult{s,:column},AbstractColoringResult{s,:row}},
71-
background::Colorant,
96+
background_color::Colorant,
97+
border_color::Colorant,
7298
scale::Int,
99+
border::Int,
73100
pad::Int,
74101
) where {s}
75102
A = sparsity_pattern(res)
76103
B = compress(A, res)
77104
Base.require_one_based_indexing(A)
78105
Base.require_one_based_indexing(B)
79-
hA, wA = size(A) .* (scale + pad) .+ pad
80-
hB, wB = size(B) .* (scale + pad) .+ pad
81-
A_img = fill(background, hA, wA)
82-
B_img = fill(background, hB, wB)
106+
hA, wA = size(A) .* (scale + 2border + pad) .+ (pad)
107+
hB, wB = size(B) .* (scale + 2border + pad) .+ (pad)
108+
A_img = fill(background_color, hA, wA)
109+
B_img = fill(background_color, hB, wB)
110+
for I in CartesianIndices(A)
111+
if !iszero(A[I])
112+
area = matrix_entry_area(I, scale, border, pad)
113+
barea = matrix_entry_plus_border_area(I, scale, border, pad)
114+
A_img[barea] .= border_color
115+
A_img[area] .= background_color
116+
end
117+
end
118+
for I in CartesianIndices(B)
119+
if !iszero(B[I])
120+
area = matrix_entry_area(I, scale, border, pad)
121+
barea = matrix_entry_plus_border_area(I, scale, border, pad)
122+
B_img[barea] .= border_color
123+
B_img[area] .= background_color
124+
end
125+
end
83126
return A_img, B_img
84127
end
85128

86129
function allocate_outputs(
87130
res::AbstractColoringResult{s,:bidirectional},
88-
background::Colorant,
131+
background_color::Colorant,
132+
border_color::Colorant,
89133
scale::Int,
134+
border::Int,
90135
pad::Int,
91136
) where {s}
92137
A = sparsity_pattern(res)
93138
Br, Bc = compress(A, res)
94139
Base.require_one_based_indexing(A)
95140
Base.require_one_based_indexing(Br)
96141
Base.require_one_based_indexing(Bc)
97-
hA, wA = size(A) .* (scale + pad) .+ pad
98-
hBr, wBr = size(Br) .* (scale + pad) .+ pad
99-
hBc, wBc = size(Bc) .* (scale + pad) .+ pad
100-
A_img = fill(background, hA, wA)
101-
Br_img = fill(background, hBr, wBr)
102-
Bc_img = fill(background, hBc, wBc)
103-
return A_img, Br_img, Bc_img
104-
end
105-
106-
# Given a CartesianIndex I of an entry in the original matrix,
107-
# this function returns the corresponding area in the output image as CartesianIndices.
108-
function matrix_entry_area(I::CartesianIndex, scale, pad)
109-
stencil = CartesianIndices((1:scale, 1:scale))
110-
return CartesianIndex(pad, pad) + (I - CartesianIndex(1, 1)) * (scale + pad) .+ stencil
142+
hA, wA = size(A) .* (scale + 2border + pad) .+ (pad)
143+
hBr, wBr = size(Br) .* (scale + 2border + pad) .+ (pad)
144+
hBc, wBc = size(Bc) .* (scale + 2border + pad) .+ (pad)
145+
Ar_img = fill(background_color, hA, wA)
146+
Ac_img = fill(background_color, hA, wA)
147+
Br_img = fill(background_color, hBr, wBr)
148+
Bc_img = fill(background_color, hBc, wBc)
149+
for I in CartesianIndices(A)
150+
if !iszero(A[I])
151+
area = matrix_entry_area(I, scale, border, pad)
152+
barea = matrix_entry_plus_border_area(I, scale, border, pad)
153+
Ar_img[barea] .= border_color
154+
Ac_img[barea] .= border_color
155+
Ar_img[area] .= background_color
156+
Ac_img[area] .= background_color
157+
end
158+
end
159+
for I in CartesianIndices(Br)
160+
if !iszero(Br[I])
161+
area = matrix_entry_area(I, scale, border, pad)
162+
barea = matrix_entry_plus_border_area(I, scale, border, pad)
163+
Br_img[barea] .= border_color
164+
Br_img[area] .= background_color
165+
end
166+
end
167+
for I in CartesianIndices(Bc)
168+
if !iszero(Bc[I])
169+
area = matrix_entry_area(I, scale, border, pad)
170+
barea = matrix_entry_plus_border_area(I, scale, border, pad)
171+
Bc_img[barea] .= border_color
172+
Bc_img[area] .= background_color
173+
end
174+
end
175+
return Ar_img, Ac_img, Br_img, Bc_img
111176
end
112177

113178
## Implementations for different AbstractColoringResult types start here
@@ -117,8 +182,9 @@ function show_colors!(
117182
B_img::AbstractMatrix{<:Colorant},
118183
res::AbstractColoringResult{s,:column},
119184
colorscheme,
120-
scale,
121-
pad,
185+
scale::Int,
186+
border::Int,
187+
pad::Int,
122188
) where {s}
123189
# cycle color indices if necessary
124190
A_color_indices = mod1.(column_colors(res), length(colorscheme))
@@ -130,7 +196,7 @@ function show_colors!(
130196
for I in CartesianIndices(A)
131197
if !iszero(A[I])
132198
r, c = Tuple(I)
133-
area = matrix_entry_area(I, scale, pad)
199+
area = matrix_entry_area(I, scale, border, pad)
134200
if column_colors(res)[c] > 0
135201
A_img[area] .= A_colors[c]
136202
end
@@ -139,7 +205,7 @@ function show_colors!(
139205
for I in CartesianIndices(B)
140206
if !iszero(B[I])
141207
r, c = Tuple(I)
142-
area = matrix_entry_area(I, scale, pad)
208+
area = matrix_entry_area(I, scale, border, pad)
143209
B_img[area] .= B_colors[c]
144210
end
145211
end
@@ -151,8 +217,9 @@ function show_colors!(
151217
B_img::AbstractMatrix{<:Colorant},
152218
res::AbstractColoringResult{s,:row},
153219
colorscheme,
154-
scale,
155-
pad,
220+
scale::Int,
221+
border::Int,
222+
pad::Int,
156223
) where {s}
157224
# cycle color indices if necessary
158225
A_color_indices = mod1.(row_colors(res), length(colorscheme))
@@ -164,7 +231,7 @@ function show_colors!(
164231
for I in CartesianIndices(A)
165232
if !iszero(A[I])
166233
r, c = Tuple(I)
167-
area = matrix_entry_area(I, scale, pad)
234+
area = matrix_entry_area(I, scale, border, pad)
168235
if row_colors(res)[r] > 0
169236
A_img[area] .= A_colors[r]
170237
end
@@ -173,21 +240,23 @@ function show_colors!(
173240
for I in CartesianIndices(B)
174241
if !iszero(B[I])
175242
r, c = Tuple(I)
176-
area = matrix_entry_area(I, scale, pad)
243+
area = matrix_entry_area(I, scale, border, pad)
177244
B_img[area] .= B_colors[r]
178245
end
179246
end
180247
return A_img, B_img
181248
end
182249

183250
function show_colors!(
184-
A_img::AbstractMatrix{<:Colorant},
251+
Ar_img::AbstractMatrix{<:Colorant},
252+
Ac_img::AbstractMatrix{<:Colorant},
185253
Br_img::AbstractMatrix{<:Colorant},
186254
Bc_img::AbstractMatrix{<:Colorant},
187255
res::AbstractColoringResult{s,:bidirectional},
188256
colorscheme,
189-
scale,
190-
pad,
257+
scale::Int,
258+
border::Int,
259+
pad::Int,
191260
) where {s}
192261
scale < 3 && throw(ArgumentError("`scale` has to be ≥ 3 to visualize bicoloring"))
193262
# cycle color indices if necessary
@@ -206,35 +275,30 @@ function show_colors!(
206275
for I in CartesianIndices(A)
207276
if !iszero(A[I])
208277
r, c = Tuple(I)
209-
area = matrix_entry_area(I, scale, pad)
210-
for i in axes(area, 1), j in axes(area, 2)
211-
if j > i
212-
if column_colors(res)[c] > 0
213-
A_img[area[i, j]] = A_ccolors[c]
214-
end
215-
elseif i > j
216-
if row_colors(res)[r] > 0
217-
A_img[area[i, j]] = A_rcolors[r]
218-
end
219-
end
278+
area = matrix_entry_area(I, scale, border, pad)
279+
if column_colors(res)[c] > 0
280+
Ac_img[area] .= A_ccolors[c]
281+
end
282+
if row_colors(res)[r] > 0
283+
Ar_img[area] .= A_rcolors[r]
220284
end
221285
end
222286
end
223287
for I in CartesianIndices(Br)
224288
if !iszero(Br[I])
225289
r, c = Tuple(I)
226-
area = matrix_entry_area(I, scale, pad)
290+
area = matrix_entry_area(I, scale, border, pad)
227291
Br_img[area] .= B_rcolors[r]
228292
end
229293
end
230294
for I in CartesianIndices(Bc)
231295
if !iszero(Bc[I])
232296
r, c = Tuple(I)
233-
area = matrix_entry_area(I, scale, pad)
297+
area = matrix_entry_area(I, scale, border, pad)
234298
Bc_img[area] .= B_ccolors[c]
235299
end
236300
end
237-
return A_img, Br_img, Bc_img
301+
return Ar_img, Ac_img, Br_img, Bc_img
238302
end
239303

240304
end # module

src/show_colors.jl

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -5,19 +5,21 @@
55
66
Create a visualization for an [`AbstractColoringResult`](@ref), with the help of the [JuliaImages](https://juliaimages.org) ecosystem.
77
8-
- For `:column` or `:row` colorings, it returns a tuple `(A_img, B_img)`.
9-
- For `:bidirectional` colorings, it returns a tuple `(A_img, Br_img, Bc_img)`.
8+
- For `:column` or `:row` colorings, it returns a couple `(A_img, B_img)`.
9+
- For `:bidirectional` colorings, it returns a 4-tuple `(Ar_img, Ac_img, Br_img, Bc_img)`.
1010
1111
!!! warning
1212
This function is implemented in a package extension, using it requires loading [Colors.jl](https://github.com/JuliaGraphics/Colors.jl).
1313
1414
# Keyword arguments
1515
1616
- `colorscheme`: colors used for non-zero matrix entries. This can be a vector of `Colorant`s or a subsampled scheme from [ColorSchemes.jl](https://github.com/JuliaGraphics/ColorSchemes.jl).
17-
- `background::Colorant`: color used for zero matrix entries and pad. Defaults to `RGBA(0,0,0,0)`, a transparent background.
18-
- `scale::Int`: scale the size of matrix entries to `scale × scale` pixels. Defaults to `1`.
17+
- `background_color::Colorant`: color used for zero matrix entries and pad. Defaults to `RGBA(0,0,0,0)`, a transparent background.
18+
- `border_color::Colorant`: color used around matrix entries. Defaults to `RGB(0, 0, 0)`, a black border.
19+
- `scale::Int`: scale the size of matrix entries to `scale × scale` pixels. Defaults to `1`.
20+
- `border::Int`: set border width around matrix entries, in pixles. Defaults to `0`.
1921
- `pad::Int`: set padding between matrix entries, in pixels. Defaults to `0`.
2022
21-
For a matrix of size `(m, n)`, the resulting output will be of size `(m * (scale + pad) + pad, n * (scale + pad) + pad)`.
23+
For a matrix of size `(m, n)`, the resulting output will be of size `(m * (scale + 2border + pad) + pad, n * (scale + 2border + pad) + pad)`.
2224
"""
2325
function show_colors end

test/show_colors.jl

Lines changed: 13 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -32,9 +32,10 @@ algo = GreedyColoringAlgorithm(; decompression=:direct)
3232
@test A_img isa Matrix{<:Colorant}
3333

3434
pad = 2
35-
A_img, B_img = show_colors(result; scale=scale, pad=pad)
36-
@test size(A_img) == size(A) .* (scale + pad) .+ pad
37-
@test size(B_img) == size(B) .* (scale + pad) .+ pad
35+
border = 3
36+
A_img, B_img = show_colors(result; scale=scale, border=border, pad=pad)
37+
@test size(A_img) == size(A) .* (scale + 2border + pad) .+ pad
38+
@test size(B_img) == size(B) .* (scale + 2border + pad) .+ pad
3839
@test A_img isa Matrix{<:Colorant}
3940

4041
@testset "color cycling" begin
@@ -51,11 +52,13 @@ algo = GreedyColoringAlgorithm(; decompression=:direct)
5152
Br, Bc = compress(A, result)
5253

5354
scale = 3
54-
A_img, Br_img, Bc_img = show_colors(result; scale=scale)
55-
@test size(A_img) == size(A) .* scale
55+
Ar_img, Ac_img, Br_img, Bc_img = show_colors(result; scale=scale)
56+
@test size(Ar_img) == size(A) .* scale
57+
@test size(Ac_img) == size(A) .* scale
5658
@test size(Br_img) == size(Br) .* scale
5759
@test size(Bc_img) == size(Bc) .* scale
58-
@test A_img isa Matrix{<:Colorant}
60+
@test Ar_img isa Matrix{<:Colorant}
61+
@test Ac_img isa Matrix{<:Colorant}
5962
@test Br_img isa Matrix{<:Colorant}
6063
@test Bc_img isa Matrix{<:Colorant}
6164
end
@@ -72,4 +75,8 @@ end
7275
@test_throws ArgumentError show_colors(result; pad=-1)
7376
@test_nowarn show_colors(result; pad=0)
7477
end
78+
@testset "border too small" begin
79+
@test_throws ArgumentError show_colors(result; border=-1)
80+
@test_nowarn show_colors(result; border=0)
81+
end
7582
end

0 commit comments

Comments
 (0)