Skip to content

Commit 89f54c5

Browse files
committed
#98 Included the AI predictions in the webhook requests.
1 parent 86d5162 commit 89f54c5

14 files changed

+135
-307
lines changed

SynoAI/AIs/DeepStack/DeepStackAI.cs

+6-4
Original file line numberDiff line numberDiff line change
@@ -20,9 +20,11 @@ public async override Task<IEnumerable<AIPrediction>> Process(ILogger logger, Ca
2020

2121
decimal minConfidence = camera.Threshold / 100m;
2222

23-
MultipartFormDataContent multipartContent = new MultipartFormDataContent();
24-
multipartContent.Add(new StreamContent(new MemoryStream(image)), "image", "image");
25-
multipartContent.Add(new StringContent(minConfidence.ToString()), "min_confidence"); // From face detection example - using JSON with MinConfidence didn't always work
23+
MultipartFormDataContent multipartContent = new()
24+
{
25+
{ new StreamContent(new MemoryStream(image)), "image", "image" },
26+
{ new StringContent(minConfidence.ToString()), "min_confidence" } // From face detection example - using JSON with MinConfidence didn't always work
27+
};
2628

2729
logger.LogDebug($"{camera.Name}: DeepStackAI: POSTing image with minimum confidence of {minConfidence} ({camera.Threshold}%) to {string.Join("/", Config.AIUrl, Config.AIPath)}.");
2830

@@ -69,7 +71,7 @@ public async override Task<IEnumerable<AIPrediction>> Process(ILogger logger, Ca
6971
/// <returns>A <see cref="Uri"/> for the combined base and resource.</returns>
7072
protected Uri GetUri(string basePath, string resourcePath)
7173
{
72-
Uri baseUri = new Uri(basePath);
74+
Uri baseUri = new(basePath);
7375
return new Uri(baseUri, resourcePath);
7476
}
7577

SynoAI/Controllers/CameraController.cs

+24-57
Original file line numberDiff line numberDiff line change
@@ -32,8 +32,8 @@ public class CameraController : ControllerBase
3232
private readonly ISynologyService _synologyService;
3333
private readonly ILogger<CameraController> _logger;
3434

35-
private static ConcurrentDictionary<string, bool> _runningCameraChecks = new ConcurrentDictionary<string, bool>(StringComparer.OrdinalIgnoreCase);
36-
private static ConcurrentDictionary<string, DateTime> _delayedCameraChecks = new ConcurrentDictionary<string, DateTime>(StringComparer.OrdinalIgnoreCase);
35+
private static ConcurrentDictionary<string, bool> _runningCameraChecks = new(StringComparer.OrdinalIgnoreCase);
36+
private static ConcurrentDictionary<string, DateTime> _delayedCameraChecks = new(StringComparer.OrdinalIgnoreCase);
3737

3838
public CameraController(IAIService aiService, ISynologyService synologyService, ILogger<CameraController> logger, IHubContext<SynoAIHub> hubContext)
3939
{
@@ -134,7 +134,7 @@ public async void Get(string id)
134134
int maxSizeX = camera.GetMaxSizeX();
135135
int maxSizeY = camera.GetMaxSizeY();
136136

137-
List<AIPrediction> validPredictions = new List<AIPrediction>();
137+
List<AIPrediction> validPredictions = new();
138138
foreach (AIPrediction prediction in predictions)
139139
{
140140
// Check if the prediction label is in the list of types the camera is looking for
@@ -178,14 +178,17 @@ public async void Get(string id)
178178

179179
if (validPredictions.Count() > 0)
180180
{
181-
// Generate text for notifications
182-
IEnumerable<string> labels = GetLabels(validPredictions);
183-
184181
// Process and save the snapshot
185182
ProcessedImage processedImage = SnapshotManager.DressImage(camera, snapshot, predictions, validPredictions, _logger);
186183

187184
// Send Notifications
188-
await SendNotifications(camera, processedImage, labels);
185+
Notification notification = new()
186+
{
187+
ProcessedImage = processedImage,
188+
ValidPredictions = validPredictions
189+
};
190+
191+
await SendNotifications(camera, notification);
189192

190193
// Inform eventual web users about this new Snapshot, for the "realtime" option thru Web
191194
await _hubContext.Clients.All.SendAsync("ReceiveSnapshot", camera.Name, processedImage.FileName);
@@ -239,10 +242,10 @@ private bool ShouldIncludePrediction(string id, Camera camera, Stopwatch overall
239242
// Check if the prediction falls within the exclusion zones
240243
if (camera.Exclusions != null && camera.Exclusions.Count() > 0)
241244
{
242-
Rectangle boundary = new Rectangle(prediction.MinX, prediction.MinY, prediction.SizeX, prediction.SizeY);
245+
Rectangle boundary = new(prediction.MinX, prediction.MinY, prediction.SizeX, prediction.SizeY);
243246
foreach (Zone exclusion in camera.Exclusions)
244247
{
245-
Rectangle exclusionZoneBoundary = new Rectangle(exclusion.Start.X, exclusion.Start.Y, exclusion.End.X - exclusion.Start.X, exclusion.End.Y - exclusion.Start.Y);
248+
Rectangle exclusionZoneBoundary = new(exclusion.Start.X, exclusion.Start.Y, exclusion.End.X - exclusion.Start.X, exclusion.End.Y - exclusion.Start.Y);
246249
bool exclude = exclusion.Mode == OverlapMode.Contains ? exclusionZoneBoundary.Contains(boundary) : exclusionZoneBoundary.IntersectsWith(boundary);
247250
if (exclude)
248251
{
@@ -256,43 +259,6 @@ private bool ShouldIncludePrediction(string id, Camera camera, Stopwatch overall
256259
return true;
257260
}
258261

259-
/// <summary>
260-
/// Gets the labels from the predictions to use in the notifications.
261-
/// </summary>
262-
/// <param name="validPredictions">The predictions to process.</param>
263-
/// <returns>A list of labels.</returns>
264-
private IEnumerable<string> GetLabels(IEnumerable<AIPrediction> validPredictions)
265-
{
266-
if (Config.AlternativeLabelling && Config.DrawMode == DrawMode.Matches)
267-
{
268-
List<String> labels = new List<String>();
269-
if (validPredictions.Count() == 1)
270-
{
271-
// If there is only a single object, then don't add a correlating number and instead just
272-
// write out the label.
273-
decimal confidence = Math.Round(validPredictions.First().Confidence, 0, MidpointRounding.AwayFromZero);
274-
labels.Add($"{validPredictions.First().Label.FirstCharToUpper()} {confidence}%");
275-
}
276-
else
277-
{
278-
// Since there is more than one object detected, include correlating number
279-
int counter = 1;
280-
foreach (AIPrediction prediction in validPredictions)
281-
{
282-
decimal confidence = Math.Round(prediction.Confidence, 0, MidpointRounding.AwayFromZero);
283-
labels.Add($"{counter}. {prediction.Label.FirstCharToUpper()} {confidence}%");
284-
counter++;
285-
}
286-
}
287-
288-
return labels;
289-
}
290-
else
291-
{
292-
return validPredictions.Select(x => x.Label.FirstCharToUpper()).ToList();
293-
}
294-
}
295-
296262
/// <summary>
297263
/// Adds a delay for the specified camera.
298264
/// </summary>
@@ -332,7 +298,7 @@ private void CleanupOldImages()
332298
{
333299
_cleanupOldImagesRunning = true;
334300

335-
DirectoryInfo directory = new DirectoryInfo(Constants.DIRECTORY_CAPTURES);
301+
DirectoryInfo directory = new(Constants.DIRECTORY_CAPTURES);
336302
IEnumerable<FileInfo> files = directory.GetFiles("*", new EnumerationOptions() { RecurseSubdirectories = true });
337303
foreach (FileInfo file in files)
338304
{
@@ -350,7 +316,7 @@ private void CleanupOldImages()
350316
}
351317
}
352318
private bool _cleanupOldImagesRunning;
353-
private object _cleanUpOldImagesLock = new object();
319+
private object _cleanUpOldImagesLock = new();
354320

355321
/// <summary>
356322
/// Handles any required preprocessing of the captured image.
@@ -399,8 +365,8 @@ private SKBitmap Rotate(SKBitmap bitmap, double angle)
399365
int rotatedWidth = (int)(cosine * originalWidth + sine * originalHeight);
400366
int rotatedHeight = (int)(cosine * originalHeight + sine * originalWidth);
401367

402-
SKBitmap rotatedBitmap = new SKBitmap(rotatedWidth, rotatedHeight);
403-
using (SKCanvas canvas = new SKCanvas(rotatedBitmap))
368+
SKBitmap rotatedBitmap = new(rotatedWidth, rotatedHeight);
369+
using (SKCanvas canvas = new(rotatedBitmap))
404370
{
405371
canvas.Clear();
406372
canvas.Translate(rotatedWidth / 2, rotatedHeight / 2);
@@ -417,22 +383,23 @@ private SKBitmap Rotate(SKBitmap bitmap, double angle)
417383
/// Sends notifications, if there is any configured
418384
/// </summary>
419385
/// <param name="camera">The camera responsible for this snapshot.</param>
420-
/// <param name="processedImage">The path information for the snapshot.</param>
421-
/// <param name="labels">The text metadata for each existing valid object.</param>
422-
private async Task SendNotifications(Camera camera, ProcessedImage processedImage, IEnumerable<string> labels)
386+
/// <param name="notification">The notification data to process.</param>
387+
private async Task SendNotifications(Camera camera, Notification notification)
423388
{
424389
Stopwatch stopwatch = Stopwatch.StartNew();
425390

391+
IEnumerable<string> labels = notification.ValidPredictions.Select(x => x.Label).Distinct().ToList();
392+
426393
IEnumerable<INotifier> notifiers = Config.Notifiers
427394
.Where(x =>
428-
(x.Cameras == null || x.Cameras.Count() == 0 || x.Cameras.Any(c => c.Equals(camera.Name, StringComparison.OrdinalIgnoreCase))) &&
429-
(x.Types == null || x.Types.Count() == 0 || x.Types.Any(t => labels.Contains(t, StringComparer.OrdinalIgnoreCase)))
395+
(x.Cameras == null || !x.Cameras.Any() || x.Cameras.Any(c => c.Equals(camera.Name, StringComparison.OrdinalIgnoreCase))) &&
396+
(x.Types == null || !x.Types.Any() || x.Types.Any(t => labels.Contains(t, StringComparer.OrdinalIgnoreCase)))
430397
).ToList();
431398

432-
List<Task> tasks = new List<Task>();
399+
List<Task> tasks = new();
433400
foreach (INotifier notifier in notifiers)
434401
{
435-
tasks.Add(notifier.SendAsync(camera, processedImage, labels, _logger));
402+
tasks.Add(notifier.SendAsync(camera, notification, _logger));
436403
}
437404

438405
await Task.WhenAll(tasks);

SynoAI/Models/Notification.cs

+67
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
using SynoAI.Extensions;
2+
using System;
3+
using System.Collections.Generic;
4+
using System.Linq;
5+
6+
namespace SynoAI.Models
7+
{
8+
public class Notification
9+
{
10+
/// <summary>
11+
/// Object for fetching the processed image
12+
/// </summary>
13+
public ProcessedImage ProcessedImage { get; set; }
14+
/// <summary>
15+
/// The list of valid predictions.
16+
/// </summary>
17+
public IEnumerable<AIPrediction> ValidPredictions { get; set; }
18+
19+
/// <summary>
20+
/// The list of types that were found.
21+
/// </summary>
22+
public IEnumerable<string> FoundTypes
23+
{
24+
get
25+
{
26+
return GetLabels();
27+
}
28+
}
29+
30+
/// <summary>
31+
/// Gets the labels from the predictions to use in the notifications.
32+
/// </summary>
33+
/// <param name="validPredictions">The predictions to process.</param>
34+
/// <returns>A list of labels.</returns>
35+
private IEnumerable<string> GetLabels()
36+
{
37+
if (Config.AlternativeLabelling && Config.DrawMode == DrawMode.Matches)
38+
{
39+
List<String> labels = new List<String>();
40+
if (ValidPredictions.Count() == 1)
41+
{
42+
// If there is only a single object, then don't add a correlating number and instead just
43+
// write out the label.
44+
decimal confidence = Math.Round(ValidPredictions.First().Confidence, 0, MidpointRounding.AwayFromZero);
45+
labels.Add($"{ValidPredictions.First().Label.FirstCharToUpper()} {confidence}%");
46+
}
47+
else
48+
{
49+
// Since there is more than one object detected, include correlating number
50+
int counter = 1;
51+
foreach (AIPrediction prediction in ValidPredictions)
52+
{
53+
decimal confidence = Math.Round(prediction.Confidence, 0, MidpointRounding.AwayFromZero);
54+
labels.Add($"{counter}. {prediction.Label.FirstCharToUpper()} {confidence}%");
55+
counter++;
56+
}
57+
}
58+
59+
return labels;
60+
}
61+
else
62+
{
63+
return ValidPredictions.Select(x => x.Label.FirstCharToUpper()).ToList();
64+
}
65+
}
66+
}
67+
}

SynoAI/Notifiers/Email/Email.cs

+3-4
Original file line numberDiff line numberDiff line change
@@ -54,16 +54,15 @@ public class Email : NotifierBase
5454
/// Sends a message and an image using the Pushbullet API.
5555
/// </summary>
5656
/// <param name="camera">The camera that triggered the notification.</param>
57-
/// <param name="processedImage">Object for fetching the processed image.</param>
58-
/// <param name="foundTypes">The list of types that were found.</param>
57+
/// <param name="notification">The notification data to process.</param>
5958
/// <param name="logger">A logger.</param>
60-
public override async Task SendAsync(Camera camera, ProcessedImage processedImage, IEnumerable<string> foundTypes, ILogger logger)
59+
public override async Task SendAsync(Camera camera, Notification notification, ILogger logger)
6160
{
6261
using (logger.BeginScope($"Email '{Destination}'"))
6362
{
6463
// Assign camera name to variable for logger placeholder
6564
string cameraName = camera.Name;
66-
string filePath = processedImage.FilePath;
65+
string filePath = notification.ProcessedImage.FilePath;
6766

6867
try
6968
{

SynoAI/Notifiers/INotifier.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,6 @@ public interface INotifier
1919
/// <summary>
2020
/// Handles the send of the notification.
2121
/// </summary>
22-
Task SendAsync(Camera camera, ProcessedImage processedImage, IEnumerable<string> foundTypes, ILogger logger);
22+
Task SendAsync(Camera camera, Notification notification, ILogger logger);
2323
}
2424
}

SynoAI/Notifiers/NotifierBase.cs

+1-1
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,7 @@ public abstract class NotifierBase : INotifier
1515
public IEnumerable<string> Cameras { get; set; }
1616
public IEnumerable<string> Types { get; set; }
1717

18-
public abstract Task SendAsync(Camera camera, ProcessedImage processedImage, IEnumerable<string> foundTypes, ILogger logger);
18+
public abstract Task SendAsync(Camera camera, Notification notification, ILogger logger);
1919

2020
protected string GetMessage(Camera camera, IEnumerable<string> foundTypes, string errorMessage = null)
2121
{

SynoAI/Notifiers/NotifierFactory.cs

-3
Original file line numberDiff line numberDiff line change
@@ -37,9 +37,6 @@ public static INotifier Create(NotifierType type, ILogger logger, IConfiguration
3737
case NotifierType.Webhook:
3838
factory = new WebhookFactory();
3939
break;
40-
case NotifierType.WebhookLegacy:
41-
factory = new WebhookFactoryLegacy();
42-
break;
4340
default:
4441
throw new NotImplementedException(type.ToString());
4542
}

SynoAI/Notifiers/NotifierType.cs

+1-5
Original file line numberDiff line numberDiff line change
@@ -25,10 +25,6 @@ public enum NotifierType
2525
/// <summary>
2626
/// Calls a webhook with the image attached.
2727
/// </summary>
28-
Webhook,
29-
/// <summary>
30-
/// Legacy implementation of Webhook
31-
/// </summary>
32-
WebhookLegacy
28+
Webhook
3329
}
3430
}

SynoAI/Notifiers/Pushbullet/Pushbullet.cs

+4-4
Original file line numberDiff line numberDiff line change
@@ -32,12 +32,12 @@ public class Pushbullet : NotifierBase
3232
/// Sends a message and an image using the Pushbullet API.
3333
/// </summary>
3434
/// <param name="camera">The camera that triggered the notification.</param>
35-
/// <param name="processedImage">Object for fetching the processed image.</param>
36-
/// <param name="foundTypes">The list of types that were found.</param>
3735
/// <param name="logger">A logger.</param>
38-
public override async Task SendAsync(Camera camera, ProcessedImage processedImage, IEnumerable<string> foundTypes, ILogger logger)
36+
public override async Task SendAsync(Camera camera, Notification notification, ILogger logger)
3937
{
4038
// Pushbullet file uploads are a two part process. First we need to request to upload a file
39+
ProcessedImage processedImage = notification.ProcessedImage;
40+
4141
string fileName = processedImage.FileName;
4242
string requestJson = JsonConvert.SerializeObject(new PushbulletUploadRequest()
4343
{
@@ -82,7 +82,7 @@ public override async Task SendAsync(Camera camera, ProcessedImage processedImag
8282
{
8383
Type = uploadSuccess ? "file" : "note",
8484
Title = $"{camera.Name}: Movement Detected",
85-
Body = GetMessage(camera, foundTypes, errorMessage: uploadError),
85+
Body = GetMessage(camera, notification.FoundTypes, errorMessage: uploadError),
8686
FileName = uploadSuccess ? uploadRequestResult.FileName : null,
8787
FileUrl = uploadSuccess ? uploadRequestResult.FileUrl : null,
8888
FileType = uploadSuccess ? uploadRequestResult.FileType : null

0 commit comments

Comments
 (0)