From 53fdf151da646aecb120007b7c8b475b51268fa1 Mon Sep 17 00:00:00 2001 From: Vitali Fedulov Date: Sun, 28 Jan 2024 22:50:59 +0100 Subject: [PATCH] Added new func: CustomSimilar --- custom.go | 65 ++++++++++++++++++++++++++++++++++++++ custom_test.go | 72 ++++++++++++++++++++++++++++++++++++++++++ testdata/custom/1.jpg | Bin 0 -> 8971 bytes testdata/custom/2.jpg | Bin 0 -> 9372 bytes 4 files changed, 137 insertions(+) create mode 100644 custom.go create mode 100644 custom_test.go create mode 100644 testdata/custom/1.jpg create mode 100644 testdata/custom/2.jpg diff --git a/custom.go b/custom.go new file mode 100644 index 0000000..175e9fe --- /dev/null +++ b/custom.go @@ -0,0 +1,65 @@ +package images4 + +// Threshold multiplication coefficients for func CustomSimilar. +// When all values equal 1.0 func CustomSimilar is equivalent +// to func Similar. By setting those values less than 1, similarity +// comparison becomes stricter (more precise). Values larger than 1 +// will generalize more and show more false positives. When uncertain, +// setting all coefficients to 1.0 is the safe starting point. +type CustomCoefficients struct { + Y float64 // Luma (grayscale information). + Cb float64 // Chrominance b (color information). + Cr float64 // Chrominance r (color information). + Prop float64 // Proportion tolerance (how similar are image borders). +} + +// CustomSimilar is like Similar, except it allows changing default +// thresholds by multiplying them. The practically useful range of +// the coefficients is [0, 1.0), but can be equal or larger than 1 +// if necessary. All coefficients set to 0 correspond to identical images, +// for example an image file copy. All coefficients equal to 1 make func +// CustomSimilar equivalent to func Similar. +func CustomSimilar(iconA, iconB IconT, coeff CustomCoefficients) bool { + + if !customPropSimilar(iconA, iconB, coeff) { + return false + } + if !customEucSimilar(iconA, iconB, coeff) { + return false + } + return true +} + +func customPropSimilar(iconA, iconB IconT, coeff CustomCoefficients) bool { + return PropMetric(iconA, iconB) <= thProp*coeff.Prop +} + +func customEucSimilar(iconA, iconB IconT, coeff CustomCoefficients) bool { + + m1, m2, m3 := EucMetric(iconA, iconB) + + return m1 <= thY*coeff.Y && + m2 <= thCbCr*coeff.Cb && + m3 <= thCbCr*coeff.Cr +} + +// Similar90270 works like Similar, but also considers rotations of ±90°. +// Those are rotations users might reasonably often do. +func CustomSimilar90270(iconA, iconB IconT, coeff CustomCoefficients) bool { + + if CustomSimilar(iconA, iconB, coeff) { + return true + } + + // iconB rotated 90 degrees. + if CustomSimilar(iconA, Rotate90(iconB), coeff) { + return true + } + + // As if iconB was rotated 270 degrees. + if CustomSimilar(Rotate90(iconA), iconB, coeff) { + return true + } + + return false +} diff --git a/custom_test.go b/custom_test.go new file mode 100644 index 0000000..17e3455 --- /dev/null +++ b/custom_test.go @@ -0,0 +1,72 @@ +package images4 + +import ( + "path" + "testing" +) + +func TestCustomSimilar(t *testing.T) { + + // Proportions test. + + i1, _ := Open(path.Join("testdata", "euclidean", "distorted.jpg")) + i2, _ := Open(path.Join("testdata", "euclidean", "large.jpg")) + + icon1 := Icon(i1) + icon2 := Icon(i2) + + if Similar(icon1, icon2) { + t.Errorf("distorted.jpg is NOT similar to large.jpg") + } + + if !CustomSimilar(icon1, icon2, CustomCoefficients{1, 1, 1, 10}) { + t.Errorf("distorted.jpg IS similar to large.jpg, assuming proportion differences are widely tolerated.") + } + + // Euclidean tests. + + i1, _ = Open(path.Join("testdata", "custom", "1.jpg")) + i2, _ = Open(path.Join("testdata", "custom", "2.jpg")) + + icon1 = Icon(i1) + icon2 = Icon(i2) + + if !Similar(icon1, icon2) { + t.Errorf("1.jpg is GENERALLY similar to 2.jpg") + } + + // Luma. + if CustomSimilar(icon1, icon2, CustomCoefficients{0, 1, 1, 1}) { + t.Errorf("1.jpg is NOT IDENTICAL to 2.jpg") + } + + // Luma. + if CustomSimilar(icon1, icon2, CustomCoefficients{0.4, 1, 1, 1}) { + t.Errorf("1.jpg is similar to 2.jpg, BUT NOT VERY SIMILAR") + } + + // Chrominance b. + if CustomSimilar(icon1, icon2, CustomCoefficients{1, 0.1, 1, 1}) { + t.Errorf("1.jpg is similar to 2.jpg, BUT NOT VERY SIMILAR") + } + + // Chrominance c. + if CustomSimilar(icon1, icon2, CustomCoefficients{1, 1, 0.1, 1}) { + t.Errorf("1.jpg is similar to 2.jpg, BUT NOT VERY SIMILAR") + } + + // Image comparison to itself (or its own copy). + + if !CustomSimilar(icon1, icon1, CustomCoefficients{0, 0, 0, 0}) { + t.Errorf("1.jpg IS IDENTICAL to itself") + } + + if !CustomSimilar(icon1, icon1, CustomCoefficients{0.5, 0.5, 0.5, 0.5}) { + t.Errorf("1.jpg IS IDENTICAL to itself") + } + + if !CustomSimilar(icon1, icon1, CustomCoefficients{1, 1, 1, 1}) { + t.Errorf("1.jpg IS IDENTICAL to itself") + } + +} diff --git a/testdata/custom/1.jpg b/testdata/custom/1.jpg new file mode 100644 index 0000000000000000000000000000000000000000..3bf1f7fd9e9c5ba482b8dd00dbac14d80a2a2217 GIT binary patch literal 8971 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 literal 0 HcmV?d00001 diff --git a/testdata/custom/2.jpg b/testdata/custom/2.jpg new file mode 100644 index 0000000000000000000000000000000000000000..7e98b972b9a601ef51bdd6fa153044e128de2116 GIT binary patch literal 9372 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 Tv-fU=OP**1rP#f1cI3YRzQ<4n literal 0 HcmV?d00001