Skip to content

Commit 105ce8f

Browse files
authored
Tweaks to accommodate new imaging setup (#8)
* Add gui buttons for color threshold * some tweaks for handling larger images * remove junk files from testing gui * add illumination calibration * tweaks to docs * fixed checkbox indexing * use position to identify stimulus spot * remove compat entries for LinearAlgebra and Statistics * fixes for docs images * add kwargs info to docs * tweak tests * add missing snippet to density_map docs
1 parent bd67d50 commit 105ce8f

File tree

9 files changed

+201
-46
lines changed

9 files changed

+201
-46
lines changed

Project.toml

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,11 +10,15 @@ Gtk4 = "9db2cae5-386f-4011-9d63-a5602296539b"
1010
GtkObservables = "8710efd8-4ad6-11eb-33ea-2d5ceb25a41c"
1111
ImageCore = "a09fc81d-aa75-5fe9-8630-4744c3626534"
1212
ImageIO = "82e4d734-157c-48bb-816b-45c225c6df19"
13+
ImageMagick = "6218d12a-5da1-5696-b52f-db25d2ecc6d1"
1314
ImageMorphology = "787d08f9-d448-5407-9aad-5290dd7ab264"
1415
ImageSegmentation = "80713f31-8817-5129-9cf8-209ff8fb23e1"
1516
ImageView = "86fae568-95e7-573e-a6b2-d8a6b900c9ef"
1617
JLD2 = "033835bb-8acc-5ee8-8aae-3f567f8a3819"
18+
LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e"
19+
OpenCV = "f878e3a2-a245-4720-8660-60795d644f2a"
1720
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
21+
Statistics = "10745b16-79ce-11e8-11f9-7d13ad32a3b2"
1822
XLSX = "fdbf4ff8-1666-58a4-91e7-1b58723a45e0"
1923

2024
[compat]
@@ -24,10 +28,12 @@ Gtk4 = "0.7"
2428
GtkObservables = "2"
2529
ImageCore = "0.10"
2630
ImageIO = "0.6"
31+
ImageMagick = "1.4.2"
2732
ImageMorphology = "0.4"
2833
ImageSegmentation = "1.9"
2934
ImageView = "0.12"
3035
JLD2 = "0.5"
36+
OpenCV = "4.6.1"
3137
XLSX = "0.10"
3238
julia = "1.10"
3339

docs/src/assets/Picture.png

8.04 MB
Loading
695 KB
Loading

docs/src/assets/gui.png

-390 KB
Loading

docs/src/index.md

Lines changed: 29 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ The yellow spot corresponds to a stimulus provided by the experimenter, and the
1313

1414
Tips on image quality:
1515

16-
- Put the stimulus near one of the four corners
16+
- Put the stimulus near one of the four corners, keeping its location as consistent as possible between images
1717
- Ensure lighting is fairly uniform
1818
- Make sure that any extraneous marks (e.g., the black writing in the image above) are of a very different color from scent marks.
1919
- 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
@@ -99,12 +99,37 @@ Alternatively, you can supply a list of files:
9999
julia> gui("results_file_name", ["PictureA.png", "mouse7.png"])
100100
```
101101

102+
Additionally, you can supply a calibration image to improve segmentation by correcting for uneven illumination.
103+
104+
![An image to be used for calibration](assets/blurred_calibration.png)
105+
106+
```
107+
julia> gui("results_file_name", ["PictureA.png", "mouse7.png"]; background_path="calibration_image.png")
108+
```
109+
110+
There are a few more keyword arguments that might be useful:
111+
- `crop_top`, `crop_bottom`, `crop_left`, and `crop_right` specify a number of pixels to be cropped from images along each side
112+
- `expectedloc` specifies the expected location of the stimulus spot in pixels (after cropping)
113+
114+
```
115+
julia> gui("results_file_name", glob"Picture*.png";
116+
background_path="calibration_image.png,
117+
crop_top=93,
118+
crop_bottom=107,
119+
crop_left=55,
120+
crop_right=45,
121+
expectedloc=[1600,3333]
122+
)
123+
```
124+
102125
However you launch it, you should see something like this:
103126

104127
![GUI](assets/gui.png)
105128

106129
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.
107-
(If not, click "Skip" to omit that file from analysis.)
130+
131+
If the default segmentation doesn't look quite right, try adjusting the Color Similarity Threshold value using the buttons at the bottom of the GUI.
132+
(If you can't obtain a segmentation that you're happy with, click "Skip" to omit that file from analysis.)
108133

109134
If you like the segmentation, your tasks are:
110135
- 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.
@@ -124,6 +149,8 @@ julia> using CounterMarking, ImageView # load packages (if this is a fresh ses
124149
125150
julia> count = density_map("results_file_name.jld2");
126151
152+
julia> dmap = count ./ maximum(count);
153+
127154
julia> dct = imshow(dmap);
128155
```
129156

src/CounterMarking.jl

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,8 @@ using Gtk4
1111
using GtkObservables
1212
using ImageView
1313
using Random
14+
using LinearAlgebra
15+
using Statistics
1416

1517
export segment_image, stimulus_index, spots, Spot, upperleft
1618
export writexlsx, process_images, density_map

src/gui.jl

Lines changed: 96 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -20,15 +20,31 @@ The JLD2 file can be used by [`density_map`](@ref).
2020
"""
2121
function gui(
2222
outbase::AbstractString, files;
23-
colors=distinguishable_colors(15, [RGB(1, 1, 1)]; dropseed=true),
23+
colors=distinguishable_colors(15, [RGB(1, 1, 1)]; dropseed=false),
2424
btnclick = Condition(), # used for testing
2525
whichbutton = Ref{Symbol}(), # used for testing
2626
preclick::Union{Int,Nothing} = nothing, # used for testing
27+
background_path = nothing, # used to correct for non-uniform illumination (see joinpath(pkgdir(@__MODULE__),"docs","src","assets","blurred_calibration.png"))
28+
crop_top::Int = 0, # crop this many pixels off of each side
29+
crop_bottom::Int = 0,
30+
crop_left::Int = 0,
31+
crop_right::Int = 0,
32+
colorproj = RGB{Float32}(1, 1, -2), # used for identifying the stimulus
33+
expectedloc = nothing, # ""
2734
)
2835
channelpct(x) = string(round(Int, x * 100)) * '%'
2936

3037
outbase, _ = splitext(outbase)
3138

39+
# Initialize segmented image and color similarity threshold
40+
img = nothing
41+
rescaledimg = nothing
42+
bkgimg = isnothing(background_path) ? nothing : Float32.(Gray.(load(background_path)[crop_top+1:end-crop_bottom, crop_left+1:end-crop_right]))
43+
bkgmean = isnothing(bkgimg) ? nothing : Float32(mean(bkgimg))
44+
seg = nothing
45+
threshold = 0.15
46+
labels2idx = Dict{Int,Int}()
47+
idx2labels = Dict{Int,Int}()
3248
# Set up basic properties of the window
3349
winsize = round.(Int, 0.8 .* screen_size())
3450
win = GtkWindow("CounterMarking", winsize...)
@@ -86,6 +102,64 @@ function gui(
86102
seggrid[col, row] = cb.widget
87103
push!(cbs, cb)
88104
end
105+
106+
thrshbx = GtkBox(:h)
107+
thrshlbl = GtkLabel("Color Similarity Threshold: $(threshold)")
108+
push!(thrshbx, thrshlbl)
109+
thrshbtnbx = GtkBox(:v)
110+
push!(thrshbx, thrshbtnbx)
111+
lgincbtn = GtkButton("+0.05")
112+
smincbtn = GtkButton("+0.01")
113+
smdecbtn = GtkButton("-0.01")
114+
lgdecbtn = GtkButton("-0.05")
115+
push!(thrshbtnbx, lgincbtn)
116+
push!(thrshbtnbx, smincbtn)
117+
push!(thrshbtnbx, smdecbtn)
118+
push!(thrshbtnbx, lgdecbtn)
119+
push!(guibx, thrshbx)
120+
121+
update_threshold = change -> begin
122+
newvalue = round(threshold + change; digits=2)
123+
try
124+
seg = segment_image(rescaledimg; threshold = newvalue)
125+
nsegs = length(segment_labels(seg))
126+
nsegs > length(colors) && @warn "More than $(length(colors)) segments ($(nsegs)). Excluded ones will be displayed in white and will not be selectable"
127+
empty!(labels2idx)
128+
empty!(idx2labels)
129+
for (i,l) in enumerate(sort(seg.segment_labels; rev=true, by=(l -> segment_pixel_count(seg,l))))
130+
idx = i == 1 ? 2 : i == 2 ? 1 : i
131+
idx = i > 15 ? 1 : idx # show remaining segments in white
132+
push!(labels2idx, l=>idx)
133+
push!(idx2labels, idx=>l)
134+
end
135+
centroidsacc, _ = get_centroidsacc(seg.image_indexmap)
136+
istim = labels2idx[stimulus_index(seg, centroidsacc; colorproj = colorproj, expectedloc = expectedloc)]
137+
for (j, cb) in enumerate(cbs)
138+
# set_gtk_property!(cb, "active", j <= nsegs)
139+
cb[] = (j == istim || j == preclick)
140+
end
141+
imshow(canvases[1, 1], img)
142+
imshow(canvases[2, 1], map(i->colors[labels2idx[i]], labels_map(seg)))
143+
threshold = newvalue
144+
thrshlbl.label = "Color Similarity Threshold: $threshold"
145+
catch e
146+
@show e
147+
# display?
148+
end
149+
end
150+
signal_connect(lgincbtn, "clicked") do w, others...
151+
update_threshold(0.05)
152+
end
153+
signal_connect(smincbtn, "clicked") do w, others...
154+
update_threshold(0.01)
155+
end
156+
signal_connect(smdecbtn, "clicked") do w, others...
157+
update_threshold(-0.01)
158+
end
159+
signal_connect(lgdecbtn, "clicked") do w, others...
160+
update_threshold(-0.05)
161+
end
162+
89163
# Add "Done & Next" and "Skip" buttons
90164
donebtn = button("Done & Next")
91165
skipbtn = button("Skip")
@@ -102,31 +176,44 @@ function gui(
102176

103177
results = []
104178
for (i, file) in enumerate(files)
105-
img = color.(load(file))
106-
seg = segment_image(img)
179+
threshold = 0.15
180+
thrshlbl.label = "Color Similarity Threshold: $threshold"
181+
img = color.(load(file))[crop_top+1:end-crop_bottom, crop_left+1:end-crop_right]
182+
rescaledimg = isnothing(bkgimg) ? img : (img ./ bkgimg .* bkgmean)
183+
184+
seg = segment_image(rescaledimg; threshold = threshold)
107185
nsegs = length(segment_labels(seg))
108-
@assert nsegs < length(colors) "Too many segments for colors"
109-
istim = stimulus_index(seg)
186+
nsegs > length(colors) && @warn "More than $(length(colors)) segments ($(nsegs)). Excluded ones will be displayed in white and will not be selectable"
187+
empty!(labels2idx)
188+
empty!(idx2labels)
189+
for (i,l) in enumerate(sort(seg.segment_labels; rev=true, by=(l -> segment_pixel_count(seg,l))))
190+
idx = i == 1 ? 2 : i == 2 ? 1 : i
191+
idx = i > 15 ? 1 : idx # show remaining segments in white
192+
push!(labels2idx, l=>idx)
193+
push!(idx2labels, idx=>l)
194+
end
195+
centroidsacc, _ = get_centroidsacc(seg.image_indexmap)
196+
istim = labels2idx[stimulus_index(seg, centroidsacc; colorproj = colorproj, expectedloc = expectedloc)]
110197
for (j, cb) in enumerate(cbs)
111198
# set_gtk_property!(cb, "active", j <= nsegs)
112199
cb[] = (j == istim || j == preclick)
113-
end
200+
end
114201
imshow(canvases[1, 1], img)
115-
imshow(canvases[2, 1], map(i->colors[i], labels_map(seg)))
202+
imshow(canvases[2, 1], map(l->colors[labels2idx[l]], labels_map(seg)))
116203

117204
wait(btnclick)
118205
whichbutton[] == :skip && continue
119206

120207
keep = Int[]
121208
for (j, cb) in enumerate(cbs)
122209
if cb[]
123-
push!(keep, j)
210+
push!(keep, idx2labels[j])
124211
end
125212
end
126213
pixelskeep = map(i -> i keep, labels_map(seg))
127214
L = label_components(pixelskeep)
128215
newseg = SegmentedImage(img, L)
129-
spotdict, stimulus = spots(newseg)
216+
spotdict, stimulus = spots(newseg; colorproj = colorproj, expectedloc = expectedloc)
130217
push!(results, (file, spotdict, stimulus, newseg))
131218
end
132219

src/segment.jl

Lines changed: 55 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,9 @@ Larger `threshold` values will result in fewer segments.
88
"""
99
function segment_image(
1010
img::AbstractMatrix{<:Color};
11-
threshold::Real = 0.2, # threshold for color similarity in region growing
11+
threshold::Real = 0.15, # threshold for color similarity in region growing
1212
prune::Bool = true, # prune small segments
13-
min_size::Int = 50, # minimum size of segments to keep
13+
min_size::Int = 500, # minimum size of segments to keep
1414
)
1515
seg = unseeded_region_growing(img, threshold)
1616
if prune
@@ -22,16 +22,30 @@ end
2222
segment_image(img::AbstractMatrix{<:Colorant}; kwargs...) = segment_image(color.(img); kwargs...)
2323

2424
"""
25-
idx = stimulus_index(seg::SegmentedImage, colorproj = RGB(1, 1, -2))
25+
idx = stimulus_index(seg::SegmentedImage, centroidsacc; colorproj = RGB(1, 1, -2), expectedloc = nothing)
2626
2727
Given a segmented image `seg`, return the index of the segment that scores
2828
highest on the product of (1) projection (dot product) with `colorproj` and (2)
2929
number of pixels.
30+
31+
Optionally, if images were taken with a fixed location for the stimulus, a segment's score
32+
is divided by the squared distance of its centroid (via `centroidsacc`) from the position given by `expectedloc`.
3033
"""
31-
function stimulus_index(seg::SegmentedImage, colorproj = RGB{Float32}(1, 1, -2))
32-
proj = [l => (colorproj segment_mean(seg, l)) * segment_pixel_count(seg, l) for l in segment_labels(seg)]
33-
(i, _) = argmax(last, proj)
34-
return i
34+
function stimulus_index(seg::SegmentedImage, centroidsacc; colorproj = RGB{Float32}(1, 1, -2), expectedloc = nothing)
35+
if !isnothing(expectedloc)
36+
proj = map(segment_labels(seg)) do l
37+
l == 0 && return 0
38+
val = centroidsacc[l]
39+
centroid = [round(Int, val[1] / val[3]), round(Int, val[2] / val[3])]
40+
return l => (colorproj segment_mean(seg, l) * segment_pixel_count(seg, l) / max(1, sum(abs2, centroid .- expectedloc)))
41+
end
42+
(i, _) = argmax(last, proj)
43+
return i
44+
else
45+
proj = [l => (colorproj segment_mean(seg, l)) * segment_pixel_count(seg, l) for l in segment_labels(seg)]
46+
(i, _) = argmax(last, proj)
47+
return i
48+
end
3549
end
3650

3751
# function contiguous(seg::SegmentedImage, img::AbstractMatrix{<:Color}; min_size::Int = 50)
@@ -47,6 +61,35 @@ end
4761
# contiguous(seg::SegmentedImage, img::AbstractMatrix{<:Colorant}; kwargs...) =
4862
# contiguous(seg, color.(img); kwargs...)
4963

64+
"""
65+
centroidsacc, nadj = get_centroidsacc(seg::SegmentedImage)
66+
67+
Given a the index map `indexmap` of a segmented image, return an accumulator for each segment's centroid
68+
as well as the number of times two segments are adjacent.
69+
"""
70+
function get_centroidsacc(indexmap::Matrix{Int64})
71+
keypair(i, j) = i < j ? (i, j) : (j, i)
72+
R = CartesianIndices(indexmap)
73+
Ibegin, Iend = extrema(R)
74+
I1 = oneunit(Ibegin)
75+
centroidsacc = Dict{Int, Tuple{Int, Int, Int}}() # accumulator for centroids
76+
nadj = Dict{Tuple{Int, Int}, Int}() # number of times two segments are adjacent
77+
for idx in R
78+
l = indexmap[idx]
79+
l == 0 && continue
80+
acc = get(centroidsacc, l, (0, 0, 0))
81+
centroidsacc[l] = (acc[1] + idx[1], acc[2] + idx[2], acc[3] + 1)
82+
for j in max(Ibegin, idx - I1):min(Iend, idx + I1)
83+
lj = indexmap[j]
84+
if lj != l && lj != 0
85+
k = keypair(l, lj)
86+
nadj[k] = get(nadj, k, 0) + 1
87+
end
88+
end
89+
end
90+
return centroidsacc, nadj
91+
end
92+
5093
struct Spot
5194
npixels::Int
5295
centroid::Tuple{Int, Int}
@@ -67,40 +110,21 @@ Spots larger than `max_size_frac * npixels` (default: 10% of the image) are igno
67110
function spots(
68111
seg::SegmentedImage;
69112
max_size_frac=0.1, # no spot is bigger than max_size_frac * npixels
113+
kwargs...
70114
)
71-
keypair(i, j) = i < j ? (i, j) : (j, i)
72-
73-
istim = stimulus_index(seg)
115+
centroidsacc, nadj = get_centroidsacc(seg.image_indexmap)
116+
istim = stimulus_index(seg, centroidsacc; kwargs...)
74117

75-
label = seg.image_indexmap
76-
R = CartesianIndices(label)
77-
Ibegin, Iend = extrema(R)
78-
I1 = oneunit(Ibegin)
79-
centroidsacc = Dict{Int, Tuple{Int, Int, Int}}() # accumulator for centroids
80-
nadj = Dict{Tuple{Int, Int}, Int}() # number of times two segments are adjacent
81-
for idx in R
82-
l = label[idx]
83-
l == 0 && continue
84-
acc = get(centroidsacc, l, (0, 0, 0))
85-
centroidsacc[l] = (acc[1] + idx[1], acc[2] + idx[2], acc[3] + 1)
86-
for j in max(Ibegin, idx - I1):min(Iend, idx + I1)
87-
lj = label[j]
88-
if lj != l && lj != 0
89-
k = keypair(l, lj)
90-
nadj[k] = get(nadj, k, 0) + 1
91-
end
92-
end
93-
end
94118
stimulus = Ref{Pair{Int,Spot}}()
95119
filter!(centroidsacc) do (key, val)
96120
if key == istim
97121
stimulus[] = key => Spot(val[3], (round(Int, val[1] / val[3]), round(Int, val[2] / val[3])))
98122
return false
99123
end
100-
val[3] <= max_size_frac * length(label) || return false
124+
val[3] <= max_size_frac * length(seg.image_indexmap) || return false
101125
# # is the centroid within the segment?
102126
# x, y = round(Int, val[1] / val[3]), round(Int, val[2] / val[3])
103-
# l = label[x, y]
127+
# l = seg.image_indexmap[x, y]
104128
# @show l
105129
# l == key || return false
106130
# is the segment lighter than most of its neighbors?

0 commit comments

Comments
 (0)