mirror of
https://github.com/wabbajack-tools/wabbajack.git
synced 2024-08-30 18:42:17 +00:00
Merge pull request #1531 from wabbajack-tools/use-textconv
Switch to using TextConv for image compression
This commit is contained in:
commit
b1f62fb6d4
@ -9,50 +9,59 @@ namespace Wabbajack.ImageHashing.Test
|
||||
[Fact]
|
||||
public async Task CanLoadAndCompareDDSImages()
|
||||
{
|
||||
var file1 = DDSImage.FromFile(AbsolutePath.EntryPoint.Combine("Resources", "test-dxt5.dds"));
|
||||
var hash1 = file1.PerceptionHash();
|
||||
var hash1 = await ImageState.GetPHash(AbsolutePath.EntryPoint.Combine("Resources", "test-dxt5.dds"));
|
||||
var state1 = await ImageState.GetState(AbsolutePath.EntryPoint.Combine("Resources", "test-dxt5.dds"));
|
||||
|
||||
Assert.Equal(512, state1.Width);
|
||||
Assert.Equal(512, state1.Height);
|
||||
Assert.Equal(DXGI_FORMAT.BC3_UNORM, state1.Format);
|
||||
|
||||
var hash2 = await ImageState.GetPHash(AbsolutePath.EntryPoint.Combine("Resources", "test-dxt5.dds"));
|
||||
|
||||
// From old embedded hashing method, we want to make sure the hashing algorithm hasn't changed so much that
|
||||
// we've broken the old caches
|
||||
var hash3 = PHash.FromBase64("cns+/2xel0ulcwCXeTlVW2x5aGtwaGl9glpthWZkb2ducnF0c2lvgQ==");
|
||||
|
||||
var file2 = DDSImage.FromFile(AbsolutePath.EntryPoint.Combine("Resources", "test-dxt5.dds"));
|
||||
var hash2 = file2.PerceptionHash();
|
||||
|
||||
Assert.Equal(1, hash1.Similarity(hash2));
|
||||
Assert.True(hash1.Similarity(hash3) > 0.99f);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CanLoadAndCompareResizedImage()
|
||||
{
|
||||
var file1 = DDSImage.FromFile(AbsolutePath.EntryPoint.Combine("Resources", "test-dxt5.dds"));
|
||||
var hash1 = file1.PerceptionHash();
|
||||
var hash1 = await ImageState.GetPHash(AbsolutePath.EntryPoint.Combine("Resources", "test-dxt5.dds"));
|
||||
|
||||
var file2 = DDSImage.FromFile(AbsolutePath.EntryPoint.Combine("Resources", "test-dxt5-small-bc7.dds"));
|
||||
var hash2 = file2.PerceptionHash();
|
||||
var hash2 = await ImageState.GetPHash(AbsolutePath.EntryPoint.Combine("Resources", "test-dxt5-small-bc7.dds"));
|
||||
|
||||
Assert.Equal(0.956666886806488, hash1.Similarity(hash2));
|
||||
var state2 = await ImageState.GetState(AbsolutePath.EntryPoint.Combine("Resources", "test-dxt5-small-bc7.dds"));
|
||||
|
||||
Assert.Equal(64, state2.Width);
|
||||
Assert.Equal(64, state2.Height);
|
||||
Assert.Equal(DXGI_FORMAT.BC7_UNORM_SRGB, state2.Format);
|
||||
|
||||
Assert.True(hash1.Similarity(hash2) >= 0.8811f);
|
||||
}
|
||||
|
||||
|
||||
[Fact]
|
||||
public async Task CanLoadAndCompareResizedVFlipImage()
|
||||
{
|
||||
var file1 = DDSImage.FromFile(AbsolutePath.EntryPoint.Combine("Resources", "test-dxt5.dds"));
|
||||
var hash1 = file1.PerceptionHash();
|
||||
var hash1 = await ImageState.GetPHash(AbsolutePath.EntryPoint.Combine("Resources", "test-dxt5.dds"));
|
||||
|
||||
var file2 = DDSImage.FromFile(AbsolutePath.EntryPoint.Combine("Resources", "test-dxt5-small-bc7-vflip.dds"));
|
||||
var hash2 = file2.PerceptionHash();
|
||||
var hash2 = await ImageState.GetPHash(AbsolutePath.EntryPoint.Combine("Resources", "test-dxt5-small-bc7-vflip.dds"));
|
||||
|
||||
Assert.Equal(0.2465425431728363, hash1.Similarity(hash2));
|
||||
Assert.True(hash1.Similarity(hash2) >= 0.1948f);
|
||||
}
|
||||
|
||||
[Fact]
|
||||
public async Task CanLoadAndCompareRecompressedImage()
|
||||
{
|
||||
var file1 = DDSImage.FromFile(AbsolutePath.EntryPoint.Combine("Resources", "test-dxt5.dds"));
|
||||
var hash1 = file1.PerceptionHash();
|
||||
var hash1 = await ImageState.GetPHash(AbsolutePath.EntryPoint.Combine("Resources", "test-dxt5.dds"));
|
||||
|
||||
var file2 = DDSImage.FromFile(AbsolutePath.EntryPoint.Combine("Resources", "test-dxt5-recompressed.dds"));
|
||||
var hash2 = file2.PerceptionHash();
|
||||
var hash2 = await ImageState.GetPHash(AbsolutePath.EntryPoint.Combine("Resources", "test-dxt5-recompressed.dds"));
|
||||
|
||||
Assert.Equal(0.9999724626541138, hash1.Similarity(hash2));
|
||||
Assert.True(hash1.Similarity(hash2) >= 0.92f);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -1,215 +0,0 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Numerics;
|
||||
using System.Runtime.CompilerServices;
|
||||
using System.Runtime.InteropServices;
|
||||
using System.Threading.Tasks;
|
||||
using DirectXTexNet;
|
||||
using Shipwreck.Phash;
|
||||
using Shipwreck.Phash.Imaging;
|
||||
using Wabbajack.Common;
|
||||
|
||||
namespace Wabbajack.ImageHashing
|
||||
{
|
||||
public class DDSImage : IDisposable
|
||||
{
|
||||
private static Lazy<DX11Device> DX11Device = new(() => new DX11Device());
|
||||
private DDSImage(ScratchImage img, TexMetadata metadata, Extension ext)
|
||||
{
|
||||
_image = img;
|
||||
_metaData = metadata;
|
||||
_extension = ext;
|
||||
}
|
||||
|
||||
private static Extension DDSExtension = new(".dds");
|
||||
private static Extension TGAExtension = new(".tga");
|
||||
private ScratchImage _image;
|
||||
private TexMetadata _metaData;
|
||||
|
||||
public static DDSImage FromFile(AbsolutePath file)
|
||||
{
|
||||
if (file.Extension != DDSExtension)
|
||||
throw new Exception("File does not end in DDS");
|
||||
|
||||
var img = TexHelper.Instance.LoadFromDDSFile(file.ToString(), DDS_FLAGS.NONE);
|
||||
|
||||
return new DDSImage(img, img.GetMetadata(), new Extension(".dds"));
|
||||
}
|
||||
|
||||
public static DDSImage FromDDSMemory(byte[] data)
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
fixed (byte* ptr = data)
|
||||
{
|
||||
var img = TexHelper.Instance.LoadFromDDSMemory((IntPtr)ptr, data.Length, DDS_FLAGS.NONE);
|
||||
return new DDSImage(img, img.GetMetadata(), new Extension(".dds"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public static DDSImage FromTGAMemory(byte[] data)
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
fixed (byte* ptr = data)
|
||||
{
|
||||
var img = TexHelper.Instance.LoadFromTGAMemory((IntPtr)ptr, data.Length);
|
||||
return new DDSImage(img, img.GetMetadata(), new Extension(".tga"));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static async Task<DDSImage> FromStream(Stream stream, IPath arg1Name)
|
||||
{
|
||||
var data = await stream.ReadAllAsync();
|
||||
if (arg1Name.FileName.Extension == DDSExtension)
|
||||
return FromDDSMemory(data);
|
||||
if (arg1Name.FileName.Extension == TGAExtension)
|
||||
return FromTGAMemory(data);
|
||||
throw new NotImplementedException("Only DDS and TGA files supported");
|
||||
}
|
||||
|
||||
public void Dispose()
|
||||
{
|
||||
if (!_image.IsDisposed)
|
||||
_image.Dispose();
|
||||
}
|
||||
|
||||
public int Width => _metaData.Width;
|
||||
public int Height => _metaData.Height;
|
||||
|
||||
// Destructively resize a Image
|
||||
public void ResizeRecompressAndSave(int width, int height, DXGI_FORMAT newFormat, AbsolutePath dest)
|
||||
{
|
||||
ScratchImage? resized = default;
|
||||
try
|
||||
{
|
||||
// First we resize the image, so that changes due to image scaling matter less in the final hash
|
||||
if (CompressedTypes.Contains(_metaData.Format))
|
||||
{
|
||||
using var decompressed = _image.Decompress(DXGI_FORMAT.UNKNOWN);
|
||||
resized = decompressed.Resize(width, height, TEX_FILTER_FLAGS.DEFAULT);
|
||||
}
|
||||
else
|
||||
{
|
||||
resized = _image.Resize(width, height, TEX_FILTER_FLAGS.DEFAULT);
|
||||
}
|
||||
|
||||
|
||||
if (CompressedTypes.Contains(newFormat))
|
||||
{
|
||||
var old = resized;
|
||||
resized = DX11Device.Value.Compress(resized, newFormat, TEX_COMPRESS_FLAGS.BC7_QUICK, 0.5f);
|
||||
old.Dispose();
|
||||
}
|
||||
|
||||
if (dest.Extension == new Extension(".dds"))
|
||||
{
|
||||
resized.SaveToDDSFile(DDS_FLAGS.NONE, dest.ToString());
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
resized?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
private static HashSet<DXGI_FORMAT> CompressedTypes = new HashSet<DXGI_FORMAT>()
|
||||
{
|
||||
DXGI_FORMAT.BC1_TYPELESS,
|
||||
DXGI_FORMAT.BC1_UNORM,
|
||||
DXGI_FORMAT.BC1_UNORM_SRGB,
|
||||
DXGI_FORMAT.BC2_TYPELESS,
|
||||
DXGI_FORMAT.BC2_UNORM,
|
||||
DXGI_FORMAT.BC2_UNORM_SRGB,
|
||||
DXGI_FORMAT.BC3_TYPELESS,
|
||||
DXGI_FORMAT.BC3_UNORM,
|
||||
DXGI_FORMAT.BC3_UNORM_SRGB,
|
||||
DXGI_FORMAT.BC4_TYPELESS,
|
||||
DXGI_FORMAT.BC4_UNORM,
|
||||
DXGI_FORMAT.BC4_SNORM,
|
||||
DXGI_FORMAT.BC5_TYPELESS,
|
||||
DXGI_FORMAT.BC5_UNORM,
|
||||
DXGI_FORMAT.BC5_SNORM,
|
||||
DXGI_FORMAT.BC6H_TYPELESS,
|
||||
DXGI_FORMAT.BC6H_UF16,
|
||||
DXGI_FORMAT.BC6H_SF16,
|
||||
DXGI_FORMAT.BC7_TYPELESS,
|
||||
DXGI_FORMAT.BC7_UNORM,
|
||||
DXGI_FORMAT.BC7_UNORM_SRGB,
|
||||
};
|
||||
|
||||
private Extension _extension;
|
||||
|
||||
public ImageState ImageState()
|
||||
{
|
||||
return new()
|
||||
{
|
||||
Width = _metaData.Width,
|
||||
Height = _metaData.Height,
|
||||
Format = _metaData.Format,
|
||||
PerceptualHash = PerceptionHash()
|
||||
};
|
||||
}
|
||||
|
||||
public PHash PerceptionHash()
|
||||
{
|
||||
ScratchImage? resized = default;
|
||||
try
|
||||
{
|
||||
// First we resize the image, so that changes due to image scaling matter less in the final hash
|
||||
if (CompressedTypes.Contains(_metaData.Format))
|
||||
{
|
||||
using var decompressed = _image.Decompress(DXGI_FORMAT.UNKNOWN);
|
||||
resized = decompressed.Resize(512, 512, TEX_FILTER_FLAGS.DEFAULT);
|
||||
}
|
||||
else
|
||||
{
|
||||
resized = _image.Resize(512, 512, TEX_FILTER_FLAGS.DEFAULT);
|
||||
}
|
||||
|
||||
var image = new byte[512 * 512];
|
||||
|
||||
unsafe void EvaluatePixels(IntPtr pixels, IntPtr width, IntPtr line)
|
||||
{
|
||||
float* ptr = (float*)pixels.ToPointer();
|
||||
|
||||
int widthV = width.ToInt32();
|
||||
if (widthV != 512) return;
|
||||
|
||||
var y = line.ToInt32();
|
||||
|
||||
for (int i = 0; i < widthV; i++)
|
||||
{
|
||||
var r = ptr[0] * 0.229f;
|
||||
var g = ptr[1] * 0.587f;
|
||||
var b = ptr[2] * 0.114f;
|
||||
|
||||
var combined = (r + g + b) * 255.0f;
|
||||
|
||||
image[(y * widthV) + i] = (byte)combined;
|
||||
ptr += 4;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
resized.EvaluateImage(EvaluatePixels);
|
||||
|
||||
var digest = ImagePhash.ComputeDigest(new ByteImage(512, 512, image));
|
||||
return PHash.FromDigest(digest);
|
||||
}
|
||||
finally
|
||||
{
|
||||
resized?.Dispose();
|
||||
}
|
||||
}
|
||||
|
||||
public void ResizeRecompressAndSave(ImageState state, AbsolutePath dest)
|
||||
{
|
||||
ResizeRecompressAndSave(state.Width, state.Height, state.Format, dest);
|
||||
}
|
||||
}
|
||||
}
|
@ -1,75 +0,0 @@
|
||||
using System;
|
||||
using System.Runtime.InteropServices;
|
||||
using DirectXTexNet;
|
||||
using Silk.NET.Core.Native;
|
||||
using Silk.NET.Direct3D11;
|
||||
|
||||
namespace Wabbajack.ImageHashing
|
||||
{
|
||||
public unsafe class DX11Device
|
||||
{
|
||||
private ID3D11Device* _device;
|
||||
|
||||
public DX11Device()
|
||||
{
|
||||
unsafe
|
||||
{
|
||||
var dxgi = Silk.NET.DXGI.DXGI.GetApi();
|
||||
var dx11 = Silk.NET.Direct3D11.D3D11.GetApi();
|
||||
|
||||
D3DFeatureLevel[] levels =
|
||||
{
|
||||
//D3DFeatureLevel.D3DFeatureLevel100, D3DFeatureLevel.D3DFeatureLevel101,
|
||||
D3DFeatureLevel.D3DFeatureLevel110
|
||||
};
|
||||
uint createDeviceFlags = 0;
|
||||
|
||||
var adapterIdx = 0;
|
||||
|
||||
D3DFeatureLevel fl;
|
||||
ID3D11Device* device;
|
||||
fixed (D3DFeatureLevel* lvls = levels)
|
||||
{
|
||||
var hr = dx11.CreateDevice(null, D3DDriverType.D3DDriverTypeHardware, IntPtr.Zero,
|
||||
createDeviceFlags, lvls,
|
||||
(uint)levels.Length,
|
||||
Silk.NET.Direct3D11.D3D11.SdkVersion, &device, &fl, null);
|
||||
|
||||
if (FAILED(hr))
|
||||
{
|
||||
_device = null;
|
||||
return;
|
||||
}
|
||||
|
||||
_device = device;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public ScratchImage Compress(ScratchImage input, DXGI_FORMAT format, TEX_COMPRESS_FLAGS compress,
|
||||
float threshold)
|
||||
{
|
||||
lock (this)
|
||||
{
|
||||
if (_device != null)
|
||||
{
|
||||
try
|
||||
{
|
||||
return input.Compress((IntPtr)_device, format, compress, threshold);
|
||||
}
|
||||
catch (COMException _)
|
||||
{
|
||||
_device->Release();
|
||||
_device = null;
|
||||
}
|
||||
}
|
||||
return input.Compress(format, compress, threshold);
|
||||
}
|
||||
}
|
||||
|
||||
private static bool FAILED(int x)
|
||||
{
|
||||
return x != 0;
|
||||
}
|
||||
}
|
||||
}
|
125
Wabbajack.ImageHashing/DXGI_FORMAT.cs
Normal file
125
Wabbajack.ImageHashing/DXGI_FORMAT.cs
Normal file
@ -0,0 +1,125 @@
|
||||
namespace Wabbajack.ImageHashing
|
||||
{
|
||||
public enum DXGI_FORMAT
|
||||
{
|
||||
UNKNOWN = 0,
|
||||
R32G32B32A32_TYPELESS = 1,
|
||||
R32G32B32A32_FLOAT = 2,
|
||||
R32G32B32A32_UINT = 3,
|
||||
R32G32B32A32_SINT = 4,
|
||||
R32G32B32_TYPELESS = 5,
|
||||
R32G32B32_FLOAT = 6,
|
||||
R32G32B32_UINT = 7,
|
||||
R32G32B32_SINT = 8,
|
||||
R16G16B16A16_TYPELESS = 9,
|
||||
R16G16B16A16_FLOAT = 10, // 0x0000000A
|
||||
R16G16B16A16_UNORM = 11, // 0x0000000B
|
||||
R16G16B16A16_UINT = 12, // 0x0000000C
|
||||
R16G16B16A16_SNORM = 13, // 0x0000000D
|
||||
R16G16B16A16_SINT = 14, // 0x0000000E
|
||||
R32G32_TYPELESS = 15, // 0x0000000F
|
||||
R32G32_FLOAT = 16, // 0x00000010
|
||||
R32G32_UINT = 17, // 0x00000011
|
||||
R32G32_SINT = 18, // 0x00000012
|
||||
R32G8X24_TYPELESS = 19, // 0x00000013
|
||||
D32_FLOAT_S8X24_UINT = 20, // 0x00000014
|
||||
R32_FLOAT_X8X24_TYPELESS = 21, // 0x00000015
|
||||
X32_TYPELESS_G8X24_UINT = 22, // 0x00000016
|
||||
R10G10B10A2_TYPELESS = 23, // 0x00000017
|
||||
R10G10B10A2_UNORM = 24, // 0x00000018
|
||||
R10G10B10A2_UINT = 25, // 0x00000019
|
||||
R11G11B10_FLOAT = 26, // 0x0000001A
|
||||
R8G8B8A8_TYPELESS = 27, // 0x0000001B
|
||||
R8G8B8A8_UNORM = 28, // 0x0000001C
|
||||
R8G8B8A8_UNORM_SRGB = 29, // 0x0000001D
|
||||
R8G8B8A8_UINT = 30, // 0x0000001E
|
||||
R8G8B8A8_SNORM = 31, // 0x0000001F
|
||||
R8G8B8A8_SINT = 32, // 0x00000020
|
||||
R16G16_TYPELESS = 33, // 0x00000021
|
||||
R16G16_FLOAT = 34, // 0x00000022
|
||||
R16G16_UNORM = 35, // 0x00000023
|
||||
R16G16_UINT = 36, // 0x00000024
|
||||
R16G16_SNORM = 37, // 0x00000025
|
||||
R16G16_SINT = 38, // 0x00000026
|
||||
R32_TYPELESS = 39, // 0x00000027
|
||||
D32_FLOAT = 40, // 0x00000028
|
||||
R32_FLOAT = 41, // 0x00000029
|
||||
R32_UINT = 42, // 0x0000002A
|
||||
R32_SINT = 43, // 0x0000002B
|
||||
R24G8_TYPELESS = 44, // 0x0000002C
|
||||
D24_UNORM_S8_UINT = 45, // 0x0000002D
|
||||
R24_UNORM_X8_TYPELESS = 46, // 0x0000002E
|
||||
X24_TYPELESS_G8_UINT = 47, // 0x0000002F
|
||||
R8G8_TYPELESS = 48, // 0x00000030
|
||||
R8G8_UNORM = 49, // 0x00000031
|
||||
R8G8_UINT = 50, // 0x00000032
|
||||
R8G8_SNORM = 51, // 0x00000033
|
||||
R8G8_SINT = 52, // 0x00000034
|
||||
R16_TYPELESS = 53, // 0x00000035
|
||||
R16_FLOAT = 54, // 0x00000036
|
||||
D16_UNORM = 55, // 0x00000037
|
||||
R16_UNORM = 56, // 0x00000038
|
||||
R16_UINT = 57, // 0x00000039
|
||||
R16_SNORM = 58, // 0x0000003A
|
||||
R16_SINT = 59, // 0x0000003B
|
||||
R8_TYPELESS = 60, // 0x0000003C
|
||||
R8_UNORM = 61, // 0x0000003D
|
||||
R8_UINT = 62, // 0x0000003E
|
||||
R8_SNORM = 63, // 0x0000003F
|
||||
R8_SINT = 64, // 0x00000040
|
||||
A8_UNORM = 65, // 0x00000041
|
||||
R1_UNORM = 66, // 0x00000042
|
||||
R9G9B9E5_SHAREDEXP = 67, // 0x00000043
|
||||
R8G8_B8G8_UNORM = 68, // 0x00000044
|
||||
G8R8_G8B8_UNORM = 69, // 0x00000045
|
||||
BC1_TYPELESS = 70, // 0x00000046
|
||||
BC1_UNORM = 71, // 0x00000047
|
||||
BC1_UNORM_SRGB = 72, // 0x00000048
|
||||
BC2_TYPELESS = 73, // 0x00000049
|
||||
BC2_UNORM = 74, // 0x0000004A
|
||||
BC2_UNORM_SRGB = 75, // 0x0000004B
|
||||
BC3_TYPELESS = 76, // 0x0000004C
|
||||
BC3_UNORM = 77, // 0x0000004D
|
||||
BC3_UNORM_SRGB = 78, // 0x0000004E
|
||||
BC4_TYPELESS = 79, // 0x0000004F
|
||||
BC4_UNORM = 80, // 0x00000050
|
||||
BC4_SNORM = 81, // 0x00000051
|
||||
BC5_TYPELESS = 82, // 0x00000052
|
||||
BC5_UNORM = 83, // 0x00000053
|
||||
BC5_SNORM = 84, // 0x00000054
|
||||
B5G6R5_UNORM = 85, // 0x00000055
|
||||
B5G5R5A1_UNORM = 86, // 0x00000056
|
||||
B8G8R8A8_UNORM = 87, // 0x00000057
|
||||
B8G8R8X8_UNORM = 88, // 0x00000058
|
||||
R10G10B10_XR_BIAS_A2_UNORM = 89, // 0x00000059
|
||||
B8G8R8A8_TYPELESS = 90, // 0x0000005A
|
||||
B8G8R8A8_UNORM_SRGB = 91, // 0x0000005B
|
||||
B8G8R8X8_TYPELESS = 92, // 0x0000005C
|
||||
B8G8R8X8_UNORM_SRGB = 93, // 0x0000005D
|
||||
BC6H_TYPELESS = 94, // 0x0000005E
|
||||
BC6H_UF16 = 95, // 0x0000005F
|
||||
BC6H_SF16 = 96, // 0x00000060
|
||||
BC7_TYPELESS = 97, // 0x00000061
|
||||
BC7_UNORM = 98, // 0x00000062
|
||||
BC7_UNORM_SRGB = 99, // 0x00000063
|
||||
AYUV = 100, // 0x00000064
|
||||
Y410 = 101, // 0x00000065
|
||||
Y416 = 102, // 0x00000066
|
||||
NV12 = 103, // 0x00000067
|
||||
P010 = 104, // 0x00000068
|
||||
P016 = 105, // 0x00000069
|
||||
OPAQUE_420 = 106, // 0x0000006A
|
||||
YUY2 = 107, // 0x0000006B
|
||||
Y210 = 108, // 0x0000006C
|
||||
Y216 = 109, // 0x0000006D
|
||||
NV11 = 110, // 0x0000006E
|
||||
AI44 = 111, // 0x0000006F
|
||||
IA44 = 112, // 0x00000070
|
||||
P8 = 113, // 0x00000071
|
||||
A8P8 = 114, // 0x00000072
|
||||
B4G4R4A4_UNORM = 115, // 0x00000073
|
||||
P208 = 130, // 0x00000082
|
||||
V208 = 131, // 0x00000083
|
||||
V408 = 132, // 0x00000084
|
||||
}
|
||||
}
|
@ -1,6 +0,0 @@
|
||||
using System;
|
||||
|
||||
namespace Wabbajack.ImageHashing
|
||||
{
|
||||
|
||||
}
|
@ -1,9 +1,15 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Drawing;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reactive.Linq;
|
||||
using System.Reactive.Subjects;
|
||||
using System.Threading.Tasks;
|
||||
using DirectXTexNet;
|
||||
using Shipwreck.Phash;
|
||||
using Wabbajack.Common;
|
||||
using Wabbajack.Common.Serialization.Json;
|
||||
using Shipwreck.Phash.Bitmaps;
|
||||
|
||||
namespace Wabbajack.ImageHashing
|
||||
{
|
||||
@ -36,36 +42,70 @@ namespace Wabbajack.ImageHashing
|
||||
|
||||
public static async Task<ImageState?> FromImageStream(Stream stream, Extension ext, bool takeStreamOwnership = true)
|
||||
{
|
||||
var ms = new MemoryStream();
|
||||
await stream.CopyToAsync(ms);
|
||||
if (takeStreamOwnership) await stream.DisposeAsync();
|
||||
await using var tf = new TempFile(ext);
|
||||
await tf.Path.WriteAllAsync(stream, takeStreamOwnership);
|
||||
return await GetState(tf.Path);
|
||||
}
|
||||
|
||||
DDSImage? img = default;
|
||||
try
|
||||
{
|
||||
if (ext == new Extension(".dds"))
|
||||
img = DDSImage.FromDDSMemory(ms.GetBuffer());
|
||||
else if (ext == new Extension(".tga"))
|
||||
{
|
||||
img = DDSImage.FromTGAMemory(ms.GetBuffer());
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new NotImplementedException("Only DDS and TGA files supported by PHash");
|
||||
}
|
||||
private static readonly Extension PNGExtension = new(".png");
|
||||
public static async Task<PHash> GetPHash(AbsolutePath path)
|
||||
{
|
||||
await using var tmp = await TempFolder.Create();
|
||||
await ConvertImage(path, tmp.Dir, 512, 512, DXGI_FORMAT.R8G8B8A8_UNORM, PNGExtension);
|
||||
|
||||
using var img = (Bitmap)Image.FromFile(path.FileName.RelativeTo(tmp.Dir).ReplaceExtension(PNGExtension).ToString());
|
||||
return PHash.FromDigest(ImagePhash.ComputeDigest(img.ToLuminanceImage()));
|
||||
}
|
||||
|
||||
return img.ImageState();
|
||||
public static async Task ConvertImage(AbsolutePath from, AbsolutePath toFolder, int w, int h, DXGI_FORMAT format, Extension fileFormat)
|
||||
{
|
||||
// User isn't renaming the file, so we don't have to create a temporary folder
|
||||
var ph = new ProcessHelper
|
||||
{
|
||||
Path = @"Tools\texconv.exe".RelativeTo(AbsolutePath.EntryPoint),
|
||||
Arguments = new object[] {from, "-ft", fileFormat.ToString()[1..], "-f", format, "-o", toFolder, "-w", w, "-h", h, "-if", "CUBIC", "-singleproc"},
|
||||
ThrowOnNonZeroExitCode = true,
|
||||
LogError = true
|
||||
};
|
||||
await ph.Start();
|
||||
|
||||
}
|
||||
catch (Exception ex)
|
||||
}
|
||||
|
||||
public static async Task ConvertImage(Stream from, ImageState state, Extension ext, AbsolutePath to)
|
||||
{
|
||||
await using var tmpFile = await TempFolder.Create();
|
||||
var inFile = to.FileName.RelativeTo(tmpFile.Dir).WithExtension(ext);
|
||||
await inFile.WriteAllAsync(from);
|
||||
await ConvertImage(inFile, to.Parent, state.Width, state.Height, state.Format, ext);
|
||||
}
|
||||
|
||||
public static async Task<ImageState> GetState(AbsolutePath path)
|
||||
{
|
||||
var ph = new ProcessHelper
|
||||
{
|
||||
Utils.Log($"Unable to read image state (this is fine)");
|
||||
return null;
|
||||
}
|
||||
finally
|
||||
Path = @"Tools\texdiag.exe".RelativeTo(AbsolutePath.EntryPoint), Arguments = new object[] {"info", path, "-nologo"},
|
||||
ThrowOnNonZeroExitCode = true,
|
||||
LogError = true
|
||||
};
|
||||
var lines = new List<string>();
|
||||
using var _ = ph.Output.Where(p => p.Type == ProcessHelper.StreamType.Output)
|
||||
.Select(p => p.Line)
|
||||
.Where(p => p.Contains(" = "))
|
||||
.Subscribe(l => lines.Add(l));
|
||||
await ph.Start();
|
||||
var data = lines.Select(l =>
|
||||
{
|
||||
img?.Dispose();
|
||||
}
|
||||
var split = l.Split(" = ");
|
||||
return (split[0].Trim(), split[1].Trim());
|
||||
}).ToDictionary(p => p.Item1, p => p.Item2);
|
||||
|
||||
return new ImageState
|
||||
{
|
||||
Width = int.Parse(data["width"]),
|
||||
Height = int.Parse(data["height"]),
|
||||
Format = Enum.Parse<DXGI_FORMAT>(data["format"]),
|
||||
PerceptualHash = await GetPHash(path)
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -83,42 +83,6 @@ namespace Wabbajack.ImageHashing
|
||||
h <<= 8;
|
||||
return (int)h;
|
||||
}
|
||||
|
||||
public static async Task<PHash> FromStream(Stream stream, Extension ext, bool takeStreamOwnership = true)
|
||||
{
|
||||
try
|
||||
{
|
||||
var ms = new MemoryStream();
|
||||
await stream.CopyToAsync(ms);
|
||||
if (takeStreamOwnership) await stream.DisposeAsync();
|
||||
|
||||
DDSImage img;
|
||||
if (ext == new Extension(".dds"))
|
||||
img = DDSImage.FromDDSMemory(ms.GetBuffer());
|
||||
else if (ext == new Extension(".tga"))
|
||||
{
|
||||
img = DDSImage.FromTGAMemory(ms.GetBuffer());
|
||||
}
|
||||
else
|
||||
{
|
||||
throw new NotImplementedException("Only DDS and TGA files supported by PHash");
|
||||
}
|
||||
|
||||
return img.PerceptionHash();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Utils.Log($"Error getting PHASH {ex}");
|
||||
return default;
|
||||
}
|
||||
}
|
||||
|
||||
public static async Task<PHash> FromFile(AbsolutePath path)
|
||||
{
|
||||
await using var s = await path.OpenRead();
|
||||
return await FromStream(s, path.Extension);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
BIN
Wabbajack.ImageHashing/Tools/texconv.exe
Normal file
BIN
Wabbajack.ImageHashing/Tools/texconv.exe
Normal file
Binary file not shown.
BIN
Wabbajack.ImageHashing/Tools/texdiag.exe
Normal file
BIN
Wabbajack.ImageHashing/Tools/texdiag.exe
Normal file
Binary file not shown.
@ -9,12 +9,27 @@
|
||||
<ItemGroup>
|
||||
<PackageReference Include="DirectXTexNet" Version="1.0.2" />
|
||||
<PackageReference Include="Shipwreck.Phash" Version="0.5.0" />
|
||||
<PackageReference Include="Shipwreck.Phash.Bitmaps" Version="0.5.0" />
|
||||
<PackageReference Include="Silk.NET.Direct3D11" Version="2.6.0" />
|
||||
<PackageReference Include="Silk.NET.DXGI" Version="2.6.0" />
|
||||
<PackageReference Include="System.Drawing.Common" Version="5.0.2" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Wabbajack.Common\Wabbajack.Common.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<Folder Include="Tools" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<None Update="Tools\texconv.exe">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
<None Update="Tools\texdiag.exe">
|
||||
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
|
||||
</None>
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
|
@ -157,9 +157,7 @@ namespace Wabbajack.Lib
|
||||
case TransformedTexture tt:
|
||||
{
|
||||
await using var s = await sf.GetStream();
|
||||
using var img = await DDSImage.FromStream(s, vf.Name);
|
||||
|
||||
img.ResizeRecompressAndSave(tt.ImageState, directive.Directive.To.RelativeTo(OutputFolder));
|
||||
await ImageState.ConvertImage(s, tt.ImageState, tt.To.Extension, directive.Directive.To.RelativeTo(OutputFolder));
|
||||
|
||||
}
|
||||
break;
|
||||
|
@ -8,13 +8,10 @@ using Wabbajack.Common;
|
||||
using Wabbajack.ImageHashing;
|
||||
using Wabbajack.Lib;
|
||||
using Wabbajack.Lib.CompilationSteps;
|
||||
using Wabbajack.Lib.CompilationSteps.CompilationErrors;
|
||||
using Xunit;
|
||||
using Xunit.Abstractions;
|
||||
using Xunit.Sdk;
|
||||
using DXGI_FORMAT = DirectXTexNet.DXGI_FORMAT;
|
||||
using File = Alphaleonis.Win32.Filesystem.File;
|
||||
using Path = Alphaleonis.Win32.Filesystem.Path;
|
||||
using DXGI_FORMAT = Wabbajack.ImageHashing.DXGI_FORMAT;
|
||||
|
||||
namespace Wabbajack.Test
|
||||
{
|
||||
@ -388,17 +385,17 @@ namespace Wabbajack.Test
|
||||
}
|
||||
|
||||
{
|
||||
using var originalDDS = DDSImage.FromFile(nativeFile);
|
||||
originalDDS.ResizeRecompressAndSave(originalDDS.Width, originalDDS.Height, DXGI_FORMAT.BC7_UNORM, recompressedFile);
|
||||
originalDDS.ResizeRecompressAndSave(128, 128, DXGI_FORMAT.BC7_UNORM, resizedFile);
|
||||
var originalDDS = await ImageState.GetState(nativeFile);
|
||||
await ImageState.ConvertImage(nativeFile, recompressedFile.Parent, originalDDS.Width, originalDDS.Height, DXGI_FORMAT.BC7_UNORM, recompressedFile.Extension);
|
||||
await ImageState.ConvertImage(nativeFile, resizedFile.Parent, 128, 128, DXGI_FORMAT.BC7_UNORM, resizedFile.Extension);
|
||||
}
|
||||
|
||||
await utils.Configure();
|
||||
|
||||
await CompileAndInstall(profile, true);
|
||||
await utils.VerifyInstalledFile(mod, @"native\whitestagbody.dds");
|
||||
Assert.True(0.99f <=(await PHash.FromFile(recompressedFile)).Similarity(await PHash.FromFile(utils.InstalledPath(mod, @"recompressed\whitestagbody.dds"))));
|
||||
Assert.True(0.98f <=(await PHash.FromFile(resizedFile)).Similarity(await PHash.FromFile(utils.InstalledPath(mod, @"resized\whitestagbody.dds"))));
|
||||
Assert.True(0.99f <=(await ImageState.GetState(recompressedFile)).PerceptualHash.Similarity(await ImageState.GetPHash(utils.InstalledPath(mod, @"recompressed\whitestagbody.dds"))));
|
||||
Assert.True(0.98f <=(await ImageState.GetState(resizedFile)).PerceptualHash.Similarity(await ImageState.GetPHash(utils.InstalledPath(mod, @"resized\whitestagbody.dds"))));
|
||||
}
|
||||
|
||||
[Fact]
|
||||
|
Loading…
Reference in New Issue
Block a user