|
| 1 | +#include "pch.h" |
| 2 | +#include "PerceptualHash.hpp" |
| 3 | +#include <bit> |
| 4 | +#include <d3d11.h> |
| 5 | +#include <wrl/client.h> |
| 6 | +#include <directxtk/WICTextureLoader.h> |
| 7 | +#include <wincodec.h> |
| 8 | +#include <cmath> |
| 9 | + |
| 10 | +using Microsoft::WRL::ComPtr; |
| 11 | + |
| 12 | +namespace Engine3DRadSpace::Testing |
| 13 | +{ |
| 14 | + PerceptualHash::Hash PerceptualHash::ComputeHash(const std::filesystem::path& imagePath) |
| 15 | + { |
| 16 | + auto resized = LoadAndResize(imagePath); |
| 17 | + return ComputeDHash(resized); |
| 18 | + } |
| 19 | + |
| 20 | + unsigned PerceptualHash::HammingDistance(Hash hash1, Hash hash2) |
| 21 | + { |
| 22 | + return static_cast<unsigned>(std::popcount(hash1 ^ hash2)); |
| 23 | + } |
| 24 | + |
| 25 | + bool PerceptualHash::AreImagesSimilar(const std::filesystem::path& imagePath1, |
| 26 | + const std::filesystem::path& imagePath2, |
| 27 | + unsigned threshold) |
| 28 | + { |
| 29 | + Hash hash1 = ComputeHash(imagePath1); |
| 30 | + Hash hash2 = ComputeHash(imagePath2); |
| 31 | + return HammingDistance(hash1, hash2) <= threshold; |
| 32 | + } |
| 33 | + |
| 34 | + PerceptualHash::ImageData PerceptualHash::LoadAndResize(const std::filesystem::path& imagePath, size_t targetWidth, size_t targetHeight) |
| 35 | + { |
| 36 | + // Initialize COM for WIC |
| 37 | + CoInitializeEx(nullptr, COINIT_MULTITHREADED); |
| 38 | + |
| 39 | + // Create WIC factory |
| 40 | + ComPtr<IWICImagingFactory> wicFactory; |
| 41 | + HRESULT hr = CoCreateInstance( |
| 42 | + CLSID_WICImagingFactory, |
| 43 | + nullptr, |
| 44 | + CLSCTX_INPROC_SERVER, |
| 45 | + IID_PPV_ARGS(&wicFactory) |
| 46 | + ); |
| 47 | + if (FAILED(hr)) throw std::runtime_error("Failed to create WIC factory"); |
| 48 | + |
| 49 | + // Load the image |
| 50 | + ComPtr<IWICBitmapDecoder> decoder; |
| 51 | + hr = wicFactory->CreateDecoderFromFilename( |
| 52 | + imagePath.wstring().c_str(), |
| 53 | + nullptr, |
| 54 | + GENERIC_READ, |
| 55 | + WICDecodeMetadataCacheOnDemand, |
| 56 | + &decoder |
| 57 | + ); |
| 58 | + if (FAILED(hr)) throw std::runtime_error("Failed to load image: " + imagePath.string()); |
| 59 | + |
| 60 | + ComPtr<IWICBitmapFrameDecode> frame; |
| 61 | + hr = decoder->GetFrame(0, &frame); |
| 62 | + if (FAILED(hr)) throw std::runtime_error("Failed to get image frame"); |
| 63 | + |
| 64 | + // Convert to RGBA format |
| 65 | + ComPtr<IWICFormatConverter> converter; |
| 66 | + hr = wicFactory->CreateFormatConverter(&converter); |
| 67 | + if (FAILED(hr)) throw std::runtime_error("Failed to create format converter"); |
| 68 | + |
| 69 | + hr = converter->Initialize( |
| 70 | + frame.Get(), |
| 71 | + GUID_WICPixelFormat32bppRGBA, |
| 72 | + WICBitmapDitherTypeNone, |
| 73 | + nullptr, |
| 74 | + 0.0, |
| 75 | + WICBitmapPaletteTypeCustom |
| 76 | + ); |
| 77 | + if (FAILED(hr)) throw std::runtime_error("Failed to initialize format converter"); |
| 78 | + |
| 79 | + // Get original dimensions |
| 80 | + UINT srcWidth, srcHeight; |
| 81 | + hr = converter->GetSize(&srcWidth, &srcHeight); |
| 82 | + if (FAILED(hr)) throw std::runtime_error("Failed to get image size"); |
| 83 | + |
| 84 | + // Read original image data |
| 85 | + size_t srcRowPitch = srcWidth * 4; // 4 bytes per pixel (RGBA) |
| 86 | + size_t srcImageSize = srcRowPitch * srcHeight; |
| 87 | + auto srcPixels = std::make_unique<uint8_t[]>(srcImageSize); |
| 88 | + |
| 89 | + hr = converter->CopyPixels( |
| 90 | + nullptr, |
| 91 | + static_cast<UINT>(srcRowPitch), |
| 92 | + static_cast<UINT>(srcImageSize), |
| 93 | + srcPixels.get() |
| 94 | + ); |
| 95 | + if (FAILED(hr)) throw std::runtime_error("Failed to copy pixels"); |
| 96 | + |
| 97 | + // Resize to target dimensions |
| 98 | + size_t dstRowPitch = targetWidth * 4; |
| 99 | + size_t dstImageSize = dstRowPitch * targetHeight; |
| 100 | + auto dstPixels = std::make_unique<uint8_t[]>(dstImageSize); |
| 101 | + |
| 102 | + BilinearResize( |
| 103 | + srcPixels.get(), srcWidth, srcHeight, srcRowPitch, |
| 104 | + dstPixels.get(), targetWidth, targetHeight, dstRowPitch |
| 105 | + ); |
| 106 | + |
| 107 | + ImageData result; |
| 108 | + result.pixels = std::move(dstPixels); |
| 109 | + result.width = targetWidth; |
| 110 | + result.height = targetHeight; |
| 111 | + result.rowPitch = dstRowPitch; |
| 112 | + |
| 113 | + return result; |
| 114 | + } |
| 115 | + |
| 116 | + void PerceptualHash::BilinearResize(const uint8_t* src, size_t srcWidth, size_t srcHeight, size_t srcPitch, |
| 117 | + uint8_t* dst, size_t dstWidth, size_t dstHeight, size_t dstPitch) |
| 118 | + { |
| 119 | + float xRatio = static_cast<float>(srcWidth) / static_cast<float>(dstWidth); |
| 120 | + float yRatio = static_cast<float>(srcHeight) / static_cast<float>(dstHeight); |
| 121 | + |
| 122 | + for (size_t y = 0; y < dstHeight; ++y) |
| 123 | + { |
| 124 | + for (size_t x = 0; x < dstWidth; ++x) |
| 125 | + { |
| 126 | + float srcX = x * xRatio; |
| 127 | + float srcY = y * yRatio; |
| 128 | + |
| 129 | + size_t x1 = static_cast<size_t>(srcX); |
| 130 | + size_t y1 = static_cast<size_t>(srcY); |
| 131 | + size_t x2 = (x1 + 1 < srcWidth) ? (x1 + 1) : (srcWidth - 1); |
| 132 | + size_t y2 = (y1 + 1 < srcHeight) ? (y1 + 1) : (srcHeight - 1); |
| 133 | + |
| 134 | + float xWeight = srcX - x1; |
| 135 | + float yWeight = srcY - y1; |
| 136 | + |
| 137 | + // Bilinear interpolation for each channel |
| 138 | + for (size_t c = 0; c < 4; ++c) // RGBA |
| 139 | + { |
| 140 | + float p1 = src[y1 * srcPitch + x1 * 4 + c]; |
| 141 | + float p2 = src[y1 * srcPitch + x2 * 4 + c]; |
| 142 | + float p3 = src[y2 * srcPitch + x1 * 4 + c]; |
| 143 | + float p4 = src[y2 * srcPitch + x2 * 4 + c]; |
| 144 | + |
| 145 | + float top = p1 * (1.0f - xWeight) + p2 * xWeight; |
| 146 | + float bottom = p3 * (1.0f - xWeight) + p4 * xWeight; |
| 147 | + float value = top * (1.0f - yWeight) + bottom * yWeight; |
| 148 | + |
| 149 | + dst[y * dstPitch + x * 4 + c] = static_cast<uint8_t>(value); |
| 150 | + } |
| 151 | + } |
| 152 | + } |
| 153 | + } |
| 154 | + |
| 155 | + PerceptualHash::Hash PerceptualHash::ComputeDHash(const ImageData& resizedImage) |
| 156 | + { |
| 157 | + // dHash algorithm: compare adjacent pixels in each row |
| 158 | + // Image should be 9x8 pixels (9 wide for 8 comparisons per row) |
| 159 | + if (resizedImage.width != 9 || resizedImage.height != 8) |
| 160 | + throw std::runtime_error("Image must be 9x8 for dHash computation"); |
| 161 | + |
| 162 | + Hash hash = 0; |
| 163 | + const uint8_t* pixels = resizedImage.pixels.get(); |
| 164 | + size_t rowPitch = resizedImage.rowPitch; |
| 165 | + |
| 166 | + // For each row, compare adjacent pixels |
| 167 | + for (size_t y = 0; y < 8; ++y) |
| 168 | + { |
| 169 | + for (size_t x = 0; x < 8; ++x) |
| 170 | + { |
| 171 | + // Get grayscale values for current and next pixel |
| 172 | + // RGBA format: offset = (y * rowPitch) + (x * 4) |
| 173 | + size_t offset1 = (y * rowPitch) + (x * 4); |
| 174 | + size_t offset2 = (y * rowPitch) + ((x + 1) * 4); |
| 175 | + |
| 176 | + // Convert to grayscale using luminance formula |
| 177 | + uint8_t gray1 = static_cast<uint8_t>( |
| 178 | + 0.299f * pixels[offset1] + // R |
| 179 | + 0.587f * pixels[offset1 + 1] + // G |
| 180 | + 0.114f * pixels[offset1 + 2] // B |
| 181 | + ); |
| 182 | + uint8_t gray2 = static_cast<uint8_t>( |
| 183 | + 0.299f * pixels[offset2] + // R |
| 184 | + 0.587f * pixels[offset2 + 1] + // G |
| 185 | + 0.114f * pixels[offset2 + 2] // B |
| 186 | + ); |
| 187 | + |
| 188 | + // Set bit if left pixel is brighter than right pixel |
| 189 | + size_t bitIndex = y * 8 + x; |
| 190 | + if (gray1 > gray2) |
| 191 | + { |
| 192 | + hash |= (1ULL << bitIndex); |
| 193 | + } |
| 194 | + } |
| 195 | + } |
| 196 | + |
| 197 | + return hash; |
| 198 | + } |
| 199 | +} |
0 commit comments