com.soviby.unity.ui.ugui-to.../Assets/Editor/Helper/ImageUtils.cs

472 lines
14 KiB
C#
Raw Normal View History

2024-12-10 10:14:38 +00:00
#if UNITY_EDITOR
2024-12-09 18:39:04 +00:00
using OpenCVForUnity.CoreModule;
using OpenCVForUnity.Features2dModule;
using OpenCVForUnity.ImgprocModule;
using OpenCVForUnity.Calib3dModule;
2024-12-10 10:14:38 +00:00
using OpenCVForUnity.UnityUtils;
2024-12-09 18:39:04 +00:00
using System;
using System.IO;
using System.Collections.Generic;
using UnityEngine;
using OpenCVForUnity.ImgcodecsModule;
using System.Threading.Tasks;
using UguiToolkit.Editor;
2024-12-10 10:14:38 +00:00
using System.Security.Cryptography;
using System.Text;
2024-12-09 18:39:04 +00:00
public static class ImageUtils
{
2024-12-10 10:14:38 +00:00
public static void SetDebugMode(bool debug)
{
Utils.setDebugMode(debug);
}
2024-12-09 18:39:04 +00:00
public static string FormatImgFilePath(string imgFilePath)
{
string projectPath = Directory.GetParent(Application.dataPath).FullName;
return Path.GetRelativePath(projectPath, imgFilePath).Replace("\\", "/");
}
2024-12-17 03:07:00 +00:00
public static void LoadPngImagesFromFolder(string folderPath, List<Mat> images, List<string> imagePaths, Func<string, bool> filterCallback)
2024-12-09 18:39:04 +00:00
{
2024-12-17 03:07:00 +00:00
foreach (string filePath in Directory.GetFiles(folderPath, "*.png"))
2024-12-09 18:39:04 +00:00
{
2024-12-17 03:07:00 +00:00
if (filterCallback(filePath)) continue;
Mat img = Imgcodecs.imread(filePath, Imgcodecs.IMREAD_UNCHANGED);
2024-12-09 18:39:04 +00:00
if (!img.empty())
{
images.Add(img);
2024-12-17 03:07:00 +00:00
imagePaths.Add(FormatImgFilePath(filePath));
}
}
}
#region SliceTexture
static List<int> hashListOfSlice;
static int ToHashCode(in Color color)
{
int a = Mathf.RoundToInt(color.a * 255);
if (a == 0) return 0;
int r = Mathf.RoundToInt(color.r * 255);
int g = Mathf.RoundToInt(color.g * 255);
int b = Mathf.RoundToInt(color.b * 255);
return (a << 24) + (r << 16) + (g << 8) + b;
}
static (int, int) CalcLine(List<int> hashList)
{
int start = 0, end = 0;
int tmpStart = 0, tmpEnd = 0;
int tmpHash = hashList[0];
for (int i = 0; i < hashList.Count; i++)
{
if (tmpHash == hashList[i])
{
tmpEnd = i;
}
else
{
if (end - start < tmpEnd - tmpStart)
{
start = tmpStart;
end = tmpEnd;
}
tmpStart = i;
tmpEnd = i;
tmpHash = hashList[i];
}
}
if (end - start < tmpEnd - tmpStart)
{
start = tmpStart;
end = tmpEnd;
}
return (start, end);
}
static List<int> CreateHashList(Mat image, in char axis)
{
if (hashListOfSlice == null) hashListOfSlice = new ();
var hashList = hashListOfSlice;
hashList.Clear();
if (axis == 'x')
{
for (int i = 0; i < image.cols(); i++)
{
int hash = 0;
for (int j = 0; j < image.rows(); j++)
{
double[] color = image.get(j, i);
hash += ToHashCode(new Color((float)color[2] / 255, (float)color[1] / 255, (float)color[0] / 255, (float)color[3] / 255));
}
hashList.Add(hash);
}
}
else
{
for (int j = 0; j < image.rows(); j++)
{
int hash = 0;
for (int i = 0; i < image.cols(); i++)
{
double[] color = image.get(j, i);
hash += ToHashCode(new Color((float)color[2] / 255, (float)color[1] / 255, (float)color[0] / 255, (float)color[3] / 255));
}
hashList.Add(hash);
2024-12-09 18:39:04 +00:00
}
}
2024-12-17 03:07:00 +00:00
return hashList;
}
public static void SliceTexture(string imagePath, string outputPath)
{
var mat = SliceTexture(imagePath);
Imgcodecs.imwrite(outputPath, mat, new MatOfInt(Imgcodecs.IMWRITE_PNG_COMPRESSION, 9));
2024-12-09 18:39:04 +00:00
}
2024-12-17 03:07:00 +00:00
public static Mat SliceTexture(string imagePath)
{
Mat image = Imgcodecs.imread(imagePath, Imgcodecs.IMREAD_UNCHANGED); // ʹ<><CAB9>OpenCV<43><56><EFBFBD><EFBFBD>ͼ<EFBFBD><CDBC>
int width = image.cols();
int height = image.rows();
List<int> hashListX = CreateHashList(image, 'x');
(int xStart, int xEnd) = CalcLine(hashListX);
List<int> hashListY = CreateHashList(image, 'y');
(int yStart, int yEnd) = CalcLine(hashListY);
int outputWidth = width - (xEnd - xStart);
int outputHeight = height - (yEnd - yStart);
var outputMat = new Mat(outputHeight, outputWidth, CvType.CV_8UC4);
for (int x = 0; x < outputWidth; x++)
{
int originalX = x < xStart ? x : x + (xEnd - xStart);
for (int y = 0; y < outputHeight; y++)
{
int originalY = y < yStart ? y : y + (yEnd - yStart);
double[] color = image.get(originalY, originalX);
outputMat.put(y, x, color);
}
}
return outputMat;
}
#endregion
2024-12-10 10:14:38 +00:00
static List<Task<(double?, (double, double)?)>> tasks;
static ObjectPool<RotationScaleDetector> detectorPool;
public static async Task ProcessFolderAsync(List<Mat> images, string targetFilePath,
double distanceDifference, Action<int, (double, (double, double), bool)> callback,
2024-12-17 03:07:00 +00:00
Action endCallback, bool isSliceTexture = false)
2024-12-09 18:39:04 +00:00
{
2024-12-10 10:14:38 +00:00
if (tasks == null) tasks = new (images.Count);
if (detectorPool == null) detectorPool = new (images.Count);
2024-12-17 03:07:00 +00:00
Mat targetImage = null;
if (isSliceTexture)
{
targetImage = SliceTexture(targetFilePath);
}
else {
targetImage = Imgcodecs.imread(targetFilePath, Imgcodecs.IMREAD_UNCHANGED);
}
2024-12-10 10:14:38 +00:00
List<RotationScaleDetector> detectors = new(images.Count);
tasks.Clear();
foreach (var img in images)
{
RotationScaleDetector detector = detectorPool.GetObject();
tasks.Add(detector.GetRotationScaleAsync(targetImage, img, distanceDifference));
detectors.Add(detector);
}
var resultsTask = await Task.WhenAll(tasks.ToArray());
foreach (var detector in detectors) detectorPool.ReturnObject(detector);
for (int index = 0; index < resultsTask.Length; index++)
2024-12-09 18:39:04 +00:00
{
int _index = index;
2024-12-10 10:14:38 +00:00
var result = resultsTask[index];
2024-12-09 18:39:04 +00:00
UnityMainThreadDispatcher.Instance().Enqueue(() =>
{
2024-12-10 10:14:38 +00:00
if (result.Item1.HasValue)
2024-12-09 18:39:04 +00:00
{
double rotationAngleDegrees = result.Item1.Value;
2024-12-10 10:14:38 +00:00
double scaleX = 0;
double scaleY = 0;
if (result.Item2.HasValue)
{
var scale = result.Item2.Value;
scaleX = scale.Item1;
scaleY = scale.Item2;
callback(_index, (rotationAngleDegrees, (scaleX, scaleY), false));
}
else
{ // SimilarityCalc
callback(_index, (rotationAngleDegrees, (scaleX, scaleY), true));
}
2024-12-09 18:39:04 +00:00
Debug.Log($"Target Image -> Image {_index}");
Debug.Log($"Rotation Angle: {rotationAngleDegrees} degrees");
Debug.Log($"Scale X: {scaleX}");
Debug.Log($"Scale Y: {scaleY}");
2024-12-10 10:14:38 +00:00
Debug.Log($"SimilarityCalc : {result.Item2 == null}");
2024-12-09 18:39:04 +00:00
}
});
}
UnityMainThreadDispatcher.Instance().Enqueue(() =>
{
endCallback();
});
}
}
public class RotationScaleDetector
{
private SIFT sift;
private BFMatcher bf;
private Mat gray;
private Mat descriptors1;
private Mat descriptors2;
private MatOfKeyPoint keypoints;
private Mat inliers;
private List<MatOfDMatch> knnMatches;
private List<DMatch> goodMatches;
private List<Point> srcPts;
private List<Point> dstPts;
2024-12-10 10:14:38 +00:00
private Mat resizedImage;
private Mat dctImage;
StringBuilder sb;
2024-12-09 18:39:04 +00:00
public RotationScaleDetector()
{
sift = SIFT.create();
bf = BFMatcher.create();
gray = new Mat();
descriptors1 = new Mat();
descriptors2 = new Mat();
keypoints = new MatOfKeyPoint();
inliers = new Mat();
knnMatches = new List<MatOfDMatch>();
goodMatches = new List<DMatch>();
srcPts = new List<Point>();
dstPts = new List<Point>();
2024-12-10 10:14:38 +00:00
resizedImage = new Mat();
dctImage = new Mat();
sb = new StringBuilder();
2024-12-09 18:39:04 +00:00
}
private KeyPoint[] Sift(Mat image, Mat descriptors)
{
Imgproc.cvtColor(image, gray, Imgproc.COLOR_BGR2GRAY);
sift.detectAndCompute(gray, new Mat(), keypoints, descriptors);
return keypoints.toArray();
}
2024-12-10 10:14:38 +00:00
#region MD5
public string CalculateMD5(Mat image)
2024-12-09 18:39:04 +00:00
{
2024-12-10 10:14:38 +00:00
// <20><>Matת<74><D7AA>Ϊ<EFBFBD>ֽ<EFBFBD><D6BD><EFBFBD><EFBFBD><EFBFBD>
byte[] imageBytes = new byte[image.total() * image.elemSize()];
image.get(0, 0, imageBytes);
2024-12-09 18:39:04 +00:00
2024-12-10 10:14:38 +00:00
// <20><><EFBFBD><EFBFBD>MD5<44><35>ϣ<EFBFBD>㷨ʵ<E3B7A8><CAB5>
using (MD5 md5 = MD5.Create())
2024-12-09 18:39:04 +00:00
{
2024-12-10 10:14:38 +00:00
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ϣֵ
byte[] hashBytes = md5.ComputeHash(imageBytes);
2024-12-09 18:39:04 +00:00
2024-12-10 10:14:38 +00:00
// <20><><EFBFBD><EFBFBD>ϣ<EFBFBD>ֽ<EFBFBD><D6BD><EFBFBD><EFBFBD><EFBFBD>ת<EFBFBD><D7AA>Ϊʮ<CEAA><CAAE><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ַ<EFBFBD><D6B7><EFBFBD>
sb.Clear();
foreach (byte b in hashBytes)
{
sb.Append(b.ToString("x2"));
}
2024-12-09 18:39:04 +00:00
2024-12-10 10:14:38 +00:00
return sb.ToString();
}
}
#endregion
#region Phash
private Mat CalculatePhash(Mat image)
{
Imgproc.cvtColor(image, gray, Imgproc.COLOR_BGR2GRAY);
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>СΪ32x32<33><32><EFBFBD><EFBFBD>ת<EFBFBD><D7AA>Ϊ32λ<32><CEBB><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
Imgproc.resize(gray, resizedImage, new Size(32, 32), 0, 0, Imgproc.INTER_AREA);
resizedImage.convertTo(resizedImage, CvType.CV_32F);
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ɢ<EFBFBD><C9A2><EFBFBD>ұ任<D2B1><E4BBBB>DCT<43><54>
Core.dct(resizedImage, dctImage);
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>С
if (dctImage.rows() < 8 || dctImage.cols() < 8)
{
Debug.LogError("DCT matrix is too small!");
return new Mat();
}
// ȡ<><C8A1><EFBFBD>Ͻ<EFBFBD>8x8<78><38>DCTϵ<54><CFB5>
Mat dctLowFreq = dctImage.submat(new OpenCVForUnity.CoreModule.Rect(0, 0, 8, 8));
// <20><>DCTϵ<54><CFB5>ת<EFBFBD><D7AA>Ϊ<EFBFBD><CEAA><EFBFBD><EFBFBD>
float[] dctArray = new float[64];
dctLowFreq.get(0, 0, dctArray);
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ֵ
float medianValue = GetMedian(dctArray);
// <20><><EFBFBD><EFBFBD>pHash
Mat phash = new Mat(dctLowFreq.size(), CvType.CV_8U);
for (int i = 0; i < dctLowFreq.rows(); i++)
2024-12-09 18:39:04 +00:00
{
2024-12-10 10:14:38 +00:00
for (int j = 0; j < dctLowFreq.cols(); j++)
2024-12-09 18:39:04 +00:00
{
2024-12-10 10:14:38 +00:00
phash.put(i, j, dctLowFreq.get(i, j)[0] > medianValue ? 1 : 0);
2024-12-09 18:39:04 +00:00
}
}
2024-12-10 10:14:38 +00:00
return phash;
}
2024-12-09 18:39:04 +00:00
2024-12-10 10:14:38 +00:00
private float GetMedian(float[] array)
{
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD><EFBFBD>
System.Array.Sort(array);
// <20><><EFBFBD><EFBFBD><EFBFBD><EFBFBD>ֵ
int middle = array.Length / 2;
if (array.Length % 2 == 0)
2024-12-09 18:39:04 +00:00
{
2024-12-10 10:14:38 +00:00
return (array[middle - 1] + array[middle]) / 2.0f;
2024-12-09 18:39:04 +00:00
}
2024-12-10 10:14:38 +00:00
else
2024-12-09 18:39:04 +00:00
{
2024-12-10 10:14:38 +00:00
return array[middle];
2024-12-09 18:39:04 +00:00
}
2024-12-10 10:14:38 +00:00
}
2024-12-09 18:39:04 +00:00
2024-12-10 10:14:38 +00:00
private bool IsPhashValid(Mat phash)
{
// <20><><EFBFBD><EFBFBD>pHash<73><68>Ϊ1<CEAA><31>λ<EFBFBD><CEBB>
return Core.countNonZero(phash) >= 13;
}
2024-12-09 18:39:04 +00:00
2024-12-10 10:14:38 +00:00
private int CalculateHammingDistance(Mat phash1, Mat phash2)
{
if (phash1.rows() != phash2.rows() || phash1.cols() != phash2.cols())
2024-12-09 18:39:04 +00:00
{
2024-12-10 10:14:38 +00:00
Debug.LogError("pHash sizes do not match!");
return -1;
2024-12-09 18:39:04 +00:00
}
2024-12-10 10:14:38 +00:00
int hammingDistance = 0;
for (int i = 0; i < phash1.rows(); i++)
{
for (int j = 0; j < phash1.cols(); j++)
{
if (phash1.get(i, j)[0] != phash2.get(i, j)[0])
{
hammingDistance++;
}
}
}
2024-12-09 18:39:04 +00:00
2024-12-10 10:14:38 +00:00
return hammingDistance;
}
#endregion
public async Task<(double?, (double, double)?)> GetRotationScaleAsync(Mat img0, Mat img1, double distanceDifference)
{
return await Task.Run<(double?, (double, double)?)>(() => {
try
{
string md5Hash0 = CalculateMD5(img0);
string md5Hash1 = CalculateMD5(img1);
if (md5Hash0 == md5Hash1)
{
return (0, null);
}
Mat phash1 = CalculatePhash(img0);
Mat phash2 = CalculatePhash(img1);
if (IsPhashValid(phash1) && IsPhashValid(phash2) && CalculateHammingDistance(phash1, phash2) < 10)
{
return (0, null);
}
KeyPoint[] kp1 = Sift(img0, descriptors1);
KeyPoint[] kp2 = Sift(img1, descriptors2);
if (kp1.Length == 0 || kp2.Length == 0)
{
return (null, null);
}
knnMatches.Clear();
goodMatches.Clear();
bf.knnMatch(descriptors1, descriptors2, knnMatches, 2);
foreach (MatOfDMatch matofDMatch in knnMatches)
{
DMatch[] matches = matofDMatch.toArray();
if (matches[0].distance < distanceDifference * matches[1].distance)
{
goodMatches.Add(matches[0]);
}
}
if (goodMatches.Count < 3)
{
return (null, null);
}
srcPts.Clear();
dstPts.Clear();
foreach (DMatch match in goodMatches)
{
srcPts.Add(kp1[match.queryIdx].pt);
dstPts.Add(kp2[match.trainIdx].pt);
}
MatOfPoint2f srcMatOfPoint2f = new MatOfPoint2f(srcPts.ToArray());
MatOfPoint2f dstMatOfPoint2f = new MatOfPoint2f(dstPts.ToArray());
2024-12-09 18:39:04 +00:00
2024-12-10 10:14:38 +00:00
Mat M = Calib3d.estimateAffinePartial2D(srcMatOfPoint2f, dstMatOfPoint2f, inliers, Calib3d.RANSAC, 5);
if (M.empty())
{
return (null, null);
}
Mat R = M.colRange(0, 2);
double theta = Math.Atan2(R.get(1, 0)[0], R.get(0, 0)[0]);
double rotationAngleDegrees = theta * 180.0 / Math.PI;
double scaleX = Core.norm(R.row(0));
double scaleY = Core.norm(R.row(1));
return (rotationAngleDegrees, (scaleX, scaleY));
}
catch (Exception e)
{
Debug.LogException(e);
return (null, null);
}
});
2024-12-09 18:39:04 +00:00
}
}
2024-12-10 10:14:38 +00:00
#endif