diff --git a/README.md b/README.md index c435d3e..eaa6f69 100644 --- a/README.md +++ b/README.md @@ -5,3 +5,4 @@ [![Coverage](https://codecov.io/gh/HolyLab/CounterMarking.jl/branch/main/graph/badge.svg)](https://codecov.io/gh/HolyLab/CounterMarking.jl) Utilities for analyzing images from mouse territorial countermarking experiments. +See the [documentation](https://HolyLab.github.io/CounterMarking.jl/stable/) for more information. diff --git a/docs/src/index.md b/docs/src/index.md index 38827e2..54ff3fc 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -15,8 +15,8 @@ Tips on image quality: - Put the stimulus near one of the four corners - Ensure lighting is fairly uniform -- Try to ensure the entire image is of the filter paper, and that there aren't any black edges to the image (see lower left corner in the example above) - Make sure that any extraneous marks (e.g., the black writing in the image above) are of a very different color from scent marks. +- Ensure that all your images are of the same size (i.e., same number of pixels horizontally and vertically), even if there are some extra pixels on the edges of the image ## Tutorial @@ -68,7 +68,9 @@ and it will automatically "activate" this project and you'll have access to all [Pkg documentation](https://pkgdocs.julialang.org/v1/getting-started/) for more information. -## Processing with the GUI +## Processing data with the GUI + +### Running the GUI From within `MyCounterMarkingFolder` created above, start Julia like this: @@ -111,6 +113,23 @@ If you like the segmentation, your tasks are: After it finishes cycling through all the images, it will save your results and close the window. +### Summarizing the results: create a "density map" of marks across multiple images + +If you have many images collected under the same conditions (e.g., with +different subject animals but the same stimuli), you can effectively overlay the +entire collection of images. For this demo, it's assumed that `"results_file_name.jld2"` is one of the files output by the GUI. + +``` +julia> using CounterMarking, ImageView # load packages (if this is a fresh session) + +julia> count = density_map("results_file_name.jld2"); + +julia> dct = imshow(dmap); +``` + +!!! tip + `density_map` requires that most or all of your images are of the same size. + ## Processing manually ### Step 1: start Julia with the right project @@ -207,13 +226,3 @@ If you have many images in one folder, you can process them all using a single c ``` julia> process_images("2025-03-15/results.xlsx", "2025-03-15/*.png") ``` - -### Step 8: create a "density map" of marks across multiple images - -If you have many images collected under the same conditions (e.g., with different subject animals but the same stimuli), you can effectively overlay the entire collection of images: - -``` -julia> dmap = density_map("2025-03-15/maleU-*.png"); - -julia> dct = imshow(dmap); -``` diff --git a/src/CounterMarking.jl b/src/CounterMarking.jl index 32f5663..edd9eff 100644 --- a/src/CounterMarking.jl +++ b/src/CounterMarking.jl @@ -13,7 +13,7 @@ using ImageView using Random export segment_image, stimulus_index, spots, Spot, upperleft -export writexlsx, process_images +export writexlsx, process_images, density_map export randshow, meanshow, gui include("segment.jl") diff --git a/src/gui.jl b/src/gui.jl index 16e0244..32d76a7 100644 --- a/src/gui.jl +++ b/src/gui.jl @@ -1,14 +1,35 @@ +""" + gui(outbase, files) + gui(outbase, glob::GlobMatch) + +Run the graphical user interface (GUI) for CounterMarking. Supply the base name +of the output files (e.g., "my_results") and a list of image files to process +(alternatively, supply a `glob"pattern"` that matches just the files you want to +process). + +The GUI will display the images and their segmentation, and allow the user to +select the segments corresponding to the stimulus (yellow) and marked spots. The +results will be save to: +- `outbase.xlsx`: an Excel file with one sheet per image, containing summary + statistics on the stimulus spot and the selected spots. +- `outbase.jld2`: a JLD2 file with one dataset per image, containing the + segmented image, the selected spots, and the stimulus segment. + +The JLD2 file can be used by [`density_map`](@ref). +""" function gui( - outbase, files; + outbase::AbstractString, files; colors=distinguishable_colors(15, [RGB(1, 1, 1)]; dropseed=true), btnclick = Condition(), # used for testing whichbutton = Ref{Symbol}(), # used for testing + preclick::Union{Int,Nothing} = nothing, # used for testing ) channelpct(x) = string(round(Int, x * 100)) * '%' outbase, _ = splitext(outbase) + # Set up basic properties of the window winsize = round.(Int, 0.8 .* screen_size()) win = GtkWindow("CounterMarking", winsize...) ag = Gtk4.GLib.GSimpleActionGroup() @@ -38,6 +59,7 @@ function gui( cssprov = GtkCssProvider(css) push!(Gtk4.display(win), cssprov) + # Create the elements of the GUI win[] = bx = GtkBox(:v) ImageView.window_wrefs[win] = nothing signal_connect(win, :destroy) do w @@ -87,7 +109,7 @@ function gui( istim = stimulus_index(seg) for (j, cb) in enumerate(cbs) # set_gtk_property!(cb, "active", j <= nsegs) - cb[] = j == istim + cb[] = (j == istim || j == preclick) end imshow(canvases[1, 1], img) imshow(canvases[2, 1], map(i->colors[i], labels_map(seg))) @@ -135,7 +157,7 @@ function gui( notify(btnclick) # used in testing return end -gui(outbase, glob::Glob.GlobMatch; kwargs...) = gui(outbase, Glob.glob(glob); kwargs...) +gui(outbase::AbstractString, glob::Glob.GlobMatch; kwargs...) = gui(outbase, Glob.glob(glob); kwargs...) function linkpair(img, imgc) zr, slicedata = roi(img) diff --git a/src/segment.jl b/src/segment.jl index 959580f..ece05d6 100644 --- a/src/segment.jl +++ b/src/segment.jl @@ -148,3 +148,48 @@ end # end # return img # end + +""" + nmarked = density_map(jldfile::AbstractString) + +Given a JLD2 file `jldfile` written by `gui`, return an array `nmarked` counting +of the number of images with a urine spot in each pixel. Before counting, the +images are flipped so that the stimulus segment is in the upper left corner. +""" +function density_map(jldfile::AbstractString) + data = load(jldfile) + fns, imgsizes = String[], Tuple{Int, Int}[] + for (filename, (seg, _, _)) in data + push!(fns, filename) + imgsize = size(seg) + push!(imgsizes, imgsize) + end + szcount = Dict{Tuple{Int, Int}, Int}() + for sz in imgsizes + szcount[sz] = get(szcount, sz, 0) + 1 + end + imgsize, n = argmax(last, szcount) + badfiles = fns[imgsizes .!= Ref(imgsize)] + if n != length(data) + if n == 1 + error("no dominant image size found in $jldfile") + else + @warn("Image sizes do not all match, skipping $badfiles") + end + end + + nmarked = zeros(Int, imgsize) + midpoint = imgsize .÷ 2 + for (fn, (seg, _, stimulus)) in data + fn ∈ badfiles && continue + sidx, ss = stimulus + if ss.centroid[1] > midpoint[1] + seg = reverse(seg; dims=1) + end + if ss.centroid[2] > midpoint[2] + seg = reverse(seg; dims=2) + end + nmarked .+= seg .∉ Ref((0, sidx)) + end + return nmarked +end diff --git a/test/runtests.jl b/test/runtests.jl index 75d828b..a8ae579 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -45,11 +45,15 @@ using Test rm(tmpfile, force=true) btnclick = Condition() whichbutton = Ref{Symbol}() - @async gui(tmpfile, [joinpath(testdir, "Picture.png")]; btnclick, whichbutton) + @async gui(tmpfile, [joinpath(testdir, "Picture.png")]; btnclick, whichbutton, preclick=3) sleep(5) whichbutton[] = :done notify(btnclick) wait(btnclick) @test isfile(tmpfile) @test isfile(splitext(tmpfile)[1] * ".jld2") + + # Test the density map + count = density_map(splitext(tmpfile)[1] * ".jld2") + @test extrema(count) == (0, 1) end