diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index fffa0b6..f33971f 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -69,14 +69,19 @@ jobs: Pkg.develop(PackageSpec(path=pwd())) Pkg.instantiate() - uses: julia-actions/julia-buildpkg@v1 + with: + prefix: xvfb-run - uses: julia-actions/julia-docdeploy@v1 + with: + prefix: xvfb-run env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} DOCUMENTER_KEY: ${{ secrets.DOCUMENTER_KEY }} - name: Run doctests - shell: julia --project=docs --color=yes {0} + shell: xvfb-run julia --project=docs --color=yes {0} run: | using Documenter: DocMeta, doctest using CounterMarking DocMeta.setdocmeta!(CounterMarking, :DocTestSetup, :(using CounterMarking); recursive=true) doctest(CounterMarking) + \ No newline at end of file diff --git a/Project.toml b/Project.toml index 8bd2933..7e3b2f3 100644 --- a/Project.toml +++ b/Project.toml @@ -5,30 +5,34 @@ version = "1.0.0" [deps] FileIO = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549" +Glob = "c27321d9-0574-5035-807b-f59d2c89b15c" +Gtk4 = "9db2cae5-386f-4011-9d63-a5602296539b" +GtkObservables = "8710efd8-4ad6-11eb-33ea-2d5ceb25a41c" ImageCore = "a09fc81d-aa75-5fe9-8630-4744c3626534" ImageIO = "82e4d734-157c-48bb-816b-45c225c6df19" ImageMorphology = "787d08f9-d448-5407-9aad-5290dd7ab264" ImageSegmentation = "80713f31-8817-5129-9cf8-209ff8fb23e1" -Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" - -[weakdeps] ImageView = "86fae568-95e7-573e-a6b2-d8a6b900c9ef" - -[extensions] -CounterMarkingImageViewExt = "ImageView" +JLD2 = "033835bb-8acc-5ee8-8aae-3f567f8a3819" +Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" +XLSX = "fdbf4ff8-1666-58a4-91e7-1b58723a45e0" [compat] FileIO = "1" +Glob = "1" +Gtk4 = "0.7" +GtkObservables = "2" ImageCore = "0.10" ImageIO = "0.6" ImageMorphology = "0.4" ImageSegmentation = "1.9" ImageView = "0.12" +JLD2 = "0.5" +XLSX = "0.10" julia = "1.10" [extras] -ImageView = "86fae568-95e7-573e-a6b2-d8a6b900c9ef" Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40" [targets] -test = ["ImageView", "Test"] +test = ["Test"] diff --git a/docs/make.jl b/docs/make.jl index 4211774..4b1d365 100644 --- a/docs/make.jl +++ b/docs/make.jl @@ -14,6 +14,7 @@ makedocs(; ), pages=[ "Home" => "index.md", + "Reference" => "reference.md", ], ) diff --git a/docs/src/assets/Picture.png b/docs/src/assets/Picture.png new file mode 100644 index 0000000..bf9f5c5 Binary files /dev/null and b/docs/src/assets/Picture.png differ diff --git a/docs/src/assets/gui.png b/docs/src/assets/gui.png new file mode 100644 index 0000000..3256fd2 Binary files /dev/null and b/docs/src/assets/gui.png differ diff --git a/docs/src/index.md b/docs/src/index.md index f81f2f2..38827e2 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -4,11 +4,216 @@ CurrentModule = CounterMarking # CounterMarking -Documentation for [CounterMarking](https://github.com/HolyLab/CounterMarking.jl). +[CounterMarking](https://github.com/HolyLab/CounterMarking.jl) analyzes experiments on [scent-marking in mice](https://www.sciencedirect.com/science/article/pii/S0003347287800167), +specifically images of urine countermarking visualized by the [ninhydrin reaction](https://pubs.acs.org/doi/full/10.1021/jf030490p): + +![A ninhydrin-stained image](assets/Picture.png) + +The yellow spot corresponds to a stimulus provided by the experimenter, and the small light-blue spots are deposited marks. + +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. + +## Tutorial + +### Installation and setup (one time setup) + +There are several ways to organize your data, but one recommended approach is to have a parent "project" folder, and then store images collected on different days in subfolders named by date: + +```sh +MyCounterMarkingFolder + Project.toml + 2025-03-15/ + 2025-03-17/ + ... +``` + +We'll create the `Project.toml` for running the analysis. From the command line within `MyCounterMarkingFolder`, the steps below will: + +- start Julia +- get into `pkg>` mode +- activate a new project +- install the packages you'll use + +Here are the steps, starting from the OS command line: + +``` +MyCounterMarkingFolder$ julia + + +julia> ] # this enters pkg mode +pkg> activate . + +pkg> add CounterMarking ImageView Glob FileIO ImageIO +``` + +This should create the `Project.toml` file in `MyCounterMarkingFolder`. If this succeeds, you shouldn't have to do this again. + + +From this point on, start Julia like this: + +```sh +MyCounterMarkingFolder$ julia --project +``` + +and it will automatically "activate" this project and you'll have access to all those packages. + +!!! tip + If you ever need to update the packages (e.g., to get any improvements + to `CounterMarking.jl`), you can update packages with `pkg> up`. See the + [Pkg documentation](https://pkgdocs.julialang.org/v1/getting-started/) for + more information. + +## Processing with the GUI + +From within `MyCounterMarkingFolder` created above, start Julia like this: + +```sh +MyCounterMarkingFolder$ julia --project +``` + +Then load the packages: + +``` +julia> using CounterMarking, Glob +``` + +Then specify the images you want to process: + +``` +julia> gui("results_file_name", glob"Picture*.png") +``` + +This will save your results to `"results_file_name.xlsx"` and `"results_file_name.jld2"`. +The syntax `glob"pattern"` means "all files that match this pattern", where `*` means "one or more characters". +See [this tutorial](https://www.malikbrowne.com/blog/a-beginners-guide-glob-patterns/) for more information about glob syntax. +Alternatively, you can supply a list of files: + +``` +julia> gui("results_file_name", ["PictureA.png", "mouse7.png"]) +``` + +However you launch it, you should see something like this: + +![GUI](assets/gui.png) + +On the top is the raw image. On the bottom is the segmented image; you should visually compare the two to check whether you're pleased with the quality of the segmentation. +(If not, click "Skip" to omit that file from analysis.) + +If you like the segmentation, your tasks are: +- click on all the checkboxes with colors that correspond to urine spots. You'll notice that the stimulus spot is pre-clicked (you can correct its choice if it didn't pick correctly). Most of the time there will be only one you need to check, but you can click more than one. + In this example image, all the urine spots are marked red, so you'd check the box that has the red border. Leave the stimulus spot checked, too. +- click "Done & Next" to advance to the next image in the sequence + +After it finishes cycling through all the images, it will save your results and close the window. + +## Processing manually + +### Step 1: start Julia with the right project + +From within `MyCounterMarkingFolder` created above, start Julia like this: + +```sh +MyCounterMarkingFolder$ julia --project +``` + +### Step 2: load the packages you'll need + +From inside Julia, load the packages: + +``` +julia> using CounterMarking, ImageView, FileIO +``` + +CounterMarking is this package, used to perform and organize the analysis. +[ImageView](https://github.com/JuliaImages/ImageView.jl) is an image display tool. +[FileIO](https://github.com/JuliaIO/FileIO.jl) loads many different file formats, including images. + +### Step 3: load a test image + +If you want to use an image in one of the subfolders, use something like + +``` +julia> img = load("2025-03-15/picture1.png"); +``` + +You'll need to replace the material inside quotes with the actual path and filename of your image. + +Alternately, if you want to use the test image that comes with CounterMarking.jl, do the following: + +``` +julia> img = load(joinpath(pkgdir(CounterMarking), "docs", "src", "assets", "Picture.png")); +``` + +### Step 4: visualize the image + +It's usually a good idea to visually check that what you're working with makes sense: + +``` +julia> dct = imshow(img); +``` + +Note that as you move your mouse cursor over the image, a little text box in the lower left updates with the position and information about the color of the pixel under your cursor. +That can occasionally be handy, especially for checking locations of spots. + +If all looks as expected, you can close the window. + +### Step 5: segment the image + +We'll split this image into different regions: + +``` +julia> seg = segment_image(img) +Pruning segments smaller than 50 pixels +Segmented Image with: + labels map: 1220×2441 Matrix{Int64} + number of labels: 153 + +julia> dct = randshow(img, seg); +``` + +After the second command, [`randshow`](@ref), you'll see two images: the original at the top, and the "segmented" image below. This displays the different segments (regions) using a randomly-chosen color, which can be handy for checking how well the analysis did in identifying separate spots. You can alternatively use [`meanshow`](@ref) to show each segment using the average color of all pixels in that segment. + +If you Ctrl-click and drag on the image, you'll zoom in on both images. This can be handy for inspecting fine details. Ctrl-double-click takes you back to the full view. + +!!! tip + If you don't like how [`segment_image`](@ref) performed, read its documentation to learn about some of the options you have for controlling it. + +### Step 6: save the spots to an Excel file + +The columns marked "raw" correspond to pixel locations in the original image; the columns marked "UL" come from flipping the image to place the stimulus spot in the Upper Left of the image. +This way of "standardizing" the location makes certain analyses easier. + +``` +julia> writexlsx("mydata.xlsx", seg) +``` + +Optionally specify a full directory path, e.g., + +``` +julia> writexlsx(raw"C:\Users\me\somefolder\mydata.xlsx", seg) +``` + +(You don't need `raw` on Linux or Mac, but it is helpful on Windows.) + +### Step 7: process a whole directory of images at once + +If you have many images in one folder, you can process them all using a single command: + +``` +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: -```@index ``` +julia> dmap = density_map("2025-03-15/maleU-*.png"); -```@autodocs -Modules = [CounterMarking] +julia> dct = imshow(dmap); ``` diff --git a/docs/src/reference.md b/docs/src/reference.md new file mode 100644 index 0000000..1c409e7 --- /dev/null +++ b/docs/src/reference.md @@ -0,0 +1,8 @@ +# Reference + +```@index +``` + +```@autodocs +Modules = [CounterMarking] +``` diff --git a/ext/CounterMarkingImageViewExt.jl b/ext/CounterMarkingImageViewExt.jl deleted file mode 100644 index 844ce3e..0000000 --- a/ext/CounterMarkingImageViewExt.jl +++ /dev/null @@ -1,40 +0,0 @@ -module CounterMarkingImageViewExt - -using CounterMarking: CounterMarking -using ImageCore -using ImageSegmentation -using ImageView -using Random - -# function colorize(seg, segidxs::AbstractSet{Int}, color::Colorant) -# label = seg.image_indexmap -# img = similar(label, promote_type(typeof(color), valtype(seg.segment_means))) -# fill!(img, zero(eltype(img))) -# for idx in eachindex(label) -# label[idx] ∈ segidxs || continue -# img[idx] = color -# end -# return img -# end - -function linkpair(img, imgc) - zr, slicedata = roi(img) - gd = imshow_gui((800, 800), (2,1); slicedata=slicedata) - imshow(gd["frame"][1,1], gd["canvas"][1,1], img, nothing, zr, slicedata) - imshow(gd["frame"][2,1], gd["canvas"][2,1], imgc, nothing, zr, slicedata) - return gd -end - -# For visualization -function get_random_color(seed) - Random.seed!(seed) - rand(RGB{N0f8}) -end - -CounterMarking.randshow(seg; kwargs...) = imshow(map(i->get_random_color(i), labels_map(seg)); kwargs...) -CounterMarking.meanshow(seg; kwargs...) = imshow(map(i->segment_mean(seg, i), labels_map(seg)); kwargs...) - -CounterMarking.randshow(img, seg; kwargs...) = linkpair(img, map(i->get_random_color(i), labels_map(seg)); kwargs...) -CounterMarking.meanshow(img, seg; kwargs...) = linkpair(img, map(i->segment_mean(seg, i), labels_map(seg)); kwargs...) - -end diff --git a/src/CounterMarking.jl b/src/CounterMarking.jl index 7ee264d..32f5663 100644 --- a/src/CounterMarking.jl +++ b/src/CounterMarking.jl @@ -4,11 +4,20 @@ using ImageCore using ImageSegmentation using ImageMorphology: label_components using FileIO +using JLD2 +using XLSX +using Glob +using Gtk4 +using GtkObservables +using ImageView +using Random export segment_image, stimulus_index, spots, Spot, upperleft -export randshow, meanshow +export writexlsx, process_images +export randshow, meanshow, gui include("segment.jl") -include("stubs.jl") +include("xlxs.jl") +include("gui.jl") end diff --git a/src/gui.jl b/src/gui.jl new file mode 100644 index 0000000..16e0244 --- /dev/null +++ b/src/gui.jl @@ -0,0 +1,184 @@ + +function gui( + outbase, files; + colors=distinguishable_colors(15, [RGB(1, 1, 1)]; dropseed=true), + btnclick = Condition(), # used for testing + whichbutton = Ref{Symbol}(), # used for testing + ) + channelpct(x) = string(round(Int, x * 100)) * '%' + + outbase, _ = splitext(outbase) + + winsize = round.(Int, 0.8 .* screen_size()) + win = GtkWindow("CounterMarking", winsize...) + ag = Gtk4.GLib.GSimpleActionGroup() + m = Gtk4.GLib.GActionMap(ag) + push!(win, Gtk4.GLib.GActionGroup(ag), "win") + Gtk4.GLib.add_action(m, "close", ImageView.close_cb, win) + Gtk4.GLib.add_action(m, "closeall", ImageView.closeall_cb, nothing) + Gtk4.GLib.add_stateful_action(m, "fullscreen", false, ImageView.fullscreen_cb, win) + sc = GtkShortcutController(win) + Gtk4.add_action_shortcut(sc,Sys.isapple() ? "W" : "W", "win.close") + Gtk4.add_action_shortcut(sc,Sys.isapple() ? "W" : "W", "win.closeall") + Gtk4.add_action_shortcut(sc,Sys.isapple() ? "F" : "F11", "win.fullscreen") + + # CSS styling for the colors + io = IOBuffer() + for (i, color) in enumerate(colors) + colorstr = "rgb(" * channelpct(color.r) * ", " * + channelpct(color.g) * ", " * + channelpct(color.b) * ")" + println(io, """ + .color$i { + background: $colorstr; + } + """) + end + css = String(take!(io)) + cssprov = GtkCssProvider(css) + push!(Gtk4.display(win), cssprov) + + win[] = bx = GtkBox(:v) + ImageView.window_wrefs[win] = nothing + signal_connect(win, :destroy) do w + delete!(ImageView.window_wrefs, win) + end + g, frames, canvases = ImageView.canvasgrid((2, 1), :auto) + push!(bx, g) + push!(bx, GtkSeparator(:h)) + guibx = GtkBox(:h) + push!(bx, guibx) + seggrid = GtkGrid() + push!(guibx, seggrid) + # Add checkboxes for each color, with the box's color set to the color + cbs = [] + for i in 1:length(colors) + cb = checkbox(false) + add_css_class(cb.widget, "color$i") + for prop in ("margin_start", "margin_end", "margin_top", "margin_bottom") + set_gtk_property!(cb.widget, prop, 5) + end + set_gtk_property!(cb.widget, "width-request", 20) + row = div(i - 1, 5) + 1 + col = mod(i - 1, 5) + 1 + seggrid[col, row] = cb.widget + push!(cbs, cb) + end + # Add "Done & Next" and "Skip" buttons + donebtn = button("Done & Next") + skipbtn = button("Skip") + push!(guibx, donebtn) + push!(guibx, skipbtn) + on(donebtn) do _ + whichbutton[] = :done + notify(btnclick) + end + on(skipbtn) do _ + whichbutton[] = :skip + notify(btnclick) + end + + results = [] + for (i, file) in enumerate(files) + img = color.(load(file)) + seg = segment_image(img) + nsegs = length(segment_labels(seg)) + @assert nsegs < length(colors) "Too many segments for colors" + istim = stimulus_index(seg) + for (j, cb) in enumerate(cbs) + # set_gtk_property!(cb, "active", j <= nsegs) + cb[] = j == istim + end + imshow(canvases[1, 1], img) + imshow(canvases[2, 1], map(i->colors[i], labels_map(seg))) + + wait(btnclick) + whichbutton[] == :skip && continue + + keep = Int[] + for (j, cb) in enumerate(cbs) + if cb[] + push!(keep, j) + end + end + pixelskeep = map(i -> i ∈ keep, labels_map(seg)) + L = label_components(pixelskeep) + newseg = SegmentedImage(img, L) + spotdict, stimulus = spots(newseg) + push!(results, (file, spotdict, stimulus, newseg)) + end + + if !isempty(results) + xlsxname = outbase * ".xlsx" + XLSX.openxlsx(xlsxname; mode="w") do xf + for (i, (file, spotdict, stimulus, seg)) in enumerate(results) + imgsize = size(labels_map(seg)) + sheetname = splitext(basename(file))[1] + sheet = if i == 1 + XLSX.rename!(xf[1], sheetname) + xf[1] + else + XLSX.addsheet!(xf, sheetname) + end + makesheet!(sheet, spotdict, stimulus, imgsize) + end + end + jldname = outbase * ".jld2" + jldopen(jldname, "w") do jf + for (file, spotdict, stimulus, seg) in results + imgname = splitext(basename(file))[1] + write(jf, imgname, (labels_map(seg), spotdict, stimulus)) + end + end + end + destroy(win) + notify(btnclick) # used in testing + return +end +gui(outbase, glob::Glob.GlobMatch; kwargs...) = gui(outbase, Glob.glob(glob); kwargs...) + +function linkpair(img, imgc) + zr, slicedata = roi(img) + gd = imshow_gui((800, 800), (2,1); slicedata=slicedata) + imshow(gd["frame"][1,1], gd["canvas"][1,1], img, nothing, zr, slicedata) + imshow(gd["frame"][2,1], gd["canvas"][2,1], imgc, nothing, zr, slicedata) + return gd +end + +# For visualization +function get_random_color(seed) + Random.seed!(seed) + rand(RGB{N0f8}) +end + +""" + randshow(seg; kwargs...) + randshow(img, seg; kwargs...) + +Display a segmented image using random colors for each segment. The version with +`img` displays the original image and the segmented image one atop the other, +and zooming on one will zoom on the other. + +!!! note You must load the `ImageView` package to use this function. +""" +function randshow end + +""" + meanshow(seg; kwargs...) + meanshow(img, seg; kwargs...) + +Display a segmented image using the mean color of each segment. The version with +`img` displays the original image and the segmented image one atop the other, +and zooming on one will zoom on the other. + +!!! note + You must load the `ImageView` package to use this function. +""" +function meanshow end + + +randshow(seg; kwargs...) = imshow(map(i->get_random_color(i), labels_map(seg)); kwargs...) +meanshow(seg; kwargs...) = imshow(map(i->segment_mean(seg, i), labels_map(seg)); kwargs...) + +randshow(img, seg; kwargs...) = linkpair(img, map(i->get_random_color(i), labels_map(seg)); kwargs...) +meanshow(img, seg; kwargs...) = linkpair(img, map(i->segment_mean(seg, i), labels_map(seg)); kwargs...) diff --git a/src/segment.jl b/src/segment.jl index 5eb5911..959580f 100644 --- a/src/segment.jl +++ b/src/segment.jl @@ -13,10 +13,8 @@ function segment_image( min_size::Int = 50, # minimum size of segments to keep ) seg = unseeded_region_growing(img, threshold) - L = label_components(labels_map(seg)) # insist on contiguous regions - seg = SegmentedImage(img, L) if prune - println("Pruning segments smaller than $min_size pixels") + # println("Pruning segments smaller than $min_size pixels") seg = prune_segments(seg, label -> segment_pixel_count(seg, label) < min_size, (l1, l2) -> colordiff(segment_mean(seg, l1), segment_mean(seg, l2))) end return seg @@ -36,6 +34,19 @@ function stimulus_index(seg::SegmentedImage, colorproj = RGB{Float32}(1, 1, -2)) return i end +# function contiguous(seg::SegmentedImage, img::AbstractMatrix{<:Color}; min_size::Int = 50) +# L = label_components(labels_map(seg)) # insist on contiguous regions +# newseg = SegmentedImage(img, L) +# newseg = prune_segments(newseg, label -> segment_pixel_count(newseg, label) < min_size, (l1, l2) -> colordiff(segment_mean(newseg, l1), segment_mean(newseg, l2))) +# mapping = Dict(k => Set{Int}() for k in segment_labels(seg)) +# for (i, l) in pairs(seg.image_indexmap) +# push!(mapping[l], newseg.image_indexmap[i]) +# end +# return mapping +# end +# contiguous(seg::SegmentedImage, img::AbstractMatrix{<:Colorant}; kwargs...) = +# contiguous(seg, color.(img); kwargs...) + struct Spot npixels::Int centroid::Tuple{Int, Int} @@ -54,7 +65,7 @@ stimulus segment and the second element is the `Spot` object for that segment. Spots larger than `max_size_frac * npixels` (default: 10% of the image) are ignored. """ function spots( - seg; + seg::SegmentedImage; max_size_frac=0.1, # no spot is bigger than max_size_frac * npixels ) keypair(i, j) = i < j ? (i, j) : (j, i) @@ -64,7 +75,7 @@ function spots( label = seg.image_indexmap R = CartesianIndices(label) Ibegin, Iend = extrema(R) - I1 = one(Ibegin) + I1 = oneunit(Ibegin) centroidsacc = Dict{Int, Tuple{Int, Int, Int}}() # accumulator for centroids nadj = Dict{Tuple{Int, Int}, Int}() # number of times two segments are adjacent for idx in R @@ -110,10 +121,10 @@ function spots( end """ - spotdict, stimulus = upperleft(spotdict::AbstractDict{Int, Spot}, stimulus, imgsize) + spotdict_ul, stimulus_ul = upperleft(spotdict::AbstractDict{Int, Spot}, stimulus, imgsize) Given a `spotdict` of `Spot` objects and a `stimulus` segment, return a new -`spotdict` where the centroids of the spots are flipped so that the stimlus spot +`spotdict_ul` corresponding to an image flipped so that `stimulus_ul` is in the upper left corner. """ function upperleft(spotdict::AbstractDict{Int, Spot}, stimulus, imgsize) @@ -128,3 +139,12 @@ function upperleft(spotdict::AbstractDict{Int, Spot}, stimulus, imgsize) end return Dict(k => flip(v) for (k, v) in spotdict), sidx => flip(ss) end + +# function colorize(seg::SegmentedImage, coloridx::AbstractDict, colors=distinguishable_colors(length(unique(values(coloridx))))) +# label = seg.image_indexmap +# img = similar(label, eltype(colors)) +# for idx in eachindex(label) +# img[idx] = colors[coloridx[label[idx]]] +# end +# return img +# end diff --git a/src/stubs.jl b/src/stubs.jl deleted file mode 100644 index 88ddb8d..0000000 --- a/src/stubs.jl +++ /dev/null @@ -1,29 +0,0 @@ -""" - randshow(seg; kwargs...) - -Display a segmented image using random colors for each segment. - -!!! note - You must load the `ImageView` package to use this function. -""" -function randshow end - -""" - meanshow(seg; kwargs...) - -Display a segmented image using the mean color of each segment. - -!!! note - You must load the `ImageView` package to use this function. -""" -function meanshow end - -function __init__() - Base.Experimental.register_error_hint(MethodError) do io, exc, _, _ - if exc.f ∈ (randshow, meanshow) - if isempty(methods(exc.f)) - printstyled(io, "\nYou may need `using ImageView` to load the appropriate methods."; color=:yellow) - end - end - end -end diff --git a/src/xlxs.jl b/src/xlxs.jl new file mode 100644 index 0000000..c81a610 --- /dev/null +++ b/src/xlxs.jl @@ -0,0 +1,79 @@ +function makesheet!(sheet::XLSX.Worksheet, spotdict::AbstractDict{Int,Spot}, stimulus, imgsize) + spotdict_ul, stimulus_ul = upperleft(spotdict, stimulus, imgsize) + sd = collect(spotdict) + p = sortperm(sd, by = x -> x.second.centroid[2]) + keyp = [sd[i].first for i in p] + + sheet["A1"] = "Spot" + sheet["B1"] = "Centroid-x, raw" + sheet["C1"] = "Centroid-y, raw" + sheet["D1"] = "Centroid-x, UL" + sheet["E1"] = "Centroid-y, UL" + sheet["F1"] = "npixels" + sheet["A2", dim=1] = vcat("Stimulus", [string(i) for i in 1:length(spotdict)]) + sheet["B2", dim=1] = vcat(stimulus.second.centroid[2], [sd[i].second.centroid[2] for i in p]) + sheet["C2", dim=1] = vcat(stimulus.second.centroid[1], [sd[i].second.centroid[1] for i in p]) + sheet["D2", dim=1] = vcat(stimulus_ul.second.centroid[2], [spotdict_ul[k].centroid[2] for k in keyp]) + sheet["E2", dim=1] = vcat(stimulus_ul.second.centroid[1], [spotdict_ul[k].centroid[1] for k in keyp]) + sheet["F2", dim=1] = vcat(stimulus.second.npixels, [sd[i].second.npixels for i in p]) +end + +function writexlsx(filename::AbstractString, spotdict::AbstractDict{Int,Spot}, stimulus, imgsize) + XLSX.openxlsx(filename; mode="w") do xf + sheet = xf[1] + makesheet!(sheet, spotdict, stimulus, imgsize) + end + return +end + +""" + writexlsx(filename::AbstractString, seg::SegmentedImage) + +Save the segmented image data to an Excel file. +""" +function writexlsx(filename::AbstractString, seg::SegmentedImage) + imgsize = size(labels_map(seg)) + spotdict, stimulus = spots(seg) + writexlsx(filename, spotdict, stimulus, imgsize) +end + +""" + process_images(outfile::AbstractString, glob::GlobMatch; dirname=pwd()) + process_images(outfile::AbstractString, glob::AbstractString; dirname=pwd()) + +Process all images with filenames matching `glob` and save the results to `outfile`. +Each image will be a separate sheet in the Excel file. + +Optionally specify the `dirname` containing the images. + +# Examples + +To process a collection of images in a different directory, and save the results to +that same directory: + +```julia +julia> process_images("2025-03-15/results.xlsx", glob"*.png"; dirname="2025-03-15") +``` +""" +function process_images(outfile::AbstractString, glob::Glob.GlobMatch; dirname=pwd()) + i = 0 + XLSX.openxlsx(outfile; mode="w") do xf + for filename in readdir(glob, dirname) + img = load(filename) + seg = segment_image(img) + imgsize = size(labels_map(seg)) + spotdict, stimulus = spots(seg) + sheetname = splitext(basename(filename))[1] + sheet = if i == 0 + i += 1 + XLSX.rename!(xf[1], sheetname) + xf[1] + else + XLSX.addsheet!(xf, sheetname) + end + makesheet!(sheet, spotdict, stimulus, imgsize) + end + end +end +process_images(outfile::AbstractString, glob::AbstractString; kwargs...) = + process_images(outfile, Glob.GlobMatch(glob); kwargs...) diff --git a/test/runtests.jl b/test/runtests.jl index 0f864ae..75d828b 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1,17 +1,14 @@ using CounterMarking using FileIO +using XLSX +using ImageView +using Glob using Test @testset "CounterMarking.jl" begin - testdir = "testimages" + testdir = joinpath(pkgdir(CounterMarking), "docs", "src", "assets") img = load(joinpath(testdir, "Picture.png")) seg = segment_image(img) - # Without ImageView loaded, we can't visualize it, but we get a helpful error - if !isdefined(@__MODULE__, :ImageView) - @test_throws "using ImageView" randshow(seg) - @test_throws "using ImageView" meanshow(seg) - end - @eval using ImageView dct = meanshow(seg) @test haskey(dct, "gui") dct = randshow(seg) @@ -33,4 +30,26 @@ using Test @test stimspot.npixels > 1000 @test stimspot.centroid[1] < size(img, 1) ÷ 2 @test stimspot.centroid[2] < size(img, 2) ÷ 2 + + # Test the xlsx writing + tmpfile = tempname() * ".xlsx" + writexlsx(tmpfile, seg) + @test isfile(tmpfile) + + # Test multi-file writing + process_images(tmpfile, glob"*.png"; dirname=testdir) + data = XLSX.readtable(tmpfile, "Picture") + @test isa(data, XLSX.DataTable) + + # Test the gui + rm(tmpfile, force=true) + btnclick = Condition() + whichbutton = Ref{Symbol}() + @async gui(tmpfile, [joinpath(testdir, "Picture.png")]; btnclick, whichbutton) + sleep(5) + whichbutton[] = :done + notify(btnclick) + wait(btnclick) + @test isfile(tmpfile) + @test isfile(splitext(tmpfile)[1] * ".jld2") end diff --git a/test/testimages/Picture.png b/test/testimages/Picture.png deleted file mode 100644 index c142b8d..0000000 Binary files a/test/testimages/Picture.png and /dev/null differ