Skip to content

Commit 50e0f7b

Browse files
committed
Merge branch 'main' into feat-segmentation-f
2 parents da8cffa + 22f99a9 commit 50e0f7b

File tree

11 files changed

+175
-91
lines changed

11 files changed

+175
-91
lines changed

scripts/ice-floe-tracker.jl

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ function main(args)
2828
required = true
2929
end
3030

31-
landmask_args = [
31+
landmask_cloudmask_args = [
3232
"input",
3333
Dict(:help => "Input image directory", :required => true),
3434
"output",
@@ -44,8 +44,8 @@ function main(args)
4444
Dict(:help => "Output image directory", :required => true),
4545
]
4646

47-
add_arg_table!(settings["landmask"], landmask_args...)
48-
add_arg_table!(settings["cloudmask"], command_common_args...)
47+
add_arg_table!(settings["landmask"], landmask_cloudmask_args...)
48+
add_arg_table!(settings["cloudmask"], landmask_cloudmask_args...)
4949

5050
parsed_args = parse_args(args, settings; as_symbols=true)
5151

src/IceFloeTracker.jl

Lines changed: 2 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,8 @@ using RegisterQD
1616
using StaticArrays
1717
using OffsetArrays: centered
1818

19-
export readdlm, padnhood, bridge, branch, landmask, @persist
19+
export readdlm,
20+
padnhood, bridge, branch, landmask, @persist, load, cloudmask, create_cloudmask
2021

2122
include("utils.jl")
2223
include("persist.jl")
@@ -73,16 +74,6 @@ function fetchdata(; output::AbstractString)
7374
return nothing
7475
end
7576

76-
function cloudmask(;
77-
metadata::AbstractString, input::AbstractString, output::AbstractString
78-
)
79-
mkpath("$output")
80-
touch("$output/a.tiff")
81-
touch("$output/b.tiff")
82-
touch("$output/c.tiff")
83-
return nothing
84-
end
85-
8677
"""
8778
MorphSE
8879

src/bwperim.jl

Lines changed: 3 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,10 @@ Locate the pixels at the boundary of objects in an binary image `bwimg` using 8-
99
# Examples
1010
1111
```jldoctest; setup = :(using IceFloeTracker)
12-
julia> A = zeros(Int, 13, 16); A[2:6, 2:6] .= 1; A[4:8, 7:10] .= 1; A[10:12,13:15] .= 1; A[10:12,3:6] .= 1;
12+
julia> A = zeros(Bool, 13, 16); A[2:6, 2:6] .= 1; A[4:8, 7:10] .= 1; A[10:12,13:15] .= 1; A[10:12,3:6] .= 1;
1313
1414
julia> A
15-
13×16 Matrix{Int64}:
15+
13×16 Matrix{Bool}:
1616
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
1717
0 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0
1818
0 1 1 1 1 1 0 0 0 0 0 0 0 0 0 0
@@ -44,13 +44,7 @@ julia> A
4444
0 0 0 0 0 0 0 0 0 0 0 0 0 0 0 0
4545
```
4646
"""
47-
function bwperim(bwimg)
48-
# work with BitArrays
49-
if typeof(bwimg) <: Matrix
50-
bwimg = BitArray(bwimg)
51-
end
52-
47+
function bwperim(bwimg::T) where {T<:AbstractMatrix{Bool}}
5348
eroded_bwimg = erode(bwimg)
54-
5549
return bwimg .& .!eroded_bwimg
5650
end

src/cloudmask.jl

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -22,19 +22,19 @@ function create_cloudmask(
2222
)::BitMatrix
2323
# Setting thresholds
2424
ref_view = channelview(ref_image)
25-
ref_image_b7 = ref_view[1, :, :]
25+
ref_image_b7 = @view ref_view[1, :, :]
2626
clouds_view = ref_image_b7 .> prelim_threshold
2727
mask_b7 = ref_image_b7 .< band_7_threshold
28-
mask_b2 = ref_view[2, :, :] .> band_2_threshold
28+
mask_b2 = @view(ref_view[2, :, :]) .> band_2_threshold
2929
# First find all the pixels that meet threshold logic in band 7 (channel 1) and band 2 (channel 2)
3030
# Masking clouds and discriminating cloud-ice
3131

3232
mask_b7b2 = mask_b7 .&& mask_b2
3333
# Next find pixels that meet both thresholds and mask them from band 7 (channel 1) and band 2 (channel 2)
3434
b7_masked = mask_b7b2 .* ref_image_b7
35-
b2_masked = mask_b7b2 .* ref_view[2, :, :]
35+
b2_masked = mask_b7b2 .* @view(ref_view[2, :, :])
3636
cloud_ice = Float64.(b7_masked) ./ Float64.(b2_masked)
37-
mask_cloud_ice = @. cloud_ice >= ratio_lower .&& cloud_ice < ratio_upper
37+
mask_cloud_ice = @. cloud_ice >= ratio_lower && cloud_ice < ratio_upper
3838
# Creating final cloudmask
3939
cloudmask = mask_cloud_ice .|| .!clouds_view
4040
return cloudmask
@@ -55,7 +55,9 @@ function apply_cloudmask(
5555
)::Matrix{RGB{Float64}}
5656
masked_image = cloudmask .* ref_image
5757
image_view = channelview(masked_image)
58-
cloudmasked_view = StackedView(zeroarray, image_view[2, :, :], image_view[3, :, :])
58+
cloudmasked_view = StackedView(
59+
zeroarray, @view(image_view[2, :, :]), @view(image_view[3, :, :])
60+
)
5961
cloudmasked_image_rgb = colorview(RGB, cloudmasked_view)
6062
return cloudmasked_image_rgb
6163
end
@@ -69,5 +71,5 @@ end
6971
function create_clouds_channel(
7072
cloudmask::AbstractArray{Bool}, ref_image::Matrix{RGB{Float64}}
7173
)::Matrix{Gray{Float64}}
72-
return Gray.(channelview(cloudmask .* ref_image)[1, :, :])
74+
return Gray.(@view(channelview(cloudmask .* ref_image)[1, :, :]))
7375
end

src/ice-water-discrimination.jl

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -57,15 +57,15 @@ function discriminate_ice_water(
5757
clouds_channel = IceFloeTracker.create_clouds_channel(
5858
cloudmask_bitmatrix, reflectance_image
5959
)
60-
reflectance_image_band7 = channelview(reflectance_image)[1, :, :]
60+
reflectance_image_band7 = @view(channelview(reflectance_image)[1, :, :])
6161
image_sharpened_gray = IceFloeTracker.imsharpen_gray(
6262
image_sharpened, landmask_bitmatrix
6363
)
6464
normalized_image = IceFloeTracker.normalize_image(
6565
image_sharpened,
6666
image_sharpened_gray,
6767
landmask_bitmatrix,
68-
collect(strel_diamond((5, 5))),
68+
strel_diamond((5, 5)),
6969
)
7070

7171
# first define all of the image variations
@@ -76,12 +76,12 @@ function discriminate_ice_water(
7676
image_floes = IceFloeTracker.apply_landmask(reflectance_image, landmask_bitmatrix) # source reflectance, landmasked
7777
image_floes_view = channelview(image_floes)
7878

79-
floes_band_2 = image_floes_view[2, :, :]
80-
floes_band_1 = image_floes_view[3, :, :]
79+
floes_band_2 = @view(image_floes_view[2, :, :])
80+
floes_band_1 = @view(image_floes_view[3, :, :])
8181

8282
# keep pixels greater than intensity 100 in bands 2 and 1
83-
floes_band_2_keep = floes_band_2[floes_band_2 .> floes_threshold]
84-
floes_band_1_keep = floes_band_1[floes_band_1 .> floes_threshold]
83+
floes_band_2_keep = floes_band_2[floes_band_2.>floes_threshold]
84+
floes_band_1_keep = floes_band_1[floes_band_1.>floes_threshold]
8585

8686
_, floes_bin_counts = ImageContrastAdjustment.build_histogram(floes_band_2_keep, nbins)
8787
_, vals = Peaks.findmaxima(floes_bin_counts)
@@ -93,7 +93,7 @@ function discriminate_ice_water(
9393
kurt_band_2 = kurtosis(floes_band_2_keep)
9494
skew_band_2 = skewness(floes_band_2_keep)
9595
kurt_band_1 = kurtosis(floes_band_1_keep)
96-
standard_dev = std(normalized_image)
96+
standard_dev = stdmult(, normalized_image)
9797

9898
# find the ratio of clouds in the image to use in threshold filtering
9999
_, clouds_bin_counts = build_histogram(image_clouds .> 0)
@@ -128,16 +128,22 @@ function discriminate_ice_water(
128128
end
129129

130130
normalized_image_copy = copy(normalized_image)
131-
normalized_image_copy[normalized_image_copy .> THRESH] .= 0
132-
normalized_filtered = normalized_image - (normalized_image_copy * 3)
131+
normalized_image_copy[normalized_image_copy.>THRESH] .= 0
132+
@. normalized_image_copy = normalized_image - (normalized_image_copy * 3)
133133

134-
mask_image_clouds = (
135-
image_clouds .< mask_clouds_lower .|| image_clouds .> mask_clouds_upper
134+
# reusing memory allocated in landmask_bitmatrix
135+
# used to be mask_image_clouds
136+
@. landmask_bitmatrix = (
137+
image_clouds < mask_clouds_lower || image_clouds > mask_clouds_upper
136138
)
137-
band7_masked = image_cloudless .* .!mask_image_clouds
138-
ice_water_discriminated_image = clamp01nan.(normalized_filtered - (band7_masked * 3))
139139

140-
return ice_water_discriminated_image
140+
# reusing image_cloudless - used to be band7_masked
141+
@. image_cloudless = image_cloudless * !landmask_bitmatrix
142+
143+
# reusing normalized_image_copy - used to be ice_water_discriminated_image
144+
@. normalized_image_copy = clamp01nan(normalized_image_copy - (image_cloudless * 3))
145+
146+
return normalized_image_copy
141147
end
142148

143149
function _check_threshold_50(

src/landmask.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ function create_landmask(
1717
fill_value_upper::Int=2000,
1818
)::BitMatrix where {T<:AbstractMatrix}
1919
landmask_binary = binarize_landmask(landmask_image)
20-
dilated = IceFloeTracker.MorphSE.dilate(landmask_binary, struct_elem)
20+
dilated = IceFloeTracker.MorphSE.dilate(landmask_binary, centered(struct_elem))
2121
return ImageMorphology.imfill(.!dilated, (fill_value_lower, fill_value_upper))
2222
end
2323

src/normalization.jl

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ function normalize_image(
1616
image_sharpened::Matrix{Float64},
1717
image_sharpened_gray::T,
1818
landmask::BitMatrix,
19-
struct_elem::Matrix{Bool};
19+
struct_elem::ImageMorphology.MorphologySEArray{2};
2020
)::Matrix{Gray{Float64}} where {T<:AbstractMatrix{Gray{Float64}}}
2121
image_dilated = MorphSE.dilate(image_sharpened_gray, struct_elem)
2222

@@ -32,7 +32,7 @@ function normalize_image(
3232
landmask::BitMatrix,
3333
)::Matrix{Gray{Float64}}
3434
return normalize_image(
35-
image_sharpened, image_sharpened_gray, landmask, collect(strel_diamond((5, 5)))
35+
image_sharpened, image_sharpened_gray, landmask, strel_diamond((5, 5))
3636
)
3737
end
3838

@@ -78,7 +78,7 @@ Sharpen `truecolor_image`.
7878
- `intensity`: amount of sharpening to perform
7979
"""
8080
function imsharpen(
81-
truecolor_image,
81+
truecolor_image::Matrix{RGB{Float64}},
8282
landmask_no_dilate::BitMatrix,
8383
lambda::Real=0.1,
8484
kappa::Real=75,
@@ -91,11 +91,12 @@ function imsharpen(
9191
intensity::Float64=2.0,
9292
)::Matrix{Float64}
9393
input_image = IceFloeTracker.apply_landmask(truecolor_image, landmask_no_dilate)
94-
image_diffused = IceFloeTracker.diffusion(input_image, lambda, kappa, niters)
95-
masked_view = Float64.(channelview(image_diffused))
94+
input_image .= IceFloeTracker.diffusion(input_image, lambda, kappa, niters)
95+
masked_view = Float64.(channelview(input_image))
9696

9797
eq = [
98-
_adjust_histogram(masked_view[i, :, :], nbins, rblocks, cblocks, clip) for i in 1:3
98+
_adjust_histogram(@view(masked_view[i, :, :]), nbins, rblocks, cblocks, clip) for
99+
i in 1:3
99100
]
100101

101102
image_equalized = colorview(RGB, eq...)
@@ -106,8 +107,7 @@ function imsharpen(
106107

107108
image_sharpened =
108109
image_equalized_gray .* (1 + intensity) .+ image_smoothed .* (-intensity)
109-
image_sharpened = max.(image_sharpened, 0.0)
110-
return min.(image_sharpened, 1.0)
110+
return min.(max.(image_sharpened, 0.0), 1.0)
111111
end
112112

113113
"""

src/pipeline/preprocess.jl

Lines changed: 94 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,94 @@
1-
function check_landmask_path(lmpath::String)::Nothing
2-
name = basename(lmpath)
3-
input = dirname(lmpath)
4-
!isfile(lmpath) && error(
5-
"`$(name)` not found in $input. Please ensure a coastline image file named `$name` exists in $input.",
6-
)
7-
return nothing
8-
end
9-
10-
"""
11-
landmask(; input, output)
12-
13-
Given an input directory with a landmask file and possibly truecolor images, create a land/soft ice mask. The resulting images are saved to the snakemake output directory.
14-
15-
# Arguments
16-
- `input`: path to image dir containing truecolor and landmask source images
17-
- `output`: path to output dir where land-masked truecolor images and the generated binary land mask are saved
18-
19-
"""
20-
function landmask(; input::String, output::String)
21-
landmask_fname = "landmask.tiff"
22-
@info "Looking for $landmask_fname in $input"
23-
24-
lmpath = joinpath(input, landmask_fname)
25-
check_landmask_path(lmpath)
26-
@info "$landmask_fname found in $input. Creating landmask..."
27-
28-
img = load(lmpath)
29-
mkpath(output)
30-
out = @persist create_landmask(img) joinpath(output, "generated_landmask.png")
31-
@info "Landmask created succesfully."
32-
return out
33-
end
1+
function check_landmask_path(lmpath::String)::Nothing
2+
name = basename(lmpath)
3+
input = dirname(lmpath)
4+
!isfile(lmpath) && error(
5+
"`$(name)` not found in $input. Please ensure a coastline image file named `$name` exists in $input.",
6+
)
7+
return nothing
8+
end
9+
10+
"""
11+
landmask(; input, output)
12+
13+
Given an input directory with a landmask file and possibly truecolor images, create a land/soft ice mask. The resulting images are saved to the snakemake output directory. Returns the landmkask object.
14+
15+
# Arguments
16+
- `input`: path to image dir containing truecolor and landmask source images
17+
- `output`: path to output dir where land-masked truecolor images and the generated binary land mask are saved
18+
19+
"""
20+
function landmask(; input::String, output::String)
21+
landmask_fname = "landmask.tiff"
22+
@info "Looking for $landmask_fname in $input"
23+
24+
lmpath = joinpath(input, landmask_fname)
25+
check_landmask_path(lmpath)
26+
@info "$landmask_fname found in $input. Creating landmask..."
27+
28+
img = load(lmpath)
29+
mkpath(output)
30+
out = @persist create_landmask(img) joinpath(output, "generated_landmask.png")
31+
@info "Landmask created succesfully."
32+
return out
33+
end
34+
35+
"""
36+
cache_vector(type::Type, numel::Int64, size::Tuple{Int64, Int64})::Vector{type}
37+
38+
Build a vector of types `type` with `numel` elements of size `size`.
39+
40+
Example
41+
42+
```jldoctest
43+
julia> cache_vector(Matrix{Float64}, 3, (2, 2))
44+
3-element Vector{Matrix{Float64}}:
45+
[0.0 6.9525705991269e-310; 6.9525705991269e-310 0.0]
46+
[0.0 6.9525705991269e-310; 6.9525705991269e-310 0.0]
47+
[0.0 6.95257028858726e-310; 6.95257029000147e-310 0.0]
48+
```
49+
"""
50+
function cache_vector(type::Type, numel::Int64, size::Tuple{Int64,Int64})::Vector{type}
51+
return [type(undef, size) for _ in 1:numel]
52+
end
53+
54+
"""
55+
load_imgs(; input::String, image_type::String)
56+
57+
Load all images of type `image_type` (either `"truecolor"` or `"reflectance"`) in `input` into a vector.
58+
"""
59+
function load_imgs(; input::String, image_type::Union{Symbol,String})
60+
return [
61+
float64.(load(joinpath(input, f))) for
62+
f in readdir(input) if contains(f, string(image_type))
63+
]
64+
end
65+
66+
function load_truecolor_imgs(; input::String)
67+
return load_imgs(; input=input, image_type="truecolor")
68+
end
69+
70+
function load_reflectance_imgs(; input::String)
71+
return load_imgs(; input=input, image_type="reflectance")
72+
end
73+
74+
function cloudmask(; input::String, output::String)::Vector{BitMatrix}
75+
# find reflectance imgs in input dir
76+
ref = [img for img in readdir(input) if contains(img, "reflectance")] # ref is sorted
77+
total_ref = length(ref)
78+
@info "Found $(total_ref) reflectance images in $input.
79+
Cloudmasking false color images..."
80+
81+
# Preallocate container for the cloudmasks
82+
ref_img = IceFloeTracker.float64.(IceFloeTracker.load(joinpath(input, ref[1]))) # read in the first one to retrieve size
83+
sz = size(ref_img)
84+
cloudmasks = [BitMatrix(undef, sz) for _ in 1:total_ref]
85+
86+
# Do the first one because it's loaded already
87+
cloudmasks[1] = IceFloeTracker.create_cloudmask(ref_img)
88+
# and now the rest
89+
for i in 2:total_ref
90+
img = IceFloeTracker.float64.(IceFloeTracker.load(joinpath(input, ref[i])))
91+
cloudmasks[i] = IceFloeTracker.create_cloudmask(img)
92+
end
93+
return cloudmasks
94+
end

test/test-bwperim.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
println("---------------- bwperim Tests ------------------")
44

55
# Create image with 3 connected components. The test consists of digging the biggests holes for each blob in the foreground using bwperim, thereby creating 3 additional connected components, 6 in total.
6-
A = zeros(Int, 13, 16)
6+
A = zeros(Bool, 13, 16)
77
A[2:6, 2:6] .= 1
88
A[4:8, 7:10] .= 1
99
A[10:12, 13:15] .= 1

test/test-normalize-image.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
@testset "Normalize Image" begin
22
println("-------------------------------------------------")
33
println("---------- Create Normalization Test ------------")
4-
struct_elem2 = collect(strel_diamond((5, 5))) #original matlab structuring element - a disk-shaped kernel with radius of 2 px
4+
struct_elem2 = strel_diamond((5, 5)) #original matlab structuring element - a disk-shaped kernel with radius of 2 px
55
matlab_normalized_img_file = "$(test_data_dir)/matlab_normalized.png"
66
matlab_sharpened_file = "$(test_data_dir)/matlab_sharpened.png"
77
matlab_diffused_file = "$(test_data_dir)/matlab_diffused.png"

0 commit comments

Comments
 (0)