using System.Windows; using System.Windows.Forms; using System.Threading; using System.IO; using System.Drawing; using System.Drawing.Imaging; using System.Runtime.InteropServices; using System; using System.Collections.Concurrent; using System.Collections.Generic; using System.Linq; using System.Runtime.CompilerServices; using System.Threading.Tasks; using System.Windows.Threading; using System.Xml.Serialization; using Accord.Video.FFMPEG; using static System.Net.WebRequestMethods; using AI.Common.Crypto; namespace AutomaticallyCropImg { enum EnumResultImgType { OrigImgWithCropRect, CroppedImg, BothImgWithCropRectAndCroppedImg, } public class ImgInfoForSegEntity { /// /// 图片ID /// public string ImgId { get; set; } = string.Empty; /// /// 图片路径 /// public string ImgLocalPath { get; set; } = string.Empty; /// /// 裁切是否成功 /// public bool SegSucceed { get; set; } = false; /// /// 裁切后,图像左上点在原始图像上的像素坐标x /// public int Left { get; set; } = 0; /// /// 裁切后,图像右下点在原始图像上的像素坐标x /// public int Right { get; set; } = 0; /// /// 裁切后,图像左上点在原始图像上的像素坐标y /// public int Top { get; set; } = 0; /// /// 裁切后,图像右下点在原始图像上的像素坐标y /// public int Bottom { get; set; } = 0; } public class FileInfosForSeg { private string _origFilePath; private bool _isVideo; private string _origFolder; private string _randomId; public string OrigFilePath { get => _origFilePath; set => _origFilePath = value; } public bool IsVideo { get => _isVideo; set => _isVideo = value; } public string OrigFolder { get => _origFolder; set => _origFolder = value; } public string RandomId { get => _randomId; set => _randomId = value; } public FileInfosForSeg(string origFilePath, bool isVideo, string origFolder, string randomId) { _origFilePath = origFilePath; _isVideo = isVideo; _origFolder = origFolder; _randomId = randomId; } } /// /// MainWindow.xaml 的交互逻辑 /// public partial class MainWindow : Window { private volatile bool _processing = false; private string _dstFolder = null; private string _dstTxtFolder = null; private Dictionary _imageFiles = new Dictionary(); private Dictionary _videoFiles = new Dictionary(); private Dictionary _cropNameOriNameDict = new Dictionary(); private int _sameImgNum = 0; private static int _processedCount = 0; private static int _totalCount = 0; private ConcurrentQueue _processingFiles = new ConcurrentQueue(); private ConcurrentBag _cropResults = new ConcurrentBag(); private readonly int _threadCount = Math.Max(1, Environment.ProcessorCount / 2 - 1); private List _processThreads = new List(); private Thread _waitThread = null; private volatile bool _closing = false; private EnumResultImgType _resulttype; private bool _saveCropInfo; private bool _rename; private string _dataSource = ""; /// /// 调用CvCore.dll裁图 /// /// /// /// 左,上,宽,高 /// [DllImport(@"CvCropUltImgRegion.dll", CallingConvention = CallingConvention.Cdecl)] [return: MarshalAs(UnmanagedType.I1)] public static extern bool CropImage(IntPtr scrImgData, int[] imgInfoIn, int[] rectInfoOut); public MainWindow() { InitializeComponent(); } private void OnSelectFolderClick(object sender, RoutedEventArgs e) { FolderBrowserDialog srcdialog = new FolderBrowserDialog(); srcdialog.Description = "请选择待裁切图片所在文件夹"; if (srcdialog.ShowDialog() == System.Windows.Forms.DialogResult.OK) { if (string.IsNullOrEmpty(srcdialog.SelectedPath)) { System.Windows.MessageBox.Show("文件夹路径不能为空", "提示"); return; } if (_dstFolder == null) { FolderBrowserDialog dstdialog = new FolderBrowserDialog(); dstdialog.Description = "请选择保存裁切结果的文件夹"; if (dstdialog.ShowDialog() == System.Windows.Forms.DialogResult.OK) { if (string.IsNullOrEmpty(dstdialog.SelectedPath)) { System.Windows.MessageBox.Show("文件夹路径不能为空", "提示"); return; } _dstFolder = dstdialog.SelectedPath; } FolderBrowserDialog dstdialog2 = new FolderBrowserDialog(); dstdialog.Description = "为后续手动裁切能找到原图,请选择保存裁切结果图像名称和原图名称组成的 cropNameAndOriName.txt 的文件夹"; if (dstdialog.ShowDialog() == System.Windows.Forms.DialogResult.OK) { if (string.IsNullOrEmpty(dstdialog.SelectedPath)) { System.Windows.MessageBox.Show("文件夹路径不能为空", "提示"); return; } _dstTxtFolder = dstdialog.SelectedPath; } EnumResultImgType resulttype = new EnumResultImgType(); if ((RadioBtnResultCropped.IsChecked ?? false) && (RadioBtnResultCropRect.IsChecked ?? false)) { resulttype = EnumResultImgType.BothImgWithCropRectAndCroppedImg; } else if (RadioBtnResultCropped.IsChecked ?? false) { resulttype = EnumResultImgType.CroppedImg; } else if (RadioBtnResultCropRect.IsChecked ?? false) { resulttype = EnumResultImgType.OrigImgWithCropRect; } else { System.Windows.MessageBox.Show("保留裁图框位置信息 和 文件重命名 必须至少选择其中一项", "提示"); return; } _resulttype = resulttype; _saveCropInfo = CheckBoxSaveCropInfo.IsChecked != null && (bool)CheckBoxSaveCropInfo.IsChecked; _rename = CheckBoxRename.IsChecked != null && (bool)CheckBoxRename.IsChecked; } SearchFiles(srcdialog.SelectedPath); if (_imageFiles.Count > 0 || _videoFiles.Count > 0) { System.Windows.MessageBox.Show("共查询到待脱敏的视频:" + _videoFiles.Count + "个,图像:" + _imageFiles.Count + "张,点击确认后立即开始脱敏,请稍候...", "提示"); foreach (KeyValuePair kvp in _imageFiles) { string imageFile = kvp.Key; string randomId = kvp.Value; var fileInfo = new FileInfosForSeg(imageFile, false, srcdialog.SelectedPath, randomId); _processingFiles.Enqueue(fileInfo); } _totalCount += _imageFiles.Count; _imageFiles.Clear(); foreach (KeyValuePair kvp in _videoFiles) { string videoFile = kvp.Key; string randomId = kvp.Value; var fileInfo = new FileInfosForSeg(videoFile, true, srcdialog.SelectedPath, randomId); _processingFiles.Enqueue(fileInfo); } _totalCount += _videoFiles.Count; _videoFiles.Clear();; int threadCount = Math.Min(_threadCount, _processingFiles.Count); for (int ni = 0; ni < threadCount; ni++) { if (_processThreads.Count < ni+1) { var thread = new Thread(() => DoImgCrop()) { IsBackground = true, Name = "CropThread_"+ni.ToString(), }; _processThreads.Add(thread); thread.Start(); } else { var thread = _processThreads[ni]; if (thread == null || !thread.IsAlive) { thread = new Thread(() => DoImgCrop()) { IsBackground = true, Name = "CropThread_" + ni.ToString(), }; thread.Start(); } } } } } } private void setDataSourceTextChanged(object sender, System.Windows.Controls.TextChangedEventArgs e) { _dataSource = ((System.Windows.Controls.TextBox)sender).Text; } private byte[] ImageToByteArray(Bitmap bitmap) { using (MemoryStream ms = new MemoryStream()) { bitmap.Save(ms, ImageFormat.Jpeg); return ms.ToArray(); } } private void DoImgCrop() { _processing = true; while (!_closing && _processingFiles.Count > 0) { if (_processingFiles.TryDequeue(out var fileInfo)) { try { // 创建子文件夹 string origFolder = fileInfo.OrigFolder; string origFilePath = fileInfo.OrigFilePath; string fileName = Path.GetFileNameWithoutExtension(origFilePath); string extension = Path.GetExtension(origFilePath); string localSubFolder = Path.GetDirectoryName(origFilePath).Substring(origFolder.Length); if (localSubFolder != string.Empty) { localSubFolder = localSubFolder.Substring(1); string[] subFolders = localSubFolder.Split('\\'); string dstFolder = _dstFolder; foreach (var subFolder in subFolders) { if (subFolder != string.Empty) { dstFolder = Path.Combine(dstFolder, subFolder); if (!Directory.Exists(dstFolder)) { Directory.CreateDirectory(dstFolder); } } } } if (fileInfo.IsVideo) { var videoFileReader = new VideoFileReader(); videoFileReader.Open(origFilePath); var frameCount = videoFileReader.FrameCount; // 一个视频用一个固定的裁切框 // 为了避免一两幅图上裁切框效果不佳导致整个视频裁切的不好 // 先将视频的每一帧取出来,计算裁切框的位置 // 将每个裁切框的左上右下角位置排序,取中值,得到最终整个视频的裁切框位置 List cropRectLeft = new List(); List cropRectTop = new List(); List cropRectBottom = new List(); List cropRectRight = new List(); for (int ni = 0; ni < frameCount; ni+=5) { if (_closing) { break; } var img = videoFileReader.ReadVideoFrame(ni); if (CropImageWithOpenCVCppdll(img, out var rect)) { cropRectLeft.Add(rect.Left); cropRectTop.Add(rect.Top); cropRectRight.Add(rect.Right); cropRectBottom.Add(rect.Bottom); } img.Dispose(); } if (cropRectLeft.Count <= 0) { continue; } cropRectLeft.Sort(); cropRectTop.Sort(); cropRectRight.Sort(); cropRectBottom.Sort(); cropRectRight.Reverse(); cropRectBottom.Reverse(); // 考虑到希望尽量取一个稍微大一点的框,左上角尽量靠左上,右下角尽量靠右下,所以这里没有直接取最中间的结果 // 而是取的1/4处(没有取最左上和最右下的点,是为了避免极个别裁切的不好的结果被选用) int index = cropRectLeft.Count / 4; var left = cropRectLeft[index]; var top = cropRectTop[index]; var bottom = cropRectBottom[index]; var right = cropRectRight[index]; if (right <= left || bottom <= top) { continue; } var cropRectForWholeVideo = new Rectangle(left, top, right - left, bottom - top); string fileId; if (_rename) { fileId = fileInfo.RandomId.ToString(); } else { fileId = fileName; } for (int ni = 0; ni < frameCount; ni++) { if (_closing) { break; } var img = videoFileReader.ReadVideoFrame(ni); byte[] imageBytes = ImageToByteArray(img); string imgHashCode = HashCode.ComputeHashCode(imageBytes); string imgNewName = _dataSource + "_" + fileId + "_" + ni.ToString() + "_" + imgHashCode + ".jpg"; string imgLocalPath = Path.Combine(localSubFolder, imgNewName); _cropNameOriNameDict.Add(imgNewName, origFilePath + "_" + ni.ToString()); var segInfo = ProcessOneImage(img, _resulttype, _dstFolder, imgLocalPath, ImageFormat.Jpeg,cropRectForWholeVideo, true); _cropResults.Add(segInfo); img.Dispose(); GC.Collect(); GC.WaitForFullGCComplete(); } videoFileReader.Close(); videoFileReader.Dispose(); Interlocked.Increment(ref _processedCount); Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(() => { ProgressBar.Value = (int)((double)_processedCount / _totalCount * 100); })); } else { var img = new Bitmap(origFilePath); string fileId; if (_rename) { fileId = fileInfo.RandomId.ToString(); } else { fileId = fileName; } byte[] imageBytes = ImageToByteArray(img); string imgHashCode = HashCode.ComputeHashCode(imageBytes); string imgNewName = _dataSource + "_" + fileId + "_" + imgHashCode + extension; string imgLocalPath = Path.Combine(localSubFolder, imgNewName); try { _cropNameOriNameDict.Add(imgNewName, origFilePath); } catch { _sameImgNum++; } var segInfo = ProcessOneImage(img, _resulttype, _dstFolder, imgLocalPath, ImageFormat.Jpeg, Rectangle.Empty); _cropResults.Add(segInfo); img.Dispose(); GC.Collect(); GC.WaitForFullGCComplete(); Interlocked.Increment(ref _processedCount); Dispatcher.BeginInvoke(DispatcherPriority.Normal, new Action(() => { ProgressBar.Value = (int)((double)_processedCount / _totalCount * 100); })); } } catch (Exception e) { System.Windows.MessageBox.Show("出错了!"+e); while (_processingFiles.Count > 0) { _processingFiles.TryDequeue(out _); } } } Thread.Sleep(1); } if (_waitThread == null || !_waitThread.IsAlive) { _waitThread = new Thread(() => DoWait()) { IsBackground = true, }; _waitThread.Start(); } } private void SaveTxt(string txtPath) { using (StreamWriter writer = new StreamWriter(txtPath)) { foreach (KeyValuePair kvp in _cropNameOriNameDict) { writer.WriteLine($"{kvp.Key}: {kvp.Value}"); } } } private void DoWait() { while (true) { bool finish = true; for (int ni = 0; ni < _threadCount; ni++) { Thread thread = _processThreads.Find(b => b.Name == "CropThread_" + ni.ToString()); if (thread == null) { continue; } if (thread.IsAlive) { finish = false; break; } _processThreads.Remove(thread); } if (finish) { // 保存结果 if (_saveCropInfo) { var datas = _cropResults.ToList(); XmlSerializer xmls = new XmlSerializer(datas.GetType()); FileInfo fileinfo = new FileInfo(_dstFolder + "\\croppeddatas_"+DateTime.Now.ToString("yyyyMMdd_HHmmss") +".xml"); if (fileinfo.Exists) { fileinfo.Delete(); } using (Stream s = fileinfo.OpenWrite()) { xmls.Serialize(s, datas); } } System.Windows.MessageBox.Show("裁切完毕! 已过滤重复图像" + _sameImgNum.ToString() + "张!"); _processing = false; _processedCount = 0; _totalCount = 0; _cropResults = new ConcurrentBag(); if (_resulttype == EnumResultImgType.BothImgWithCropRectAndCroppedImg) { string croppedImgFolder = Path.Combine(_dstFolder, "裁切结果图"); string cropRectImgFolder = Path.Combine(_dstFolder, "带裁切框原图"); string txtPath = Path.Combine(croppedImgFolder, "cropNameAndOriName.txt"); SaveTxt(txtPath); txtPath = Path.Combine(cropRectImgFolder, "cropNameAndOriName.txt"); SaveTxt(txtPath); } else { string txtPath = Path.Combine(_dstTxtFolder, "cropNameAndOriName.txt"); SaveTxt(txtPath); } System.Windows.MessageBox.Show("cropNameAndOriName.txt 保存完毕!"); break; } else { Thread.Sleep(1); } } } public static bool CropImageWithOpenCVCppdll(Bitmap imagesrc, out Rectangle rectcrop) { if ((imagesrc.PixelFormat != PixelFormat.Format24bppRgb) && (imagesrc.PixelFormat != PixelFormat.Format32bppArgb) && (imagesrc.PixelFormat != PixelFormat.Format32bppPArgb) && (imagesrc.PixelFormat != PixelFormat.Format32bppRgb)) { rectcrop = Rectangle.Empty; return false; } BitmapData bmData; int imgWidth = imagesrc.Width; int imgHeight = imagesrc.Height; int channels = (imagesrc.PixelFormat == PixelFormat.Format24bppRgb) ? 3 : 4; Rectangle rect = new Rectangle(0, 0, imgWidth, imgHeight); bmData = imagesrc.LockBits(rect, ImageLockMode.ReadOnly, imagesrc.PixelFormat); int[] imginfo = new int[4]; imginfo[0] = imgWidth; imginfo[1] = imgHeight; imginfo[2] = bmData.Stride; imginfo[3] = channels; int[] rectinfo = new int[4]; if (!CropImage(bmData.Scan0, imginfo, rectinfo)) { imagesrc.UnlockBits(bmData); bmData = null; rectcrop = Rectangle.Empty; return false; } imagesrc.UnlockBits(bmData); bmData = null; int rectX = rectinfo[0]; int rectY = rectinfo[1]; int rectWidth = rectinfo[2]; int rectHeight = rectinfo[3]; if ((rectX < 0) || (rectY < 0) || (rectWidth <= 0) || (rectHeight <= 0) || (rectX + rectWidth > imgWidth) || (rectY + rectHeight > imgHeight)) { rectcrop = Rectangle.Empty; return false; } rectcrop = new Rectangle(rectX, rectY, rectWidth, rectHeight); return true; } public void SearchFiles(string folderName) { DirectoryInfo folder = new DirectoryInfo(folderName); string singleFolderRandomId = Guid.NewGuid().ToString(); foreach (FileInfo file in folder.GetFiles()) { if (file.Extension == ".png" || file.Extension == ".PNG" || file.Extension == ".jpg" || file.Extension == ".JPG" || file.Extension == ".jpeg" || file.Extension == ".JPEG" || file.Extension == ".bmp" || file.Extension == ".BMP") { _imageFiles.Add(file.FullName, singleFolderRandomId); } if (file.Extension == ".avi" || file.Extension == ".AVI" || file.Extension == ".mp4" || file.Extension == ".MP4") { // 对每个视频添加唯一的随机码,后续标注的时候就不会打乱 string singleVideorRandomId = Guid.NewGuid().ToString(); _videoFiles.Add(file.FullName, singleVideorRandomId); } } foreach (var subFolder in folder.GetDirectories()) { SearchFiles(subFolder.FullName); } } private ImgInfoForSegEntity ProcessOneImage(Bitmap img, EnumResultImgType resulttype, string dstimgfolder, string imgLocalPath, ImageFormat dstFormat, Rectangle rectIn, bool useInputRect = false) { string dstImgPath = Path.Combine(dstimgfolder,imgLocalPath); string imgid = Path.GetFileNameWithoutExtension(imgLocalPath); bool segSucceed = true; System.Drawing.Rectangle rect = System.Drawing.Rectangle.Empty; if (!useInputRect) { if (!CropImageWithOpenCVCppdll(img, out rect)) { img.Dispose(); segSucceed = false; } } else { rect = rectIn; } if (segSucceed) { Bitmap dstimg = null; if (resulttype == EnumResultImgType.CroppedImg) { dstimg = new Bitmap(rect.Width, rect.Height, img.PixelFormat); using (var g = Graphics.FromImage(dstimg)) { g.DrawImage(img, new System.Drawing.Rectangle(0, 0, rect.Width, rect.Height), new System.Drawing.Rectangle(rect.Left, rect.Top, rect.Width, rect.Height), GraphicsUnit.Pixel); g.Dispose(); } dstimg.Save(dstImgPath, dstFormat); dstimg.Dispose(); } if (resulttype == EnumResultImgType.OrigImgWithCropRect) { dstimg = img.Clone(new Rectangle(0, 0, img.Width, img.Height), img.PixelFormat); using (var g = Graphics.FromImage(dstimg)) { g.DrawRectangle(new System.Drawing.Pen(System.Drawing.Color.Yellow, 8), rect); g.Dispose(); } dstimg.Save(dstImgPath, dstFormat); dstimg.Dispose(); } if (resulttype == EnumResultImgType.BothImgWithCropRectAndCroppedImg) { Bitmap dstCroppedImg = new Bitmap(rect.Width, rect.Height, img.PixelFormat); Bitmap dstCropImgWithCropRect = img.Clone(new Rectangle(0, 0, img.Width, img.Height), img.PixelFormat); using (var g = Graphics.FromImage(dstCroppedImg)) { g.DrawImage(img, new System.Drawing.Rectangle(0, 0, rect.Width, rect.Height), new System.Drawing.Rectangle(rect.Left, rect.Top, rect.Width, rect.Height), GraphicsUnit.Pixel); g.Dispose(); } using (var g = Graphics.FromImage(dstCropImgWithCropRect)) { g.DrawRectangle(new System.Drawing.Pen(System.Drawing.Color.Yellow, 8), rect); g.Dispose(); } string croppedImgFolder = Path.Combine(dstimgfolder, "裁切结果图"); string cropRectImgFolder = Path.Combine(dstimgfolder, "带裁切框原图"); Directory.CreateDirectory(croppedImgFolder); Directory.CreateDirectory(cropRectImgFolder); dstCroppedImg.Save(Path.Combine(croppedImgFolder, imgLocalPath), dstFormat); dstCroppedImg.Dispose(); dstCropImgWithCropRect.Save(Path.Combine(cropRectImgFolder, imgLocalPath), dstFormat); dstCropImgWithCropRect.Dispose(); } } var segInfo = new ImgInfoForSegEntity { ImgId = imgid, ImgLocalPath = imgLocalPath, SegSucceed = segSucceed, Left = rect.Left, Right = rect.Right, Top = rect.Top, Bottom = rect.Bottom, }; return segInfo; } private void WindowClosing(object sender, System.ComponentModel.CancelEventArgs e) { _closing = true; while (_processing) { Thread.Sleep(1); } _closing = false; } } }