Skip to content

Commit f36d5f9

Browse files
committed
Initial version
1 parent 6f2350c commit f36d5f9

File tree

6 files changed

+227
-2
lines changed

6 files changed

+227
-2
lines changed

Project.toml

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,25 @@ uuid = "454fbcff-fa78-4492-9c80-2cfc3f2a4b52"
33
authors = ["Tim Holy <[email protected]> and contributors"]
44
version = "1.0.0-DEV"
55

6+
[deps]
7+
FileIO = "5789e2e9-d7fb-5bc7-8068-2c6fae9b9549"
8+
ImageCore = "a09fc81d-aa75-5fe9-8630-4744c3626534"
9+
ImageMorphology = "787d08f9-d448-5407-9aad-5290dd7ab264"
10+
ImageSegmentation = "80713f31-8817-5129-9cf8-209ff8fb23e1"
11+
Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c"
12+
13+
[weakdeps]
14+
ImageView = "86fae568-95e7-573e-a6b2-d8a6b900c9ef"
15+
16+
[extensions]
17+
CounterMarkingImageViewExt = "ImageView"
18+
619
[compat]
7-
julia = "1.6.7"
20+
FileIO = "1"
21+
ImageCore = "0.10"
22+
ImageMorphology = "0.4"
23+
ImageSegmentation = "1.9"
24+
julia = "1.10"
825

926
[extras]
1027
Test = "8dfed614-e22c-5e08-85e1-65c5234f0b40"

docs/Project.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,6 @@
11
[deps]
22
CounterMarking = "454fbcff-fa78-4492-9c80-2cfc3f2a4b52"
33
Documenter = "e30172f5-a6a5-5a46-863b-614d45cd2de4"
4+
5+
[compat]
6+
Documenter = "1"

ext/CounterMarkingImageViewExt.jl

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,37 @@
1+
module CounterMarkingImageViewExt
2+
3+
using CounterMarking: CounterMarking
4+
using ImageCore
5+
using ImageSegmentation
6+
using ImageView
7+
using Random
8+
9+
function colorize(seg, segidxs::AbstractSet{Int}, color::Colorant)
10+
label = seg.image_indexmap
11+
img = similar(label, promote_type(typeof(color), valtype(seg.segment_means)))
12+
fill!(img, zero(eltype(img)))
13+
for idx in eachindex(label)
14+
label[idx] segidxs || continue
15+
img[idx] = color
16+
end
17+
return img
18+
end
19+
20+
function linkpair(img, imgc)
21+
gd = imshow(img)
22+
zr = gd["roi"]["zoomregion"]
23+
slicedata = gd["roi"]["slicedata"]
24+
gdc = imshow(imgc, nothing, zr, slicedata)
25+
return (gd, gdc)
26+
end
27+
28+
# For visualization
29+
function get_random_color(seed)
30+
Random.seed!(seed)
31+
rand(RGB{N0f8})
32+
end
33+
34+
CounterMarking.randshow(seg; kwargs...) = imshow(map(i->get_random_color(i), labels_map(seg)); kwargs...)
35+
CounterMarking.meanshow(seg; kwargs...) = imshow(map(i->segment_mean(seg, i), labels_map(seg)); kwargs...)
36+
37+
end

src/CounterMarking.jl

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,14 @@
11
module CounterMarking
22

3-
# Write your package code here.
3+
using ImageCore
4+
using ImageSegmentation
5+
using ImageMorphology: label_components
6+
using FileIO
7+
8+
export segment_image, stimulus_index, spots, Spot, upperleft
9+
export randshow, meanshow
10+
11+
include("segment.jl")
12+
include("stubs.jl")
413

514
end

src/segment.jl

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
"""
2+
seg = segment_image(img; threshold=0.1, min_size=20)
3+
4+
Given an image `img`, segment it into regions using a region growing algorithm.
5+
`min_size` is the minimum number of pixels per segment, and `threshold` determines
6+
how different two colors must be to be considered different segments.
7+
Larger `threshold` values will result in fewer segments.
8+
"""
9+
function segment_image(
10+
img::AbstractMatrix{<:Color};
11+
threshold::Real = 0.2, # threshold for color similarity in region growing
12+
prune::Bool = true, # prune small segments
13+
min_size::Int = 50, # minimum size of segments to keep
14+
)
15+
seg = unseeded_region_growing(img, threshold)
16+
L = label_components(labels_map(seg)) # insist on contiguous regions
17+
seg = SegmentedImage(img, L)
18+
if prune
19+
println("Pruning segments smaller than $min_size pixels")
20+
seg = prune_segments(seg, label -> segment_pixel_count(seg, label) < min_size, (l1, l2) -> colordiff(segment_mean(seg, l1), segment_mean(seg, l2)))
21+
end
22+
return seg
23+
end
24+
segment_image(img::AbstractMatrix{<:Colorant}; kwargs...) = segment_image(color.(img); kwargs...)
25+
26+
"""
27+
idx = stimulus_index(seg::SegmentedImage, colorproj = RGB(1, 1, -2))
28+
29+
Given a segmented image `seg`, return the index of the segment that scores
30+
highest on the product of (1) projection (dot product) with `colorproj` and (2)
31+
number of pixels.
32+
"""
33+
function stimulus_index(seg::SegmentedImage, colorproj = RGB{Float32}(1, 1, -2))
34+
proj = [l => (colorproj segment_mean(seg, l)) * segment_pixel_count(seg, l) for l in segment_labels(seg)]
35+
(i, _) = argmax(last, proj)
36+
return i
37+
end
38+
39+
struct Spot
40+
npixels::Int
41+
centroid::Tuple{Int, Int}
42+
end
43+
44+
"""
45+
spotdict, stimulus = spots(seg; max_size_frac=0.1)
46+
47+
Given a segmented image `seg`, return a `Dict(idx => spot)` where `idx` is the segment index
48+
and `spot` is a `Spot` object where `spot.npixels` is the number of pixels in the segment
49+
and `spot.centroid` is the centroid of the segment.
50+
51+
`stimulus` is a `Pair{Int, Spot}` where the first element is the index of the
52+
stimulus segment and the second element is the `Spot` object for that segment.
53+
54+
Spots larger than `max_size_frac * npixels` (default: 10% of the image) are ignored.
55+
"""
56+
function spots(
57+
seg;
58+
max_size_frac=0.1, # no spot is bigger than max_size_frac * npixels
59+
)
60+
keypair(i, j) = i < j ? (i, j) : (j, i)
61+
62+
istim = stimulus_index(seg)
63+
64+
label = seg.image_indexmap
65+
R = CartesianIndices(label)
66+
Ibegin, Iend = extrema(R)
67+
I1 = one(Ibegin)
68+
centroidsacc = Dict{Int, Tuple{Int, Int, Int}}() # accumulator for centroids
69+
nadj = Dict{Tuple{Int, Int}, Int}() # number of times two segments are adjacent
70+
for idx in R
71+
l = label[idx]
72+
l == 0 && continue
73+
acc = get(centroidsacc, l, (0, 0, 0))
74+
centroidsacc[l] = (acc[1] + idx[1], acc[2] + idx[2], acc[3] + 1)
75+
for j in max(Ibegin, idx - I1):min(Iend, idx + I1)
76+
lj = label[j]
77+
if lj != l && lj != 0
78+
k = keypair(l, lj)
79+
nadj[k] = get(nadj, k, 0) + 1
80+
end
81+
end
82+
end
83+
stimulus = Ref{Pair{Int,Spot}}()
84+
filter!(centroidsacc) do (key, val)
85+
if key == istim
86+
stimulus[] = key => Spot(val[3], (round(Int, val[1] / val[3]), round(Int, val[2] / val[3])))
87+
return false
88+
end
89+
val[3] <= max_size_frac * length(label) || return false
90+
# # is the centroid within the segment?
91+
# x, y = round(Int, val[1] / val[3]), round(Int, val[2] / val[3])
92+
# l = label[x, y]
93+
# @show l
94+
# l == key || return false
95+
# is the segment lighter than most of its neighbors?
96+
dcol, ncol = zero(valtype(seg.segment_means)), 0
97+
for (k, n) in nadj
98+
if key == k[1] || key == k[2]
99+
l1, l2 = k[1], k[2]
100+
if l1 == key
101+
l1, l2 = l2, l1
102+
end
103+
dcol += n * (segment_mean(seg, l1) - segment_mean(seg, l2))
104+
ncol += n
105+
end
106+
end
107+
return reducec(+, dcol) < 0
108+
end
109+
return Dict(l => Spot(val[3], (round(Int, val[1] / val[3]), round(Int, val[2] / val[3]))) for (l, val) in centroidsacc), stimulus[]
110+
end
111+
112+
"""
113+
spotdict, stimulus = upperleft(spotdict::AbstractDict{Int, Spot}, stimulus, imgsize)
114+
115+
Given a `spotdict` of `Spot` objects and a `stimulus` segment, return a new
116+
`spotdict` where the centroids of the spots are flipped so that the stimlus spot
117+
is in the upper left corner.
118+
"""
119+
function upperleft(spotdict::AbstractDict{Int, Spot}, stimulus, imgsize)
120+
sidx, ss = stimulus
121+
midpoint = imgsize 2
122+
c1, c2 = ss.centroid .< midpoint
123+
imsz1, imsz2 = imgsize
124+
125+
function flip(spot::Spot)
126+
x1, x2 = spot.centroid
127+
return Spot(spot.npixels, (c1 * x1 + (1 - c1) * (imsz1 - x1), c2 * x2 + (1 - c2) * (imsz2 - x2)))
128+
end
129+
return Dict(k => flip(v) for (k, v) in spotdict), sidx => flip(ss)
130+
end

src/stubs.jl

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
"""
2+
randshow(seg; kwargs...)
3+
4+
Display a segmented image using random colors for each segment.
5+
6+
!!! note
7+
You must load the `ImageView` package to use this function.
8+
"""
9+
function randshow end
10+
11+
"""
12+
meanshow(seg; kwargs...)
13+
14+
Display a segmented image using the mean color of each segment.
15+
16+
!!! note
17+
You must load the `ImageView` package to use this function.
18+
"""
19+
function meanshow end
20+
21+
function __init__()
22+
Base.Experimental.register_error_hint(MethodError) do io, exc, _, _
23+
if exc.f (randshow, meanshow)
24+
if isempty(methods(exc.f))
25+
printstyled(io, "\nYou may need `using ImageView` to load the appropriate methods."; color=:yellow)
26+
end
27+
end
28+
end
29+
end

0 commit comments

Comments
 (0)