diff --git a/.gitignore b/.gitignore index 062d8d4..70f4d6f 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,8 @@ ## ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore +SynoAI.sln + # User-specific files *.rsuser *.suo diff --git a/SynoAI.sln b/SynoAI.sln index ff275c6..f2ee1cf 100644 --- a/SynoAI.sln +++ b/SynoAI.sln @@ -5,8 +5,8 @@ VisualStudioVersion = 15.0.26124.0 MinimumVisualStudioVersion = 15.0.26124.0 Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SynoAI", "SynoAI\SynoAI.csproj", "{D55517BF-4185-4B3D-956F-9CCE6425D88B}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SynoAI.Tests", "SynoAI.Tests\SynoAI.Tests.csproj", "{C3A70D73-D1DD-46E4-882E-34CA833BE3EF}" -EndProject +#Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "SynoAI.Tests", "SynoAI.Tests\SynoAI.Tests.csproj", "{C3A70D73-D1DD-46E4-882E-34CA833BE3EF}" +#EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU diff --git a/SynoAI/AIs/DeepStack/DeepStackAI.cs b/SynoAI/AIs/DeepStack/DeepStackAI.cs index d77fae9..306b3a6 100644 --- a/SynoAI/AIs/DeepStack/DeepStackAI.cs +++ b/SynoAI/AIs/DeepStack/DeepStackAI.cs @@ -7,15 +7,12 @@ using System.IO; using System.Linq; using System.Net.Http; -using System.Net.Http.Json; using System.Threading.Tasks; namespace SynoAI.AIs.DeepStack { public class DeepStackAI : AI { - private const string URL_VISION_DETECTION = "v1/vision/detection"; - public async override Task> Process(ILogger logger, Camera camera, byte[] image) { using (HttpClient client = new HttpClient()) @@ -36,13 +33,14 @@ public async override Task> Process(ILogger logger, Ca logger.LogDebug($"{camera.Name}: DeepStackAI: Sending image."); - HttpResponseMessage response = await client.PostAsync(URL_VISION_DETECTION, multipartContent); + HttpResponseMessage response = await client.PostAsync(Config.AIPath, multipartContent); if (response.IsSuccessStatusCode) { DeepStackResponse deepStackResponse = await GetResponse(response); if (deepStackResponse.Success) { - IEnumerable predictions = deepStackResponse.Predictions.Where(x=> x.Confidence >= minConfidence).Select(x => new AIPrediction() + + IEnumerable predictions = deepStackResponse.Predictions.Where(x=> x.Confidence >= minConfidence).Select(x => new AIPrediction() { Confidence = x.Confidence * 100, Label = x.Label, diff --git a/SynoAI/Config.cs b/SynoAI/Config.cs index 26c9924..0ec06ae 100644 --- a/SynoAI/Config.cs +++ b/SynoAI/Config.cs @@ -4,12 +4,8 @@ using SynoAI.AIs; using SynoAI.Models; using SynoAI.Notifiers; -using SynoAI.Notifiers.Pushbullet; using System; using System.Collections.Generic; -using System.Dynamic; -using System.Linq; -using System.Threading.Tasks; namespace SynoAI { @@ -51,12 +47,6 @@ public static class Config /// 2 = Low bandwidth /// public static CameraQuality Quality { get; private set; } - - /// - /// The amount of time that needs to have passed between the last call to check the camera and the current call. - /// - public static int Delay { get; private set; } - /// /// The hex code of the colour to use for the boxing around image matches. /// @@ -83,6 +73,19 @@ public static class Config /// public static int TextOffsetY { get; private set; } /// + /// True will only place a reference number on each label image, later detailing object type and confidence percentage on the notification text + /// + public static bool AlternativeLabelling { get; private set; } + /// + /// True will place each image label below the boundary box. + /// + public static bool LabelBelowBox { get; private set; } + /// + /// + /// Upon movement, the maximum number of snapshots sequentially retrieved from SSS until finding an object of interest (i.e. 4 snapshots) + /// + public static int MaxSnapshots { get; private set; } + /// /// Whether this original snapshot generated from the API should be saved to the file system. /// public static bool SaveOriginalSnapshot { get; private set; } @@ -92,6 +95,7 @@ public static class Config /// public static AIType AI { get; private set; } public static string AIUrl { get; private set; } + public static string AIPath { get; private set; } public static int MinSizeX { get; private set; } public static int MinSizeY { get; private set; } @@ -131,8 +135,7 @@ public static void Generate(ILogger logger, IConfiguration configuration) ApiVersionCamera = configuration.GetValue("ApiVersionCamera", 9); // Surveillance Station 8.0 Quality = configuration.GetValue("Quality", CameraQuality.Balanced); - - Delay = configuration.GetValue("Delay", 5000); + DrawMode = configuration.GetValue("DrawMode", DrawMode.Matches); BoxColor = configuration.GetValue("BoxColor", SKColors.Red.ToString()); @@ -147,11 +150,20 @@ public static void Generate(ILogger logger, IConfiguration configuration) MinSizeX = configuration.GetValue("MinSizeX", 50); MinSizeY = configuration.GetValue("MinSizeY", 50); + LabelBelowBox = configuration.GetValue("LabelBelowBox", false); + AlternativeLabelling = configuration.GetValue("AlternativeLabelling", false); + MaxSnapshots = configuration.GetValue("MaxSnapshots", 1); + if (MaxSnapshots > 254) { + MaxSnapshots = 254; + logger.LogWarning("ATTENTION: Config parameter MaxSnapshots is too big: Maximum accepted value is 254 "); + } + SaveOriginalSnapshot = configuration.GetValue("SaveOriginalSnapshot", false); IConfigurationSection aiSection = configuration.GetSection("AI"); AI = aiSection.GetValue("Type", AIType.DeepStack); AIUrl = aiSection.GetValue("Url"); + AIPath = aiSection.GetValue("Path","v1/vision/detection"); Cameras = GenerateCameras(logger, configuration); Notifiers = GenerateNotifiers(logger, configuration); diff --git a/SynoAI/Controllers/CameraController.cs b/SynoAI/Controllers/CameraController.cs index 0baea0e..d189d3f 100644 --- a/SynoAI/Controllers/CameraController.cs +++ b/SynoAI/Controllers/CameraController.cs @@ -1,8 +1,6 @@ using Microsoft.AspNetCore.Mvc; -using Microsoft.Extensions.Configuration; using Microsoft.Extensions.Logging; using SkiaSharp; -using SynoAI.AIs; using SynoAI.Models; using SynoAI.Notifiers; using SynoAI.Services; @@ -10,12 +8,9 @@ using System.Collections.Concurrent; using System.Collections.Generic; using System.Diagnostics; -using System.Drawing; -using System.Drawing.Drawing2D; -using System.Drawing.Imaging; +using SynoAI.Extensions; using System.IO; using System.Linq; -using System.Runtime.InteropServices; using System.Threading.Tasks; namespace SynoAI.Controllers @@ -63,63 +58,100 @@ public async void Get(string id) return; } - // Enforce a delay between checks - if (!HasSufficientDelay(id)) - { - return; - } + // Get the min X and Y values for object; initialize snapshots counter. + int minX = camera.GetMinSizeX(); + int minY = camera.GetMinSizeY(); + int snapshotCount= 1; // Create the stopwatches for reporting timings Stopwatch overallStopwatch = Stopwatch.StartNew(); - // Take the snapshot from Surveillance Station - byte[] snapshot = await GetSnapshot(id); - snapshot = PreProcessSnapshot(camera, snapshot); - - // Save the original unprocessed image if required - if (Config.SaveOriginalSnapshot) + //Start bucle for asking snapshots until a valid prediction is found or MaxSnapshots is reached + while (snapshotCount > 0 && snapshotCount <= Config.MaxSnapshots) { - _logger.LogInformation($"{id}: Saving original image before processing"); - SnapshotManager.SaveOriginalImage(_logger, camera, snapshot); - } + _logger.LogInformation($"Snapshot {snapshotCount} of {Config.MaxSnapshots} asked at EVENT TIME {overallStopwatch.ElapsedMilliseconds}ms."); + // Take the snapshot from Surveillance Station + byte[] snapshot = await GetSnapshot(id); + _logger.LogInformation($"Snapshot {snapshotCount} of {Config.MaxSnapshots} received at EVENT TIME {overallStopwatch.ElapsedMilliseconds}ms."); - // Get the min X and Y values - int minX = camera.GetMinSizeX(); - int minY = camera.GetMinSizeY(); + //See if the image needs to be rotated (or further processing in the future ?) previous to being analyzed by AI + snapshot = PreProcessSnapshot(camera, snapshot); - // Use the AI to get the valid predictions and then get all the valid predictions, which are all the AI predictions where the result from the AI is - // in the list of types and where the size of the object is bigger than the defined value. - IEnumerable predictions = await GetAIPredications(camera, snapshot); - if (predictions != null) - { - IEnumerable validPredictions = predictions.Where(x => - camera.Types.Contains(x.Label, StringComparer.OrdinalIgnoreCase) && // Is a type we care about - x.SizeX >= minX && x.SizeY >= minY) // Is bigger than the minimum size - .ToList(); + // Use the AI to get the valid predictions and then get all the valid predictions, which are all the AI predictions where the result from the AI is + // in the list of types and where the size of the object is bigger than the defined value. + IEnumerable predictions = await GetAIPredications(camera, snapshot); - if (validPredictions.Count() > 0) - { - // Because we don't want to process the image if it isn't even required, then we pass the snapshot manager to the notifiers. It will then perform - // the necessary actions when it's GetImage method is called. - SnapshotManager snapshotManager = new SnapshotManager(snapshot, predictions, validPredictions, _snapshotManagerLogger); - - // Limit the predictions to just those defined by the camera - predictions = predictions.Where(x => camera.Types.Contains(x.Label, StringComparer.OrdinalIgnoreCase)).ToList(); - await SendNotifications(camera, snapshotManager, predictions.Select(x=> x.Label).ToList()); - } - else if (predictions.Count() > 0) + _logger.LogInformation($"Snapshot {snapshotCount} of {Config.MaxSnapshots} processed {predictions.Count()} objects at EVENT TIME {overallStopwatch.ElapsedMilliseconds}ms."); + + if (predictions != null) { - // We got predictions back from the AI, but nothing that should trigger an alert - _logger.LogInformation($"{id}: Nothing detected by the AI exceeding the defined confidence level and/or minimum size"); + IEnumerable validPredictions = predictions.Where(x => + camera.Types.Contains(x.Label, StringComparer.OrdinalIgnoreCase) && // Is a type we care about + x.SizeX >= minX && x.SizeY >= minY) // Is bigger than the minimum size + .ToList(); + + if (validPredictions.Count() > 0) + { + + // Because we don't want to process the image if it isn't even required, then we pass the snapshot manager to the notifiers. It will then perform + // the necessary actions when it's GetImage method is called. + SnapshotManager snapshotManager = new SnapshotManager(snapshot, predictions, validPredictions, _snapshotManagerLogger); + + // Save the original unprocessed image if required + if (Config.SaveOriginalSnapshot) + { + _logger.LogInformation($"{id}: Saving original image"); + SnapshotManager.SaveOriginalImage(_logger, camera, snapshot); + } + + // Generate text for notifications + IList labels = new List(); + + if (Config.AlternativeLabelling && Config.DrawMode == DrawMode.Matches) + { + if (validPredictions.Count() == 1) + { + decimal confidence = Math.Round(validPredictions.First().Confidence, 0, MidpointRounding.AwayFromZero); + labels.Add($"{validPredictions.First().Label.FirstCharToUpper()} {confidence}%"); + } + else + { + //Since there is more than one object detected, include correlating number + int counter = 1; + foreach (AIPrediction prediction in validPredictions) + { + decimal confidence = Math.Round(prediction.Confidence, 0, MidpointRounding.AwayFromZero); + labels.Add($"{counter}. {prediction.Label.FirstCharToUpper()} {confidence}%"); + counter++; + } + } + } + else + { + labels = validPredictions.Select(x => x.Label.FirstCharToUpper()).ToList(); + } + + //Send Notifications + await SendNotifications(camera, snapshotManager, labels); + _logger.LogInformation($"{id}: Valid object found in snapshot {snapshotCount} of {Config.MaxSnapshots} at EVENT TIME {overallStopwatch.ElapsedMilliseconds}ms."); + + //Stop snapshot bucle iteration: + snapshotCount = -1; + } + else if (predictions.Count() > 0) + { + // We got predictions back from the AI, but nothing that should trigger an alert + _logger.LogInformation($"{id}: No valid objects at EVENT TIME {overallStopwatch.ElapsedMilliseconds}ms."); + } + else + { + // We didn't get any predictions whatsoever from the AI + _logger.LogInformation($"{id}: Nothing detected by the AI at EVENT TIME {overallStopwatch.ElapsedMilliseconds}ms."); + } } - else - { - // We didn't get any predictions whatsoever from the AI - _logger.LogInformation($"{id}: Nothing detected by the AI"); - } - - _logger.LogInformation($"{id}: Finished ({overallStopwatch.ElapsedMilliseconds}ms)."); + snapshotCount++; } + _logger.LogInformation($"{id}: FINISHED EVENT at EVENT TIME {overallStopwatch.ElapsedMilliseconds}ms."); } /// @@ -130,23 +162,27 @@ public async void Get(string id) /// A byte array of the image. private byte[] PreProcessSnapshot(Camera camera, byte[] snapshot) { - if (camera.Rotate == 0) + if (camera.Rotate != 0) { - return snapshot; - } + Stopwatch stopwatch = Stopwatch.StartNew(); - Stopwatch stopwatch = Stopwatch.StartNew(); + // Load the bitmap & rotate the image + //SKBitmap bitmap = SKBitmap.Decode(new MemoryStream(snapshot)); + SKBitmap bitmap = SKBitmap.Decode(snapshot); - // Load the bitmap & rotate the image - SKBitmap bitmap = SKBitmap.Decode(new MemoryStream(snapshot)); - _logger.LogInformation($"{camera.Name}: Rotating image {camera.Rotate} degrees."); - bitmap = Rotate(bitmap, camera.Rotate); + _logger.LogInformation($"{camera.Name}: Rotating image {camera.Rotate} degrees."); + bitmap = Rotate(bitmap, camera.Rotate); - using (SKPixmap pixmap = bitmap.PeekPixels()) - using (SKData data = pixmap.Encode(SKEncodedImageFormat.Jpeg, 100)) - { - _logger.LogInformation($"{camera.Name}: Image preprocessing complete ({stopwatch.ElapsedMilliseconds}ms)."); - return data.ToArray(); + using (SKPixmap pixmap = bitmap.PeekPixels()) + using (SKData data = pixmap.Encode(SKEncodedImageFormat.Jpeg, 100)) + { + _logger.LogInformation($"{camera.Name}: Image preprocessing complete ({stopwatch.ElapsedMilliseconds}ms)."); + return data.ToArray(); + } + } + else + { + return snapshot; } } @@ -179,7 +215,7 @@ private SKBitmap Rotate(SKBitmap bitmap, double angle) return rotatedBitmap; } - private async Task SendNotifications(Camera camera, ISnapshotManager snapshotManager, IEnumerable labels) + private async Task SendNotifications(Camera camera, ISnapshotManager snapshotManager, IList labels) { Stopwatch stopwatch = Stopwatch.StartNew(); @@ -204,8 +240,6 @@ private async Task SendNotifications(Camera camera, ISnapshotManager snapshotMan /// A byte array for the image, or null on failure. private async Task GetSnapshot(string cameraName) { - _logger.LogInformation($"{cameraName}: Motion detected, fetching snapshot."); - Stopwatch stopwatch = Stopwatch.StartNew(); byte[] imageBytes = await _synologyService.TakeSnapshotAsync(cameraName); @@ -217,7 +251,7 @@ private async Task GetSnapshot(string cameraName) else { stopwatch.Stop(); - _logger.LogInformation($"{cameraName}: Snapshot received ({stopwatch.ElapsedMilliseconds}ms)."); + _logger.LogInformation($"{cameraName}: Snapshot received in {stopwatch.ElapsedMilliseconds}ms."); } return imageBytes; @@ -231,59 +265,19 @@ private async Task GetSnapshot(string cameraName) /// A list of predictions, or null on failure. private async Task> GetAIPredications(Camera camera, byte[] imageBytes) { - _logger.LogInformation($"{camera}: Processing."); - IEnumerable predictions = await _aiService.ProcessAsync(camera, imageBytes); if (predictions == null) { _logger.LogError($"{camera}: Failed to get get predictions."); - return null; } - else + else if (_logger.IsEnabled(LogLevel.Information)) { foreach (AIPrediction prediction in predictions) { _logger.LogInformation($"{camera}: {prediction.Label} ({prediction.Confidence}%) [Size: {prediction.SizeX}x{prediction.SizeY}] [Start: {prediction.MinX},{prediction.MinY} | End: {prediction.MaxX},{prediction.MaxY}]"); } } - return predictions; } - - /// - /// Ensures that the camera doesn't get called too often. - /// - /// The ID of the camera to check. - /// True if enough time has passed. - private bool HasSufficientDelay(string id) - { - if (_lastCameraChecks.TryGetValue(id, out DateTime lastCheck)) - { - TimeSpan timeSpan = DateTime.UtcNow - lastCheck; - _logger.LogInformation($"{id}: Camera last checked {timeSpan.Milliseconds}ms ago"); - - if (timeSpan.TotalMilliseconds < Config.Delay) - { - _logger.LogInformation($"{id}: Ignoring request due to last check being under {Config.Delay}ms."); - return false; - } - - if (!_lastCameraChecks.TryUpdate(id, DateTime.UtcNow, lastCheck)) - { - _logger.LogInformation($"{id}: Ignoring request due multiple concurrent calls."); - return false; - } - } - else - { - if (!_lastCameraChecks.TryAdd(id, DateTime.UtcNow)) - { - _logger.LogInformation($"{id}: Ignoring request due multiple concurrent calls."); - return false; - } - } - - return true; - } } } diff --git a/SynoAI/Notifiers/Email/Email.cs b/SynoAI/Notifiers/Email/Email.cs index b3539d1..27cab7c 100644 --- a/SynoAI/Notifiers/Email/Email.cs +++ b/SynoAI/Notifiers/Email/Email.cs @@ -53,7 +53,7 @@ public class Email : NotifierBase /// A thread safe object for fetching the processed image. /// The list of types that were found. /// A logger. - public override async Task SendAsync(Camera camera, ISnapshotManager snapshotManager, IEnumerable foundTypes, ILogger logger) + public override async Task SendAsync(Camera camera, ISnapshotManager snapshotManager, IList foundTypes, ILogger logger) { using (logger.BeginScope($"Email '{Destination}'")) { diff --git a/SynoAI/Notifiers/INotifier.cs b/SynoAI/Notifiers/INotifier.cs index 176ba08..7161fce 100644 --- a/SynoAI/Notifiers/INotifier.cs +++ b/SynoAI/Notifiers/INotifier.cs @@ -15,6 +15,6 @@ public interface INotifier /// /// Handles the send of the notification. /// - Task SendAsync(Camera camera, ISnapshotManager snapshotManager, IEnumerable foundTypes, ILogger logger); + Task SendAsync(Camera camera, ISnapshotManager snapshotManager, IList foundTypes, ILogger logger); } } diff --git a/SynoAI/Notifiers/NotifierBase.cs b/SynoAI/Notifiers/NotifierBase.cs index 7fc40a1..e3122f5 100644 --- a/SynoAI/Notifiers/NotifierBase.cs +++ b/SynoAI/Notifiers/NotifierBase.cs @@ -3,20 +3,45 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.Extensions.Logging; -using SynoAI.Extensions; using SynoAI.Models; using SynoAI.Services; +using SynoAI.Extensions; namespace SynoAI.Notifiers { public abstract class NotifierBase : INotifier { public IEnumerable Cameras { get; set;} - public abstract Task SendAsync(Camera camera, ISnapshotManager fileAccessor, IEnumerable foundTypes, ILogger logger); + public abstract Task SendAsync(Camera camera, ISnapshotManager fileAccessor, IList foundTypes, ILogger logger); - protected string GetMessage(Camera camera, IEnumerable foundTypes) + protected string GetMessage(Camera camera, IList foundTypes) { - return $"Motion detected on {camera.Name}\n\nDetected {foundTypes.Count()} objects:\n{String.Join("\n", foundTypes.Select(x => x.FirstCharToUpper()).ToArray())}"; + if (Config.AlternativeLabelling && Config.DrawMode == DrawMode.Matches) + { + //defaulting into generic label type + String typeLabel = "object"; + + if (camera.Types.Count() == 1) { + //Only one object type configured: use it instead of generic "object" label + typeLabel = camera.Types.First(); + } + + if (foundTypes.Count() > 1) + { + //Several objects detected + return $"{camera.Name}: {foundTypes.Count()} {typeLabel}s\n{String.Join("\n", foundTypes.Select(x => x).ToArray())}"; + } + else + { + //Just one object detected + return $"{camera.Name}: {foundTypes.First()}"; + } + } + else + { + //Standard (old) labelling + return $"Motion detected on {camera.Name}\n\nDetected {foundTypes.Count()} objects:\n{String.Join("\n", foundTypes.Select(x => x).ToArray())}"; + } } } } \ No newline at end of file diff --git a/SynoAI/Notifiers/Pushbullet/Pushbullet.cs b/SynoAI/Notifiers/Pushbullet/Pushbullet.cs index c1d0eea..4384241 100644 --- a/SynoAI/Notifiers/Pushbullet/Pushbullet.cs +++ b/SynoAI/Notifiers/Pushbullet/Pushbullet.cs @@ -34,7 +34,7 @@ public class Pushbullet : NotifierBase /// A thread safe object for fetching the processed image. /// The list of types that were found. /// A logger. - public override async Task SendAsync(Camera camera, ISnapshotManager snapshotManager, IEnumerable foundTypes, ILogger logger) + public override async Task SendAsync(Camera camera, ISnapshotManager snapshotManager, IList foundTypes, ILogger logger) { // Pushbullet file uploads are a two part process. First we need to request to upload a file using (HttpClient client = new HttpClient()) diff --git a/SynoAI/Notifiers/Telegram/Telegram.cs b/SynoAI/Notifiers/Telegram/Telegram.cs index c4147b8..a714345 100644 --- a/SynoAI/Notifiers/Telegram/Telegram.cs +++ b/SynoAI/Notifiers/Telegram/Telegram.cs @@ -42,7 +42,7 @@ public class Telegram : NotifierBase /// A thread safe object for fetching the processed image. /// The list of types that were found. /// A logger. - public override async Task SendAsync(Camera camera, ISnapshotManager snapshotManager, IEnumerable foundTypes, ILogger logger) + public override async Task SendAsync(Camera camera, ISnapshotManager snapshotManager, IList foundTypes, ILogger logger) { using (logger.BeginScope($"Telegram '{ChatID}'")) { diff --git a/SynoAI/Notifiers/Webhook/Webhook.cs b/SynoAI/Notifiers/Webhook/Webhook.cs index fc77eb7..2dd793f 100644 --- a/SynoAI/Notifiers/Webhook/Webhook.cs +++ b/SynoAI/Notifiers/Webhook/Webhook.cs @@ -65,7 +65,7 @@ public class Webhook : NotifierBase /// A thread safe object for fetching a readonly file stream. /// The list of types that were found. /// A logger. - public override async Task SendAsync(Camera camera, ISnapshotManager snapshotManager, IEnumerable foundTypes, ILogger logger) + public override async Task SendAsync(Camera camera, ISnapshotManager snapshotManager, IList foundTypes, ILogger logger) { logger.LogInformation($"{camera.Name}: Webhook: Processing"); using (HttpClient client = new HttpClient()) diff --git a/SynoAI/Services/SnapshotManager.cs b/SynoAI/Services/SnapshotManager.cs index 4909ebd..43b056f 100644 --- a/SynoAI/Services/SnapshotManager.cs +++ b/SynoAI/Services/SnapshotManager.cs @@ -6,6 +6,7 @@ using Microsoft.Extensions.Logging; using SkiaSharp; using SynoAI.Models; +using SynoAI.Extensions; namespace SynoAI.Services { @@ -81,7 +82,7 @@ private SKBitmap ProcessImage(Camera camera) _logger.LogInformation($"{camera.Name}: Processing image boundaries."); // Load the bitmap - SKBitmap image = SKBitmap.Decode(new MemoryStream(_snapshot)); + SKBitmap image = SKBitmap.Decode(_snapshot); // Don't process the drawing if the drawing mode is off if (Config.DrawMode == DrawMode.Off) @@ -93,6 +94,8 @@ private SKBitmap ProcessImage(Camera camera) // Draw the predictions using (SKCanvas canvas = new SKCanvas(image)) { + int counter = 1; //used for assigning a reference number on each prediction if AlternativeLabelling is true + foreach (AIPrediction prediction in Config.DrawMode == DrawMode.All ? _predictions : _validPredictions) { // Write out anything detected that was above the minimum size @@ -100,9 +103,6 @@ private SKBitmap ProcessImage(Camera camera) int minSizeY = camera.GetMinSizeY(); if (prediction.SizeX >= minSizeX && prediction.SizeY >= minSizeY) { - decimal confidence = Math.Round(prediction.Confidence, 0, MidpointRounding.AwayFromZero); - string label = $"{prediction.Label} ({confidence}%)"; - // Draw the box SKRect rectangle = SKRect.Create(prediction.MinX, prediction.MinY, prediction.SizeX, prediction.SizeY); canvas.DrawRect(rectangle, new SKPaint @@ -111,9 +111,34 @@ private SKBitmap ProcessImage(Camera camera) Color = GetColour(Config.BoxColor) }); + //Label creation, either classic label or alternative labelling (and only if there is more than one object) + string label = String.Empty; + + if (Config.AlternativeLabelling && Config.DrawMode == DrawMode.Matches) + { + //On alternatie labelling, just place a reference number and only if there is more than one object + if (_validPredictions.Count() > 1) + { + label = counter.ToString(); + counter++; + } + } + else + { + decimal confidence = Math.Round(prediction.Confidence, 0, MidpointRounding.AwayFromZero); + label = $"{prediction.Label.FirstCharToUpper()} {confidence}%"; + } + + //Label positioning int x = prediction.MinX + Config.TextOffsetX; int y = prediction.MinY + Config.FontSize + Config.TextOffsetY; + //Consider below box placement + if (Config.LabelBelowBox) + { + y += prediction.SizeY; + } + // Draw the text SKFont font = new SKFont(SKTypeface.FromFamilyName(Config.Font), Config.FontSize); canvas.DrawText(label, x, y, font, new SKPaint diff --git a/SynoAI/appsettings.json b/SynoAI/appsettings.json index 75f9f5e..798dfdb 100644 --- a/SynoAI/appsettings.json +++ b/SynoAI/appsettings.json @@ -14,7 +14,6 @@ "Password": "", "AllowInsecureUrl": false, - "Delay": 5000, "DrawMode": "Matches", "AI": {