Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat: long tracker #529

Merged
merged 46 commits into from
Dec 16, 2024
Merged
Show file tree
Hide file tree
Changes from 38 commits
Commits
Show all changes
46 commits
Select commit Hold shift + click to select a range
c5c3b9c
feat: get_unmatched
cpaniaguam Dec 2, 2024
f75a650
temp: add unmatched floes
cpaniaguam Dec 3, 2024
586134f
refactor: second step
cpaniaguam Dec 3, 2024
364eeac
feat: enhance get_unmatched to dynamically determine unmatched columns
cpaniaguam Dec 3, 2024
7ff09fa
feat: update getbestmatchdata to use dynamic column names for matchin…
cpaniaguam Dec 3, 2024
6b1f4b3
feat: update MatchedPairs to create an empty dataframe with specified…
cpaniaguam Dec 3, 2024
39d9238
feat: add get_trajectory_heads
cpaniaguam Dec 4, 2024
c8e1923
feat: swap last values
cpaniaguam Dec 4, 2024
c30ca5c
feat: get_dt
cpaniaguam Dec 4, 2024
1189614
feat: enhance floe tracking with new functions and data handling impr…
cpaniaguam Dec 4, 2024
efc0bd4
feat: abstraction find_floe_matches
cpaniaguam Dec 4, 2024
478ac1d
feat: enhance get_unmatched
cpaniaguam Dec 4, 2024
13ffabb
feat: reset_id
cpaniaguam Dec 5, 2024
f802fa4
feat: update floe area threshold and enhance unmatched floe handling
cpaniaguam Dec 5, 2024
7152321
feat: add unique ID assignment for floes in properties tables
cpaniaguam Dec 6, 2024
ce03121
feat: consolidate_matched_pairs
cpaniaguam Dec 6, 2024
cc4d228
feat: enhance consolidate_matched_pairs
cpaniaguam Dec 6, 2024
765a417
feat: improve floe matching algorithm and optimize performance
cpaniaguam Dec 6, 2024
212087d
feat: add default parameter for reset_id!
cpaniaguam Dec 6, 2024
cd96d0c
doc: consolidate_matched_pairs to include iteration context
cpaniaguam Dec 6, 2024
4cf9897
fix: consolidate_matched_pairs
cpaniaguam Dec 6, 2024
3a97c5d
feat: add get_matches
cpaniaguam Dec 6, 2024
e560c55
feat: long_tracker
cpaniaguam Dec 6, 2024
a071caa
feat: include long_tracker in IceFloeTracker
cpaniaguam Dec 6, 2024
6c2df05
fix: modify long_tracker to return trajectories with uuid in the left…
cpaniaguam Dec 6, 2024
6300002
test: long_tracker
cpaniaguam Dec 7, 2024
e126f38
refactor: track-floes.jl sandbox file
cpaniaguam Dec 7, 2024
001e668
test: pad_string func
cpaniaguam Dec 7, 2024
50b556c
feat: n(amed)testset
cpaniaguam Dec 7, 2024
941e722
test: update test-matchcorr.jl
cpaniaguam Dec 7, 2024
13dc002
refactor: remove repeated find_floe_matches
cpaniaguam Dec 7, 2024
50ecf06
refactor: update notebook
cpaniaguam Dec 7, 2024
4fcb514
refactor: remove comment
cpaniaguam Dec 9, 2024
a75b6e6
refactor: reorder function calls
cpaniaguam Dec 9, 2024
88fc304
refactor: restore uuid addition
cpaniaguam Dec 9, 2024
a0eb8b1
refactor: update tracker nb
cpaniaguam Dec 9, 2024
7edb6b6
refactor: uuid to ID
cpaniaguam Dec 9, 2024
cd726ce
refactor: replace uuid with ID in test-long-tracker
cpaniaguam Dec 9, 2024
c3ac6a9
refactor: remove TODO
cpaniaguam Dec 11, 2024
6372a3a
docs: update long_tracker docstring
cpaniaguam Dec 11, 2024
a6a87c7
test: add addgaps
cpaniaguam Dec 11, 2024
2cec347
refactor: relocate prereqs
cpaniaguam Dec 11, 2024
d9d2087
test: test cases with gaps
cpaniaguam Dec 11, 2024
dd231b3
test: use named test cases
cpaniaguam Dec 11, 2024
3cadbde
test: props_test_case augment scope
cpaniaguam Dec 11, 2024
003930a
test: simplify trajectory ID assertions
cpaniaguam Dec 16, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
181 changes: 165 additions & 16 deletions notebooks/track-floes/track-floes.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,7 @@
"metadata": {},
"outputs": [],
"source": [
"using IceFloeTracker: pairfloes, deserialize, PaddedView, float64, mosaicview, Gray\n",
"using DataFrames\n",
"imshow(x) = Gray.(x);\n"
"using IceFloeTracker: deserialize, addfloemasks!, adduuid!, add_passtimes!, addψs!, long_tracker, addlatlon!, imshow"
]
},
{
Expand Down Expand Up @@ -80,26 +78,133 @@
"metadata": {},
"outputs": [],
"source": [
"# Load data\n",
"pth = joinpath(HOME, \"test\", \"test_inputs\", \"tracker\")\n",
"floedata = deserialize(joinpath(pth,\"tracker_test_data.dat\"))\n",
"passtimes = deserialize(joinpath(pth,\"passtimes.dat\"))\n",
"begin # Load data\n",
" pth = joinpath(HOME, \"test\", \"test_inputs\", \"tracker\")\n",
" floedata = deserialize(joinpath(pth, \"tracker_test_data.dat\"))\n",
" # test/test_inputs/tracker/tracker_test_data.dat\n",
" passtimes = deserialize(joinpath(pth, \"passtimes.dat\"))\n",
" props, imgs = deepcopy.([floedata.props, floedata.imgs])\n",
"\n",
"latlonimgpth = joinpath(HOME, \"test\", \"test_inputs\", \"NE_Greenland_truecolor.2020162.aqua.250m.tiff\")\n",
" # Add required fields to props\n",
" addfloemasks!(props, imgs)\n",
" add_passtimes!(props, passtimes)\n",
" addψs!(props)\n",
" adduuid!(props)\n",
"end\n",
"\n",
"begin # Filter out floes with area less than `floe_area_threshold` pixels\n",
" floe_area_threshold = 400\n",
" for (i, prop) in enumerate(props)\n",
" props[i] = prop[prop[:, :area].>=floe_area_threshold, :];\n",
" sort!(props[i], :area, rev=true);\n",
" end\n",
"end\n",
"\n",
"props, imgs = deepcopy(floedata.props), deepcopy(floedata.imgs);\n",
"# Delete some floes\n",
"deleteat!(props[1], 1); # delete the first floe in day 1 so it doesn't have a match in day 2\n",
"deleteat!(props[2], 5); # delete the fifth floe in day 2 so it doesn't have a match in day 1\n",
"\n",
"# Filter out floes with area less than 350 pixels\n",
"for (i, prop) in enumerate(props)\n",
" props[i] = prop[prop[:, :area].>=350, :]\n",
"end"
"# All floes in days 1 and 2 have a match in day 3\n",
"# Expected: 5 trajectories, 3 of length 3 and 2 of length 2\n",
"nothing # suppress output -- not particularly informative. See the next block."
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 4. Pair and label floes"
"### 4. View floe data"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Day 1\n",
"props[1][!, [:uuid, :passtime, :area]]"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"imshow(imgs[1])"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"imshow(props[1][1, :mask])"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Day 2\n",
"props[2][!, [:uuid, :passtime, :area]]"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"imshow(imgs[2]) # slightly rotated version of the image in day 1"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"imshow(props[2][1, :mask])"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# Day 3\n",
"props[3][!, [:uuid, :passtime, :area]]"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"imshow(props[3][1, :mask]) # missing in day 1"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"imshow(props[3][5, :mask]) # missing in day 2"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 5. Pair and label floes"
]
},
{
Expand All @@ -109,13 +214,57 @@
"outputs": [],
"source": [
"# Get paired floes with labels\n",
"pairs = pairfloes(imgs, props, passtimes, latlonimgpth, condition_thresholds, mc_thresholds)"
"trajectories = long_tracker(props, condition_thresholds, mc_thresholds);"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"### 6. View trajectories and _goodness_ of pairings"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"trajectories[!, [:ID, :passtime, :area_mismatch, :corr]]"
]
},
{
"cell_type": "markdown",
"metadata": {},
"source": [
"7. Add latitude/longitude data to trajectories"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"latlonimgpth = joinpath(HOME, \"test\", \"test_inputs\", \"NE_Greenland_truecolor.2020162.aqua.250m.tiff\")\n",
"addlatlon!(trajectories, latlonimgpth)"
]
},
{
"cell_type": "code",
"execution_count": null,
"metadata": {},
"outputs": [],
"source": [
"# View trajectories with lat/lon data\n",
"cols = [:ID, :area, :passtime, :latitude, :longitude]\n",
"trajectories[!, cols]"
]
}
],
"metadata": {
"kernelspec": {
"display_name": "Julia 1.9.0",
"display_name": "Julia 1.9.4",
"language": "julia",
"name": "julia-1.9"
},
Expand Down
1 change: 1 addition & 0 deletions src/IceFloeTracker.jl
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ include("segmentation_f.jl")
include("tracker/tracker-funcs.jl")
include("tracker/matchcorr.jl")
include("tracker/tracker.jl")
include("tracker/long_tracker.jl")

"""
MorphSE
Expand Down
161 changes: 161 additions & 0 deletions src/tracker/long_tracker.jl
Original file line number Diff line number Diff line change
@@ -0,0 +1,161 @@
"""
long_tracker(props, condition_thresholds, mc_thresholds)

Track ice floes over multiple days.

Trajectories are built in two steps:
0. Get pairs of floes in day 1 and day 2. Any unmatched floes, in both day 1 and day 2, become the "heads" of their respective trajectories.
1. For each subsequent day, find pairs of floes for the current trajectory heads. Again, any unmatched floe in the new prop table starts a new trajectory.

# Arguments
- `props::Vector{DataFrame}`: A vector of DataFrames, each containing ice floe properties for a single day. Each DataFrame must have the following columns in the order specified:
cpaniaguam marked this conversation as resolved.
Show resolved Hide resolved
- "area"
- "min_row"
- "min_col"
- "max_row"
- "max_col"
- "row_centroid"
- "col_centroid"
- "convex_area"
- "major_axis_length"
- "minor_axis_length"
- "orientation"
- "perimeter"
- "mask": 2D array of booleans
- "ID": unique identifier
- "passtime": A timestamp for the floe
- "psi": the psi-s curve for the floe
- `condition_thresholds`: 3-tuple of thresholds (each a named tuple) for deciding whether to match floe `i` from day `k` to floe j from day `k+1`
- `mc_thresholds`: thresholds for area mismatch and psi-s shape correlation

# Returns
A DataFrame with the above columns, plus two extra columns, "area_mismatch" and "corr", which are the area mismatch and correlation between a floe and the one that follows it in the trajectory. Trajectories are identified by a unique identifier, "uuid".
"""
function long_tracker(props::Vector{DataFrame}, condition_thresholds, mc_thresholds)
begin # 0th iteration: pair floes in day 1 and day 2 and add unmatched floes to _pairs
props1, props2 = props[1:2]
matched_pairs0 = find_floe_matches(props1, props2, condition_thresholds, mc_thresholds)

# Get unmatched floes from day 1/2
unmatched1 = get_unmatched(props1, matched_pairs0.props1)
unmatched2 = get_unmatched(props2, matched_pairs0.props2)
unmatched = vcat(unmatched1, unmatched2)
consolidated_matched_pairs = consolidate_matched_pairs(matched_pairs0)

# Get _pairs: preliminary matched and unmatched floes
trajectories = vcat(consolidated_matched_pairs, unmatched)
trajectories[:, [:uuid, :passtime, :area_mismatch, :corr]]
end

begin # Start 3:end iterations
for i in 3:length(props)
trajectory_heads = get_trajectory_heads(trajectories)
new_pairs = IceFloeTracker.find_floe_matches(trajectory_heads, props[i], condition_thresholds, mc_thresholds)
# Get unmatched floes in day 2 (iterations > 2)
unmatched2 = get_unmatched(props[i], new_pairs.props2)
new_pairs = IceFloeTracker.get_matches(new_pairs)

# Attach new matches and unmatched floes to trajectories
trajectories = vcat(trajectories, new_pairs, unmatched2)
DataFrames.sort!(trajectories, [:uuid, :passtime])
_swap_last_values!(trajectories)
end
end
IceFloeTracker.reset_id!(trajectories)
trajectories.ID = trajectories.uuid
# list the uuid in the leftmost column
cols = [col for col in names(trajectories) if col ∉ ["ID", "uuid"]]
return trajectories[!, ["ID", cols...]]
end

"""
find_floe_matches(
tracked,
candidate_props,
condition_thresholds,
mc_thresholds
)

Find matches for floes in `tracked` from floes in `candidate_props`.

# Arguments
- `tracked`: dataframe containing floe trajectories.
- `candidate_props`: dataframe containing floe candidate properties.
- `condition_thresholds`: thresholds for deciding whether to match floe `i` from tracked to floe j from `candidate_props`
- `mc_thresholds`: thresholds for area mismatch and psi-s shape correlation
"""
function find_floe_matches(
tracked::T,
candidate_props::T,
condition_thresholds,
mc_thresholds
) where {T<:AbstractDataFrame}
props1 = deepcopy(tracked)
props2 = deepcopy(candidate_props)
match_total = MatchedPairs(props2)
while true # there are no more floes to match in props1

# This routine mutates both props1 and props2.
# Get preliminary matches for floe r in props1 from floes in props2

# Container for props of matched floe pairs and their similarity ratios. Matches will be updated and added to match_total
matched_pairs = MatchedPairs(props2)
for r in 1:nrow(props1) # TODO: consider using eachrow(props1) to iterate over rows
# 1. Collect preliminary matches for floe r in matching_floes
matching_floes = makeemptydffrom(props2)

for s in 1:nrow(props2) # TODO: consider using eachrow(props2) to iterate over rows
Δt = get_dt(props1, r, props2, s)
@debug "Considering floe 2:$s for floe 1:$r"
ratios, conditions, dist = compute_ratios_conditions(
(props1, r), (props2, s), Δt, condition_thresholds
)

if callmatchcorr(conditions)
@debug "Getting mismatch and correlation for floe 1:$r and floe 2:$s"
(area_mismatch, corr) = matchcorr(
props1.mask[r], props2.mask[s], Δt; mc_thresholds.comp...
)

if isfloegoodmatch(
conditions, mc_thresholds.goodness, area_mismatch, corr
)
@debug "** Found a good match for floe 1:$r => 2:$s"
appendrows!(
matching_floes,
props2[s, :],
(ratios..., area_mismatch, corr),
s,
dist,
)
@debug "Matching floes" matching_floes
end
end
end # of s for loop

# 2. Find the best match for floe r
hollandjg marked this conversation as resolved.
Show resolved Hide resolved
@debug "Finding best match for floe 1:$r"
best_match_idx = getidxmostminimumeverything(matching_floes.ratios)
@debug "Best match index for floe 1:$r: $best_match_idx"
if isnotnan(best_match_idx)
bestmatchdata = getbestmatchdata(
best_match_idx, r, props1, matching_floes
) # might be copying data unnecessarily
addmatch!(matched_pairs, bestmatchdata)
@debug "Matched pairs" matched_pairs
end
end # of for r = 1:nrow(props1)

# exit while loop if there are no more floes to match
@debug "Matched pairs" matched_pairs
isempty(matched_pairs) && break

#= Resolve collisions:
Are there floes in day k+1 paired with more than one
floe in day k? If so, keep the best matching pair and remove all others. =#
resolvecollisions!(matched_pairs)
cpaniaguam marked this conversation as resolved.
Show resolved Hide resolved
deletematched!((props1, props2), matched_pairs)
update!(match_total, matched_pairs)
end # of while loop
return match_total
end
Loading
Loading