public static bool IsFileValidInternal(string Filename) { using var Trace = new Trace(); //This c# 8.0 using feature will auto dispose when the function is done. bool Ret = false; try { if (File.Exists(Filename)) { FileInfo fi = new FileInfo(Filename); if (fi.Length > 800) { //try to prevent multiple threads from erroring out writing the json file... Global.WaitFileAccessResult result = Global.WaitForFileAccess(Filename, FileAccess.Read, FileShare.ReadWrite, 5000); if (result.Success) { //check its contents, 0 bytes indicate corruption string contents = File.ReadAllText(Filename); if (!contents.Contains("\0")) { if (contents.TrimStart().StartsWith("{") && contents.TrimEnd().EndsWith("}")) { Ret = true; } else { Log($"Error: Settings file does not look like JSON (size={fi.Length} bytes): {Filename}"); } } else { Log("Error: Settings file contains null bytes, corrupt: (size={fi.Length} bytes)" + Filename); } } else { Log($"Error: Could not gain access to file for {result.TimeMS}ms - {Filename}"); } } else { Log($"Error: Settings file is too small at {fi.Length} bytes: {Filename}"); } } else { Log("Settings file does not exist yet: " + Filename); } } catch (Exception ex) { Log($"Error: While validating settings file '{Filename}' got error '{ex.Message}'."); } return(Ret); }
private List <ClsLogItm> LoadLogFile(string Filename, bool Import, bool LimitEntries) { List <ClsLogItm> ret = new List <ClsLogItm>(); //this.LastLoadMessages.Clear(); if (Import) { this.Enabled = false; //disable while we import } string ExtractZipPath = ""; string file = Path.GetFileName(Filename); Stopwatch sw = Stopwatch.StartNew(); if (File.Exists(Filename)) { try { Global.UpdateProgressBar($"Loading {Path.GetFileName(Filename)}...", 1, 1, 1); Global.WaitFileAccessResult result = Global.WaitForFileAccess(Filename, FileAccess.Read, FileShare.Read, 30000, 20); if (result.Success) { //if its a zip file, extract that puppy... string NewFilename = ""; if (Filename.EndsWith("zip", StringComparison.OrdinalIgnoreCase)) { ExtractZipPath = Path.Combine(Environment.GetEnvironmentVariable("TEMP"), "_" + file); if (!Directory.Exists(ExtractZipPath)) { Directory.CreateDirectory(ExtractZipPath); } //just extract the first file in the archive using (ZipArchive archive = ZipFile.OpenRead(Filename)) { foreach (ZipArchiveEntry entry in archive.Entries) { string destinationPath = Path.GetFullPath(Path.Combine(ExtractZipPath, entry.FullName)); entry.ExtractToFile(destinationPath, true); NewFilename = destinationPath; break; } } } else { NewFilename = Filename; } lock (this._LockObj) { file = Path.GetFileName(NewFilename); int Invalid = 0; bool OldFile = false; using (StreamReader sr = new StreamReader(NewFilename)) { int cnt = 0; while (!sr.EndOfStream) { cnt++; if (cnt > 1) { string line = sr.ReadLine(); if (!OldFile && line.TrimStart().StartsWith("[")) //old log format, ignore { OldFile = true; this._LastIDX.WriteFullFence(0); break; } if (!Import) { //just spit out a list of log lines ClsLogItm CLI = new ClsLogItm(line); if (CLI.Level != LogLevel.Off) //off indicates invalid - for example the old log format { CLI.FromFile = true; CLI.Filename = file; ret.Add(CLI); } else { Invalid++; if (Invalid > 50) { this.Log($"Error: Too many invalid lines ({Invalid}) stopping load."); ret.Clear(); break; } else { this.Log($"Debug: {Invalid} line(s) in log file '{line}'"); } } } else { //load into current log manager if (this._Store) { ClsLogItm CLI = new ClsLogItm(line); if (CLI.Level != LogLevel.Off) //off indicates invalid - for example the old log format { this.LastLogItm = CLI; if (this.LastLogItm.Level >= this.MinLevel) { CLI.FromFile = true; CLI.Filename = file; this.Values.Add(this.LastLogItm); this.RecentlyAdded.Enqueue(this.LastLogItm); //keep the log list size down if (LimitEntries && this.Values.Count > this.MaxGUILogItems) { this.RecentlyDeleted.Enqueue(this.Values[0]); this.Values.RemoveAt(0); } } } else { Invalid++; if (Invalid > 50) { this.Log($"Error: Too many invalid lines ({Invalid}) stopping load."); ret.Clear(); this._LastIDX.WriteFullFence(0); break; } else { this.Log($"Debug: {Invalid} line(s) in log file '{line}'"); } } } } } } } if (OldFile) { //rename it to keep it out of our way next time try { this.Log($"Debug: File was in the old log format, renaming to OLDLOGFORMAT. {NewFilename}"); File.Move(NewFilename, NewFilename + ".OLDLOGFORMAT"); } catch (Exception ex) { this.Log($"Error: While renaming log to OLDLOGFORMAT, got: {ex.Message}"); } } } } else { this.Log($"Error: Gave up waiting for exclusive file access after {result.TimeMS}ms with {result.ErrRetryCnt} retries for {Filename}"); } if (Directory.Exists(ExtractZipPath)) { Directory.Delete(ExtractZipPath, true); } } catch (Exception ex) { this.Log($"Error: {Global.ExMsg(ex)}"); } } if (Import) { this.Enabled = true; //enable after we import } Global.UpdateProgressBar($"", 0, 0, 0); this.LastLoadTimeMS = sw.ElapsedMilliseconds; return(ret); }
public bool MigrateHistoryCSV(string Filename) { using var Trace = new Trace(); //This c# 8.0 using feature will auto dispose when the function is done. bool ret = false; lock (DBLock) { try { //if (!await this.IsSQLiteDBConnectedAsync()) // await this.CreateConnectionAsync(); //run in another thread so we dont block UI //await Task.Run(async () => //{ if (System.IO.File.Exists(Filename)) { Global.UpdateProgressBar("Migrating history.csv...", 1, 1, 1); Log($"Debug: Migrating history list from {Filename} ..."); Stopwatch SW = Stopwatch.StartNew(); //delete obsolete entries from history.csv //CleanCSVList(); //removed to load the history list faster List <string> result = new List <string>(); //List that later on will be containing all lines of the csv file Global.WaitFileAccessResult wresult = Global.WaitForFileAccess(Filename); if (wresult.Success) { //load all lines except the first line into List (the first line is the table heading and not an alert entry) foreach (string line in System.IO.File.ReadAllLines(Filename).Skip(1)) { result.Add(line); } Log($"...Found {result.Count} lines."); List <string> itemsToDelete = new List <string>(); //stores all filenames of history.csv entries that need to be removed //load all List elements into the ListView for each row int added = 0; int removed = 0; int cnt = 0; foreach (var val in result) { cnt++; //if (cnt == 1 || cnt == result.Count || (cnt % (result.Count / 10) > 0)) //{ // Global.UpdateProgressBar("Migrating history.csv...", cnt, 1, result.Count); //} History hist = new History().CreateFromCSV(val); if (File.Exists(hist.Filename)) { if (this.InsertHistoryItem(hist)) { added++; //this.AddedCount.AtomicIncrementAndGet(); } else { removed++; } } else { removed++; } } ret = (added > 0); //this.AddedCount.AtomicAddAndGet(added); //this.DeletedCount.AtomicAddAndGet(removed); //try to get a better feel how much time this function consumes - Vorlon Log($"Debug: ...Added {added} out of {result.Count} history items ({removed} removed) in {SW.ElapsedMilliseconds}ms, {this.HistoryDic.Count} lines."); } else { Log($"Error: Could not gain access to history file for {wresult.TimeMS}ms with {wresult.ErrRetryCnt} retries - {AppSettings.Settings.HistoryFileName}"); } } else { Log($"Debug: Old history file does not exist, could not migrate: {Filename}"); } //}); } catch (Exception ex) { Log("Error: " + Global.ExMsg(ex)); } } Global.UpdateProgressBar("", 0, 0, 0); return(ret); }
public bool CopyFileTo(string outputFilePath) { using var Trace = new Trace(); //This c# 8.0 using feature will auto dispose when the function is done. bool ret = false; int bufferSize = 1024 * 1024; string copydir = ""; try { if (!outputFilePath.Contains("\\")) { AITOOL.Log($"Error: Must specify a full path: {outputFilePath}"); return(false); } if (this.IsValid()) //loads into memory if not already loaded { copydir = Path.GetDirectoryName(outputFilePath); DirectoryInfo d = new DirectoryInfo(copydir); if (d.Root != null && !d.Exists) { //dont try to create if working off root drive d.Create(); } //If the destination file exists, wait for exclusive access Global.WaitFileAccessResult result2 = new Global.WaitFileAccessResult(); if (File.Exists(outputFilePath)) { result2 = Global.WaitForFileAccess(outputFilePath, FileAccess.ReadWrite, FileShare.None, 3000, MinFileSize: 0); if (result2.Success) { File.Delete(outputFilePath); } } else { result2.Success = true; } if (result2.Success) { Stream inStream = this.ToStream(); using (FileStream fileStream = new FileStream(outputFilePath, FileMode.OpenOrCreate, FileAccess.ReadWrite)) { fileStream.SetLength(inStream.Length); int bytesRead = -1; byte[] bytes = new byte[bufferSize]; while ((bytesRead = inStream.Read(bytes, 0, bufferSize)) > 0) { fileStream.Write(bytes, 0, bytesRead); } } //wait for a small amount of time to allow the file to become accessible to blue iris - trying to prevent blank alert image in BI //Thread.Sleep(50); ret = true; } else { AITOOL.Log($"Error: Could not gain access to destination file ({result2.TimeMS}ms, '{result2.ResultString}') {outputFilePath}"); } } else { AITOOL.Log($"Error: File not valid: {this.image_path}"); } } catch (Exception ex) { AITOOL.Log($"Error: Copying to {outputFilePath}: {Global.ExMsg(ex)}"); } return(ret); }
public void LoadImage() { using var Trace = new Trace(); //This c# 8.0 using feature will auto dispose when the function is done. //since having a lot of trouble with image access problems, try to wait for image to become available, validate the image and load //a single time rather than multiple Global.WaitFileAccessResult result = new Global.WaitFileAccessResult(); string LastError = ""; this._valid = false; bool validate = !this._Temp; try { if (!string.IsNullOrEmpty(this.image_path) && File.Exists(this.image_path)) { Stopwatch sw = Stopwatch.StartNew(); do { int MaxWaitMS = 0; int MaxRetries = 0; if (this._Temp) { MaxWaitMS = 500; MaxRetries = 2; } else { MaxWaitMS = 10000; MaxRetries = 100; } result = Global.WaitForFileAccess(this.image_path, FileAccess.Read, FileShare.None, MaxWaitMS, AppSettings.Settings.file_access_delay_ms, true, 4096, MaxRetries); this.FileLockMS = sw.ElapsedMilliseconds; this.FileLockErrRetryCnt += result.ErrRetryCnt; if (result.Success) { try { sw.Restart(); // Open a FileStream object using the passed in safe file handle. using (FileStream fileStream = new FileStream(result.Handle, FileAccess.Read)) { using System.Drawing.Image img = System.Drawing.Image.FromStream(fileStream, true, validate); this._valid = img != null && img.RawFormat.Equals(System.Drawing.Imaging.ImageFormat.Jpeg) && img.Width > 0 && img.Height > 0; this.FileLoadMS = sw.ElapsedMilliseconds; if (!this._valid) { LastError = $"Error: Image file is not jpeg? {this.image_path}"; AITOOL.Log(LastError); break; } else { this.Width = img.Width; this.Height = img.Height; this.DPI = img.HorizontalResolution; if (this._Temp) { this.FileLoadMS = sw.ElapsedMilliseconds; } else { using MemoryStream ms = new MemoryStream(); //fileStream.CopyTo(ms); img.Save(ms, System.Drawing.Imaging.ImageFormat.Jpeg); this.ImageByteArray = ms.ToArray(); this.FileSize = this.ImageByteArray.Length; this.FileLoadMS = sw.ElapsedMilliseconds; AITOOL.Log($"Debug: Image file is valid. Resolution={this.Width}x{this.Height}, LockMS={this.FileLockMS}ms (max={MaxWaitMS}ms), retries={this.FileLockErrRetryCnt}, size={Global.FormatBytes(ms.Length)}: {Path.GetFileName(this.image_path)}"); } break; } } } catch (Exception ex) { this._valid = false; LastError = $"Error: Image is corrupt. LockMS={this.FileLockMS}ms (max={MaxWaitMS}ms), retries={this.FileLockErrRetryCnt}: {Path.GetFileName(this.image_path)} - {Global.ExMsg(ex)}"; } finally { this._loaded = true; if (!result.Handle.IsClosed) { result.Handle.Close(); result.Handle.Dispose(); } } } else { if (this._Temp) { LastError = $"Debug: Could not gain access to the image in {result.TimeMS}ms, retries={result.ErrRetryCnt}: {Path.GetFileName(this.image_path)}"; } else { LastError = $"Error: Could not gain access to the image in {result.TimeMS}ms, retries={result.ErrRetryCnt}: {Path.GetFileName(this.image_path)}"; } } if (this._Temp) //only one loop { break; } } while ((!result.Success || !this._valid) && sw.ElapsedMilliseconds < 30000); } else { //AITOOL.Log("Error: Tried to load the image too soon?"); } } catch (Exception ex) { AITOOL.Log($"Error: {Global.ExMsg(ex)}"); } finally { if (result.Handle != null && !result.Handle.IsInvalid && !result.Handle.IsClosed) { result.Handle.Close(); result.Handle.Dispose(); } if (!this._valid && !string.IsNullOrEmpty(LastError)) { AITOOL.Log(LastError); } } }
public void LoadImage() { //since having a lot of trouble with image access problems, try to wait for image to become available, validate the image and load //a single time rather than multiple Global.WaitFileAccessResult result = new Global.WaitFileAccessResult(); string LastError = ""; this._valid = false; try { if (!string.IsNullOrEmpty(this.image_path) && File.Exists(this.image_path)) { Stopwatch sw = Stopwatch.StartNew(); do { result = Global.WaitForFileAccess(this.image_path, FileAccess.Read, FileShare.None, 10000, 50, true, 4096); this.FileLockMS = sw.ElapsedMilliseconds; this.FileLockErrRetryCnt += result.ErrRetryCnt; if (result.Success) { try { sw.Restart(); // Open a FileStream object using the passed in safe file handle. using (FileStream fileStream = new FileStream(result.Handle, FileAccess.Read)) { using System.Drawing.Image img = System.Drawing.Image.FromStream(fileStream, true, true); this._valid = img != null && img.RawFormat.Equals(System.Drawing.Imaging.ImageFormat.Jpeg); this.FileLoadMS = sw.ElapsedMilliseconds; if (!this._valid) { LastError = $"Error: Image file is not jpeg? LockMS={this.FileLockMS}ms, retries={this.FileLockErrRetryCnt} - ({img.RawFormat}): {this.image_path}"; AITOOL.Log(LastError); break; } else { this.Width = img.Width; this.Height = img.Height; using MemoryStream ms = new MemoryStream(); //fileStream.CopyTo(ms); img.Save(ms, System.Drawing.Imaging.ImageFormat.Jpeg); this.ImageByteArray = ms.ToArray(); this.FileLoadMS = sw.ElapsedMilliseconds; AITOOL.Log($"Debug: Image file is valid. LockMS={this.FileLockMS}ms, retries={this.FileLockErrRetryCnt}, size={Global.FormatBytes(ms.Length)}: {Path.GetFileName(this.image_path)}"); break; } } } catch (Exception ex) { this._valid = false; LastError = $"Error: Image is corrupt. LockMS={this.FileLockMS}ms, retries={this.FileLockErrRetryCnt}: {Global.ExMsg(ex)}"; } finally { this._loaded = true; if (!result.Handle.IsClosed) { result.Handle.Close(); result.Handle.Dispose(); } } } else { LastError = $"Error: Could not gain access to the image in {result.TimeMS}ms, retries={result.ErrRetryCnt}."; } } while ((!result.Success || !this._valid) && sw.ElapsedMilliseconds < 30000); } else { //AITOOL.Log("Error: Tried to load the image too soon?"); } } catch (Exception ex) { AITOOL.Log($"Error: {Global.ExMsg(ex)}"); } finally { if (result.Handle != null && !result.Handle.IsInvalid && !result.Handle.IsClosed) { result.Handle.Close(); result.Handle.Dispose(); } if (!this._valid && !string.IsNullOrEmpty(LastError)) { AITOOL.Log(LastError); } } }
public async Task <string> MergeImageAnnotations(ClsTriggerActionQueueItem AQI) { int countr = 0; string detections = ""; string lasttext = ""; string lastposition = ""; string OutputImageFile = ""; bool bSendTelegramMessage = false; try { Global.LogMessage("Merging image annotations: " + AQI.CurImg.image_path); if (System.IO.File.Exists(AQI.CurImg.image_path)) { Stopwatch sw = Stopwatch.StartNew(); using (Bitmap img = new Bitmap(AQI.CurImg.image_path)) { using (Graphics g = Graphics.FromImage(img)) { g.InterpolationMode = InterpolationMode.HighQualityBicubic; g.SmoothingMode = SmoothingMode.HighQuality; g.PixelOffsetMode = PixelOffsetMode.HighQuality; //http://csharphelper.com/blog/2014/09/understand-font-aliasing-issues-in-c/ g.TextRenderingHint = TextRenderingHint.SingleBitPerPixelGridFit; System.Drawing.Color color = new System.Drawing.Color(); if (AQI.Hist != null && !string.IsNullOrEmpty(AQI.Hist.PredictionsJSON)) { System.Drawing.Rectangle rect; System.Drawing.SizeF size; Brush rectBrush; Color boxColor; List <ClsPrediction> predictions = new List <ClsPrediction>(); predictions = Global.SetJSONString <List <ClsPrediction> >(AQI.Hist.PredictionsJSON); foreach (var pred in predictions) { bool Merge = false; if (AppSettings.Settings.HistoryOnlyDisplayRelevantObjects && pred.Result == ResultType.Relevant) { Merge = true; } else if (!AppSettings.Settings.HistoryOnlyDisplayRelevantObjects) { Merge = true; } if (Merge) { lasttext = pred.ToString(); //lasttext = $"{cam.last_detections[i]} {String.Format(AppSettings.Settings.DisplayPercentageFormat, AQI.cam.last_confidences[i] * 100)}"; double xmin = pred.XMin + AQI.cam.XOffset; double ymin = pred.YMin + AQI.cam.YOffset; double xmax = pred.XMax; double ymax = pred.YMax; if (AQI.cam.telegram_mask_enabled && !bSendTelegramMessage) { bSendTelegramMessage ^= this.TelegramOutsideMask(AQI.cam.Name, xmin, xmax, ymin, ymax, img.Width, img.Height); } int penSize = 2; if (img.Height > 1200) { penSize = 4; } else if (img.Height >= 800 && img.Height <= 1200) { penSize = 3; } boxColor = Color.FromArgb(150, this.GetBoxColor(AQI.cam.last_detections[countr])); rect = new System.Drawing.Rectangle(xmin.ToInt(), ymin.ToInt(), xmax.ToInt() - xmin.ToInt(), ymax.ToInt() - ymin.ToInt()); using (Pen pen = new Pen(boxColor, penSize)) { g.DrawRectangle(pen, rect); } //draw rectangle // Text Color Brush textColor = (boxColor.GetBrightness() > 0.5 ? Brushes.Black : Brushes.White); float fontSize = AppSettings.Settings.RectDetectionTextSize * ((float)img.Height / 1080); // Scale for image sizes if (fontSize < 8) { fontSize = 8; } Font textFont = new Font(AppSettings.Settings.RectDetectionTextFont, fontSize); //object name text below rectangle rect = new System.Drawing.Rectangle(xmin.ToInt() - 1, ymax.ToInt(), (int)img.Width, (int)img.Height); //sets bounding box for drawn text rectBrush = new SolidBrush(boxColor); //sets background rectangle color size = g.MeasureString(lasttext, textFont); //finds size of text to draw the background rectangle g.FillRectangle(rectBrush, xmin.ToInt() - 1, ymax.ToInt(), size.Width, size.Height); //draw background rectangle for detection text g.DrawString(lasttext, textFont, textColor, rect); //draw detection text g.Flush(); countr++; } } } else { //Use the old way -this code really doesnt need to be here but leaving just to make sure detections = AQI.cam.last_detections_summary; if (string.IsNullOrEmpty(detections)) { detections = ""; } string label = Global.GetWordBetween(detections, "", ":"); if (label.Contains("irrelevant") || label.Contains("confidence") || label.Contains("masked") || label.Contains("errors")) { detections = detections.Split(':')[1]; //removes the "1x masked, 3x irrelevant:" before the actual detection, otherwise this would be displayed in the detection tags if (label.Contains("masked")) { color = System.Drawing.Color.FromArgb(AppSettings.Settings.RectMaskedColorAlpha, AppSettings.Settings.RectMaskedColor); } else { color = System.Drawing.Color.FromArgb(AppSettings.Settings.RectIrrelevantColorAlpha, AppSettings.Settings.RectIrrelevantColor); } } else { color = System.Drawing.Color.FromArgb(AppSettings.Settings.RectRelevantColorAlpha, AppSettings.Settings.RectRelevantColor); } //List<string> detectlist = Global.Split(detections, "|;"); countr = AQI.cam.last_detections.Count(); //display a rectangle around each relevant object for (int i = 0; i < countr; i++) { //({ Math.Round((user.confidence * 100), 2).ToString() }%) lasttext = $"{AQI.cam.last_detections[i]} {String.Format(AppSettings.Settings.DisplayPercentageFormat, AQI.cam.last_confidences[i])}"; lastposition = AQI.cam.last_positions[i]; //load 'xmin,ymin,xmax,ymax' from third column into a string //store xmin, ymin, xmax, ymax in separate variables Int32.TryParse(lastposition.Split(',')[0], out int xmin); Int32.TryParse(lastposition.Split(',')[1], out int ymin); Int32.TryParse(lastposition.Split(',')[2], out int xmax); Int32.TryParse(lastposition.Split(',')[3], out int ymax); xmin = xmin + AQI.cam.XOffset; ymin = ymin + AQI.cam.YOffset; System.Drawing.Rectangle rect = new System.Drawing.Rectangle(xmin, ymin, xmax - xmin, ymax - ymin); using (Pen pen = new Pen(color, AppSettings.Settings.RectBorderWidth)) { g.DrawRectangle(pen, rect); //draw rectangle } //we need this since people can change the border width in the json file int halfbrd = AppSettings.Settings.RectBorderWidth / 2; //object name text below rectangle rect = new System.Drawing.Rectangle(xmin - halfbrd, ymax + halfbrd, img.Width, img.Height); //sets bounding box for drawn text Brush brush = new SolidBrush(color); //sets background rectangle color System.Drawing.SizeF size = g.MeasureString(lasttext, new Font(AppSettings.Settings.RectDetectionTextFont, AppSettings.Settings.RectDetectionTextSize)); //finds size of text to draw the background rectangle g.FillRectangle(brush, xmin - halfbrd, ymax + halfbrd, size.Width, size.Height); //draw grey background rectangle for detection text g.DrawString(lasttext, new Font(AppSettings.Settings.RectDetectionTextFont, AppSettings.Settings.RectDetectionTextSize), Brushes.Black, rect); //draw detection text g.Flush(); //Global.LogMessage($"...{i}, LastText='{lasttext}' - LastPosition='{lastposition}'"); } } if (countr > 0) { GraphicsState gs = g.Save(); ImageCodecInfo jpgEncoder = this.GetImageEncoder(ImageFormat.Jpeg); // Create an Encoder object based on the GUID // for the Quality parameter category. System.Drawing.Imaging.Encoder myEncoder = System.Drawing.Imaging.Encoder.Quality; // Create an EncoderParameters object. // An EncoderParameters object has an array of EncoderParameter // objects. In this case, there is only one // EncoderParameter object in the array. EncoderParameters myEncoderParameters = new EncoderParameters(1); EncoderParameter myEncoderParameter = new EncoderParameter(myEncoder, AQI.cam.Action_image_merge_jpegquality); //100=least compression, largest file size, best quality myEncoderParameters.Param[0] = myEncoderParameter; Global.WaitFileAccessResult result = new Global.WaitFileAccessResult(); result.Success = true; //assume true if (AQI.cam.Action_image_merge_detections_makecopy) { OutputImageFile = Path.Combine(Environment.GetEnvironmentVariable("TEMP"), Path.GetFileName(AQI.CurImg.image_path)); } else { OutputImageFile = AQI.CurImg.image_path; } if (System.IO.File.Exists(OutputImageFile)) { result = await Global.WaitForFileAccessAsync(OutputImageFile, FileAccess.ReadWrite, FileShare.ReadWrite); } if (result.Success) { img.Save(OutputImageFile, jpgEncoder, myEncoderParameters); if (AQI.cam.telegram_mask_enabled && bSendTelegramMessage) { string telegram_file = "temp\\" + Path.GetFileName(OutputImageFile).Insert((Path.GetFileName(OutputImageFile).Length - 4), "_telegram"); img.Save(telegram_file, jpgEncoder, myEncoderParameters); } Log($"Debug: Merged {countr} detections in {sw.ElapsedMilliseconds}ms into image {OutputImageFile}"); } else { Log($"Error: Could not gain access to write merged file {OutputImageFile}"); } } else { Log($"Debug: No detections to merge. Time={sw.ElapsedMilliseconds}ms, {OutputImageFile}"); } } } } else { Global.LogMessage("Error: could not find last image with detections: " + AQI.CurImg.image_path); } } catch (Exception ex) { Global.LogMessage($"Error: Detections='{detections}', LastText='{lasttext}', LastPostions='{lastposition}' - " + Global.ExMsg(ex)); } return(OutputImageFile); }