Compare commits
56 Commits
Author | SHA1 | Date |
---|---|---|
|
ca2e7db1ef | |
|
8be73dbe53 | |
|
93bdbb726e | |
|
c678c1ca69 | |
|
9f6aaf3559 | |
|
025ab4f57c | |
|
9fe8146963 | |
|
9d98521dde | |
|
9014e3e259 | |
|
f7f7753d86 | |
|
53fdf151da | |
|
c075bc302b | |
|
993aa184dd | |
|
4232862259 | |
|
f543cf2753 | |
|
74342b23ec | |
|
02d2b3820f | |
|
55b39084a6 | |
|
345978fc35 | |
|
a2d3d82283 | |
|
671cb5804b | |
|
f053061208 | |
|
93d6a8e184 | |
|
8ce8881dc3 | |
|
2508efb096 | |
|
b2158637eb | |
|
2e9897112b | |
|
1397aba8f7 | |
|
000989d598 | |
|
452a1e3c4e | |
|
1b6203e206 | |
|
5bdf5c20eb | |
|
6b727a318d | |
|
80fc36e5d9 | |
|
cf61a088fb | |
|
3ab82bae8c | |
|
29ed7bcd63 | |
|
b449cd165a | |
|
5789b77bae | |
|
ca8ad8c9a3 | |
|
53ce9b0e5b | |
|
16fff10800 | |
|
1c65899315 | |
|
1ef1b21753 | |
|
882661b349 | |
|
1e5072e9ce | |
|
1c8f4aad8f | |
|
302c8b5483 | |
|
c66aa66a66 | |
|
622a3c989c | |
|
009a20cb7c | |
|
4258b41f4d | |
|
4a91e3f704 | |
|
e87c2e6f87 | |
|
91b8862cbc | |
|
cae2d51031 |
81
README.md
81
README.md
|
@ -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.
|
||||
|
|
22
const.go
22
const.go
|
@ -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)
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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
39
icon.go
|
@ -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
|
||||
}
|
||||
|
|
17
icon_test.go
17
icon_test.go
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
8
image.go
8
image.go
|
@ -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),
|
||||
|
|
|
@ -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) ||
|
||||
|
|
|
@ -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
|
||||
}
|
||||
|
|
|
@ -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")
|
||||
}
|
||||
}
|
||||
|
|
Binary file not shown.
After Width: | Height: | Size: 8.8 KiB |
Binary file not shown.
After Width: | Height: | Size: 9.2 KiB |
Binary file not shown.
After Width: | Height: | Size: 51 KiB |
Binary file not shown.
After Width: | Height: | Size: 58 KiB |
Binary file not shown.
After Width: | Height: | Size: 58 KiB |
Binary file not shown.
After Width: | Height: | Size: 52 KiB |
Loading…
Reference in New Issue