Compare commits

...

56 Commits

Author SHA1 Message Date
Vitali Fedulov ca2e7db1ef
- 2024-04-02 01:02:06 +02:00
Vitali Fedulov 8be73dbe53
- 2024-03-30 23:30:54 +01:00
Vitali Fedulov 93bdbb726e
- 2024-03-30 23:23:48 +01:00
Vitali Fedulov c678c1ca69
- 2024-03-30 05:48:18 +01:00
Vitali Fedulov 9f6aaf3559
- 2024-03-30 05:40:43 +01:00
Vitali Fedulov 025ab4f57c
- 2024-02-17 06:17:24 +01:00
Vitali Fedulov 9fe8146963 New release, February 2024 2024-02-06 05:20:46 +01:00
Vitali Fedulov 9d98521dde New release 2024-02-06 05:12:09 +01:00
Vitali Fedulov 9014e3e259 Added new func: CustomSimilar (3) 2024-01-30 01:18:45 +01:00
Vitali Fedulov f7f7753d86 Added new func: CustomSimilar (2) 2024-01-29 01:06:44 +01:00
Vitali Fedulov 53fdf151da Added new func: CustomSimilar 2024-01-28 22:50:59 +01:00
Vitali Fedulov c075bc302b
Update README.md 2024-01-03 06:38:10 +01:00
Vitali Fedulov 993aa184dd
Update README.md 2024-01-03 06:36:37 +01:00
Vitali Fedulov 4232862259
Update README.md 2024-01-03 06:18:43 +01:00
Vitali Fedulov f543cf2753
Update README.md 2024-01-03 06:17:36 +01:00
Vitali Fedulov 74342b23ec
Update README.md 2024-01-03 06:14:15 +01:00
Vitali Fedulov 02d2b3820f
Update README.md 2024-01-03 06:13:47 +01:00
Vitali Fedulov 55b39084a6
Update README.md 2024-01-03 06:10:23 +01:00
Vitali Fedulov 345978fc35
Update README.md 2024-01-03 06:09:40 +01:00
Vitali Fedulov a2d3d82283
Update README.md 2024-01-03 06:05:11 +01:00
Vitali Fedulov 671cb5804b
Update README.md 2024-01-03 06:02:59 +01:00
Vitali Fedulov f053061208 Exporting ResizeByNearest func 2023-12-03 17:04:57 +01:00
Vitali Fedulov 93d6a8e184
Update README.md 2023-11-04 01:48:32 +01:00
Vitali Fedulov 8ce8881dc3
Merge pull request #3 from yosg/fix-floating-point-calculation-error
Thanks!
2023-08-08 22:02:00 +02:00
yosg 2508efb096
Update image.go
Fix floating point calculation error.
2023-08-06 22:10:43 +08:00
Vitali Fedulov b2158637eb
- 2023-08-05 01:02:10 +02:00
Vitali Fedulov 2e9897112b
Update README.md 2023-08-01 03:58:16 +02:00
Vitali Fedulov 1397aba8f7
Update README.md 2023-08-01 03:47:44 +02:00
Vitali Fedulov 000989d598
Update README.md 2023-08-01 03:46:51 +02:00
Vitali Fedulov 452a1e3c4e
Update README.md 2023-08-01 03:43:26 +02:00
Vitali Fedulov 1b6203e206 text update 2023-05-05 02:44:09 +02:00
Vitali Fedulov 5bdf5c20eb text update 2023-04-27 19:20:40 +02:00
Vitali Fedulov 6b727a318d example 2023-04-27 19:14:00 +02:00
Vitali Fedulov 80fc36e5d9 text update 2023-04-27 15:48:39 +02:00
Vitali Fedulov cf61a088fb text update 2023-04-27 15:45:38 +02:00
Vitali Fedulov 3ab82bae8c text update 2023-04-21 23:06:22 +02:00
Vitali Fedulov 29ed7bcd63 text update 2023-04-18 18:49:44 +02:00
Vitali Fedulov b449cd165a text update 2023-04-18 18:46:55 +02:00
Vitali Fedulov 5789b77bae text update 2023-04-18 18:44:13 +02:00
Vitali Fedulov ca8ad8c9a3 Adding new funcs. Related to #2 (2) 2023-04-18 18:31:25 +02:00
Vitali Fedulov 53ce9b0e5b Adding new funcs. Related to #2 2023-04-18 17:52:02 +02:00
Vitali Fedulov 16fff10800
Update README.md 2022-08-21 20:12:38 +02:00
Vitali Fedulov 1c65899315
Update README.md 2022-08-21 18:02:33 +02:00
Vitali Fedulov 1ef1b21753
Update README.md 2022-08-19 19:17:59 +02:00
Vitali Fedulov 882661b349
Update README.md 2022-08-18 20:06:44 +02:00
Vitali Fedulov 1e5072e9ce
Update README.md 2022-08-18 19:56:13 +02:00
Vitali Fedulov 1c8f4aad8f
- 2022-08-10 22:33:29 +02:00
Vitali Fedulov 302c8b5483
- 2022-08-10 22:26:45 +02:00
Vitali Fedulov c66aa66a66
- 2022-08-10 22:13:02 +02:00
Vitali Fedulov 622a3c989c - 2022-08-10 03:18:12 +02:00
Vitali Fedulov 009a20cb7c Exporting 1 const and 2 funcs 2022-08-09 01:43:12 +02:00
Vitali Fedulov 4258b41f4d
Update README.md 2022-08-08 19:46:06 +02:00
Vitali Fedulov 4a91e3f704
Update README.md 2022-08-08 17:44:16 +02:00
Vitali Fedulov e87c2e6f87
Update README.md 2022-08-08 17:41:56 +02:00
Vitali Fedulov 91b8862cbc
Update README.md 2022-08-08 17:37:50 +02:00
Vitali Fedulov cae2d51031
Update README.md 2022-08-08 17:20:11 +02:00
16 changed files with 312 additions and 56 deletions

View File

@ -1,18 +1,17 @@
# Find similar images with Go
# Find similar images with Go (LATEST VERSION)
Near duplicates and resized images can be found with the package. No dependencies.
Resized and near duplicate image comparison. **No dependencies**. For search in very large image sets use [imagehash2](https://github.com/vitali-fedulov/imagehash2) as a fast pre-filtering step.
**Demo**: [similar image clustering](https://vitali-fedulov.github.io/similar.pictures/) based on similar algorithm.
**Demo**: [similar pictures search and clustering](https://vitali-fedulov.github.io/similar.pictures/) (pure in-browser JS app served [from](https://github.com/vitali-fedulov/similar.pictures)).
**This is the latest major version (v4)** of [v1/2](https://github.com/vitali-fedulov/images) and [v3](https://github.com/vitali-fedulov/images3). The changes vs v3 are: simplified func `Icon` input, more than 2x smaller memory footprint of icons, additional IconNN function, fixed GIF support, removal of dependencies, removal of hashes (a separate package for hashes will be created and linked from here soon).
Major (semantic) versions have their own repositories and are mutually incompatible:
| Major version | Repository | Comment |
| ----------- | ---------- | ----------|
| 4 | images4 - this | recommended; fast hash prefiltering (re)moved to [imagehash2](https://github.com/vitali-fedulov/imagehash2) |
| 3 | [images3](https://github.com/vitali-fedulov/images3) | good, but less optimized |
| 1, 2 | [images](https://github.com/vitali-fedulov/images) | good, legacy code |
Func `Similar` gives a verdict whether 2 images are similar with well-tested default thresholds.
Func `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.
Func `Open` supports JPEG, PNG and GIF. But other image types can be used through third-party decoders, because input for func `Icon` is Golang `image.Image`.
[Go doc](https://pkg.go.dev/github.com/vitali-fedulov/images4) for code reference.
[Go doc](https://pkg.go.dev/github.com/vitali-fedulov/images4) - for full code documentation.
## Example of comparing 2 images
@ -26,38 +25,62 @@ import (
func main() {
// Photos to compare.
path1 := "1.jpg"
path2 := "2.jpg"
// Opening and decoding images. Silently discarding errors.
img1, _ := images4.Open("1.jpg")
img2, _ := images4.Open("2.jpg")
// Open files (discarding errors here).
img1, _ := images4.Open(path1)
img2, _ := images4.Open(path2)
// Icons are compact image representations (image "hashes").
// Name "hash" is not used intentionally.
// Icons are compact hash-like image representations.
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.
// Comparison. Images are not used directly.
// Use func CustomSimilar for different precision.
if images4.Similar(icon1, icon2) {
fmt.Println("Images are similar.")
fmt.Println("Images are similar")
} else {
fmt.Println("Images are distinct.")
fmt.Println("Not similar")
}
}
```
## Main functions
- `Open` decodes JPEG, PNG and GIF. But other types can be opened with third-party decoders, because the input to func 'Icon' is Golang image.Image. [Example fork](https://github.com/Pineapples27/images4) (not mine) expanded with support of WEBP images.
- `Icon` produces an image hash-like struct called "icon", which will be used for comparision. Side note: name "hash" is reserved for true hash tables in related package for faster comparison [imagehash2](https://github.com/vitali-fedulov/imagehash2).
- `Similar` gives a verdict whether 2 images are similar with well-tested default thresholds. Rotations and mirrors are not taken in account.
- `CustomSimilar` is like 'Similar' above, but allows modifying the default thresholds by multiplication coefficients. When the coefficients equal 1.0, those two functions are equivalent. When the coefficients are less than 1.0, the comparison is more precise, down to 0.0 for identical images.
## Advanced functions
- `Similar90270` is a superset of 'Similar' by additional comparison to images rotated ±90°. Such rotations are relatively common, even by accident when taking pictures on mobile phones.
- `CustomSimilar90270` is a custom func for rotations as above with 'CustomSimilar'.
- `EucMetric` is an alternative to 'CustomSimilar' when you need to know metric values, for example to sort by similarity. [Example](https://github.com/egor-romanov/png2gif/blob/main/main.go#L450) (not mine) of custom similarity function.
- `PropMetric` is as above for image proportions.
- `DefaultThresholds` prints default thresholds used in func 'Similar' and 'Similar90270', as a starting point for selecting thresholds on 'EucMetric' and 'PropMetric'.
- `Rotate90` turns an icon 90° clockwise. This is useful for developing custom similarity function for rotated images with 'EucMetric' and 'PropMetric'. With the function you can also compare to images rotated 180° (by applying 'Rotate90' twice).
- `ResizeByNearest` is an image resizing function useful for fast identification of identical images and development of custom distance metrics not involving any of the above comparison functions.
## Algorithm
Images are resampled and resized to squares of fixed size called "icons". Euclidean distance between the icons is used to give the similarity verdict. Also image proportions are used to avoid matching images of distinct shape.
[Detailed explanation](https://vitali-fedulov.github.io/similar.pictures/algorithm-for-perceptual-image-comparison.html), also as a [PDF](https://github.com/vitali-fedulov/research/blob/main/Algorithm%20for%20perceptual%20image%20comparison.pdf).
Summary: Images are resized in a special way to squares of fixed size called "icons". Euclidean distance between the icons is used to give the similarity verdict. Also image proportions are used to avoid matching images of distinct shape.
## Speed and precision
## Customization suggestions
**To considerably accelerate comparison in large image collections** (thousands and more), use hash-table pre-filtering with package [imagehash2](https://github.com/vitali-fedulov/imagehash2).
To increase precision you can either use your own thresholds in func `EucMetric` (and `PropMetric`) OR generate icons for image sub-regions and compare those icons.
**To considerably accelerate image decoding** you can generate icons for embedded image thumbnails. Specifically, many JPEG images contain [EXIF thumbnails](https://vitali-fedulov.github.io/similar.pictures/jpeg-thumbnail-reader.html). Packages to read thumbnails: [1](https://github.com/dsoprea/go-exif) and [2](https://github.com/rwcarlsen/goexif). A note of caution: in rare cases there could be [issues](https://security.stackexchange.com/questions/116552/the-history-of-thumbnails-or-just-a-previous-thumbnail-is-embedded-in-an-image/201785#201785) with thumbnails not matching image content. EXIF standard specification: [1](https://www.media.mit.edu/pia/Research/deepview/exif.html) and [2](https://www.exif.org/Exif2-2.PDF).
To speedup file processing you may want to generate icons for available image thumbnails. Specifically, many JPEG images contain [EXIF thumbnails](https://vitali-fedulov.github.io/similar.pictures/jpeg-thumbnail-reader.html), you could considerably speedup the reads by using decoded thumbnails to feed into func `Icon`. External packages to read thumbnails: [1](https://github.com/dsoprea/go-exif) and [2](https://github.com/rwcarlsen/goexif). A note of caution: in rare cases there could be [issues](https://security.stackexchange.com/questions/116552/the-history-of-thumbnails-or-just-a-previous-thumbnail-is-embedded-in-an-image/201785#201785) with thumbnails not matching image content. EXIF standard specification: [1](https://www.media.mit.edu/pia/Research/deepview/exif.html) and [2](https://www.exif.org/Exif2-2.PDF).
**An alternative method to increase precision** instead of func 'CustomSimilar' is to generate icons for image sub-regions and compare those icons.

View File

@ -4,14 +4,14 @@ const (
// Icon parameters.
// Image resolution of the icon
// is very small (11x11 pixels), therefore original
// image details are lost in downsampling, except
// when source images have very low resolution
// (e.g. favicons or simple logos). This is useful
// from the privacy perspective if you are to use
// generated icons in a large searchable database.
iconSize = 11
// Image resolution of the icon is very small
// (11x11 pixels), therefore original image details
// are lost in downsampling, except when source images
// have very low resolution (e.g. favicons or simple
// logos). This is useful from the privacy perspective
// if you are to use generated icons in a large searchable
// database.
IconSize = 11 // Exported to be used in package imagehash.
// Resampling rate defines how much information
// (how many pixels) from the source image are used
// to generate an icon. Too few will produce worse
@ -30,7 +30,7 @@ const (
// Similarity thresholds.
// Euclidean distance threshold (squared) for Y-channel.
thY = float64(iconSize*iconSize) * float64(colorDiff*colorDiff) * euclCoeff
thY = float64(IconSize*IconSize) * float64(colorDiff*colorDiff) * euclCoeff
// Euclidean distance threshold (squared) for Cb and Cr channels.
thCbCr = thY * chanCoeff
// Proportion similarity threshold (5%).
@ -38,8 +38,8 @@ const (
// Auxiliary constants.
numPix = iconSize * iconSize
largeIconSize = iconSize*2 + 1
numPix = IconSize * IconSize
largeIconSize = IconSize*2 + 1
resizedImgSize = largeIconSize * samples
invSamplePixels2 = 1 / float64(samples*samples)
oneNinth = 1 / float64(9)

65
custom.go Normal file
View File

@ -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
}

72
custom_test.go Normal file
View File

@ -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")
}
}

39
icon.go
View File

@ -42,7 +42,7 @@ func IconNN(img image.Image) IconT {
// Resizing to a large icon approximating average color
// values of the source image. YCbCr space is used instead
// of RGB for better results in image comparison.
resImg, imgSize := resizeByNearest(
resImg, imgSize := ResizeByNearest(
img, image.Point{resizedImgSize, resizedImgSize})
largeIcon := sizedIcon(largeIconSize)
var r, g, b, sumR, sumG, sumB uint32
@ -62,7 +62,7 @@ func IconNN(img image.Image) IconT {
sumB += b >> 8
}
}
set(largeIcon, largeIconSize, image.Point{x, y},
Set(largeIcon, largeIconSize, image.Point{x, y},
float64(sumR)*invSamplePixels2,
float64(sumG)*invSamplePixels2,
float64(sumB)*invSamplePixels2)
@ -71,7 +71,7 @@ func IconNN(img image.Image) IconT {
// Box blur filter with resizing to the final icon of smaller size.
icon := sizedIcon(iconSize)
icon := sizedIcon(IconSize)
// Pixel positions in the final icon.
var xd, yd int
var c1, c2, c3, s1, s2, s3 float64
@ -85,14 +85,14 @@ func IconNN(img image.Image) IconT {
for n := -1; n <= 1; n++ {
for m := -1; m <= 1; m++ {
c1, c2, c3 =
get(largeIcon, largeIconSize,
Get(largeIcon, largeIconSize,
image.Point{x + n, y + m})
s1, s2, s3 = s1+c1, s2+c2, s3+c3
}
}
yc, cb, cr = yCbCr(
s1*oneNinth, s2*oneNinth, s3*oneNinth)
set(icon, iconSize, image.Point{xd, yd},
Set(icon, IconSize, image.Point{xd, yd},
yc, cb, cr)
s1, s2, s3 = 0, 0, 0
}
@ -106,7 +106,7 @@ func IconNN(img image.Image) IconT {
// with nil values, for example for convenient error handling.
// Then you can use icon.Pixels == nil condition.
func EmptyIcon() (icon IconT) {
icon = sizedIcon(iconSize)
icon = sizedIcon(IconSize)
icon.Pixels = nil
return icon
}
@ -125,7 +125,8 @@ func arrIndex(p image.Point, size, ch int) (index int) {
// Set places pixel values in an icon at a point.
// c1, c2, c3 are color values for each channel
// (RGB for example). Size is icon size.
func set(icon IconT, size int, p image.Point, c1, c2, c3 float64) {
// Exported to be used in package imagehash.
func Set(icon IconT, size int, p image.Point, c1, c2, c3 float64) {
// Multiplication by 255 is basically encoding float64 as uint16.
icon.Pixels[arrIndex(p, size, 0)] = uint16(c1 * 255)
icon.Pixels[arrIndex(p, size, 1)] = uint16(c2 * 255)
@ -135,7 +136,8 @@ func set(icon IconT, size int, p image.Point, c1, c2, c3 float64) {
// Get reads pixel values in an icon at a point.
// c1, c2, c3 are color values for each channel
// (RGB for example).
func get(icon IconT, size int, p image.Point) (c1, c2, c3 float64) {
// Exported to be used in package imagehash.
func Get(icon IconT, size int, p image.Point) (c1, c2, c3 float64) {
// Division by 255 is basically decoding uint16 into float64.
c1 = float64(icon.Pixels[arrIndex(p, size, 0)]) * one255th
c2 = float64(icon.Pixels[arrIndex(p, size, 1)]) * one255th
@ -224,10 +226,29 @@ func (icon IconT) ToRGBA(size int) *image.RGBA {
img := image.NewRGBA(image.Rect(0, 0, size, size))
for x := 0; x < size; x++ {
for y := 0; y < size; y++ {
r, g, b := get(icon, size, image.Point{x, y})
r, g, b := Get(icon, size, image.Point{x, y})
img.Set(x, y,
color.RGBA{uint8(r), uint8(g), uint8(b), 255})
}
}
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
}

View File

@ -59,7 +59,7 @@ func TestArrIndex(t *testing.T) {
func TestSet(t *testing.T) {
icon := sizedIcon(4)
set(icon, 4, image.Point{1, 1}, 13.5, 29.9, 95.9)
Set(icon, 4, image.Point{1, 1}, 13.5, 29.9, 95.9)
expected := sizedIcon(4)
expectedPixels := []float64{0, 0, 0, 0, 0, 13.5, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 29.9, 0, 0, 0, 0,
@ -82,7 +82,7 @@ func TestGet(t *testing.T) {
for i := range iconPix {
icon.Pixels[i] = uint16(iconPix[i] * 255)
}
c1, c2, c3 := get(icon, 4, image.Point{1, 1})
c1, c2, c3 := Get(icon, 4, image.Point{1, 1})
if math.Abs(float64(c1)-13.5) > 0.1 || math.Abs(float64(c2)-29.9) > 0.1 || math.Abs(float64(c3)-95.9) > 0.1 {
t.Errorf(
"Expected near 13.5, 29.9, 95.9, got %v, %v, %v.",
@ -156,3 +156,16 @@ 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
}
}

View File

@ -24,10 +24,10 @@ func Open(path string) (img image.Image, err error) {
return img, err
}
// resizeByNearest resizes an image to the destination size
// ResizeByNearest resizes an image to the destination size
// with the nearest neighbour method. It also returns the source
// image size.
func resizeByNearest(
func ResizeByNearest(
src image.Image, dstSize image.Point) (
dst image.RGBA, srcSize image.Point) {
// Original image size.
@ -49,8 +49,8 @@ func resizeByNearest(
for y := 0; y < dstSize.Y; y++ {
for x := 0; x < dstSize.X; x++ {
r, g, b, a = src.At(
int(float64(x)*xScale+float64(xMin)),
int(float64(y)*yScale+float64(yMin))).RGBA()
int(float64(x)*xScale)+xMin,
int(float64(y)*yScale)+yMin).RGBA()
dst.Set(x, y, color.RGBA{
uint8(r >> 8),
uint8(g >> 8),

View File

@ -35,7 +35,7 @@ func TestResizeByNearest(t *testing.T) {
if err != nil {
t.Error("Cannot decode", path.Join(testDir, table.outFile))
}
resampled, srcSize := resizeByNearest(inImg,
resampled, srcSize := ResizeByNearest(inImg,
image.Point{table.dstX, table.dstY})
if !reflect.DeepEqual(
outImg.(*image.RGBA), &resampled) ||

View File

@ -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,31 @@ 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
}
// iconB rotated 90 degrees.
if Similar(iconA, Rotate90(iconB)) {
return true
}
// As if iconB was rotated 270 degrees.
if Similar(Rotate90(iconA), iconB) {
return true
}
return false
}

View File

@ -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")
}
}

BIN
testdata/custom/1.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.8 KiB

BIN
testdata/custom/2.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 9.2 KiB

BIN
testdata/rotate/0.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

BIN
testdata/rotate/180.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

BIN
testdata/rotate/270.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 58 KiB

BIN
testdata/rotate/90.jpg vendored Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB