using OpenCvSharp; using System; using System.Collections.Concurrent; using System.IO; using System.Threading; using System.Threading.Tasks; using System.Windows; using System.Windows.Media.Imaging; namespace ILSOS_KS.Views.PopUP { public sealed class ImageProcessor { private static readonly Lazy _instance = new Lazy(() => new ImageProcessor()); public static ImageProcessor Instance => _instance.Value; private readonly ConcurrentQueue<(Mat mat, string filePath, int quality)> _saveQueue = new ConcurrentQueue<(Mat, string, int)>(); private readonly int _maxConcurrency = 4; private readonly SemaphoreSlim _semaphore; private int _totalToSave; private int _savedCount; private bool _progressVisible; private DateTime _lastProgressUpdate = DateTime.MinValue; private bool _isWorkerRunning; public BitmapSource ImagePreview { get; private set; } public bool IsLoading { get; private set; } public event Action ProgressChanged; // 0.0 ~ 1.0 public event Action ProgressBarVisibilityChanged; private ImageProcessor() { _semaphore = new SemaphoreSlim(_maxConcurrency); AsyncLog.I("[ImageProcessor] 초기화 완료"); } public void SaveImageFile(Mat matTop, string filePath = null, int jpegQuality = 95, int maxWidth = 1920, int maxHeight = 1080) { if (matTop == null || matTop.Empty()) return; IsLoading = true; string pathToSave = filePath ?? Path.Combine( "C:\\Temp", $"capture_{DateTime.Now:yyyyMMdd_HHmmssfff}.jpg" ); string directory = Path.GetDirectoryName(pathToSave); if (!Directory.Exists(directory)) Directory.CreateDirectory(directory); Mat matToSave = matTop; if (matTop.Width > maxWidth || matTop.Height > maxHeight) { double scale = Math.Min((double)maxWidth / matTop.Width, (double)maxHeight / matTop.Height); int newWidth = (int)(matTop.Width * scale); int newHeight = (int)(matTop.Height * scale); matToSave = new Mat(); Cv2.Resize(matTop, matToSave, new OpenCvSharp.Size(newWidth, newHeight)); } _saveQueue.Enqueue((matToSave, pathToSave, jpegQuality)); Interlocked.Increment(ref _totalToSave); ShowProgressBar(); SafeReportProgress(); if (!_isWorkerRunning) { _isWorkerRunning = true; Task.Run(() => SaveWorkerAsync()); } } private async Task SaveWorkerAsync() { try { while (!_saveQueue.IsEmpty) { if (_saveQueue.TryDequeue(out var item)) { try { await _semaphore.WaitAsync(); try { Cv2.ImWrite(item.filePath, item.mat, new ImageEncodingParam(ImwriteFlags.JpegQuality, item.quality)); AsyncLog.I($"[저장 완료] {item.filePath} (품질: {item.quality})"); } catch (Exception ex) { AsyncLog.I($"[이미지 저장 실패] {item.filePath}", ex); } finally { item.mat?.Dispose(); Interlocked.Increment(ref _savedCount); SafeReportProgress(); _semaphore.Release(); } } catch (Exception ex) { AsyncLog.I("[세마포어 처리 실패]", ex); } } else { await Task.Delay(20); } } } catch (Exception ex) { AsyncLog.I("[SaveWorkerAsync 전체 실패]", ex); } finally { _isWorkerRunning = false; _totalToSave = 0; _savedCount = 0; SafeHideProgressBar(); } } private void SafeReportProgress() { try { Application.Current.Dispatcher.BeginInvoke(new Action(() => { int total = _totalToSave; int saved = _savedCount; double progress = total > 0 ? Math.Min((double)saved / total, 1.0) : 0.0; if ((DateTime.Now - _lastProgressUpdate).TotalMilliseconds < 50) return; _lastProgressUpdate = DateTime.Now; ProgressChanged?.Invoke(progress); })); } catch (Exception ex) { AsyncLog.I("[ProgressChanged 호출 실패]", ex); } } private void ShowProgressBar() { if (_progressVisible) return; _progressVisible = true; try { Application.Current.Dispatcher.BeginInvoke(new Action(() => { ProgressBarVisibilityChanged?.Invoke(true); })); } catch (Exception ex) { AsyncLog.I("[ProgressBar 표시 실패]", ex); } } private void SafeHideProgressBar() { if (!_progressVisible) return; _progressVisible = false; try { Application.Current.Dispatcher.BeginInvoke(new Action(() => { ProgressBarVisibilityChanged?.Invoke(false); IsLoading = false; })); } catch (Exception ex) { AsyncLog.I("[ProgressBar 숨기기 실패]", ex); } } } public static class MatExtensions { public static BitmapSource ToBitmapSource(this Mat mat) { try { if (mat == null || mat.Empty()) return null; return OpenCvSharp.WpfExtensions.BitmapSourceConverter.ToBitmapSource(mat); } catch { return null; } } } public static class BitmapSourceExtensions { public static byte[] ToJpegBytes(this BitmapSource bitmapSource, int quality = 95) { if (bitmapSource == null) return null; JpegBitmapEncoder encoder = new JpegBitmapEncoder { QualityLevel = quality }; encoder.Frames.Add(BitmapFrame.Create(bitmapSource)); using (MemoryStream ms = new MemoryStream()) { encoder.Save(ms); return ms.ToArray(); } } public static MemoryStream ToJpegStream(this BitmapSource bitmapSource, int quality = 95) { if (bitmapSource == null) return null; JpegBitmapEncoder encoder = new JpegBitmapEncoder { QualityLevel = quality }; encoder.Frames.Add(BitmapFrame.Create(bitmapSource)); MemoryStream ms = new MemoryStream(); encoder.Save(ms); ms.Position = 0; return ms; } } }