diff --git a/README.md b/README.md index 53269fa..c35faaf 100644 --- a/README.md +++ b/README.md @@ -19,9 +19,15 @@ Release note (v4): simplified func `Icon`; more than 2x reduction of icon memory `Icon` produces "image hashes" called "icons", which will be used for comparision. -`Similar` gives a verdict whether 2 images are similar with well-tested default thresholds. +`Similar` gives a verdict whether 2 images are similar with well-tested default thresholds. To see the thresholds use `DefaultThresholds`. Rotations and mirrors are not taken in account in `Similar`. Note that orientations can be coded as flags in image file EXIF. -`EucMetric` can be used instead, when you need different precision or want to sort by similarity. Func `PropMetric` can be used for customization of image proportion threshold. +`Similar90270` is like above, but in addition compares to images rotated ±90°. This function will return more results than `Similar` and includes similarities of `Similar`. + +`EucMetric` can be used instead of `Similar`, when you need different precision or want to sort by similarity. Func `PropMetric` can be used for customization of image proportion threshold. Both functions relate to non-rotated images, as in func `Similar`. + +`DefaultThresholds` prints default thresholds used in func `Similar` and `Similar90270`, as a starting point for selecting thresholds on `EucMetric` and `PropMetric`. + +`Rotate90` and `Rotate270` turn an icon +90° or -90° clockwise. Those are useful if you test for custom similarity with `EucMetric` and `PropMetric` for rotated images. Or if you also decide to compare to images rotated +180° (by applying `Rotate90` twice). [Go doc](https://pkg.go.dev/github.com/vitali-fedulov/images4) for code reference. @@ -46,13 +52,14 @@ func main() { img2, _ := images4.Open(path2) // Icons are compact image representations (image "hashes"). - // Name "hash" is not used intentionally. + // Name "hash" is reserved for hash tables (in package imagehash). icon1 := images4.Icon(img1) icon2 := images4.Icon(img2) // Comparison. // Images are not used directly. Icons are used instead, // because they have tiny memory footprint and fast to compare. + // Use func Similar90270 to include images rotated right and left. if images4.Similar(icon1, icon2) { fmt.Println("Images are similar.") } else { diff --git a/icon.go b/icon.go index f70b725..0fa3365 100644 --- a/icon.go +++ b/icon.go @@ -233,3 +233,41 @@ func (icon IconT) ToRGBA(size int) *image.RGBA { } return img } + +// Rotate rotates an icon by 90 degrees clockwise. +func Rotate90(icon IconT) IconT { + + var c1, c2, c3 float64 + rotated := sizedIcon(IconSize) + for x := 0; x < IconSize; x++ { + for y := 0; y < IconSize; y++ { + c1, c2, c3 = Get(icon, IconSize, image.Point{y, IconSize - 1 - x}) + Set(rotated, IconSize, image.Point{x, y}, + c1, c2, c3) + } + } + + // Swap image sizes. + rotated.ImgSize.X, rotated.ImgSize.Y = icon.ImgSize.Y, icon.ImgSize.X + + return rotated +} + +// Rotate rotates an icon by 270 degrees clockwise. +func Rotate270(icon IconT) IconT { + + var c1, c2, c3 float64 + rotated := sizedIcon(IconSize) + for x := 0; x < IconSize; x++ { + for y := 0; y < IconSize; y++ { + c1, c2, c3 = Get(icon, IconSize, image.Point{x, y}) + Set(rotated, IconSize, image.Point{y, IconSize - 1 - x}, + c1, c2, c3) + } + } + + // Swap image sizes. + rotated.ImgSize.X, rotated.ImgSize.Y = icon.ImgSize.Y, icon.ImgSize.X + + return rotated +} diff --git a/icon_test.go b/icon_test.go index da1149a..1b0efc0 100644 --- a/icon_test.go +++ b/icon_test.go @@ -156,3 +156,25 @@ func TestNormalize(t *testing.T) { testNormalize(src, want, t) } + +func TestRotate(t *testing.T) { + + img0, _ := Open(path.Join("testdata", "rotate", "0.jpg")) + img90, _ := Open(path.Join("testdata", "rotate", "90.jpg")) + icon0 := Icon(img0) + icon90 := Icon(img90) + + if !Similar(Rotate90(icon0), icon90) { + t.Errorf("Rotate(icon0) is not similar to icon90") + return + } + + img270, _ := Open(path.Join("testdata", "rotate", "270.jpg")) + icon270 := Icon(img270) + + if !Similar(Rotate270(icon0), icon270) { + t.Errorf("Rotate(icon0) is not similar to icon270") + return + } + +} diff --git a/similarity.go b/similarity.go index 3d1bedf..45264be 100644 --- a/similarity.go +++ b/similarity.go @@ -1,5 +1,7 @@ package images4 +import "fmt" + // Similar returns similarity verdict based on Euclidean // and proportion similarity. func Similar(iconA, iconB IconT) bool { @@ -89,3 +91,29 @@ func EucMetric(iconA, iconB IconT) (m1, m2, m3 float64) { return m1, m2, m3 } + +// Print default thresholds for func Similar. +func DefaultThresholds() { + fmt.Printf("*** Default thresholds ***") + fmt.Printf("\nEuclidean distance thresholds (YCbCr): m1=%v, m2=%v, m3=%v", thY, thCbCr, thCbCr) + fmt.Printf("\nProportion threshold: m=%v\n\n", thProp) +} + +// Similar90270 works like Similar, but also considers rotations of ±90°. +// Those are rotations users might reasonably often do. +func Similar90270(iconA, iconB IconT) bool { + + if Similar(iconA, iconB) { + return true + } + + if Similar(iconA, Rotate90(iconB)) { + return true + } + + if Similar(iconA, Rotate270(iconB)) { + return true + } + + return false +} diff --git a/similarity_test.go b/similarity_test.go index 543596c..0208ccc 100644 --- a/similarity_test.go +++ b/similarity_test.go @@ -81,3 +81,35 @@ func TestEucSimilar(t *testing.T) { testEucSimilar("uniform-green.png", "uniform-white.png", false, t) testEucSimilar("uniform-white.png", "uniform-white.png", true, t) } + +func TestSimilar90270(t *testing.T) { + + img0, _ := Open(path.Join("testdata", "rotate", "0.jpg")) + img90, _ := Open(path.Join("testdata", "rotate", "90.jpg")) + img180, _ := Open(path.Join("testdata", "rotate", "180.jpg")) + img270, _ := Open(path.Join("testdata", "rotate", "270.jpg")) + + icon0 := Icon(img0) + icon90 := Icon(img90) + icon180 := Icon(img180) + icon270 := Icon(img270) + + if !Similar90270(icon0, icon90) { + t.Errorf("0.jpg must be similar to 90.jpg") + } + if Similar90270(icon0, icon180) { + t.Errorf("0.jpg must be NOT similar to 180.jpg") + } + + if !Similar90270(icon0, icon270) { + t.Errorf("0.jpg must be similar to 270.jpg") + } + + if !Similar90270(icon90, icon180) { + t.Errorf("90.jpg must be similar to 180.jpg") + } + + if Similar90270(icon90, icon270) { + t.Errorf("90.jpg must be NOT similar to 270.jpg") + } +} diff --git a/testdata/rotate/0.jpg b/testdata/rotate/0.jpg new file mode 100644 index 0000000..9e83ee4 Binary files /dev/null and b/testdata/rotate/0.jpg differ diff --git a/testdata/rotate/180.jpg b/testdata/rotate/180.jpg new file mode 100644 index 0000000..14584c3 Binary files /dev/null and b/testdata/rotate/180.jpg differ diff --git a/testdata/rotate/270.jpg b/testdata/rotate/270.jpg new file mode 100644 index 0000000..b85dd0c Binary files /dev/null and b/testdata/rotate/270.jpg differ diff --git a/testdata/rotate/90.jpg b/testdata/rotate/90.jpg new file mode 100644 index 0000000..9131157 Binary files /dev/null and b/testdata/rotate/90.jpg differ