diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 96ef366..41b74cc 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -2,119 +2,65 @@ name: Build and Test on: push: - branches: [ main ] + branches: [main] pull_request: - branches: [ main ] + branches: [main] jobs: - build-and-test: name: Build and test strategy: matrix: - os: [ubuntu-latest] - #os: [ubuntu-latest, windows-latest, macos-latest] + os: [ubuntu-latest] + #os: [ubuntu-latest, windows-latest, macos-latest] runs-on: ${{ matrix.os }} steps: + - uses: actions/checkout@v2 - - uses: actions/checkout@v2 - - - name: Setup .NET Core 2.1 - uses: actions/setup-dotnet@v1 - with: - dotnet-version: 2.1.x - - name: Setup .NET Core 3.1 - uses: actions/setup-dotnet@v1 - with: - dotnet-version: 3.1.x - - name: Setup .NET Core 5.0 - uses: actions/setup-dotnet@v1 - with: - dotnet-version: 5.0.x - - - name: Restore dependencies - run: dotnet restore + - name: Setup .NET Core 9.0 + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 9.0.x - - name: Build - run: dotnet build --no-restore + - name: Restore dependencies + run: dotnet restore - - name: Test with dotnet - run: dotnet test --no-restore --verbosity normal --logger trx --results-directory "TestResults-${{ matrix.os }}" + - name: Build + run: dotnet build --no-restore - - name: Upload dotnet test results - uses: actions/upload-artifact@v2 - with: - name: dotnet-results-${{ matrix.os }} - path: TestResults-${{ matrix.os }} - # Use always() to always run this step to publish test results when there are test failures - if: ${{ always() }} - - - sonar: - runs-on: windows-latest - needs: build-and-test - steps: + - name: Test with dotnet + run: dotnet test --no-restore --verbosity normal --logger trx --results-directory "TestResults-${{ matrix.os }}" - - name: Set up JDK 11 - uses: actions/setup-java@v1 - with: - java-version: 1.11 - - uses: actions/checkout@v2 - with: - fetch-depth: 0 # Shallow clones should be disabled for a better relevancy of analysis - - name: Cache SonarCloud packages - uses: actions/cache@v1 - with: - path: ~\sonar\cache - key: ${{ runner.os }}-sonar - restore-keys: ${{ runner.os }}-sonar - - name: Cache SonarCloud scanner - id: cache-sonar-scanner - uses: actions/cache@v1 - with: - path: .\.sonar\scanner - key: ${{ runner.os }}-sonar-scanner - restore-keys: ${{ runner.os }}-sonar-scanner - - name: Install SonarCloud scanner - if: steps.cache-sonar-scanner.outputs.cache-hit != 'true' - shell: powershell - run: | - New-Item -Path .\.sonar\scanner -ItemType Directory - dotnet tool update dotnet-sonarscanner --tool-path .\.sonar\scanner - - name: Build and analyze - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # Needed to get PR information, if any - SONAR_TOKEN: ${{ secrets.SONAR_TOKEN }} - shell: powershell - run: | - .\.sonar\scanner\dotnet-sonarscanner begin /k:"keesschollaart81_SortCS" /o:"keesschollaart81" /d:sonar.login="${{ secrets.SONAR_TOKEN }}" /d:sonar.host.url="https://sonarcloud.io" - dotnet build - .\.sonar\scanner\dotnet-sonarscanner end /d:sonar.login="${{ secrets.SONAR_TOKEN }}" + - name: Upload dotnet test results + uses: actions/upload-artifact@v4 + with: + name: dotnet-results-${{ matrix.os }} + path: TestResults-${{ matrix.os }} + # Use always() to always run this step to publish test results when there are test failures + if: ${{ always() }} deploy: runs-on: ubuntu-latest needs: build-and-test steps: + - uses: actions/checkout@v2 + + - name: Setup .NET Core 9.0 + uses: actions/setup-dotnet@v1 + with: + dotnet-version: 9.0.x - - uses: actions/checkout@v2 + - name: Restore dependencies + run: dotnet restore + working-directory: src/SortCS - - name: Setup .NET Core 5.0 - uses: actions/setup-dotnet@v1 - with: - dotnet-version: 5.0.x - - - name: Restore dependencies - run: dotnet restore - working-directory: src/SortCS - - - name: Build - run: dotnet build -c Release --no-restore - working-directory: src/SortCS + - name: Build + run: dotnet build -c Release --no-restore + working-directory: src/SortCS - - name: Build - run: dotnet pack -c Release --no-restore - working-directory: src/SortCS - \ No newline at end of file + - name: Build + run: dotnet pack -c Release --no-restore + working-directory: src/SortCS diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index ae464fd..9a5df36 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -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 5.0 + - name: Setup .NET Core 9.0 uses: actions/setup-dotnet@v1 with: - dotnet-version: 5.0.x + dotnet-version: 9.0.x - name: Build working-directory: src/SortCS diff --git a/SortCS.sln b/SortCS.sln index 4f31d11..30ecbf2 100644 --- a/SortCS.sln +++ b/SortCS.sln @@ -16,10 +16,13 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution LICENSE = LICENSE README.md = README.md .github\workflows\release.yml = .github\workflows\release.yml + sonar-project.properties = sonar-project.properties EndProjectSection EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "SortCS.Tests", "src\SortCS.Tests\SortCS.Tests.csproj", "{13002E85-6B03-49DA-9700-906A24692C98}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SortCS.Benchmarks", "src\SortCS.Benchmarks\SortCS.Benchmarks.csproj", "{9A2468E6-4556-4C7C-8D29-996200A10EF3}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -38,6 +41,10 @@ Global {13002E85-6B03-49DA-9700-906A24692C98}.Debug|Any CPU.Build.0 = Debug|Any CPU {13002E85-6B03-49DA-9700-906A24692C98}.Release|Any CPU.ActiveCfg = Release|Any CPU {13002E85-6B03-49DA-9700-906A24692C98}.Release|Any CPU.Build.0 = Release|Any CPU + {9A2468E6-4556-4C7C-8D29-996200A10EF3}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {9A2468E6-4556-4C7C-8D29-996200A10EF3}.Debug|Any CPU.Build.0 = Debug|Any CPU + {9A2468E6-4556-4C7C-8D29-996200A10EF3}.Release|Any CPU.ActiveCfg = Release|Any CPU + {9A2468E6-4556-4C7C-8D29-996200A10EF3}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -45,4 +52,6 @@ Global GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {8C31716C-280A-44DD-ADAB-E48CEDFBF779} EndGlobalSection + GlobalSection(NestedProjects) = preSolution + EndGlobalSection EndGlobal diff --git a/analyzers.targets b/analyzers.targets index 801eed6..0f340e9 100644 --- a/analyzers.targets +++ b/analyzers.targets @@ -1,32 +1,27 @@ - + - - true - - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - + + true + - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - + + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - - - - all - runtime; build; native; contentfiles; analyzers; buildtransitive - + + all + runtime; build; native; contentfiles; analyzers; buildtransitive + + + + + + - + \ No newline at end of file diff --git a/resources/logo.psd b/resources/logo.psd index a2797a4..7b49138 100644 Binary files a/resources/logo.psd and b/resources/logo.psd differ diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 0000000..8f0d442 --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,16 @@ +sonar.projectKey=keesschollaart81_SortCS +sonar.organization=keesschollaart81 + +# This is the name and version displayed in the SonarCloud UI. +sonar.projectName=SortCS +#sonar.projectVersion=1.0 + +# Path is relative to the sonar-project.properties file. Replace "\" by "/" on Windows. +sonar.sources=. + +# Encoding of the source code. Default is default system encoding +#sonar.sourceEncoding=UTF-8 + +# Exclusions +sonar.exclusions=**/Benchmarks/**,**/obj/**,**/*.dll +sonar.coverage.exclusions=**/*.Tests.cs,**/*.Tests/** diff --git a/src/SortCS.Benchmarks/Program.cs b/src/SortCS.Benchmarks/Program.cs new file mode 100644 index 0000000..5093cae --- /dev/null +++ b/src/SortCS.Benchmarks/Program.cs @@ -0,0 +1,52 @@ +using BenchmarkDotNet.Attributes; +using BenchmarkDotNet.Running; +using SortCS; +using System.Drawing; + +BenchmarkRunner.Run(); + +[MemoryDiagnoser] +public class SortCSBenchmarks +{ + private ITracker? _tracker; + private List? _frames; + + [GlobalSetup] + public void Setup() + { + _tracker = new SortTracker(); + _frames = GenerateTestFrames(100, 10); + } + + [Benchmark] + public void TrackMultipleFrames() + { + foreach (var frame in _frames ?? []) + { + _tracker!.Track(frame); + } + } + + private List GenerateTestFrames(int numFrames, int objectsPerFrame) + { + var random = new Random(42); + var frames = new List(); + + for (var i = 0; i < numFrames; i++) + { + var objects = new RectangleF[objectsPerFrame]; + for (var j = 0; j < objectsPerFrame; j++) + { + objects[j] = new RectangleF( + random.Next(0, 1000), + random.Next(0, 1000), + random.Next(50, 200), + random.Next(50, 200) + ); + } + frames.Add(objects); + } + + return frames; + } +} \ No newline at end of file diff --git a/src/SortCS.Benchmarks/SortCS.Benchmarks.csproj b/src/SortCS.Benchmarks/SortCS.Benchmarks.csproj new file mode 100644 index 0000000..ae86ee2 --- /dev/null +++ b/src/SortCS.Benchmarks/SortCS.Benchmarks.csproj @@ -0,0 +1,17 @@ + + + + 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 14f9550..004d232 100644 --- a/src/SortCS.Evaluate/Program.cs +++ b/src/SortCS.Evaluate/Program.cs @@ -8,68 +8,71 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; -namespace SortCS.Evaluate +namespace SortCS.Evaluate; + +class Program { - class Program - { - private static ILogger _logger; + private static ILogger _logger; - static async Task Main(string[] args) + static async Task Main(string[] args) + { + var services = new ServiceCollection(); + services.AddLogging(loggerBuilder => { - 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>(); + _logger = serviceProvider.GetService>(); - var rootCommand = new RootCommand - { - 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"), - new Option( - "--benchmark", - getDefaultValue: () => "MOT20", - description: "Name of the benchmark, e.g. MOT15, MO16, MOT17 or MOT20 (default : MOT20)"), - new Option( - "--split-to-eval", - getDefaultValue: () => "train", - description: "Data split on which to evalute e.g. train, test (default : train)"), - }; + 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"); - rootCommand.Description = "App to evaluate the SortCS tracking algoritm"; - rootCommand.Handler = CommandHandler.Create(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(); - }); + var benchmarkOption = new Option( + "--benchmark", + getDefaultValue: () => "MOT20", + description: "Name of the benchmark, e.g. MOT15, MO16, MOT17 or MOT20 (default : MOT20)"); - return await rootCommand.InvokeAsync(args); - } + var splitOption = new Option( + "--split-to-eval", + getDefaultValue: () => "train", + description: "Data split on which to evalute e.g. train, test (default : train)"); - private static async Task DownloadTrackEvalExampleAsync(DirectoryInfo groundTruthFolder) + var rootCommand = new RootCommand { - 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)); + 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); + + return await rootCommand.InvokeAsync(args); + } + + 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)); - _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"); - } + _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 diff --git a/src/SortCS.Evaluate/SortCS.Evaluate.csproj b/src/SortCS.Evaluate/SortCS.Evaluate.csproj index b376855..b5ed445 100644 --- a/src/SortCS.Evaluate/SortCS.Evaluate.csproj +++ b/src/SortCS.Evaluate/SortCS.Evaluate.csproj @@ -2,14 +2,14 @@ Exe - net5.0 + net9.0 latest - - + + diff --git a/src/SortCS.Evaluate/SortCsEvaluator.cs b/src/SortCS.Evaluate/SortCsEvaluator.cs index b787190..987dd12 100644 --- a/src/SortCS.Evaluate/SortCsEvaluator.cs +++ b/src/SortCS.Evaluate/SortCsEvaluator.cs @@ -9,134 +9,133 @@ using Microsoft.Extensions.Logging; using System.Diagnostics; -namespace SortCS.Evaluate +namespace SortCS.Evaluate; + +public class SortCsEvaluator { - public class SortCsEvaluator - { - private readonly DirectoryInfo _dataFolderMot; + private readonly DirectoryInfo _dataFolderMot; - private readonly DirectoryInfo _destinationDir; - private readonly ILogger _logger; + private readonly DirectoryInfo _destinationDir; + private readonly ILogger _logger; - public SortCsEvaluator(DirectoryInfo dataFolder, string benchmark, string splitToEval, ILogger logger) + public SortCsEvaluator(DirectoryInfo dataFolder, string benchmark, string splitToEval, ILogger logger) + { + _dataFolderMot = new DirectoryInfo(Path.Combine($"{dataFolder}", "gt", "mot_challenge", $"{benchmark}-{splitToEval}")); + _destinationDir = new DirectoryInfo(Path.Combine($"{dataFolder}", "trackers", "mot_challenge", $"{benchmark}-{splitToEval}", "SortCS", "data")); + if (_destinationDir.Exists) { - _dataFolderMot = new DirectoryInfo(Path.Combine($"{dataFolder}", "gt", "mot_challenge", $"{benchmark}-{splitToEval}")); - _destinationDir = new DirectoryInfo(Path.Combine($"{dataFolder}", "trackers", "mot_challenge", $"{benchmark}-{splitToEval}", "SortCS", "data")); - if (_destinationDir.Exists) - { - _destinationDir.Delete(true); - } - _destinationDir.Create(); - _logger = logger; + _destinationDir.Delete(true); } + _destinationDir.Create(); + _logger = logger; + } - public async Task EvaluateAsync() + public async Task EvaluateAsync() + { + var stopwatch = Stopwatch.StartNew(); + var tasks = new List>(); + foreach (var benchmarkDir in _dataFolderMot.GetDirectories()) { - var stopwatch = Stopwatch.StartNew(); - var tasks = new List>(); - foreach (var benchmarkDir in _dataFolderMot.GetDirectories()) - { - tasks.Add(EvaluateBenchMark(benchmarkDir)); - } - await Task.WhenAll(tasks); - stopwatch.Stop(); - var totalFrames = tasks.Sum(x => x.Result); - _logger.LogInformation("Finished evaluating {totalFrames} frames in {totalSeconds:0.} seconds ({fps:0.0} fps)", totalFrames, stopwatch.Elapsed.TotalSeconds, totalFrames / stopwatch.Elapsed.TotalSeconds); + tasks.Add(EvaluateBenchMark(benchmarkDir)); } + await Task.WhenAll(tasks); + stopwatch.Stop(); + var totalFrames = tasks.Sum(x => x.Result); + _logger.LogInformation("Finished evaluating {totalFrames} frames in {totalSeconds:0.} seconds ({fps:0.0} fps)", totalFrames, stopwatch.Elapsed.TotalSeconds, totalFrames / stopwatch.Elapsed.TotalSeconds); + } - private async Task EvaluateBenchMark(DirectoryInfo benchmarkFolder) + private async Task EvaluateBenchMark(DirectoryInfo benchmarkFolder) + { + try { - try - { - var detFile = new FileInfo(Path.Combine($"{benchmarkFolder}", "gt", "gt.txt")); - var sequenceIniFile = new FileInfo(Path.Combine($"{benchmarkFolder}", "seqinfo.ini")); + var detFile = new FileInfo(Path.Combine($"{benchmarkFolder}", "gt", "gt.txt")); + var sequenceIniFile = new FileInfo(Path.Combine($"{benchmarkFolder}", "seqinfo.ini")); + if (!detFile.Exists) + { + detFile = new FileInfo(Path.Combine($"{benchmarkFolder}", "det", "det.txt")); if (!detFile.Exists) { - detFile = new FileInfo(Path.Combine($"{benchmarkFolder}", "det", "det.txt")); - if (!detFile.Exists) - { - _logger.LogWarning("Benchmark folder {benchmarkFolder} has no GroundTruth file (gt/gt.txt)", benchmarkFolder); - return 0; - } + _logger.LogWarning("Benchmark folder {benchmarkFolder} has no GroundTruth file (gt/gt.txt)", benchmarkFolder); + return 0; } + } - var benchmarkKey = GetBenchmarkKeyFromSeqIni(sequenceIniFile); - var frames = await GetFramesFromFile(detFile); + var benchmarkKey = GetBenchmarkKeyFromSeqIni(sequenceIniFile); + var frames = await GetFramesFromFile(detFile); - var path = Path.Combine(_destinationDir.ToString(), $"{benchmarkKey}.txt"); - _logger.LogInformation("Read {framesCount} frames, output to {outputFile}", frames.Count, path); + var path = Path.Combine(_destinationDir.ToString(), $"{benchmarkKey}.txt"); + _logger.LogInformation("Read {framesCount} frames, output to {outputFile}", frames.Count, path); - await FramesToDetectionsFile(frames, path); + await FramesToDetectionsFile(frames, path); - return frames.Count; - } - catch (Exception ex) - { - _logger.LogError(ex, "Exception evaluating benchmark {benchmarkFolder}: {ex.Message}", benchmarkFolder, ex.Message); - throw; - } + return frames.Count; } - - private async Task FramesToDetectionsFile(Dictionary> frames, string path) + catch (Exception ex) { - using var file = new StreamWriter(path, false); - - ITracker tracker = new SortTracker(_logger); - foreach (var frame in frames) - { - var tracks = tracker.Track(frame.Value); - foreach (var track in tracks) - { - if (track.State == TrackState.Started || track.State == TrackState.Active) - { - //var boxForLog = track.History.Last(); - var boxForLog = track.Prediction; - //, , , , , , , , , - var line = $"{frame.Key:0.},{track.TrackId:0.},{boxForLog.Left:0.},{boxForLog.Top:0.},{boxForLog.Width:0.},{boxForLog.Height:0.},1,-1,-1,-1"; - await file.WriteLineAsync(line); - } - } - } + _logger.LogError(ex, "Exception evaluating benchmark {benchmarkFolder}: {ex.Message}", benchmarkFolder, ex.Message); + throw; } + } - private static string GetBenchmarkKeyFromSeqIni(FileInfo sequenceIniFile) - { - var iniString = File.ReadAllText(sequenceIniFile.FullName); - var parser = new IniDataParser(); - var data = parser.Parse(iniString); - var benchmarkKey = data["Sequence"]["name"]; - return benchmarkKey; - } + private async Task FramesToDetectionsFile(Dictionary> frames, string path) + { + using var file = new StreamWriter(path, false); - private static async Task>> GetFramesFromFile(FileInfo detFile) + ITracker tracker = new SortTracker(_logger); + foreach (var frame in frames) { - // GT file format (no header): , , , , , , , , , - var lines = await File.ReadAllLinesAsync(detFile.FullName); - - var frames = new Dictionary>(); - var numberInfo = new NumberFormatInfo() { NumberDecimalSeparator = "." }; - foreach (var line in lines) + var tracks = tracker.Track(frame.Value); + foreach (var track in tracks) { - var parts = line.Split(','); - var frameId = int.Parse(parts[0]); - var gtTrackId = int.Parse(parts[1]); - 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)) - { - frames.Add(frameId, new List()); - } - if (bbConf > 0) + if (track.State == TrackState.Started || track.State == TrackState.Active) { - frames[frameId].Add(new RectangleF(bbLeft, bbTop, bbWidth, bbHeight)); + //var boxForLog = track.History.Last(); + var boxForLog = track.Prediction; + //, , , , , , , , , + var line = $"{frame.Key:0.},{track.TrackId:0.},{boxForLog.Left:0.},{boxForLog.Top:0.},{boxForLog.Width:0.},{boxForLog.Height:0.},1,-1,-1,-1"; + await file.WriteLineAsync(line); } } + } + } + + private static string GetBenchmarkKeyFromSeqIni(FileInfo sequenceIniFile) + { + var iniString = File.ReadAllText(sequenceIniFile.FullName); + var parser = new IniDataParser(); + var data = parser.Parse(iniString); + var benchmarkKey = data["Sequence"]["name"]; + return benchmarkKey; + } - return frames; + private static async Task>> GetFramesFromFile(FileInfo detFile) + { + // GT file format (no header): , , , , , , , , , + var lines = await File.ReadAllLinesAsync(detFile.FullName); + + var frames = new Dictionary>(); + var numberInfo = new NumberFormatInfo() { NumberDecimalSeparator = "." }; + foreach (var line in lines) + { + var parts = line.Split(','); + var frameId = int.Parse(parts[0]); + var gtTrackId = int.Parse(parts[1]); + 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)) + { + frames.Add(frameId, new List()); + } + if (bbConf > 0) + { + frames[frameId].Add(new RectangleF(bbLeft, bbTop, bbWidth, bbHeight)); + } } + + return frames; } -} +} \ No newline at end of file diff --git a/src/SortCS.Tests/Frame.cs b/src/SortCS.Tests/Frame.cs index 5a01c7d..f3fbcd5 100644 --- a/src/SortCS.Tests/Frame.cs +++ b/src/SortCS.Tests/Frame.cs @@ -1,14 +1,13 @@ using System.Collections.Generic; using System.Drawing; -namespace SortCS.Tests +namespace SortCS.Tests; + +public class Frame { - public class Frame + public Frame(List boundingBoxes) { - public Frame(List boundingBoxes) - { - BoundingBoxes = boundingBoxes; - } - public List BoundingBoxes { get; set; } + BoundingBoxes = boundingBoxes; } -} + public List BoundingBoxes { get; set; } +} \ No newline at end of file diff --git a/src/SortCS.Tests/SortCS.Tests.csproj b/src/SortCS.Tests/SortCS.Tests.csproj index ce886a4..088a9e3 100644 --- a/src/SortCS.Tests/SortCS.Tests.csproj +++ b/src/SortCS.Tests/SortCS.Tests.csproj @@ -1,16 +1,16 @@ - net5.0 + 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 b7a19a7..0bfb7b0 100644 --- a/src/SortCS.Tests/SortTrackerTests.cs +++ b/src/SortCS.Tests/SortTrackerTests.cs @@ -3,119 +3,118 @@ using System.Drawing; using System.Linq; -namespace SortCS.Tests +namespace SortCS.Tests; + +[TestClass] +public class SortTrackerTests { - [TestClass] - public class SortTrackerTests + [TestMethod] + public void SortTracker_FourEasyTracks_TrackedToEnd() { - [TestMethod] - public void SortTracker_FourEasyTracks_TrackedToEnd() - { - // Arrange - var mot15Track = new List{ - new Frame(new List{ - new RectangleF(1703,385,157,339), - new RectangleF(1293,455,83,213), - new RectangleF(259,449,101,261), - new RectangleF(1253,529,55,127) - }), - new Frame(new List{ - new RectangleF(1699,383,159,341), - new RectangleF(1293,455,83,213), - new RectangleF(261,447,101,263), - new RectangleF(1253,529,55,127) - }), - new Frame(new List{ - new RectangleF(1697,383,159,343), - new RectangleF(1293,455,83,213), - new RectangleF(263,447,101,263), - new RectangleF(1255,529,55,127), - new RectangleF(429,300,55,127) - }), - new Frame(new List{ - new RectangleF(1695,383,159,343), - new RectangleF(1293,455,83,213), - new RectangleF(265,447,101,263), - new RectangleF(1257,529,55,127) - }), - new Frame(new List{ - new RectangleF(1693,381,159,347), - new RectangleF(1295,455,83,213), - new RectangleF(267,447,101,263), - new RectangleF(1259, 529,55,129) - }), - }; + // Arrange + var mot15Track = new List{ + new(new List{ + new(1703,385,157,339), + new(1293,455,83,213), + new(259,449,101,261), + new(1253,529,55,127) + }), + new(new List{ + new(1699,383,159,341), + new(1293,455,83,213), + new(261,447,101,263), + new(1253,529,55,127) + }), + new(new List{ + new(1697,383,159,343), + new(1293,455,83,213), + new(263,447,101,263), + new(1255,529,55,127), + new(429,300,55,127) + }), + new(new List{ + new(1695,383,159,343), + new(1293,455,83,213), + new(265,447,101,263), + new(1257,529,55,127) + }), + new(new List{ + new(1693,381,159,347), + new(1295,455,83,213), + new(267,447,101,263), + new(1259, 529,55,129) + }), + }; - var tracks = Enumerable.Empty(); - var sut = new SortTracker(); + var tracks = Enumerable.Empty(); + var sut = new SortTracker(); - // Act - foreach (var frame in mot15Track) - { - // ToArray because otherwise the IEnumerable is not evaluated. - tracks = sut.Track(frame.BoundingBoxes).ToArray(); - } - - // Assert - Assert.AreEqual(4, tracks.Where(x => x.State == TrackState.Active).Count()); + // Act + foreach (var frame in mot15Track) + { + // ToArray because otherwise the IEnumerable is not evaluated. + tracks = sut.Track(frame.BoundingBoxes).ToArray(); } - [TestMethod] - public void SortTracker_CrossingTracks_EndInCorrectEndLocation() - { - // Arrange - var crossingTrack = new List{ - new Frame(new List{ - new RectangleF(0.8f, 0.3f, 0.1f, 0.1f), - new RectangleF(0.1f, 0.1f, 0.15f, 0.15f) - }), - new Frame(new List{ - new RectangleF(0.8f, 0.35f, 0.1f, 0.1f), - new RectangleF(0.2f, 0.2f, 0.15f, 0.15f) - }), - new Frame(new List{ - new RectangleF(0.3f, 0.3f, 0.15f, 0.15f), - new RectangleF(0.8f, 0.4f, 0.1f, 0.1f) - }), - new Frame(new List{ - new RectangleF(0.4f, 0.4f, 0.15f, 0.15f), - new RectangleF(0.8f, 0.45f, 0.1f, 0.1f) - }), - new Frame(new List{ - new RectangleF(0.5f, 0.5f, 0.15f, 0.15f), - new RectangleF(0.8f, 0.5f, 0.1f, 0.1f) - }), - new Frame(new List()), - new Frame(new List()), - new Frame(new List()), - new Frame(new List()), - new Frame(new List()) - }; - var tracks = Enumerable.Empty(); + // Assert + Assert.AreEqual(4, tracks.Where(x => x.State == TrackState.Active).Count()); + } + + [TestMethod] + public void SortTracker_CrossingTracks_EndInCorrectEndLocation() + { + // Arrange + var crossingTrack = new List{ + new(new List{ + new(0.8f, 0.3f, 0.1f, 0.1f), + new(0.1f, 0.1f, 0.15f, 0.15f) + }), + new(new List{ + new(0.8f, 0.35f, 0.1f, 0.1f), + new(0.2f, 0.2f, 0.15f, 0.15f) + }), + new(new List{ + new(0.3f, 0.3f, 0.15f, 0.15f), + new(0.8f, 0.4f, 0.1f, 0.1f) + }), + new(new List{ + new(0.4f, 0.4f, 0.15f, 0.15f), + new(0.8f, 0.45f, 0.1f, 0.1f) + }), + new(new List{ + new(0.5f, 0.5f, 0.15f, 0.15f), + new(0.8f, 0.5f, 0.1f, 0.1f) + }), + new(new List()), + new(new List()), + new(new List()), + new(new List()), + new(new List()) + }; + var tracks = Enumerable.Empty(); - var sut = new SortTracker(0.2f); + var sut = new SortTracker(0.2f); - // Act - foreach (var frame in crossingTrack) + // Act + foreach (var frame in crossingTrack) + { + var result = sut.Track(frame.BoundingBoxes).ToArray(); + if (result.Any()) { - var result = sut.Track(frame.BoundingBoxes).ToArray(); - if (result.Any()) - { - tracks = result; - } + tracks = result; } + } - var complexTrack1 = tracks.ElementAt(0); - var complexTrack2 = tracks.ElementAt(1); - var firstBoxOfTrack2 = complexTrack2.History.FirstOrDefault(); - var lastBoxOfTrack2 = complexTrack2.History.LastOrDefault(); + var complexTrack1 = tracks.ElementAt(0); + var complexTrack2 = tracks.ElementAt(1); + var firstBoxOfTrack2 = complexTrack2.History.FirstOrDefault(); + var lastBoxOfTrack2 = complexTrack2.History.LastOrDefault(); - // Assert - Assert.AreEqual(TrackState.Ended, complexTrack1.State); - Assert.AreEqual(TrackState.Ended, complexTrack2.State); - Assert.AreEqual(0.5, lastBoxOfTrack2.Top, 0.00); - Assert.AreEqual(5, complexTrack1.History.Count); - Assert.AreEqual(5, complexTrack2.History.Count); - } + // Assert + Assert.AreEqual(TrackState.Ended, complexTrack1.State); + Assert.AreEqual(TrackState.Ended, complexTrack2.State); + Assert.AreEqual(0.5, lastBoxOfTrack2.Top, 0.00); + Assert.AreEqual(5, complexTrack1.History.Count); + Assert.AreEqual(5, complexTrack2.History.Count); } -} +} \ No newline at end of file diff --git a/src/SortCS/EnumerableExtensions.cs b/src/SortCS/EnumerableExtensions.cs index 1080cd6..eeb9fba 100644 --- a/src/SortCS/EnumerableExtensions.cs +++ b/src/SortCS/EnumerableExtensions.cs @@ -1,21 +1,20 @@ using System.Collections.Generic; using System.Linq; -namespace SortCS +namespace SortCS; + +internal static class EnumerableExtensions { - internal static class EnumerableExtensions + public static T[,] ToArray(this IEnumerable source, int firstDimensionLength, int secondDimensionLength) { - public static T[,] ToArray(this IEnumerable source, int firstDimensionLength, int secondDimensionLength) - { - var array = source.ToArray(); - var result = new T[firstDimensionLength, secondDimensionLength]; - - for (var i = 0; i < array.Length; i++) - { - result[i / secondDimensionLength, i % secondDimensionLength] = array[i]; - } + var array = source as T[] ?? source.ToArray(); + var result = new T[firstDimensionLength, secondDimensionLength]; - return result; + for (var i = 0; i < array.Length; i++) + { + result[i / secondDimensionLength, i % secondDimensionLength] = array[i]; } + + return result; } -} +} \ No newline at end of file diff --git a/src/SortCS/ITracker.cs b/src/SortCS/ITracker.cs index d47afb9..267d5ec 100644 --- a/src/SortCS/ITracker.cs +++ b/src/SortCS/ITracker.cs @@ -1,10 +1,9 @@ using System.Collections.Generic; using System.Drawing; -namespace SortCS +namespace SortCS; + +public interface ITracker { - public interface ITracker - { - IEnumerable Track(IEnumerable boxes); - } + IEnumerable Track(IEnumerable boxes); } \ No newline at end of file diff --git a/src/SortCS/IsExternalInit.cs b/src/SortCS/IsExternalInit.cs deleted file mode 100644 index 74305fd..0000000 --- a/src/SortCS/IsExternalInit.cs +++ /dev/null @@ -1,6 +0,0 @@ -namespace System.Runtime.CompilerServices -{ - internal static class IsExternalInit - { - } -} \ No newline at end of file diff --git a/src/SortCS/Kalman/KalmanBoxTracker.cs b/src/SortCS/Kalman/KalmanBoxTracker.cs index ac43924..88191ab 100644 --- a/src/SortCS/Kalman/KalmanBoxTracker.cs +++ b/src/SortCS/Kalman/KalmanBoxTracker.cs @@ -1,106 +1,112 @@ using System; -using System.Collections.Generic; using System.Drawing; -namespace SortCS.Kalman -{ - internal class KalmanBoxTracker - { - private readonly KalmanFilter _filter; +namespace SortCS.Kalman; - public KalmanBoxTracker(RectangleF box) +internal class KalmanBoxTracker +{ + private static readonly Matrix _stateTransitioningMatrix = new( + new double[,] { - _filter = new KalmanFilter(7, 4) - { - StateTransitionMatrix = new Matrix( - new double[,] - { - { 1, 0, 0, 0, 1, 0, 0 }, - { 0, 1, 0, 0, 0, 1, 0 }, - { 0, 0, 1, 0, 0, 0, 1 }, - { 0, 0, 0, 1, 0, 0, 0 }, - { 0, 0, 0, 0, 1, 0, 0 }, - { 0, 0, 0, 0, 0, 1, 0 }, - { 0, 0, 0, 0, 0, 0, 1 } - }), - MeasurementFunction = new Matrix( - new double[,] - { - { 1, 0, 0, 0, 0, 0, 0 }, - { 0, 1, 0, 0, 0, 0, 0 }, - { 0, 0, 1, 0, 0, 0, 0 }, - { 0, 0, 0, 1, 0, 0, 0 } - }), - UncertaintyCovariances = new Matrix( - new double[,] - { - { 10, 0, 0, 0, 0, 0, 0 }, - { 0, 10, 0, 0, 0, 0, 0 }, - { 0, 0, 10, 0, 0, 0, 0 }, - { 0, 0, 0, 10, 0, 0, 0 }, - { 0, 0, 0, 0, 10000, 0, 0 }, - { 0, 0, 0, 0, 0, 10000, 0 }, - { 0, 0, 0, 0, 0, 0, 10000 } - }), - MeasurementUncertainty = new Matrix(new double[,] - { - { 1, 0, 0, 0 }, - { 0, 1, 0, 0 }, - { 0, 0, 10, 0 }, - { 0, 0, 0, 10 }, - }), - ProcessUncertainty = new Matrix( - new double[,] - { - { 1, 0, 0, 0, 0, 0, 0 }, - { 0, 1, 0, 0, 0, 0, 0 }, - { 0, 0, 1, 0, 0, 0, 0 }, - { 0, 0, 0, 1, 0, 0, 0 }, - { 0, 0, 0, 0, .01, 0, 0 }, - { 0, 0, 0, 0, 0, .01, 0 }, - { 0, 0, 0, 0, 0, 0, .0001 } - }), - CurrentState = ToMeasurement(box).Append(0, 0, 0) - }; - } + { 1, 0, 0, 0, 1, 0, 0 }, + { 0, 1, 0, 0, 0, 1, 0 }, + { 0, 0, 1, 0, 0, 0, 1 }, + { 0, 0, 0, 1, 0, 0, 0 }, + { 0, 0, 0, 0, 1, 0, 0 }, + { 0, 0, 0, 0, 0, 1, 0 }, + { 0, 0, 0, 0, 0, 0, 1 } + }); - public void Update(RectangleF box) + private static readonly Matrix _measurementFunction = new( + new double[,] { - _filter.Update(ToMeasurement(box)); - } + { 1, 0, 0, 0, 0, 0, 0 }, + { 0, 1, 0, 0, 0, 0, 0 }, + { 0, 0, 1, 0, 0, 0, 0 }, + { 0, 0, 0, 1, 0, 0, 0 } + }); - public RectangleF Predict() + private static readonly Matrix _uncertaintyCovariances = new( + new double[,] { - if (_filter.CurrentState[6] + _filter.CurrentState[2] <= 0) - { - var state = _filter.CurrentState.ToArray(); - state[6] = 0; - _filter.CurrentState = new Vector(state); - } + { 10, 0, 0, 0, 0, 0, 0 }, + { 0, 10, 0, 0, 0, 0, 0 }, + { 0, 0, 10, 0, 0, 0, 0 }, + { 0, 0, 0, 10, 0, 0, 0 }, + { 0, 0, 0, 0, 10000, 0, 0 }, + { 0, 0, 0, 0, 0, 10000, 0 }, + { 0, 0, 0, 0, 0, 0, 10000 } + }); - _filter.Predict(); + private static readonly Matrix _measurementUncertainty = new(new double[,] + { + { 1, 0, 0, 0 }, + { 0, 1, 0, 0 }, + { 0, 0, 10, 0 }, + { 0, 0, 0, 10 }, + }); - var prediction = ToBoundingBox(_filter.CurrentState); + private static readonly Matrix _processUncertainty = new( + new[,] + { + { 1, 0, 0, 0, 0, 0, 0 }, + { 0, 1, 0, 0, 0, 0, 0 }, + { 0, 0, 1, 0, 0, 0, 0 }, + { 0, 0, 0, 1, 0, 0, 0 }, + { 0, 0, 0, 0, .01, 0, 0 }, + { 0, 0, 0, 0, 0, .01, 0 }, + { 0, 0, 0, 0, 0, 0, .0001 } + }); - return prediction; - } + private readonly KalmanFilter _filter; - private static Vector ToMeasurement(RectangleF box) + public KalmanBoxTracker(RectangleF box) + { + _filter = new KalmanFilter(7, 4) { - var center = new PointF(box.Left + (box.Width / 2f), box.Top + (box.Height / 2f)); - return new Vector(center.X, center.Y, box.Width * (double)box.Height, box.Width / (double)box.Height); - } + StateTransitionMatrix = _stateTransitioningMatrix, + MeasurementFunction = _measurementFunction, + UncertaintyCovariances = _uncertaintyCovariances, + MeasurementUncertainty = _measurementUncertainty, + ProcessUncertainty = _processUncertainty, + CurrentState = ToMeasurement(box).Append(0, 0, 0) + }; + } - private static RectangleF ToBoundingBox(Vector currentState) + public void Update(RectangleF box) + { + _filter.Update(ToMeasurement(box)); + } + + public RectangleF Predict() + { + if (_filter.CurrentState[6] + _filter.CurrentState[2] <= 0) { - var w = Math.Sqrt(currentState[2] * currentState[3]); - var h = currentState[2] / w; - - return new RectangleF( - (float)(currentState[0] - (w / 2)), - (float)(currentState[1] - (h / 2)), - (float)w, - (float)h); + _filter.SetState(6, 0); } + + _filter.Predict(); + + var prediction = ToBoundingBox(_filter.CurrentState); + + return prediction; + } + + private static Vector ToMeasurement(RectangleF box) + { + var center = new PointF(box.Left + (box.Width / 2f), box.Top + (box.Height / 2f)); + return new Vector(center.X, center.Y, box.Width * (double)box.Height, box.Width / (double)box.Height); + } + + private static RectangleF ToBoundingBox(Vector currentState) + { + var w = Math.Sqrt(currentState[2] * currentState[3]); + var h = currentState[2] / w; + + return new RectangleF( + (float)(currentState[0] - (w / 2)), + (float)(currentState[1] - (h / 2)), + (float)w, + (float)h); } } \ No newline at end of file diff --git a/src/SortCS/Kalman/KalmanFilter.cs b/src/SortCS/Kalman/KalmanFilter.cs index cb216d2..50689d3 100644 --- a/src/SortCS/Kalman/KalmanFilter.cs +++ b/src/SortCS/Kalman/KalmanFilter.cs @@ -1,127 +1,131 @@ using System; using System.Diagnostics.CodeAnalysis; -namespace SortCS.Kalman +namespace SortCS.Kalman; + +[SuppressMessage("Major Code Smell", "S3928:Parameter names used into ArgumentException constructors should match an existing one ", + Justification = "Properties throw ArgumentException for 'value'")] +internal class KalmanFilter { - [SuppressMessage("Major Code Smell", "S3928:Parameter names used into ArgumentException constructors should match an existing one ", Justification = "Properties throw ArgumentException for 'value'")] - internal class KalmanFilter + private readonly int _stateSize; + private readonly int _measurementSize; + private readonly Matrix _identity; + private readonly double _alphaSq; + + private Vector _currentState; + private Matrix _uncertaintyCovariances; + private Matrix _pht; + private Matrix _s; + private Matrix _si; + private Matrix _k; + private Matrix _kh; + private Matrix _ikh; + + public KalmanFilter(int stateSize, int measurementSize) + { + _stateSize = stateSize; + _measurementSize = measurementSize; + _identity = Matrix.Identity(stateSize); + _alphaSq = 1.0d; + + StateTransitionMatrix = _identity; // F + MeasurementFunction = new Matrix(_measurementSize, _stateSize); // H + UncertaintyCovariances = Matrix.Identity(_stateSize); // P + MeasurementUncertainty = Matrix.Identity(_measurementSize); // R + ProcessUncertainty = _identity; // Q + CurrentState = new Vector(stateSize); + } + + /// + /// Gets or sets the current state. + /// + public Vector CurrentState + { + get => _currentState; + set => _currentState = value.Size == _stateSize + ? value + : throw new ArgumentException($"Vector must be of size {_stateSize}.", nameof(value)); + } + + /// + /// Gets the uncertainty covariances. + /// + public Matrix UncertaintyCovariances + { + get => _uncertaintyCovariances; + init => _uncertaintyCovariances = value.Rows == _stateSize && value.Columns == _stateSize + ? value + : throw new ArgumentException($"Matrix must be of size {_stateSize}x{_stateSize}.", nameof(value)); + } + + /// + /// Gets the process uncertainty. + /// + public Matrix ProcessUncertainty + { + get; + init => field = value.Rows == _stateSize && value.Columns == _stateSize + ? value + : throw new ArgumentException($"Matrix must be of size {_stateSize}x{_stateSize}.", nameof(value)); + } + + public Matrix MeasurementUncertainty + { + get; + init => field = value.Rows == _measurementSize && value.Columns == _measurementSize + ? value + : throw new ArgumentException($"Matrix must be of size {_measurementSize}x{_measurementSize}.", nameof(value)); + } + + /// + /// Gets the state transition matrix. + /// + public Matrix StateTransitionMatrix { - private readonly int _stateSize; - private readonly int _measurementSize; - private readonly Matrix _identity; - private readonly Matrix _processUncertainty; - private readonly Matrix _stateTransitionMatrix; - private readonly Matrix _measurementFunction; - private readonly Matrix _measurementUncertainty; - private readonly double _alphaSq; - - private Vector _currentState; - private Matrix _uncertaintyCovariances; - - public KalmanFilter(int stateSize, int measurementSize) - { - _stateSize = stateSize; - _measurementSize = measurementSize; - _identity = Matrix.Identity(stateSize); - _alphaSq = 1.0d; - - StateTransitionMatrix = _identity; // F - MeasurementFunction = new Matrix(_measurementSize, _stateSize); // H - UncertaintyCovariances = Matrix.Identity(_stateSize); // P - MeasurementUncertainty = Matrix.Identity(_measurementSize); // R - ProcessUncertainty = _identity; // Q - CurrentState = new Vector(stateSize); - } - - /// - /// Gets or sets the current state. - /// - public Vector CurrentState - { - get => _currentState; - set => _currentState = value.Length == _stateSize - ? value - : throw new ArgumentException($"Vector must be of size {_stateSize}.", nameof(value)); - } - - /// - /// Gets the uncertainty covariances. - /// - public Matrix UncertaintyCovariances - { - get => _uncertaintyCovariances; - init => _uncertaintyCovariances = value.Rows == _stateSize && value.Columns == _stateSize - ? value - : throw new ArgumentException($"Matrix must be of size {_stateSize}x{_stateSize}.", nameof(value)); - } - - /// - /// Gets the process uncertainty. - /// - public Matrix ProcessUncertainty - { - get => _processUncertainty; - init => _processUncertainty = value.Rows == _stateSize && value.Columns == _stateSize - ? value - : throw new ArgumentException($"Matrix must be of size {_stateSize}x{_stateSize}.", nameof(value)); - } - - public Matrix MeasurementUncertainty - { - get => _measurementUncertainty; - init => _measurementUncertainty = value.Rows == _measurementSize && value.Columns == _measurementSize - ? value - : throw new ArgumentException($"Matrix must be of size {_measurementSize}x{_measurementSize}.", nameof(value)); - } - - /// - /// Gets the state transition matrix. - /// - public Matrix StateTransitionMatrix - { - get => _stateTransitionMatrix; - init => _stateTransitionMatrix = value.Rows == _stateSize && value.Columns == _stateSize - ? value - : throw new ArgumentException($"Matrix must be of size {_stateSize}x{_stateSize}.", nameof(value)); - } - - /// - /// Gets the measurement function. - /// - public Matrix MeasurementFunction - { - get => _measurementFunction; - init => _measurementFunction = value.Rows == _measurementSize && value.Columns == _stateSize - ? value - : throw new ArgumentException($"Matrix must be of size {_measurementSize}x{_stateSize}.", nameof(value)); - } - - public void Predict(Matrix stateTransitionMatrix = null, Matrix processNoiseMatrix = null) - { - stateTransitionMatrix ??= StateTransitionMatrix; - processNoiseMatrix ??= ProcessUncertainty; - - _currentState = stateTransitionMatrix.Dot(CurrentState); - _uncertaintyCovariances = (_alphaSq * stateTransitionMatrix * UncertaintyCovariances * stateTransitionMatrix.Transposed) + processNoiseMatrix; - } - - [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1312:Variable names should begin with lower-case letter", Justification = "These are well known abbreviations for the Kalman Filter")] - public void Update(Vector measurement, Matrix measurementNoise = null, Matrix measurementFunction = null) - { - measurementNoise ??= MeasurementUncertainty; - measurementFunction ??= MeasurementFunction; - - var y = measurement - measurementFunction.Dot(CurrentState); - var pht = UncertaintyCovariances * measurementFunction.Transposed; - var S = (measurementFunction * pht) + measurementNoise; - var SI = S.Inverted; - var K = pht * SI; - - _currentState += K.Dot(y); - - var I_KH = _identity - (K * measurementFunction); - - _uncertaintyCovariances = (I_KH * UncertaintyCovariances * I_KH.Transposed) + (K * measurementNoise * K.Transposed); - } + get; + init => field = value.Rows == _stateSize && value.Columns == _stateSize + ? value + : throw new ArgumentException($"Matrix must be of size {_stateSize}x{_stateSize}.", nameof(value)); + } + + /// + /// Gets the measurement function. + /// + public Matrix MeasurementFunction + { + get; + init => field = value.Rows == _measurementSize && value.Columns == _stateSize + ? value + : throw new ArgumentException($"Matrix must be of size {_measurementSize}x{_stateSize}.", nameof(value)); + } + + public void SetState(int index, double values) + { + _currentState[index] = values; + } + + public void Predict() + { + _currentState = StateTransitionMatrix.Dot(CurrentState); + _uncertaintyCovariances = (_alphaSq * StateTransitionMatrix * UncertaintyCovariances * StateTransitionMatrix.Transposed) + + ProcessUncertainty; + } + + [SuppressMessage("StyleCop.CSharp.NamingRules", "SA1312:Variable names should begin with lower-case letter", + Justification = "These are well known abbreviations for the Kalman Filter")] + public void Update(Vector measurement) + { + _pht ??= UncertaintyCovariances * MeasurementFunction.Transposed; + _s ??= (MeasurementFunction * _pht) + MeasurementUncertainty; + _si ??= _s.Inverted; + _k ??= _pht * _si; + _kh ??= _k * MeasurementFunction; + _ikh ??= _identity - _kh; + + var y = measurement - MeasurementFunction.Dot(CurrentState); + + _currentState += _k.Dot(y); + + _uncertaintyCovariances = (_ikh * UncertaintyCovariances * _ikh.Transposed) + (_k * MeasurementUncertainty * _k.Transposed); } } \ No newline at end of file diff --git a/src/SortCS/Kalman/Matrix.cs b/src/SortCS/Kalman/Matrix.cs index 5612f79..e181b14 100644 --- a/src/SortCS/Kalman/Matrix.cs +++ b/src/SortCS/Kalman/Matrix.cs @@ -1,332 +1,367 @@ using System; +using System.Buffers; +using System.Collections.Generic; using System.Diagnostics; using System.Linq; -namespace SortCS.Kalman +namespace SortCS.Kalman; + +[DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] +[DebuggerTypeProxy(typeof(MatrixDisplay))] +internal class Matrix { - [DebuggerDisplay("{" + nameof(DebuggerDisplay) + ",nq}")] - [DebuggerTypeProxy(typeof(MatrixDisplay))] - internal class Matrix + private readonly double[,] _values; + + public Matrix(double[,] values) { - private readonly double[,] _values; + _values = values; + Rows = _values.GetLength(0); + Columns = _values.GetLength(1); + } - public Matrix(double[,] values) + public Matrix(int[,] values) + : this(values.GetLength(0), values.GetLength(1)) + { + for (var row = 0; row < Rows; row++) { - _values = values; - Rows = _values.GetLength(0); - Columns = _values.GetLength(1); + for (var col = 0; col < Columns; col++) + { + _values[row, col] = (double)values[row, col]; + } } + } - public Matrix(int[,] values) - : this(values.GetLength(0), values.GetLength(1)) + public Matrix(int rows, int columns) + : this(new double[rows, columns]) + { + } + + public int Rows { get; } + + public int Columns { get; } + + public Matrix Transposed + { + get { + if (field != null) + { + return field; + } + + var result = new double[Columns, Rows]; + for (var row = 0; row < Rows; row++) { for (var col = 0; col < Columns; col++) { - _values[row, col] = (double)values[row, col]; + result[col, row] = _values[row, col]; } } - } - public Matrix(int rows, int columns) - : this(new double[rows, columns]) - { + field = new Matrix(result); + + return field; } + } - public int Rows { get; } + public Matrix Inverted + { + get + { + Debug.Assert(Rows == Columns); - public int Columns { get; } + var (lu, indices) = GetDecomposition(); + var result = new double[Rows, Columns]; - public Matrix Transposed - { - get + for (var col = 0; col < Columns; col++) { - var result = new double[Columns, Rows]; + var column = new double[Columns]; + + column[col] = 1.0d; + + var x = BackSubstitution(lu, indices, column); for (var row = 0; row < Rows; row++) { - for (var col = 0; col < Columns; col++) - { - result[col, row] = _values[row, col]; - } + result[row, col] = x[row]; } - - return new Matrix(result); } - } - - public Matrix Inverted - { - get - { - Debug.Assert(Rows == Columns, "Matrix must be square."); - var (lu, indices, d) = GetDecomposition(); - var result = new double[Rows, Columns]; - - for (var col = 0; col < Columns; col++) - { - var column = new double[Columns]; + return new Matrix(result); + } + } - column[col] = 1.0d; + private string DebuggerDisplay => ToString(); - var x = BackSubstition(lu, indices, column); + public static Matrix operator +(Matrix first, Matrix second) + { + Debug.Assert(first.Rows == second.Rows && first.Columns == second.Columns); - for (var row = 0; row < Rows; row++) - { - result[row, col] = x[row]; - } - } + var result = new double[first.Rows, first.Columns]; - return new Matrix(result); + for (var row = 0; row < first.Rows; row++) + { + for (var col = 0; col < first.Columns; col++) + { + result[row, col] = first._values[row, col] + second._values[row, col]; } } - private string DebuggerDisplay => ToString(); + return new Matrix(result); + } - public static Matrix operator +(Matrix first, Matrix second) - { - Debug.Assert(first.Rows == second.Rows && first.Columns == second.Columns, "Matrices must have the same size."); + public static Matrix operator -(Matrix first, Matrix second) + { + Debug.Assert(first.Rows == second.Rows && first.Columns == second.Columns); - var result = new double[first.Rows, first.Columns]; + var result = new double[first.Rows, first.Columns]; - for (var row = 0; row < first.Rows; row++) + for (var row = 0; row < first.Rows; row++) + { + for (var col = 0; col < first.Columns; col++) { - for (var col = 0; col < first.Columns; col++) - { - result[row, col] = first._values[row, col] + second._values[row, col]; - } + result[row, col] = first._values[row, col] - second._values[row, col]; } - - return new Matrix(result); } - public static Matrix operator -(Matrix first, Matrix second) - { - Debug.Assert(first.Rows == second.Rows && first.Columns == second.Columns, "Matrices must have the same size."); + return new Matrix(result); + } - var result = new double[first.Rows, first.Columns]; + public static Matrix operator *(double scalar, Matrix matrix) + { + var result = new double[matrix.Rows, matrix.Columns]; - for (var row = 0; row < first.Rows; row++) + for (var row = 0; row < matrix.Rows; row++) + { + for (var col = 0; col < matrix.Columns; col++) { - for (var col = 0; col < first.Columns; col++) - { - result[row, col] = first._values[row, col] - second._values[row, col]; - } + result[row, col] = matrix._values[row, col] * scalar; } - - return new Matrix(result); } - public static Matrix operator *(double scalar, Matrix matrix) - { - var result = new double[matrix.Rows, matrix.Columns]; + return new Matrix(result); + } - for (var row = 0; row < matrix.Rows; row++) - { - for (var col = 0; col < matrix.Columns; col++) - { - result[row, col] = matrix._values[row, col] * scalar; - } - } + public static Matrix operator *(Matrix matrix, double scalar) + { + return scalar * matrix; + } - return new Matrix(result); - } + public static Matrix operator *(Matrix first, Matrix second) + { + var result = new double[first.Rows, second.Columns]; + var rows = result.GetLength(0); + var cols = result.GetLength(1); - public static Matrix operator *(Matrix matrix, double scalar) + for (var row = 0; row < rows; row++) { - return scalar * matrix; + for (var col = 0; col < cols; col++) + { + var bufFirst = ArrayPool.Shared.Rent(first.Columns); + var bufSecond = ArrayPool.Shared.Rent(first.Rows); + result[row, col] = first.Row(row, bufFirst).Dot(second.Column(col, bufSecond)); + ArrayPool.Shared.Return(bufFirst, true); + ArrayPool.Shared.Return(bufSecond, true); + } } - public static Matrix operator *(Matrix first, Matrix second) - { - var result = new double[first.Rows, second.Columns]; - var rows = result.GetLength(0); - var cols = result.GetLength(1); + return new Matrix(result); + } - for (var row = 0; row < rows; row++) + public static Matrix Identity(int size) + { + var identity = new double[size, size]; + + for (var row = 0; row < size; row++) + { + for (var col = 0; col < size; col++) { - for (var col = 0; col < cols; col++) - { - result[row, col] = first.Row(row).Dot(second.Column(col)); - } + identity[row, col] = row == col ? 1.0d : 0d; } - - return new Matrix(result); } - public static Matrix Identity(int size) - { - var identity = new double[size, size]; + return new Matrix(identity); + } - for (var row = 0; row < size; row++) - { - for (var col = 0; col < size; col++) - { - identity[row, col] = row == col ? 1.0d : 0d; - } - } + public override string ToString() + { + return $"{{{Rows}x{Columns}}} |{string.Join("|", Enumerable.Range(0, Rows).Select(row => $" {Row(row):###0.##} "))}|"; + } - return new Matrix(identity); - } + public Vector Dot(Vector vector) + { + Debug.Assert(Columns == vector.Size); - public override string ToString() + var result = new double[Rows]; + for (var i = 0; i < Rows; i++) { - return $"{{{Rows}x{Columns}}} |{string.Join("|", Enumerable.Range(0, Rows).Select(row => $" {Row(row):###0.##} "))}|"; + var buf = ArrayPool.Shared.Rent(Columns); + var row = Row(i, buf); + result[i] = row.Dot(vector); + ArrayPool.Shared.Return(buf); } - public Vector Dot(Vector vector) - { - Debug.Assert(Columns == vector.Length, "Matrix should have the same number of columns as the vector has rows."); + return new Vector(result); + } - return new Vector(Enumerable.Range(0, Rows).Select(Row).Select(row => row.Dot(vector)).ToArray()); - } + public Vector Row(int index) + { + return Row(index, new double[Columns]); + } - public Vector Row(int index) + public Vector Row(int index, double[] buffer) + { + Debug.Assert(index <= Rows); + for (var col = 0; col < Columns; col++) { - Debug.Assert(index <= Rows, "Row index out of range."); - return new Vector(Enumerable.Range(0, Columns).Select(col => _values[index, col]).ToArray()); + buffer[col] = _values[index, col]; } - public Vector Column(int index) + return new Vector(buffer, Columns); + } + + public Vector Column(int index, double[] buf) + { + Debug.Assert(index <= Columns); + for (var row = 0; row < Rows; row++) { - Debug.Assert(index <= Columns, "Column index out of range."); - return new Vector(Enumerable.Range(0, Rows).Select(row => _values[row, index]).ToArray()); + buf[row] = _values[row, index]; } - private double[] BackSubstition(double[,] lu, int[] indices, double[] b) - { - var x = (double[])b.Clone(); - var ii = 0; - for (var row = 0; row < Rows; row++) - { - var ip = indices[row]; - var sum = x[ip]; + return new Vector(buf, Rows); + } - x[ip] = x[row]; + private double[] BackSubstitution(double[,] lu, int[] indices, double[] b) + { + var x = (double[])b.Clone(); + var ii = 0; + for (var row = 0; row < Rows; row++) + { + var ip = indices[row]; + var sum = x[ip]; - if (ii == 0) - { - for (var col = ii; col <= row - 1; col++) - { - sum -= lu[row, col] * x[col]; - } - } - else if (sum == 0) - { - ii = row; - } + x[ip] = x[row]; - x[row] = sum; - } - - for (var row = Rows - 1; row >= 0; row--) + if (Math.Sign(ii) == 0) { - var sum = x[row]; - for (var col = row + 1; col < Columns; col++) + for (var col = ii; col <= row - 1; col++) { sum -= lu[row, col] * x[col]; } - - x[row] = sum / lu[row, row]; + } + else if (Math.Sign(sum) == 0) + { + ii = row; } - return x; + x[row] = sum; } - private (double[,] Result, int[] Indices, double D) GetDecomposition() + for (var row = Rows - 1; row >= 0; row--) { - var max_row = 0; - var vv = Enumerable.Range(0, this.Rows).Select(row => 1.0d / Enumerable.Range(0, this.Columns).Select(col => Math.Abs(_values[row, col])).Max()).ToArray(); - var result = (double[,])_values.Clone(); - var index = new int[this.Rows]; - var d = 1.0d; + var sum = x[row]; + for (var col = row + 1; col < Columns; col++) + { + sum -= lu[row, col] * x[col]; + } - for (var col = 0; col < Columns; col++) + x[row] = sum / lu[row, row]; + } + + return x; + } + + private (double[,] Result, int[] Indices) GetDecomposition() + { + var maxRow = 0; + var vv = Enumerable.Range(0, Rows) + .Select(row => 1.0d / Enumerable.Range(0, Columns).Select(col => Math.Abs(_values[row, col])).Max()).ToArray(); + var result = (double[,])_values.Clone(); + var index = new int[Rows]; + var d = 1.0d; + + for (var col = 0; col < Columns; col++) + { + for (var row = 0; row < col; row++) { - for (var row = 0; row < col; row++) + var sum = result[row, col]; + for (var k = 0; k < row; k++) { - var sum = result[row, col]; - for (var k = 0; k < row; k++) - { - sum -= result[row, k] * result[k, col]; - } - - result[row, col] = sum; + sum -= result[row, k] * result[k, col]; } - var max = 0d; - for (var row = col; row < Rows; row++) + result[row, col] = sum; + } + + var max = 0d; + for (var row = col; row < Rows; row++) + { + var sum = result[row, col]; + for (var k = 0; k < col; k++) { - var sum = result[row, col]; - for (var k = 0; k < col; k++) - { - sum -= result[row, k] * result[k, col]; - } + sum -= result[row, k] * result[k, col]; + } - result[row, col] = sum; + result[row, col] = sum; - var tmp = vv[row] * Math.Abs(sum); + var tmp = vv[row] * Math.Abs(sum); - if (tmp >= max) - { - max = tmp; - max_row = row; - } + if (tmp >= max) + { + max = tmp; + maxRow = row; } + } - if (col != max_row) + if (col != maxRow) + { + for (var k = 0; k < Rows; k++) { - for (var k = 0; k < Rows; k++) - { - var tmp = result[max_row, k]; - result[max_row, k] = result[col, k]; - result[col, k] = tmp; - } - - d = -d; - vv[max_row] = vv[col]; + (result[maxRow, k], result[col, k]) = (result[col, k], result[maxRow, k]); } - index[col] = max_row; + d = -d; + vv[maxRow] = vv[col]; + } + + index[col] = maxRow; - if (col != Rows - 1) + if (col != Rows - 1) + { + var tmp = 1.0d / result[col, col]; + for (var row = col + 1; row < Rows; row++) { - var tmp = 1.0d / result[col, col]; - for (var row = col + 1; row < Rows; row++) - { - result[row, col] *= tmp; - } + result[row, col] *= tmp; } } - - return (result, index, d); } - internal class MatrixDisplay + return (result, index); + } + + internal class MatrixDisplay + { + public MatrixDisplay(Matrix matrix) { - public MatrixDisplay(Matrix matrix) - { - Cells = Enumerable.Range(0, matrix.Rows) - .Select(row => - new Cell(string.Join(" ", Enumerable.Range(0, matrix.Columns).Select(col => matrix._values[row, col])))) - .ToArray(); - } + Cells = Enumerable.Range(0, matrix.Rows) + .Select(row => + new Cell(string.Join(" ", Enumerable.Range(0, matrix.Columns).Select(col => matrix._values[row, col])))) + .ToArray(); + } - [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] - public Cell[] Cells { get; } + [DebuggerBrowsable(DebuggerBrowsableState.RootHidden)] + public Cell[] Cells { get; } - [DebuggerDisplay("{" + nameof(Value) + ", nq}")] - internal class Cell + [DebuggerDisplay("{" + nameof(Value) + ", nq}")] + internal class Cell + { + public Cell(string value) { - public Cell(string value) - { - Value = value; - } - - public string Value { get; } + Value = value; } + + public string Value { get; } } } } \ No newline at end of file diff --git a/src/SortCS/Kalman/Vector.cs b/src/SortCS/Kalman/Vector.cs index 32e484c..7b5cab0 100644 --- a/src/SortCS/Kalman/Vector.cs +++ b/src/SortCS/Kalman/Vector.cs @@ -1,60 +1,97 @@ +using System; using System.Diagnostics; using System.Globalization; using System.Linq; -namespace SortCS.Kalman +namespace SortCS.Kalman; + +internal struct Vector { - internal class Vector + private readonly double[] _values; + + public Vector(params double[] values) { - private readonly double[] _values; + _values = values; + Size = values.Length; + } - public Vector(params double[] values) + public Vector(double[] values, int size) + { + if (size > values.Length) { - _values = values; + throw new ArgumentOutOfRangeException(nameof(size)); } - public Vector(int size) - { - _values = new double[size]; - } + _values = values; + Size = size; + } - public int Length => _values.Length; + public Vector(int size) + { + _values = new double[size]; + Size = size; + } - public double this[int index] => _values[index]; + public int Size { get; } - public static Vector operator -(Vector first, Vector second) + public double this[int index] + { + get => index <= Size ? _values[index] : throw new ArgumentOutOfRangeException(nameof(index)); + set { - Debug.Assert(first.Length == second.Length, "Vectors should be of equal size"); - return new Vector(first._values.Zip(second._values, (a, b) => a - b).ToArray()); - } + if (index > Size) + { + throw new ArgumentOutOfRangeException(nameof(index)); + } - public static Vector operator +(Vector first, Vector second) - { - Debug.Assert(first.Length == second.Length, "Vectors should be of equal size"); - return new Vector(first._values.Zip(second._values, (a, b) => a + b).ToArray()); + _values[index] = value; } + } - public double Dot(Vector other) + public static Vector operator -(Vector first, Vector second) + { + Debug.Assert(first.Size == second.Size); + var resultArray = new double[first.Size]; + for (var i = 0; i < first.Size; i++) { - Debug.Assert(_values.Length == other._values.Length, "Vectors should be of equal length."); - Debug.Assert(_values.Length > 0, "Vectors must have at least one element."); - - return _values.Zip(other._values, (a, b) => a * b).Sum(); + resultArray[i] = first[i] - second[i]; } - public override string ToString() - { - return string.Join(", ", _values.Select(v => v.ToString("###0.00", CultureInfo.InvariantCulture))); - } + return new Vector(resultArray); + } - internal Vector Append(params double[] extraElements) + public static Vector operator +(Vector first, Vector second) + { + Debug.Assert(first.Size == second.Size); + var resultArray = new double[first.Size]; + for (var i = 0; i < first.Size; i++) { - return new Vector(_values.Concat(extraElements).ToArray()); + resultArray[i] = first[i] + second[i]; } - internal double[] ToArray() + return new Vector(resultArray); + } + + public double Dot(Vector other) + { + Debug.Assert(Size == other.Size, $"Vectors should be of equal length {Size} != {other.Size}."); + Debug.Assert(Size > 0); + double sum = 0; + for (var i = 0; i < Size; i++) { - return _values.ToArray(); + sum += _values[i] * other[i]; } + + return sum; + } + + public override string ToString() + { + return string.Join(", ", _values.Select(v => v.ToString("###0.00", CultureInfo.InvariantCulture))); + } + + internal Vector Append(params double[] extraElements) + { + return new Vector(_values.Take(Size).Concat(extraElements).ToArray()); } } \ No newline at end of file diff --git a/src/SortCS/SortCS.csproj b/src/SortCS/SortCS.csproj index c8045c6..3a53d04 100644 --- a/src/SortCS/SortCS.csproj +++ b/src/SortCS/SortCS.csproj @@ -1,16 +1,16 @@ - netstandard2.0; net5.0 + net9.0 preview true true Kees Schollaart, Maarten van Sambeek Vision Intelligence AnyCPU - 0.9.0.0 - 0.9.0.0 - 0.9.0.0 + 1.0.0.0 + 1.0.0.0 + 1.0.0.0 SortCS SortCS is a 'Multiple Object Tracker'. SortCS is a 'Multiple Object Tracker' @@ -36,8 +36,8 @@ - - + + diff --git a/src/SortCS/SortTracker.cs b/src/SortCS/SortTracker.cs index 720ce5e..871061d 100644 --- a/src/SortCS/SortTracker.cs +++ b/src/SortCS/SortTracker.cs @@ -5,164 +5,169 @@ using Microsoft.Extensions.Logging; using SortCS.Kalman; -namespace SortCS +namespace SortCS; + +public class SortTracker : ITracker { - public class SortTracker : ITracker + private readonly Dictionary _trackers; + private readonly ILogger _logger; + private int _trackerIndex = 1; // MOT Evaluations requires a start index of 1 + + public SortTracker(float iouThreshold = 0.3f, int maxMisses = 3) { - private readonly Dictionary _trackers; - private readonly ILogger _logger; - private int _trackerIndex = 1; // MOT Evaluations requires a start index of 1 + _trackers = new Dictionary(); + IouThreshold = iouThreshold; + MaxMisses = maxMisses; + } - public SortTracker(float iouThreshold = 0.3f, int maxMisses = 3) - { - _trackers = new Dictionary(); - IouThreshold = iouThreshold; - MaxMisses = maxMisses; - } + public SortTracker(ILogger logger, float iouThreshold = 0.3f, int maxMisses = 3) + : this(iouThreshold, maxMisses) + { + _logger = logger; + } - public SortTracker(ILogger logger, float iouThreshold = 0.3f, int maxMisses = 3) - : this(iouThreshold, maxMisses) - { - _logger = logger; - } + public float IouThreshold { get; private init; } - public float IouThreshold { get; private init; } + public int MaxMisses { get; private init; } - public int MaxMisses { get; private init; } + public IEnumerable Track(IEnumerable boxes) + { + var predictions = new Dictionary(); - public IEnumerable Track(IEnumerable boxes) + foreach (var tracker in _trackers) { - var predictions = new Dictionary(); + var prediction = tracker.Value.Tracker.Predict(); + predictions.Add(tracker.Key, prediction); + } - foreach (var tracker in _trackers) - { - var prediction = tracker.Value.Tracker.Predict(); - predictions.Add(tracker.Key, prediction); - } + var boxesArray = boxes.ToArray(); - var boxesArray = boxes.ToArray(); + var (matchedBoxes, unmatchedBoxes) = MatchDetectionsWithPredictions(boxesArray, predictions.Values); - var (matchedBoxes, unmatchedBoxes) = MatchDetectionsWithPredictions(boxesArray, predictions.Values); + var activeTrackids = new HashSet(); + foreach (var item in matchedBoxes) + { + var prediction = predictions.ElementAt(item.Key); + var track = _trackers[prediction.Key]; + track.Track.History.Add(item.Value); + track.Track.Misses = 0; + track.Track.State = TrackState.Active; + track.Tracker.Update(item.Value); + track.Track.Prediction = prediction.Value; + + activeTrackids.Add(track.Track.TrackId); + } - var activeTrackids = new HashSet(); - foreach (var item in matchedBoxes) - { - var prediction = predictions.ElementAt(item.Key); - var track = _trackers[prediction.Key]; - track.Track.History.Add(item.Value); - track.Track.Misses = 0; - track.Track.State = TrackState.Active; - track.Tracker.Update(item.Value); - track.Track.Prediction = prediction.Value; - - activeTrackids.Add(track.Track.TrackId); - } + var missedTracks = _trackers + .Where(x => !activeTrackids.Contains(x.Key)) + .Select(x => x.Value.Track); + foreach (var missedTrack in missedTracks) + { + missedTrack.Misses++; + missedTrack.TotalMisses++; + missedTrack.State = TrackState.Ending; + } - var missedTracks = _trackers.Where(x => !activeTrackids.Contains(x.Key)); - foreach (var missedTrack in missedTracks) - { - missedTrack.Value.Track.Misses++; - missedTrack.Value.Track.TotalMisses++; - missedTrack.Value.Track.State = TrackState.Ending; - } + var toRemove = _trackers.Where(x => x.Value.Track.Misses > MaxMisses).ToList(); + foreach (var tr in toRemove) + { + tr.Value.Track.State = TrackState.Ended; + _trackers.Remove(tr.Key); + } - var toRemove = _trackers.Where(x => x.Value.Track.Misses > MaxMisses).ToList(); - foreach (var tr in toRemove) + foreach (var unmatchedBox in unmatchedBoxes) + { + var track = new Track { - tr.Value.Track.State = TrackState.Ended; - _trackers.Remove(tr.Key); - } + TrackId = _trackerIndex++, + History = new List() { unmatchedBox }, + Misses = 0, + State = TrackState.Started, + TotalMisses = 0, + Prediction = unmatchedBox + }; + _trackers.Add(track.TrackId, (track, new KalmanBoxTracker(unmatchedBox))); + } - foreach (var unmatchedBox in unmatchedBoxes) - { - var track = new Track - { - TrackId = _trackerIndex++, - History = new List() { unmatchedBox }, - Misses = 0, - State = TrackState.Started, - TotalMisses = 0, - Prediction = unmatchedBox - }; - _trackers.Add(track.TrackId, (track, new KalmanBoxTracker(unmatchedBox))); - } + var result = _trackers.Select(x => x.Value.Track).Concat(toRemove.Select(y => y.Value.Track)); + Log(result); + return result; + } - var result = _trackers.Select(x => x.Value.Track).Concat(toRemove.Select(y => y.Value.Track)); - Log(result); - return result; + private void Log(IEnumerable tracks) + { + if (_logger == null || !tracks.Any()) + { + return; } - private void Log(IEnumerable tracks) + var tracksWithHistory = tracks.Where(x => x.History != null); + var longest = tracksWithHistory.Max(x => x.History.Count); + var anyStarted = tracksWithHistory.Any(x => x.History.Count == 1 && x.Misses == 0); + var ended = tracks.Count(x => x.State == TrackState.Ended); + if (anyStarted || ended > 0) { - if (_logger == null || !tracks.Any()) - { - return; - } + var tracksStr = tracks.Select(x => $"{x.TrackId}{(x.State == TrackState.Active ? null : $": {x.State}")}"); - var tracksWithHistory = tracks.Where(x => x.History != null); - var longest = tracksWithHistory.Max(x => x.History.Count); - var anyStarted = tracksWithHistory.Any(x => x.History.Count == 1 && x.Misses == 0); - var ended = tracks.Count(x => x.State == TrackState.Ended); - if (anyStarted || ended > 0) - { - var tracksStr = tracks.Select(x => $"{x.TrackId}{(x.State == TrackState.Active ? null : $": {x.State}")}"); - - _logger.LogDebug("Tracks: [{tracks}], Longest: {longest}, Ended: {ended}", string.Join(",", tracksStr), longest, ended); - } + _logger.LogDebug("Tracks: [{Tracks}], Longest: {Longest}, Ended: {Ended}", string.Join(",", tracksStr), longest, ended); } + } - private (Dictionary Matched, ICollection Unmatched) MatchDetectionsWithPredictions( - ICollection boxes, - ICollection trackPredictions) + private (Dictionary Matched, ICollection Unmatched) MatchDetectionsWithPredictions( + RectangleF[] boxes, + ICollection trackPredictions) + { + if (trackPredictions.Count == 0) { - if (trackPredictions.Count == 0) - { - return (new(), boxes); - } - - var matrix = boxes.SelectMany((box) => trackPredictions.Select((trackPrediction) => - { - var iou = IoU(box, trackPrediction); + return (new(), boxes); + } - return (int)(100 * -iou); - })).ToArray(boxes.Count, trackPredictions.Count); + var matrix = new int[boxes.Length, trackPredictions.Count]; + var trackPredictionsArray = trackPredictions.ToArray(); - if (boxes.Count > trackPredictions.Count) + for (var i = 0; i < boxes.Length; i++) + { + for (var j = 0; j < trackPredictionsArray.Length; j++) { - var extra = new int[boxes.Count - trackPredictions.Count]; - matrix = Enumerable.Range(0, boxes.Count) - .SelectMany(row => Enumerable.Range(0, trackPredictions.Count).Select(col => matrix[row, col]).Concat(extra)) - .ToArray(boxes.Count, boxes.Count); + matrix[i, j] = (int)(-100 * IoU(boxes[i], trackPredictionsArray[j])); } - - var original = (int[,])matrix.Clone(); - var minimalThreshold = (int)(-IouThreshold * 100); - var boxTrackerMapping = matrix.FindAssignments() - .Select((ti, bi) => (bi, ti)) - .Where(bt => bt.ti < trackPredictions.Count && original[bt.bi, bt.ti] <= minimalThreshold) - .ToDictionary(bt => bt.bi, bt => bt.ti); - - var unmatchedBoxes = boxes.Where((_, index) => !boxTrackerMapping.ContainsKey(index)).ToArray(); - var matchedBoxes = boxes.Select((box, index) => boxTrackerMapping.TryGetValue(index, out var tracker) - ? (Tracker: tracker, Box: box) - : (Tracker: -1, Box: RectangleF.Empty)) - .Where(tb => tb.Tracker != -1) - .ToDictionary(tb => tb.Tracker, tb => tb.Box); - - return (matchedBoxes, unmatchedBoxes); } - private double IoU(RectangleF a, RectangleF b) + if (boxes.Length > trackPredictions.Count) { - RectangleF intersection = RectangleF.Intersect(a, b); - if (intersection.IsEmpty) - { - return 0; - } + var extra = new int[boxes.Length - trackPredictions.Count]; + matrix = Enumerable.Range(0, boxes.Length) + .SelectMany(row => Enumerable.Range(0, trackPredictions.Count).Select(col => matrix[row, col]).Concat(extra)) + .ToArray(boxes.Length, boxes.Length); + } + + var original = (int[,])matrix.Clone(); + var minimalThreshold = (int)(-IouThreshold * 100); + var boxTrackerMapping = matrix.FindAssignments() + .Select((ti, bi) => (bi, ti)) + .Where(bt => bt.ti < trackPredictions.Count && original[bt.bi, bt.ti] <= minimalThreshold) + .ToDictionary(bt => bt.bi, bt => bt.ti); + + var unmatchedBoxes = boxes.Where((_, index) => !boxTrackerMapping.ContainsKey(index)).ToArray(); + var matchedBoxes = boxes.Select((box, index) => boxTrackerMapping.TryGetValue(index, out var tracker) + ? (Tracker: tracker, Box: box) + : (Tracker: -1, Box: RectangleF.Empty)) + .Where(tb => tb.Tracker != -1) + .ToDictionary(tb => tb.Tracker, tb => tb.Box); + + return (matchedBoxes, unmatchedBoxes); + } - double intersectArea = (1.0 + intersection.Width) * (1.0 + intersection.Height); - double unionArea = ((1.0 + a.Width) * (1.0 + a.Height)) + ((1.0 + b.Width) * (1.0 + b.Height)) - intersectArea; - return intersectArea / (unionArea + 1e-5); + private static double IoU(RectangleF a, RectangleF b) + { + var intersection = RectangleF.Intersect(a, b); + if (intersection.IsEmpty) + { + return 0; } + + var intersectArea = (1.0 + intersection.Width) * (1.0 + intersection.Height); + var unionArea = ((1.0 + a.Width) * (1.0 + a.Height)) + ((1.0 + b.Width) * (1.0 + b.Height)) - intersectArea; + return intersectArea / (unionArea + 1e-5); } -} +} \ No newline at end of file diff --git a/src/SortCS/Track.cs b/src/SortCS/Track.cs index a445358..3d50f94 100644 --- a/src/SortCS/Track.cs +++ b/src/SortCS/Track.cs @@ -1,20 +1,19 @@ using System.Collections.Generic; using System.Drawing; -namespace SortCS +namespace SortCS; + +public record Track { - public record Track - { - public int TrackId { get; set; } + public int TrackId { get; set; } - public int TotalMisses { get; set; } + public int TotalMisses { get; set; } - public int Misses { get; set; } + public int Misses { get; set; } - public List History { get; set; } + public List History { get; set; } - public TrackState State { get; set; } + public TrackState State { get; set; } - public RectangleF Prediction { get; set; } - } + public RectangleF Prediction { get; set; } } \ No newline at end of file diff --git a/src/SortCS/TrackState.cs b/src/SortCS/TrackState.cs index b422deb..502746f 100644 --- a/src/SortCS/TrackState.cs +++ b/src/SortCS/TrackState.cs @@ -1,10 +1,9 @@ -namespace SortCS +namespace SortCS; + +public enum TrackState { - public enum TrackState - { - Started, - Active, - Ending, - Ended - } + Started, + Active, + Ending, + Ended } \ No newline at end of file