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;
}
}
}