498 lines
16 KiB
C#
498 lines
16 KiB
C#
// Copyright Epic Games, Inc. All Rights Reserved.
|
|
|
|
using System;
|
|
using System.Collections.Generic;
|
|
using System.Linq;
|
|
using System.Text;
|
|
using System.Threading.Tasks;
|
|
using System.Drawing; // Color
|
|
using System.IO; // Directory.
|
|
using System.Diagnostics; // Debug.
|
|
using System.Drawing.Imaging; // ImageData
|
|
|
|
namespace ImageValidator
|
|
{
|
|
// per folder, later stored in xml file
|
|
public class ImageValidatorSettings
|
|
{
|
|
public string TestDir;
|
|
public string RefDir;
|
|
public uint Threshold;
|
|
public uint PixelCountToFail;
|
|
|
|
public ImageValidatorSettings()
|
|
{
|
|
Threshold = 16;
|
|
PixelCountToFail = 32;
|
|
}
|
|
|
|
public static string GetVersionString()
|
|
{
|
|
return "V1.0";
|
|
}
|
|
};
|
|
|
|
public struct ImageValidatorIntermediate
|
|
{
|
|
public struct PixelElement
|
|
{
|
|
// blue, green, red, alpha
|
|
public byte b, g, r, a;
|
|
|
|
public void SetErrorColor(float squaredError)
|
|
{
|
|
// opaque
|
|
a = 0xff;
|
|
|
|
r = 0xff;
|
|
g = 0;
|
|
b = 0;
|
|
|
|
g = (byte)(Math.Min(0xff, squaredError / 8));
|
|
b = (byte)(Math.Min(0xff, squaredError / 64));
|
|
}
|
|
|
|
/*
|
|
public void SetErrorColor(float squaredError)
|
|
{
|
|
a = 0xff;
|
|
|
|
if (squaredError > 0.0f)
|
|
{
|
|
// no error is black, minor error is a noticeable color
|
|
squaredError += 800;
|
|
}
|
|
squaredError /= 8;
|
|
|
|
r = (byte)(Math.Min(0xff, squaredError));
|
|
g = (byte)(Math.Min(0xff, squaredError / 8));
|
|
b = (byte)(Math.Min(0xff, squaredError / 64));
|
|
}
|
|
*/
|
|
|
|
public static float ComputeSquaredError(PixelElement Test, PixelElement Ref)
|
|
{
|
|
float R = (float)Test.r - (float)Ref.r;
|
|
float G = (float)Test.g - (float)Ref.g;
|
|
float B = (float)Test.b - (float)Ref.b;
|
|
|
|
return R * R + G * G + B * B;
|
|
}
|
|
public static uint ComputeAbsDiff(PixelElement Test, PixelElement Ref)
|
|
{
|
|
int R = Math.Abs((int)Test.r - (int)Ref.r);
|
|
int G = Math.Abs((int)Test.g - (int)Ref.g);
|
|
int B = Math.Abs((int)Test.b - (int)Ref.b);
|
|
|
|
return (uint)Math.Max(R, Math.Max(G, B));
|
|
}
|
|
};
|
|
|
|
public Bitmap imageTest;
|
|
public Bitmap imageDiff;
|
|
public Bitmap imageRef;
|
|
|
|
private Bitmap LoadBitmap(string filename)
|
|
{
|
|
Image tmpImage;
|
|
Bitmap Ret = null;
|
|
|
|
try
|
|
{
|
|
// http://stackoverflow.com/questions/788335/why-does-image-fromfile-keep-a-file-handle-open-sometimes
|
|
// load without keeping the file locked
|
|
|
|
using (var fs = new FileStream(filename, FileMode.Open, FileAccess.Read))
|
|
{
|
|
tmpImage = Image.FromStream(fs);
|
|
Ret = new Bitmap(tmpImage);
|
|
tmpImage.Dispose();
|
|
}
|
|
}
|
|
catch (System.IO.DirectoryNotFoundException)
|
|
{
|
|
// no need to handle this
|
|
}
|
|
catch (System.IO.FileNotFoundException)
|
|
{
|
|
// no need to handle this
|
|
}
|
|
|
|
return Ret;
|
|
}
|
|
|
|
public void Process(ImageValidatorSettings settings, ImageValidatorData.ImageEntry imageEntry)
|
|
{
|
|
if (imageTest != null) { imageTest.Dispose(); imageTest = null; }
|
|
if (imageRef != null) { imageRef.Dispose(); imageRef = null; }
|
|
|
|
imageTest = LoadBitmap(settings.TestDir + imageEntry.Name);
|
|
imageRef = LoadBitmap(settings.RefDir + imageEntry.Name);
|
|
|
|
imageEntry.testResult = ComputeDiff(settings.Threshold);
|
|
}
|
|
|
|
public static System.Drawing.Size GetImageSize(Bitmap bitmap)
|
|
{
|
|
GraphicsUnit Unit = GraphicsUnit.Pixel;
|
|
|
|
System.Drawing.Size Ret = new System.Drawing.Size(1, 1);
|
|
|
|
if (bitmap != null)
|
|
{
|
|
RectangleF bounds = bitmap.GetBounds(ref Unit);
|
|
|
|
Ret.Width = (int)bounds.Width;
|
|
Ret.Height = (int)bounds.Height;
|
|
}
|
|
|
|
return Ret;
|
|
}
|
|
|
|
public System.Drawing.Size GetImagesSize()
|
|
{
|
|
System.Drawing.Size sizeTest = GetImageSize(imageTest);
|
|
System.Drawing.Size sizeRef = GetImageSize(imageRef);
|
|
|
|
return new System.Drawing.Size(
|
|
Math.Max(sizeTest.Width, sizeRef.Width),
|
|
Math.Max(sizeTest.Height, sizeRef.Height)
|
|
);
|
|
}
|
|
|
|
public static Bitmap resizeImage(Image image)
|
|
{
|
|
// uint MaxSize = 64;
|
|
uint MaxSize = 128;
|
|
|
|
float Scale = MaxSize / (float)Math.Max(image.Width, image.Height);
|
|
|
|
int new_width = Math.Max(1, (int)(image.Width * Scale));
|
|
int new_height = Math.Max(1, (int)(image.Height * Scale));
|
|
|
|
// see http://base64image.org/
|
|
|
|
Bitmap new_image = new Bitmap(new_width, new_height);
|
|
Graphics g = Graphics.FromImage((Image)new_image);
|
|
//g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.High; // creates alpha channel from border pixels - doesn't look good for the Report
|
|
g.InterpolationMode = System.Drawing.Drawing2D.InterpolationMode.Low;
|
|
g.DrawImage(image, 0, 0, new_width, new_height);
|
|
|
|
return new_image;
|
|
}
|
|
|
|
// compute imageDiff from imageTest and imageRef
|
|
public TestResult ComputeDiff(uint Threshold)
|
|
{
|
|
if (imageDiff != null)
|
|
{
|
|
imageDiff.Dispose();
|
|
imageDiff = null;
|
|
}
|
|
|
|
TestResult Ret = new TestResult();
|
|
if (imageTest == null)
|
|
{
|
|
Ret.ErrorText = "missing Test";
|
|
return Ret;
|
|
}
|
|
if (imageRef == null)
|
|
{
|
|
Ret.ErrorText = "missing Ref";
|
|
return Ret;
|
|
}
|
|
|
|
System.Drawing.Size sizeTest = GetImageSize(imageTest);
|
|
System.Drawing.Size sizeRef = GetImageSize(imageRef);
|
|
|
|
if (sizeTest != sizeRef)
|
|
{
|
|
Ret.ErrorText = "Size " +
|
|
sizeTest.Width.ToString() + "x" + sizeTest.Height.ToString() +
|
|
" != " +
|
|
sizeRef.Width.ToString() + "x" + sizeRef.Height.ToString();
|
|
return Ret;
|
|
}
|
|
|
|
System.Drawing.Size size = GetImagesSize();
|
|
|
|
// todo: exit if Size or format is not the same
|
|
Debug.Assert(imageTest.PixelFormat == PixelFormat.Format32bppArgb);
|
|
|
|
imageDiff = new Bitmap(size.Width, size.Height);
|
|
|
|
uint CountedErrorPixels = 0;
|
|
|
|
BitmapData dataTest = imageTest.LockBits(new Rectangle(0, 0, size.Width, size.Height), ImageLockMode.ReadOnly, imageTest.PixelFormat);
|
|
BitmapData dataDiff = imageDiff.LockBits(new Rectangle(0, 0, size.Width, size.Height), ImageLockMode.WriteOnly, imageTest.PixelFormat);
|
|
BitmapData dataRef = imageRef.LockBits(new Rectangle(0, 0, size.Width, size.Height), ImageLockMode.ReadOnly, imageTest.PixelFormat);
|
|
unsafe
|
|
{
|
|
for (int x = 0; x < size.Width; ++x)
|
|
{
|
|
for (int y = 0; y < size.Height; ++y)
|
|
{
|
|
PixelElement* valueTest = (PixelElement*)((byte*)dataTest.Scan0.ToPointer() + dataTest.Stride * y + x * sizeof(PixelElement));
|
|
PixelElement* valueDiff = (PixelElement*)((byte*)dataDiff.Scan0.ToPointer() + dataDiff.Stride * y + x * sizeof(PixelElement));
|
|
PixelElement* valueRef = (PixelElement*)((byte*)dataRef.Scan0.ToPointer() + dataRef.Stride * y + x * sizeof(PixelElement));
|
|
|
|
// float Diff = PixelElement.ComputeSquaredError(*valueDiff, *valueRef);
|
|
uint localError = PixelElement.ComputeAbsDiff(*valueTest, *valueRef);
|
|
|
|
// all pixels opaque
|
|
valueDiff->a = 0xff;
|
|
|
|
if (localError >= Threshold)
|
|
{
|
|
++CountedErrorPixels;
|
|
// valueDiff->SetErrorColor(PixelElement.ComputeSquaredError(*valueTest, *valueRef));
|
|
valueDiff->SetErrorColor(localError);
|
|
// *valueDiff = *valueRef;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
imageRef.UnlockBits(dataRef);
|
|
imageDiff.UnlockBits(dataDiff);
|
|
imageTest.UnlockBits(dataTest);
|
|
|
|
Ret.ErrorPixels = CountedErrorPixels;
|
|
|
|
// update Thumbnails
|
|
{
|
|
Ret.ThumbnailTest = resizeImage(imageTest);
|
|
Ret.ThumbnailDiff = resizeImage(imageDiff);
|
|
Ret.ThumbnailRef = resizeImage(imageRef);
|
|
}
|
|
|
|
|
|
|
|
return Ret;
|
|
}
|
|
};
|
|
|
|
public class TestResult
|
|
{
|
|
public static Color GetColor(bool bPassed)
|
|
{
|
|
return bPassed ? Color.FromArgb(0x88ff88) : Color.FromArgb(0xff8888);
|
|
}
|
|
|
|
public Color GetColor(ref ImageValidatorSettings settings)
|
|
{
|
|
return GetColor(IsPassed(ref settings));
|
|
}
|
|
|
|
public bool IsPassed(ref ImageValidatorSettings settings)
|
|
{
|
|
return ErrorText == null && ErrorPixels < settings.PixelCountToFail;
|
|
}
|
|
|
|
public string GetString(ref ImageValidatorSettings settings)
|
|
{
|
|
if (ErrorText != null)
|
|
{
|
|
return ErrorText;
|
|
}
|
|
else
|
|
{
|
|
string Ret = IsPassed(ref settings) ? "Passed" : "Failed";
|
|
|
|
Ret += " (" + ErrorPixels.ToString() + ")";
|
|
|
|
return Ret;
|
|
}
|
|
}
|
|
|
|
//
|
|
public string ErrorText;
|
|
// only used if ErrorText is null
|
|
public uint ErrorPixels;
|
|
|
|
public Bitmap ThumbnailTest;
|
|
public Bitmap ThumbnailDiff;
|
|
public Bitmap ThumbnailRef;
|
|
};
|
|
|
|
public struct ImageValidatorData
|
|
{
|
|
public struct ImageEntryColumnData
|
|
{
|
|
public string Platform;
|
|
public string Map;
|
|
public string Time;
|
|
public string Actor;
|
|
|
|
public ImageEntryColumnData(string Name)
|
|
{
|
|
Platform = Path.GetDirectoryName(Name);
|
|
if (Platform.StartsWith(@"\"))
|
|
{
|
|
Platform = Platform.Substring(1);
|
|
}
|
|
|
|
string FileNameWithoutExtension = Path.GetFileNameWithoutExtension(Name);
|
|
string[] keyValuePairs = FileNameWithoutExtension.Split(' ');
|
|
|
|
Map = "";
|
|
Time = "";
|
|
Actor = "";
|
|
|
|
foreach (string keyvalue in keyValuePairs)
|
|
{
|
|
string trimKeyValue = keyvalue;
|
|
|
|
if (trimKeyValue.EndsWith(")"))
|
|
{
|
|
trimKeyValue = trimKeyValue.Substring(0, trimKeyValue.Length - 1);
|
|
}
|
|
|
|
if (trimKeyValue.StartsWith("Map("))
|
|
{
|
|
Map = trimKeyValue.Substring(4);
|
|
}
|
|
if (trimKeyValue.StartsWith("Time("))
|
|
{
|
|
Time = trimKeyValue.Substring(5);
|
|
}
|
|
if (trimKeyValue.StartsWith("Actor("))
|
|
{
|
|
Actor = trimKeyValue.Substring(6);
|
|
}
|
|
}
|
|
|
|
// clernup some legacy nameing convention
|
|
if (Time.EndsWith("s"))
|
|
{
|
|
Time = Time.Substring(0, Time.Length - 1);
|
|
}
|
|
}
|
|
}
|
|
|
|
public class ImageEntry : IComparable
|
|
{
|
|
public ImageEntry()
|
|
{
|
|
bRefExists = false;
|
|
bTestExists = false;
|
|
}
|
|
|
|
// key, without front path
|
|
public string Name;
|
|
//
|
|
public bool bRefExists;
|
|
//
|
|
public bool bTestExists;
|
|
// can be null
|
|
public TestResult testResult;
|
|
|
|
public int CompareTo(object _rhs)
|
|
{
|
|
ImageEntry rhs = _rhs as ImageEntry;
|
|
|
|
if (rhs == null)
|
|
return 1;
|
|
|
|
ImageValidatorData.ImageEntryColumnData columnThis = new ImageValidatorData.ImageEntryColumnData(Name);
|
|
ImageValidatorData.ImageEntryColumnData columnRhs = new ImageValidatorData.ImageEntryColumnData(rhs.Name);
|
|
|
|
try
|
|
{
|
|
float timeThis = float.Parse(columnThis.Time);
|
|
float timeRhs = float.Parse(columnRhs.Time);
|
|
|
|
int x = timeThis.CompareTo(timeRhs);
|
|
|
|
if(x != 0)
|
|
return x;
|
|
}
|
|
catch (Exception)
|
|
{
|
|
// if time is not part of the name we cannot sort by it
|
|
}
|
|
|
|
return Name.CompareTo(rhs.Name);
|
|
}
|
|
};
|
|
|
|
|
|
// --------------------------------------------------------------------
|
|
|
|
public List<ImageEntry> imageEntries;
|
|
|
|
private void PopulatePartList(bool bRef, string path)
|
|
{
|
|
try
|
|
{
|
|
string[] files = Directory.GetFiles(path, "*.png", SearchOption.AllDirectories);
|
|
int pathlen = path.Length;
|
|
|
|
foreach (string it in files)
|
|
{
|
|
string substr = it.Substring(pathlen);
|
|
|
|
ImageEntry thisEntry = null;
|
|
|
|
foreach (ImageEntry entry in imageEntries)
|
|
{
|
|
if (entry.Name == substr)
|
|
{
|
|
thisEntry = entry;
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (thisEntry == null)
|
|
{
|
|
thisEntry = new ImageEntry();
|
|
thisEntry.Name = substr;
|
|
imageEntries.Add(thisEntry);
|
|
}
|
|
|
|
if (bRef)
|
|
{
|
|
thisEntry.bRefExists = true;
|
|
}
|
|
else
|
|
{
|
|
thisEntry.bTestExists = true;
|
|
}
|
|
}
|
|
}
|
|
catch (System.ArgumentException)
|
|
{
|
|
}
|
|
catch (System.IO.DirectoryNotFoundException)
|
|
{
|
|
}
|
|
catch (System.IO.FileNotFoundException)
|
|
{
|
|
}
|
|
}
|
|
|
|
// @retur if a element was processed
|
|
public bool ProcessOneElement(ImageValidatorSettings settings)
|
|
{
|
|
foreach (ImageEntry entry in imageEntries)
|
|
{
|
|
if (entry.testResult == null)
|
|
{
|
|
ImageValidatorIntermediate intermediate = new ImageValidatorIntermediate();
|
|
intermediate.Process(settings, entry);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
public void PopulateList(string TestDir, string RefDir)
|
|
{
|
|
imageEntries = new List<ImageEntry>();
|
|
PopulatePartList(false, TestDir);
|
|
PopulatePartList(true, RefDir);
|
|
imageEntries.Sort();
|
|
}
|
|
}
|
|
}
|