Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions Project.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name = "ImageView"
uuid = "86fae568-95e7-573e-a6b2-d8a6b900c9ef"
author = ["Tim Holy <tim.holy@gmail.com"]
version = "0.13.0"
author = ["Tim Holy <tim.holy@gmail.com", "Jared Wahlstrand <jwahlstrand@gmail.com>"]
version = "0.13.1"

[deps]
AxisArrays = "39de3d68-74b9-583c-8d2d-e117c070f3a9"
Expand Down
45 changes: 44 additions & 1 deletion src/ImageView.jl
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,7 @@ using Compat # for @constprop :none
export AnnotationText, AnnotationPoint, AnnotationPoints,
AnnotationLine, AnnotationLines, AnnotationBox
export CLim, annotate!, annotations, canvasgrid, imshow, imshow!, imshow_gui, imlink,
roi, scalebar, slice2d
roi, scalebar, setup_contrast_popup!, slice2d

const AbstractGray{T} = Color{T,1}
const GrayLike = Union{AbstractGray,Number}
Expand Down Expand Up @@ -733,6 +733,49 @@ function create_contrast_popup(canvas, enabled, hists, clim)
end
end

function dummy_histsig(clim::Observable{CLim{T}}; floor=nothing) where {T<:GrayLike}
Th = float(T)
cl = clim[]
lo, hi = Th(cl.min), Th(cl.max)
if !(lo < hi)
lo, hi = zero(Th), one(Th)
end
span = hi - lo
rnglo = floor === nothing ? lo - span/2 : max(Th(floor), lo - span/2)
rng = LinRange(rnglo, hi + span/2, 300)
Observable(Histogram(rng, zeros(Int, 299), :right, false))
end

dummy_histsigs(clim::Observable{CLim{T}}; floor=nothing) where {T<:GrayLike} =
[dummy_histsig(clim; floor)]

dummy_histsigs(clim::Observable{CLim{T}}; floor=nothing) where {T<:AbstractRGB} =
[dummy_histsig(map(x->channel_clim(red, x), clim); floor),
dummy_histsig(map(x->channel_clim(green, x), clim); floor),
dummy_histsig(map(x->channel_clim(blue, x), clim); floor)]

"""
setup_contrast_popup!(canvas, clim; img=nothing)

Set up a right-click context menu on `canvas` that allows interactive
adjustment of `clim` via a contrast GUI window. If `img` is an
`Observable` wrapping an array compatible with `clim`, a histogram of
pixel intensities is computed and shown whenever the GUI is opened.
Without `img`, the GUI shows sliders only (no histogram).

This is a lower-level complement to [`imshow`](@ref), useful when
contrast is managed internally by the image type but an interactive
right-click popup is still desired.
"""
function setup_contrast_popup!(canvas, clim::Observable{CLim{T}};
img::Union{Nothing,Observable}=nothing,
floor=nothing) where T
enabled = Observable(false)
histsigs = img === nothing ? dummy_histsigs(clim; floor) : histsignals(enabled, img, clim)
push!(canvas.preserved, create_contrast_popup(canvas, enabled, histsigs, clim))
return canvas
end

function map_image_roi(@nospecialize(img), zr::Observable{ZoomRegion{T}}, slices...) where T
map(zr, slices...) do r, s...
cv = r.currentview
Expand Down
45 changes: 45 additions & 0 deletions test/contrast.jl
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
using ImageView, ImageCore, ImageView.Observables, MultiChannelColors
using Gtk4: Gtk4
using GtkObservables: GtkObservables
using Test

@testset "contrast GUI" begin
Expand Down Expand Up @@ -32,3 +33,47 @@ using Test
@test_broken sum(h.weights) > 0
end
end

@testset "setup_contrast_popup!" begin
# dummy_histsig: range is [lo-span/2, hi+span/2] with zero counts
clim = Observable(CLim(0.2f0, 0.8f0))
hsig = ImageView.dummy_histsig(clim)
h = hsig[]
@test all(iszero, h.weights)
@test length(h.weights) == 299 # 300-point LinRange → 299 bins
@test first(h.edges[1]) ≈ -0.1f0 # 0.2 - 0.6/2
@test last(h.edges[1]) ≈ 1.1f0 # 0.8 + 0.6/2

# degenerate CLim (min == max) falls back to [0,1]-based range
hsig_degen = ImageView.dummy_histsig(Observable(CLim(0.5f0, 0.5f0)))
h_degen = hsig_degen[]
@test first(h_degen.edges[1]) ≈ -0.5f0 # 0 - 1/2
@test last(h_degen.edges[1]) ≈ 1.5f0 # 1 + 1/2

# dummy_histsigs: 1 signal for GrayLike, 3 for AbstractRGB
@test length(ImageView.dummy_histsigs(Observable(CLim(0.0f0, 1.0f0)))) == 1
@test length(ImageView.dummy_histsigs(
Observable(CLim(RGB(0f0,0f0,0f0), RGB(1f0,1f0,1f0))))) == 3

# setup_contrast_popup! registers the contrast_gui action on the canvas
# (gridsize (1,1) default → gd["canvas"] is a single Canvas, not a matrix)
gd = imshow_gui((50, 50))
canvas = gd["canvas"]
clim2 = Observable(CLim(0.0f0, 1.0f0))
n_preserved = length(canvas.preserved)
ret = setup_contrast_popup!(canvas, clim2)
@test ret === canvas # returns the canvas
@test "contrast_gui" in keys(canvas.action_group)
@test length(canvas.preserved) > n_preserved # callback was preserved

# with img kwarg: uses histsignals; action still registered
gd2 = imshow_gui((50, 50))
canvas2 = gd2["canvas"]
clim3 = Observable(CLim(0.0f0, 1.0f0))
img = Observable(rand(Float32, 10, 10))
setup_contrast_popup!(canvas2, clim3; img=img)
@test "contrast_gui" in keys(canvas2.action_group)

Gtk4.destroy(gd["window"])
Gtk4.destroy(gd2["window"])
end
Loading