Skip to content

Commit cf8de4c

Browse files
committed
Add setup_contrast_popup!
This allows non-AbstractArrays to exploit the contrast GUI.
1 parent a62584d commit cf8de4c

3 files changed

Lines changed: 96 additions & 2 deletions

File tree

Project.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
name = "ImageView"
22
uuid = "86fae568-95e7-573e-a6b2-d8a6b900c9ef"
33
author = ["Tim Holy <[email protected]", "Jared Wahlstrand <[email protected]>"]
4-
version = "0.13.0"
4+
version = "0.13.1"
55

66
[deps]
77
AxisArrays = "39de3d68-74b9-583c-8d2d-e117c070f3a9"

src/ImageView.jl

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ using Compat # for @constprop :none
1818
export AnnotationText, AnnotationPoint, AnnotationPoints,
1919
AnnotationLine, AnnotationLines, AnnotationBox
2020
export CLim, annotate!, annotations, canvasgrid, imshow, imshow!, imshow_gui, imlink,
21-
roi, scalebar, slice2d
21+
roi, scalebar, setup_contrast_popup!, slice2d
2222

2323
const AbstractGray{T} = Color{T,1}
2424
const GrayLike = Union{AbstractGray,Number}
@@ -733,6 +733,49 @@ function create_contrast_popup(canvas, enabled, hists, clim)
733733
end
734734
end
735735

736+
function dummy_histsig(clim::Observable{CLim{T}}; floor=nothing) where {T<:GrayLike}
737+
Th = float(T)
738+
cl = clim[]
739+
lo, hi = Th(cl.min), Th(cl.max)
740+
if !(lo < hi)
741+
lo, hi = zero(Th), one(Th)
742+
end
743+
span = hi - lo
744+
rnglo = floor === nothing ? lo - span/2 : max(Th(floor), lo - span/2)
745+
rng = LinRange(rnglo, hi + span/2, 300)
746+
Observable(Histogram(rng, zeros(Int, 299), :right, false))
747+
end
748+
749+
dummy_histsigs(clim::Observable{CLim{T}}; floor=nothing) where {T<:GrayLike} =
750+
[dummy_histsig(clim; floor)]
751+
752+
dummy_histsigs(clim::Observable{CLim{T}}; floor=nothing) where {T<:AbstractRGB} =
753+
[dummy_histsig(map(x->channel_clim(red, x), clim); floor),
754+
dummy_histsig(map(x->channel_clim(green, x), clim); floor),
755+
dummy_histsig(map(x->channel_clim(blue, x), clim); floor)]
756+
757+
"""
758+
setup_contrast_popup!(canvas, clim; img=nothing)
759+
760+
Set up a right-click context menu on `canvas` that allows interactive
761+
adjustment of `clim` via a contrast GUI window. If `img` is an
762+
`Observable` wrapping an array compatible with `clim`, a histogram of
763+
pixel intensities is computed and shown whenever the GUI is opened.
764+
Without `img`, the GUI shows sliders only (no histogram).
765+
766+
This is a lower-level complement to [`imshow`](@ref), useful when
767+
contrast is managed internally by the image type but an interactive
768+
right-click popup is still desired.
769+
"""
770+
function setup_contrast_popup!(canvas, clim::Observable{CLim{T}};
771+
img::Union{Nothing,Observable}=nothing,
772+
floor=nothing) where T
773+
enabled = Observable(false)
774+
histsigs = img === nothing ? dummy_histsigs(clim; floor) : histsignals(enabled, img, clim)
775+
push!(canvas.preserved, create_contrast_popup(canvas, enabled, histsigs, clim))
776+
return canvas
777+
end
778+
736779
function map_image_roi(@nospecialize(img), zr::Observable{ZoomRegion{T}}, slices...) where T
737780
map(zr, slices...) do r, s...
738781
cv = r.currentview

test/contrast.jl

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using ImageView, ImageCore, ImageView.Observables, MultiChannelColors
22
using Gtk4: Gtk4
3+
using GtkObservables: GtkObservables
34
using Test
45

56
@testset "contrast GUI" begin
@@ -32,3 +33,53 @@ using Test
3233
@test_broken sum(h.weights) > 0
3334
end
3435
end
36+
37+
@testset "setup_contrast_popup!" begin
38+
# dummy_histsig: range is [lo-span/2, hi+span/2] with zero counts
39+
clim = Observable(CLim(0.2f0, 0.8f0))
40+
hsig = ImageView.dummy_histsig(clim)
41+
h = hsig[]
42+
@test all(iszero, h.weights)
43+
@test length(h.weights) == 299 # 300-point LinRange → 299 bins
44+
@test first(h.edges[1]) -0.1f0 # 0.2 - 0.6/2
45+
@test last(h.edges[1]) 1.1f0 # 0.8 + 0.6/2
46+
47+
# histogram range updates reactively when clim changes
48+
clim[] = CLim(0.0f0, 0.5f0)
49+
h2 = hsig[]
50+
@test first(h2.edges[1]) -0.25f0 # 0.0 - 0.5/2
51+
@test last(h2.edges[1]) 0.75f0 # 0.5 + 0.5/2
52+
53+
# degenerate CLim (min == max) falls back to [0,1]-based range
54+
hsig_degen = ImageView.dummy_histsig(Observable(CLim(0.5f0, 0.5f0)))
55+
h_degen = hsig_degen[]
56+
@test first(h_degen.edges[1]) -0.5f0 # 0 - 1/2
57+
@test last(h_degen.edges[1]) 1.5f0 # 1 + 1/2
58+
59+
# dummy_histsigs: 1 signal for GrayLike, 3 for AbstractRGB
60+
@test length(ImageView.dummy_histsigs(Observable(CLim(0.0f0, 1.0f0)))) == 1
61+
@test length(ImageView.dummy_histsigs(
62+
Observable(CLim(RGB(0f0,0f0,0f0), RGB(1f0,1f0,1f0))))) == 3
63+
64+
# setup_contrast_popup! registers the contrast_gui action on the canvas
65+
# (gridsize (1,1) default → gd["canvas"] is a single Canvas, not a matrix)
66+
gd = imshow_gui((50, 50))
67+
canvas = gd["canvas"]
68+
clim2 = Observable(CLim(0.0f0, 1.0f0))
69+
n_preserved = length(canvas.preserved)
70+
ret = setup_contrast_popup!(canvas, clim2)
71+
@test ret === canvas # returns the canvas
72+
@test "contrast_gui" in keys(canvas.action_group)
73+
@test length(canvas.preserved) > n_preserved # callback was preserved
74+
75+
# with img kwarg: uses histsignals; action still registered
76+
gd2 = imshow_gui((50, 50))
77+
canvas2 = gd2["canvas"]
78+
clim3 = Observable(CLim(0.0f0, 1.0f0))
79+
img = Observable(rand(Float32, 10, 10))
80+
setup_contrast_popup!(canvas2, clim3; img=img)
81+
@test "contrast_gui" in keys(canvas2.action_group)
82+
83+
Gtk4.destroy(gd["window"])
84+
Gtk4.destroy(gd2["window"])
85+
end

0 commit comments

Comments
 (0)