///|
pub(all) struct TrackObservation {
frame : Int
point : ThermalPoint
temperature : Double
area : Int
} derive(Debug, Eq, ToJson)
///|
pub(all) struct ThermalTrack {
id : Int
observations : Array[TrackObservation]
mut active : Bool
} derive(Debug, Eq, ToJson)
///|
pub(all) struct TrackingConfig {
maximum_distance : Int
maximum_gap : Int
minimum_temperature : Double
} derive(Debug, Eq, ToJson)
///|
pub fn TrackingConfig::default() -> TrackingConfig {
{ maximum_distance: 8, maximum_gap: 2, minimum_temperature: 35.0 }
}
///|
pub fn ThermalTrack::last(track : ThermalTrack) -> TrackObservation? {
if track.observations.is_empty() {
None
} else {
Some(track.observations[track.observations.length() - 1])
}
}
///|
pub fn ThermalTrack::length(track : ThermalTrack) -> Int {
track.observations.length()
}
///|
pub fn ThermalTrack::velocity(track : ThermalTrack) -> ThermalPoint {
if track.observations.length() < 2 {
ThermalPoint::new(x=0, y=0)
} else {
let a = track.observations[track.observations.length() - 2].point
let b = track.observations[track.observations.length() - 1].point
ThermalPoint::new(x=b.x - a.x, y=b.y - a.y)
}
}
///|
pub fn ThermalTrack::peak(track : ThermalTrack) -> TrackObservation? {
let mut result : TrackObservation? = None
for observation in track.observations {
match result {
None => result = Some(observation)
Some(current) =>
if observation.temperature > current.temperature {
result = Some(observation)
}
}
}
result
}
///|
fn region_observation(region : ThermalRegion, frame : Int) -> TrackObservation {
{
frame,
point: region.centroid(),
temperature: region.max_temp,
area: region.area(),
}
}
///|
fn nearest_track(
region : ThermalRegion,
tracks : Array[ThermalTrack],
config : TrackingConfig,
frame : Int,
) -> Int? {
let point = region.centroid()
let mut selected : Int? = None
let mut best = config.maximum_distance + 1
for index, track in tracks {
match track.last() {
Some(last) if last.frame + config.maximum_gap >= frame => {
let distance = last.point.manhattan(point)
if distance < best {
best = distance
selected = Some(index)
}
}
_ => ()
}
}
selected
}
///|
pub fn track_regions(
frames : Array[Array[ThermalRegion]],
config? : TrackingConfig = TrackingConfig::default(),
) -> Array[ThermalTrack] {
let tracks : Array[ThermalTrack] = []
let mut next_id = 1
for frame, regions in frames {
let used = Array::make(tracks.length(), false)
for region in regions {
if region.max_temp < config.minimum_temperature {
continue
}
match nearest_track(region, tracks, config, frame) {
Some(index) if !used[index] => {
used[index] = true
tracks[index].observations.push(region_observation(region, frame))
tracks[index].active = true
}
_ => {
tracks.push({
id: next_id,
observations: [region_observation(region, frame)],
active: true,
})
next_id += 1
}
}
}
for track in tracks {
match track.last() {
Some(last) =>
if last.frame < frame - config.maximum_gap {
track.active = false
}
None => track.active = false
}
}
}
tracks
}
///|
pub fn ThermalMatrix::track_frame_sequence(
frames : Array[ThermalMatrix],
threshold~ : Double,
config? : TrackingConfig = TrackingConfig::default(),
) -> Array[ThermalTrack] {
let regions : Array[Array[ThermalRegion]] = []
for matrix in frames {
regions.push(matrix.threshold_regions(min_temp=threshold))
}
track_regions(regions, config~)
}
///|
pub(all) struct MotionSummary {
track_count : Int
active_count : Int
total_observations : Int
fastest_track : Int
hottest_track : Int
} derive(Debug, Eq, ToJson)
///|
pub fn summarize_tracks(tracks : Array[ThermalTrack]) -> MotionSummary {
let mut active = 0
let mut observations = 0
let mut fastest = 0
let mut fastest_speed = 0
let mut hottest = 0
let mut hottest_value = -1.0e300
for track in tracks {
if track.active {
active += 1
}
observations += track.length()
let velocity = track.velocity()
let speed = velocity.x.abs() + velocity.y.abs()
if speed > fastest_speed {
fastest_speed = speed
fastest = track.id
}
match track.peak() {
Some(peak) if peak.temperature > hottest_value => {
hottest_value = peak.temperature
hottest = track.id
}
_ => ()
}
}
{
track_count: tracks.length(),
active_count: active,
total_observations: observations,
fastest_track: fastest,
hottest_track: hottest,
}
}
///|
pub fn ThermalTrack::to_markdown(track : ThermalTrack) -> String {
let lines : Array[String] = [
"### Track " + track.id.to_string(),
"",
"| Frame | X | Y | Temperature | Area |",
"| ---: | ---: | ---: | ---: | ---: |",
]
for observation in track.observations {
lines.push(
"| " +
observation.frame.to_string() +
" | " +
observation.point.x.to_string() +
" | " +
observation.point.y.to_string() +
" | " +
observation.temperature.to_string() +
" | " +
observation.area.to_string() +
" |",
)
}
lines.join("\n")
}