diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml
index 41b74cc..2dc2ad5 100644
--- a/.github/workflows/build.yml
+++ b/.github/workflows/build.yml
@@ -18,12 +18,12 @@ jobs:
runs-on: ${{ matrix.os }}
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v4
- - name: Setup .NET Core 9.0
- uses: actions/setup-dotnet@v1
+ - name: Setup .NET 10.0
+ uses: actions/setup-dotnet@v4
with:
- dotnet-version: 9.0.x
+ dotnet-version: 10.0.x
- name: Restore dependencies
run: dotnet restore
@@ -46,12 +46,12 @@ jobs:
runs-on: ubuntu-latest
needs: build-and-test
steps:
- - uses: actions/checkout@v2
+ - uses: actions/checkout@v4
- - name: Setup .NET Core 9.0
- uses: actions/setup-dotnet@v1
+ - name: Setup .NET 10.0
+ uses: actions/setup-dotnet@v4
with:
- dotnet-version: 9.0.x
+ dotnet-version: 10.0.x
- name: Restore dependencies
run: dotnet restore
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
index 9a5df36..4419b61 100644
--- a/.github/workflows/release.yml
+++ b/.github/workflows/release.yml
@@ -11,8 +11,8 @@ jobs:
runs-on: ubuntu-latest
steps:
- - uses: actions/checkout@v2
-
+ - uses: actions/checkout@v4
+
- name: Verify commit exists in origin/main
run: |
git fetch --no-tags --prune --depth=1 origin +refs/heads/*:refs/remotes/origin/*
@@ -21,10 +21,10 @@ jobs:
- name: Set VERSION variable from tag
run: echo "VERSION=${GITHUB_REF/refs\/tags\/v/}" >> $GITHUB_ENV
- - name: Setup .NET Core 9.0
- uses: actions/setup-dotnet@v1
+ - name: Setup .NET 10.0
+ uses: actions/setup-dotnet@v4
with:
- dotnet-version: 9.0.x
+ dotnet-version: 10.0.x
- name: Build
working-directory: src/SortCS
diff --git a/.vscode/launch.json b/.vscode/launch.json
index 47bdd6c..11b33ff 100644
--- a/.vscode/launch.json
+++ b/.vscode/launch.json
@@ -10,9 +10,9 @@
"request": "launch",
"preLaunchTask": "build",
// If you have changed target frameworks, make sure to update the program path.
- "program": "${workspaceFolder}/src/SortCS.Evaluate/bin/Debug/net5.0/SortCS.Evaluate.dll",
+ "program": "${workspaceFolder}/src/SortCS.Evaluate/bin/Debug/net10.0/SortCS.Evaluate.dll",
"args": [],
- "cwd": "${workspaceFolder}/src/SortCS.Evaluate/bin/Debug/net5.0/",
+ "cwd": "${workspaceFolder}/src/SortCS.Evaluate/bin/Debug/net10.0/",
// For more information about the 'console' field, see https://aka.ms/VSCode-CS-LaunchJson-Console
"console": "internalConsole",
"stopAtEntry": false
diff --git a/Directory.Build.props b/Directory.Build.props
new file mode 100644
index 0000000..573b8e2
--- /dev/null
+++ b/Directory.Build.props
@@ -0,0 +1,10 @@
+
+
+
+ net10.0
+ latest
+ enable
+ enable
+
+
+
diff --git a/Directory.Packages.props b/Directory.Packages.props
new file mode 100644
index 0000000..e5a9e22
--- /dev/null
+++ b/Directory.Packages.props
@@ -0,0 +1,31 @@
+
+
+
+ true
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/README.md b/README.md
index ca29290..f0b70f2 100644
--- a/README.md
+++ b/README.md
@@ -25,7 +25,12 @@ SortCS is a 'Multiple Object Tracker' as described in [this paper](https://arxiv
```cs
using SortCS;
-ITracker tracker = new SortTracker(iouThreshold = 0.3f, maxMisses = 3);
+// maxMisses is the number of consecutive frames an object may be occluded before its track ends.
+var tracker = new SortTracker(iouThreshold: 0.3f, maxMisses: 3);
+
+// MaxMisses can be adjusted at runtime; the new value takes effect on the next Track(..) call.
+tracker.MaxMisses = 5;
+
tracker.Track(new[]
{
new RectangleF(1695,383,159,343),
diff --git a/analyzers.targets b/analyzers.targets
index 0f340e9..72c1af7 100644
--- a/analyzers.targets
+++ b/analyzers.targets
@@ -7,12 +7,12 @@
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
-
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/src/SortCS.Benchmarks/SortCS.Benchmarks.csproj b/src/SortCS.Benchmarks/SortCS.Benchmarks.csproj
index ae86ee2..7775381 100644
--- a/src/SortCS.Benchmarks/SortCS.Benchmarks.csproj
+++ b/src/SortCS.Benchmarks/SortCS.Benchmarks.csproj
@@ -1,17 +1,15 @@
-
+
Exe
- net9.0
- enable
- enable
-
+
-
\ No newline at end of file
+
+
diff --git a/src/SortCS.Evaluate/Program.cs b/src/SortCS.Evaluate/Program.cs
index 004d232..94e344e 100644
--- a/src/SortCS.Evaluate/Program.cs
+++ b/src/SortCS.Evaluate/Program.cs
@@ -1,78 +1,74 @@
-using System.IO;
using System.CommandLine;
-using System.CommandLine.Invocation;
-using System.Threading.Tasks;
-using System.Linq;
-using System.Net.Http;
using System.IO.Compression;
using Microsoft.Extensions.DependencyInjection;
using Microsoft.Extensions.Logging;
+using SortCS;
+using SortCS.Evaluate;
-namespace SortCS.Evaluate;
-
-class Program
+var services = new ServiceCollection();
+services.AddLogging(loggerBuilder =>
{
- private static ILogger _logger;
-
- static async Task Main(string[] args)
- {
- var services = new ServiceCollection();
- services.AddLogging(loggerBuilder =>
- {
- loggerBuilder.ClearProviders();
- loggerBuilder.AddConsole();
- });
- var serviceProvider = services.BuildServiceProvider();
+ loggerBuilder.ClearProviders();
+ loggerBuilder.AddConsole();
+});
+var serviceProvider = services.BuildServiceProvider();
- _logger = serviceProvider.GetService>();
+var logger = serviceProvider.GetRequiredService>();
- var dataFolderOption = new Option(
- "--data-folder",
- getDefaultValue: () => new DirectoryInfo(@"../../../../../../TrackEval/data"), // Sssuming SortCS/src/SortCS.Evaluate/bin/debug/net5.0 is working directory
- description: "Location where data is stored using this format: https://github.com/JonathonLuiten/TrackEval/blob/master/docs/MOTChallenge-format.txt");
+var dataFolderOption = new Option("--data-folder")
+{
+ Description = "Location where data is stored using this format: https://github.com/JonathonLuiten/TrackEval/blob/master/docs/MOTChallenge-format.txt",
+ DefaultValueFactory = _ => new DirectoryInfo(@"../../../../../../TrackEval/data") // Assuming SortCS/src/SortCS.Evaluate/bin/debug/net10.0 is working directory
+};
- var benchmarkOption = new Option(
- "--benchmark",
- getDefaultValue: () => "MOT20",
- description: "Name of the benchmark, e.g. MOT15, MO16, MOT17 or MOT20 (default : MOT20)");
+var benchmarkOption = new Option("--benchmark")
+{
+ Description = "Name of the benchmark, e.g. MOT15, MO16, MOT17 or MOT20 (default : MOT20)",
+ DefaultValueFactory = _ => "MOT20"
+};
- var splitOption = new Option(
- "--split-to-eval",
- getDefaultValue: () => "train",
- description: "Data split on which to evalute e.g. train, test (default : train)");
+var splitOption = new Option("--split-to-eval")
+{
+ Description = "Data split on which to evalute e.g. train, test (default : train)",
+ DefaultValueFactory = _ => "train"
+};
- var rootCommand = new RootCommand
- {
- dataFolderOption,benchmarkOption,splitOption
- };
+var rootCommand = new RootCommand("App to evaluate the SortCS tracking algoritm")
+{
+ dataFolderOption,
+ benchmarkOption,
+ splitOption
+};
- rootCommand.Description = "App to evaluate the SortCS tracking algoritm";
- rootCommand.SetHandler(async (dataFolder, benchmark, splitToEval) =>
- {
- if (!dataFolder.Exists || !dataFolder.GetDirectories().Any())
- {
- await DownloadTrackEvalExampleAsync(dataFolder);
- }
- var sortCsEvaluator = new SortCsEvaluator(dataFolder, benchmark, splitToEval, serviceProvider.GetService>());
- await sortCsEvaluator.EvaluateAsync();
- }, dataFolderOption, benchmarkOption, splitOption);
+rootCommand.SetAction(async (parseResult, cancellationToken) =>
+{
+ var dataFolder = parseResult.GetValue(dataFolderOption)!;
+ var benchmark = parseResult.GetValue(benchmarkOption)!;
+ var splitToEval = parseResult.GetValue(splitOption)!;
- return await rootCommand.InvokeAsync(args);
+ if (!dataFolder.Exists || !dataFolder.GetDirectories().Any())
+ {
+ await DownloadTrackEvalExampleAsync(dataFolder, cancellationToken);
}
- private static async Task DownloadTrackEvalExampleAsync(DirectoryInfo groundTruthFolder)
- {
- var dataZipUrl = "https://omnomnom.vision.rwth-aachen.de/data/TrackEval/data.zip";
- groundTruthFolder.Create();
- var targetZipFile = Path.Combine(groundTruthFolder.ToString(), "..", "data.zip");
- _logger.LogInformation(Path.GetFullPath(targetZipFile));
+ var sortCsEvaluator = new SortCsEvaluator(dataFolder, benchmark, splitToEval, serviceProvider.GetRequiredService>());
+ await sortCsEvaluator.EvaluateAsync();
+});
- _logger.LogInformation($"Downloading data.zip (150mb) from {dataZipUrl} to {targetZipFile}");
- using var httpClient = new HttpClient();
- var zipStream = await httpClient.GetStreamAsync(dataZipUrl);
- using var fs = new FileStream(targetZipFile, FileMode.CreateNew);
- await zipStream.CopyToAsync(fs);
- ZipFile.ExtractToDirectory(targetZipFile, Path.Combine(groundTruthFolder.ToString(), ".."));
- _logger.LogInformation("data.zip downloaded & extracted");
- }
-}
\ No newline at end of file
+return await rootCommand.Parse(args).InvokeAsync();
+
+async Task DownloadTrackEvalExampleAsync(DirectoryInfo groundTruthFolder, CancellationToken cancellationToken)
+{
+ const string dataZipUrl = "https://omnomnom.vision.rwth-aachen.de/data/TrackEval/data.zip";
+ groundTruthFolder.Create();
+ var targetZipFile = Path.Combine(groundTruthFolder.ToString(), "..", "data.zip");
+ logger.LogInformation("{TargetZipFile}", Path.GetFullPath(targetZipFile));
+
+ logger.LogInformation("Downloading data.zip (150mb) from {DataZipUrl} to {TargetZipFile}", dataZipUrl, targetZipFile);
+ using var httpClient = new HttpClient();
+ var zipStream = await httpClient.GetStreamAsync(dataZipUrl, cancellationToken);
+ await using var fs = new FileStream(targetZipFile, FileMode.CreateNew);
+ await zipStream.CopyToAsync(fs, cancellationToken);
+ ZipFile.ExtractToDirectory(targetZipFile, Path.Combine(groundTruthFolder.ToString(), ".."));
+ logger.LogInformation("data.zip downloaded & extracted");
+}
diff --git a/src/SortCS.Evaluate/SortCS.Evaluate.csproj b/src/SortCS.Evaluate/SortCS.Evaluate.csproj
index b5ed445..dbef47d 100644
--- a/src/SortCS.Evaluate/SortCS.Evaluate.csproj
+++ b/src/SortCS.Evaluate/SortCS.Evaluate.csproj
@@ -2,14 +2,12 @@
Exe
- net9.0
- latest
-
-
-
+
+
+
diff --git a/src/SortCS.Evaluate/SortCsEvaluator.cs b/src/SortCS.Evaluate/SortCsEvaluator.cs
index 987dd12..18694b9 100644
--- a/src/SortCS.Evaluate/SortCsEvaluator.cs
+++ b/src/SortCS.Evaluate/SortCsEvaluator.cs
@@ -1,13 +1,8 @@
-using System;
-using System.IO;
-using System.Threading.Tasks;
-using System.Linq;
-using System.Collections.Generic;
-using IniParser;
-using System.Globalization;
+using System.Diagnostics;
using System.Drawing;
+using System.Globalization;
+using IniParser;
using Microsoft.Extensions.Logging;
-using System.Diagnostics;
namespace SortCS.Evaluate;
@@ -33,14 +28,9 @@ public SortCsEvaluator(DirectoryInfo dataFolder, string benchmark, string splitT
public async Task EvaluateAsync()
{
var stopwatch = Stopwatch.StartNew();
- var tasks = new List>();
- foreach (var benchmarkDir in _dataFolderMot.GetDirectories())
- {
- tasks.Add(EvaluateBenchMark(benchmarkDir));
- }
- await Task.WhenAll(tasks);
+ var frameCounts = await Task.WhenAll(_dataFolderMot.GetDirectories().Select(EvaluateBenchMark));
stopwatch.Stop();
- var totalFrames = tasks.Sum(x => x.Result);
+ var totalFrames = frameCounts.Sum();
_logger.LogInformation("Finished evaluating {totalFrames} frames in {totalSeconds:0.} seconds ({fps:0.0} fps)", totalFrames, stopwatch.Elapsed.TotalSeconds, totalFrames / stopwatch.Elapsed.TotalSeconds);
}
@@ -80,7 +70,7 @@ private async Task EvaluateBenchMark(DirectoryInfo benchmarkFolder)
private async Task FramesToDetectionsFile(Dictionary> frames, string path)
{
- using var file = new StreamWriter(path, false);
+ await using var file = new StreamWriter(path, false);
ITracker tracker = new SortTracker(_logger);
foreach (var frame in frames)
@@ -88,7 +78,7 @@ private async Task FramesToDetectionsFile(Dictionary> fram
var tracks = tracker.Track(frame.Value);
foreach (var track in tracks)
{
- if (track.State == TrackState.Started || track.State == TrackState.Active)
+ if (track.State is TrackState.Started or TrackState.Active)
{
//var boxForLog = track.History.Last();
var boxForLog = track.Prediction;
@@ -119,20 +109,22 @@ private static async Task>> GetFramesFromFile(F
foreach (var line in lines)
{
var parts = line.Split(',');
- var frameId = int.Parse(parts[0]);
- var gtTrackId = int.Parse(parts[1]);
+ var frameId = int.Parse(parts[0], CultureInfo.InvariantCulture);
var bbLeft = float.Parse(parts[2], numberInfo);
var bbTop = float.Parse(parts[3], numberInfo);
var bbWidth = float.Parse(parts[4], numberInfo);
var bbHeight = float.Parse(parts[5], numberInfo);
var bbConf = float.Parse(parts[6], numberInfo);
- if (!frames.ContainsKey(frameId))
+
+ if (!frames.TryGetValue(frameId, out var boxes))
{
- frames.Add(frameId, new List());
+ boxes = [];
+ frames[frameId] = boxes;
}
+
if (bbConf > 0)
{
- frames[frameId].Add(new RectangleF(bbLeft, bbTop, bbWidth, bbHeight));
+ boxes.Add(new RectangleF(bbLeft, bbTop, bbWidth, bbHeight));
}
}
diff --git a/src/SortCS.Tests/Frame.cs b/src/SortCS.Tests/Frame.cs
index f3fbcd5..8d192be 100644
--- a/src/SortCS.Tests/Frame.cs
+++ b/src/SortCS.Tests/Frame.cs
@@ -1,13 +1,8 @@
-using System.Collections.Generic;
using System.Drawing;
namespace SortCS.Tests;
-public class Frame
+public class Frame(List boundingBoxes)
{
- public Frame(List boundingBoxes)
- {
- BoundingBoxes = boundingBoxes;
- }
- public List BoundingBoxes { get; set; }
-}
\ No newline at end of file
+ public List BoundingBoxes { get; set; } = boundingBoxes;
+}
diff --git a/src/SortCS.Tests/SortCS.Tests.csproj b/src/SortCS.Tests/SortCS.Tests.csproj
index 088a9e3..49367a8 100644
--- a/src/SortCS.Tests/SortCS.Tests.csproj
+++ b/src/SortCS.Tests/SortCS.Tests.csproj
@@ -1,16 +1,13 @@
- net9.0
- latest
false
-
-
-
-
+
+
+
all
runtime; build; native; contentfiles; analyzers; buildtransitive
diff --git a/src/SortCS.Tests/SortTrackerTests.cs b/src/SortCS.Tests/SortTrackerTests.cs
index 0bfb7b0..2fec416 100644
--- a/src/SortCS.Tests/SortTrackerTests.cs
+++ b/src/SortCS.Tests/SortTrackerTests.cs
@@ -1,7 +1,5 @@
-using Microsoft.VisualStudio.TestTools.UnitTesting;
-using System.Collections.Generic;
using System.Drawing;
-using System.Linq;
+using Microsoft.VisualStudio.TestTools.UnitTesting;
namespace SortCS.Tests;
@@ -57,7 +55,7 @@ public void SortTracker_FourEasyTracks_TrackedToEnd()
}
// Assert
- Assert.AreEqual(4, tracks.Where(x => x.State == TrackState.Active).Count());
+ Assert.AreEqual(4, tracks.Count(x => x.State == TrackState.Active));
}
[TestMethod]
@@ -117,4 +115,29 @@ public void SortTracker_CrossingTracks_EndInCorrectEndLocation()
Assert.AreEqual(5, complexTrack1.History.Count);
Assert.AreEqual(5, complexTrack2.History.Count);
}
+
+ [TestMethod]
+ public void SortTracker_MaxMisses_CanBeAdjustedAtRuntime()
+ {
+ // Arrange: a tracker that would normally drop a track after a single missed frame.
+ var sut = new SortTracker(iouThreshold: 0.3f, maxMisses: 1);
+ var box = new RectangleF(100, 100, 50, 50);
+
+ sut.Track([box]); // Started
+ sut.Track([box]); // Active
+
+ // Act: widen the allowed occlusion gap on the fly.
+ sut.MaxMisses = 5;
+
+ Track[] tracks = [];
+ for (var i = 0; i < 4; i++)
+ {
+ tracks = sut.Track([]).ToArray(); // four consecutive misses
+ }
+
+ // Assert: with MaxMisses raised to 5, the track survives four misses instead of being removed.
+ Assert.AreEqual(5, sut.MaxMisses);
+ Assert.IsTrue(tracks.Any(t => t.State == TrackState.Ending));
+ Assert.IsFalse(tracks.Any(t => t.State == TrackState.Ended));
+ }
}
\ No newline at end of file
diff --git a/src/SortCS/EnumerableExtensions.cs b/src/SortCS/EnumerableExtensions.cs
index eeb9fba..b1a00b6 100644
--- a/src/SortCS/EnumerableExtensions.cs
+++ b/src/SortCS/EnumerableExtensions.cs
@@ -1,6 +1,3 @@
-using System.Collections.Generic;
-using System.Linq;
-
namespace SortCS;
internal static class EnumerableExtensions
@@ -17,4 +14,4 @@ internal static class EnumerableExtensions
return result;
}
-}
\ No newline at end of file
+}
diff --git a/src/SortCS/ITracker.cs b/src/SortCS/ITracker.cs
index 267d5ec..6e092e3 100644
--- a/src/SortCS/ITracker.cs
+++ b/src/SortCS/ITracker.cs
@@ -1,4 +1,3 @@
-using System.Collections.Generic;
using System.Drawing;
namespace SortCS;
@@ -6,4 +5,4 @@ namespace SortCS;
public interface ITracker
{
IEnumerable