namespace Compare.Test; using CoenM.ImageHash; using CoenM.ImageHash.HashAlgorithms; using Serilog; public static class Program { public static void Main() { Log.Logger = new LoggerConfiguration() .MinimumLevel.Debug() .WriteTo.Console() .WriteTo.File( "logs/log-.txt", rollingInterval: RollingInterval.Day, outputTemplate: "{Timestamp:yyyy-MM-dd HH:mm:ss.fff zzz} [{Level:u3}] {Message:lj}{NewLine}{Exception}") .CreateLogger(); try { Log.Information("Application starting up..."); RunImageHashing(); Log.Information("Application has finished successfully."); } catch (Exception ex) { Log.Fatal(ex, "An unhandled exception occurred, terminating the application."); } finally { Log.CloseAndFlush(); } } private static void RunImageHashing() { // --- Configuration folders --- // Takes the folder directories of each contestant // Each image should be .PNG and have "XX_" prefix e.g : 01_Image.png, 02_Image2.png, etc string[] inputDirectories = [ "/home/ibnufadhil/Documents/projects/StitcherResultTest/Test2.0/renjayarz", "/home/ibnufadhil/Documents/projects/StitcherResultTest/Test2.0/reinardras", "/home/ibnufadhil/Documents/projects/StitcherResultTest/Test2.0/meizar", "/home/ibnufadhil/Documents/projects/StitcherResultTest/Test2.0/olymrifki", "/home/ibnufadhil/Documents/projects/StitcherResultTest/Test2.0/fikribahru", "/home/ibnufadhil/Documents/projects/StitcherResultTest/Test2.0/benscode", "/home/ibnufadhil/Documents/projects/StitcherResultTest/Test2.0/dennisarfan" ]; string outputCsvPath = "hash_analysis_results.csv"; IImageHash hashAlgorithm = new PerceptualHash(); Log.Information("Generating hashes using {AlgorithmName}...", hashAlgorithm.GetType().Name); var groupedImages = new Dictionary>(); // Group images by prefix foreach (string dir in inputDirectories) { if (!Directory.Exists(dir)) { Log.Warning("Directory not found: {DirectoryPath}", dir); continue; } foreach (string file in Directory.GetFiles(dir, "*.png")) { string prefix = Path.GetFileName(file).Split('_')[0]; if (!groupedImages.ContainsKey(prefix)) groupedImages[prefix] = new List(); groupedImages[prefix].Add(file); } } var csvLines = new List { "TestPrefix,FileName,ImageHash,SimilarityToRepresentative(%)" }; foreach (var group in groupedImages.OrderBy(g => g.Key)) { string prefix = group.Key; var imagePaths = group.Value; if (imagePaths.Count < 2) { Log.Warning("Skipping {Prefix}: Not enough images to compare ({ImageCount}).", prefix, imagePaths.Count); continue; } Log.Information("=== Processing group {Prefix} ({ImageCount} images) ===", prefix, imagePaths.Count); // Calculate hashes var hashResults = new Dictionary(); foreach (var path in imagePaths) { try { using var stream = File.OpenRead(path); ulong hash = hashAlgorithm.Hash(stream); hashResults[path] = hash; string relativePath = $"{Path.GetFileName(Path.GetDirectoryName(path))}/{Path.GetFileName(path)}"; Log.Debug("Hashed {ImagePath}: {ImageHash}", relativePath, hash); } catch (Exception ex) { Log.Error(ex, "Error hashing {ImagePath}", path); } } if (hashResults.Count < 2) { Log.Warning("Skipping {Prefix}: Not enough valid images after hashing ({HashedCount}).", prefix, hashResults.Count); continue; } // --- Compute Representative Hash --- ulong representativeHash = ComputeRepresentativeHash(hashResults.Values.ToList()); Log.Information("-> Representative Hash for {Prefix}: {RepresentativeHash}", prefix, representativeHash); Log.Information("-----------------------------------------------------"); // --- Compute similarity to representative hash --- foreach (var entry in hashResults) { double similarity = CompareHash.Similarity(entry.Value, representativeHash); string fileName = $"{Path.GetFileName(Path.GetDirectoryName(entry.Key))}/{Path.GetFileName(entry.Key)}"; csvLines.Add($"{prefix},{fileName},{entry.Value},{similarity:F6}"); Log.Information("{FileName,-70} | Similarity: {Similarity:F2}%", fileName, similarity); } Log.Information("-----------------------------------------------------\n"); } // --- Write CSV output --- try { File.WriteAllLines(outputCsvPath, csvLines); Log.Information("Results successfully saved to: {CsvPath}", Path.GetFullPath(outputCsvPath)); } catch (Exception ex) { Log.Error(ex, "Failed to write CSV file to {CsvPath}", outputCsvPath); } } // === Compute the bitwise representative hash (majority vote using consensus) === private static ulong ComputeRepresentativeHash(List hashes) { int[] bitCounts = new int[64]; // Count how many 1s are in each bit foreach (ulong hash in hashes) { for (int bit = 0; bit < 64; bit++) { if (((hash >> bit) & 1UL) == 1UL) bitCounts[bit]++; } } ulong result = 0UL; int threshold = hashes.Count / 2; // If more than half of the hashes have a 1 in a bit, set the corresponding bit in the result, otherwise 0 for (int bit = 0; bit < 64; bit++) { if (bitCounts[bit] > threshold) result |= 1UL << bit; } return result; } }