Skip to content

Commit

Permalink
init
Browse files Browse the repository at this point in the history
  • Loading branch information
n7olkachev committed Dec 12, 2020
0 parents commit aa53d49
Show file tree
Hide file tree
Showing 7 changed files with 302 additions and 0 deletions.
102 changes: 102 additions & 0 deletions cmd/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
package main

import (
"bufio"
"fmt"
"image"
_ "image/jpeg"
"image/png"
_ "image/png"
"imgdiff/pkg/imgdiff"
"log"
"os"
"sync"

"github.com/alexflint/go-arg"
. "github.com/logrusorgru/aurora"
)

func loadImages(filePathes ...string) []image.Image {
images := make([]image.Image, len(filePathes))

wg := sync.WaitGroup{}

for i, path := range filePathes {
wg.Add(1)

go func(i int, path string) {
file, err := os.Open(path)

defer file.Close()

if err != nil {
log.Fatalf("can't open image %s %s", path, err.Error())
}

image, _, err := image.Decode(file)

if err != nil {
log.Fatalf("can't decode image %s %s", path, err.Error())
}

images[i] = image

wg.Done()
}(i, path)
}

wg.Wait()

return images
}

func main() {
var args struct {
Threshold float64 `arg:"-t,--threshold" help:"Color difference threshold (from 0 to 1). Less more precise." default:"0.1"`
DiffImage bool `arg:"--diff-image" help:"Render image to the diff output instead of transparent background." default:"false"`
FailOnLayout bool `arg:"--fail-on-layout" help:"Do not compare images and produce output if images layout is different." default:"false"`
Base string `arg:"positional" help:"Base image."`
Compare string `arg:"positional" help:"Image to compare with."`
Output string `arg:"positional" help:"Output image path."`
}

arg.MustParse(&args)

images := loadImages(args.Base, args.Compare)

image1, image2 := images[0], images[1]

if args.FailOnLayout && !image1.Bounds().Eq(image2.Bounds()) {
fmt.Println(Red("Failure!").Bold(), "Images have different layout.")

os.Exit(2)
}

result := imgdiff.Diff(image1, image2, &imgdiff.Options{
Threshold: args.Threshold,
DiffImage: args.DiffImage,
})

if result.Equal {
fmt.Println(Green("Success!").Bold(), "Images are equal.")
return
}

enc := &png.Encoder{
CompressionLevel: png.BestSpeed,
}

f, _ := os.Create(args.Output)
defer f.Close()

writer := bufio.NewWriter(f)
defer writer.Flush()

enc.Encode(writer, result.Image)

fmt.Println(Red("Failure!").Bold(), "Images are different.")

fmt.Printf("Different pixels: %d\n", Red(result.DiffPixelsCount).Bold())

os.Exit(1)
}
8 changes: 8 additions & 0 deletions go.mod
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
module imgdiff

go 1.15

require (
github.com/alexflint/go-arg v1.3.0 // indirect
github.com/logrusorgru/aurora v2.0.3+incompatible // indirect
)
9 changes: 9 additions & 0 deletions go.sum
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
github.com/alexflint/go-arg v1.3.0 h1:UfldqSdFWeLtoOuVRosqofU4nmhI1pYEbT4ZFS34Bdo=
github.com/alexflint/go-arg v1.3.0/go.mod h1:9iRbDxne7LcR/GSvEr7ma++GLpdIU1zrghf2y2768kM=
github.com/alexflint/go-scalar v1.0.0 h1:NGupf1XV/Xb04wXskDFzS0KWOLH632W/EO4fAFi+A70=
github.com/alexflint/go-scalar v1.0.0/go.mod h1:GpHzbCOZXEKMEcygYQ5n/aa4Aq84zbxjy3MxYW0gjYw=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8=
github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
76 changes: 76 additions & 0 deletions pkg/imgdiff/imgdiff.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package imgdiff

import (
"image"
"image/color"
"imgdiff/pkg/yiq"
"runtime"
"sync"
"sync/atomic"
)

// Options struct.
type Options struct {
Threshold float64
DiffImage bool
}

// Result struct.
type Result struct {
Equal bool
Image image.Image
DiffPixelsCount uint64
}

// Diff between two images.
func Diff(image1 image.Image, image2 image.Image, options *Options) *Result {
diffPixelsCount := uint64(0)

maxDelta := yiq.MaxDelta * options.Threshold * options.Threshold

diff := image.NewNRGBA(image1.Bounds())

wg := sync.WaitGroup{}

cpus := runtime.NumCPU()

for i := 0; i < cpus; i++ {
wg.Add(1)

go func(i int) {
diffPixelsCounter := 0

for y := i; y <= image1.Bounds().Max.Y; y += cpus {
for x := 0; x <= image1.Bounds().Max.X; x++ {
pixel1, pixel2 := image1.At(x, y), image2.At(x, y)

if pixel1 != pixel2 {
delta := yiq.Delta(pixel1, pixel2)

if delta > maxDelta {
diff.SetNRGBA(x, y, color.NRGBA{R: 255, G: 0, B: 0, A: 255})

diffPixelsCounter++
}
} else if options.DiffImage {
diff.Set(x, y, pixel1)
}
}
}

if diffPixelsCounter > 0 {
atomic.AddUint64(&diffPixelsCount, uint64(diffPixelsCounter))
}

wg.Done()
}(i)
}

wg.Wait()

return &Result{
Equal: diffPixelsCount == 0,
DiffPixelsCount: diffPixelsCount,
Image: diff,
}
}
38 changes: 38 additions & 0 deletions pkg/yiq/delta.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
package yiq

import (
"image/color"
)

func normalize(rgba color.Color) (uint8, uint8, uint8, uint8) {
r, g, b, a := rgba.RGBA()

return uint8(r), uint8(g), uint8(b), uint8(a)
}

func rgb2y(r, g, b uint8) float64 {
return float64(r)*0.29889531 + float64(g)*0.58662247 + float64(b)*0.11448223
}

func rgb2i(r, g, b uint8) float64 {
return float64(r)*0.59597799 - float64(g)*0.27417610 - float64(b)*0.32180189
}

func rgb2q(r, g, b uint8) float64 {
return float64(r)*0.21147017 - float64(g)*0.52261711 + float64(b)*0.31114694
}

// Delta between two pixels.
func Delta(pixelA, pixelB color.Color) float64 {
r1, g1, b1, _ := normalize(pixelA)
r2, g2, b2, _ := normalize(pixelB)

y := rgb2y(r1, g1, b1) - rgb2y(r2, g2, b2)
i := rgb2i(r1, g1, b1) - rgb2i(r2, g2, b2)
q := rgb2q(r1, g1, b1) - rgb2q(r2, g2, b2)

return 0.5053*y*y + 0.299*i*i + 0.1957*q*q
}

// MaxDelta is a max value of Delta func.
var MaxDelta = 35215.0
47 changes: 47 additions & 0 deletions readme.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# imgdiff

Faster than [the fastest in the world pixel-by-pixel image difference tool](https://github.com/dmtrKovalenko/odiff).

## Why?

imgdiff isn't as fast as a tool like this should be and I'm not proud of it, but it is 3X faster than
[the fastest in the world pixel-by-pixel image difference tool](https://github.com/dmtrKovalenko/odiff),
so maybe you'll find it useful.

## Features

It can do everything [odiff](https://github.com/dmtrKovalenko/odiff) can. Faster.

## Benchmarks

[Cypress image](https://github.com/dmtrKovalenko/odiff/blob/main/images/www.cypress.io.png) 3446 x 10728

| Command | Mean [s] | Min [s] | Max [s] | Relative |
| :------------------------------------------------------------- | ------------: | ------: | ------: | -------: |
| `imgdiff images/cypress-1.png images/cypress-2.png output.png` | 1.442 ± 0.012 | 1.420 | 1.462 | 1.00 |
| `odiff images/cypress-1.png images/cypress-2.png output.png` | 6.475 ± 0.092 | 6.300 | 6.583 | 4.49 |

[Water image](https://github.com/dmtrKovalenko/odiff/blob/main/images/water-4k.png) 8400 x 4725

| Command | Mean [s] | Min [s] | Max [s] | Relative |
| :--------------------------------------------------------- | -------------: | ------: | ------: | -------: |
| `imgdiff images/water-1.png images/water-2.png output.png` | 1.908 ± 0.0058 | 1.841 | 2.002 | 1.00 |
| `odiff images/water-1.png images/water-2.png output.png` | 6.016 ± 0.415 | 5.643 | 7.140 | 3.15 |

## Usage

```
Usage: imgdiff [--threshold THRESHOLD] [--diff-image] [--fail-on-layout] BASE COMPARE OUTPUT
Positional arguments:
BASE Base image.
COMPARE Image to compare with.
OUTPUT Output image path.
Options:
--threshold THRESHOLD, -t THRESHOLD
Color difference threshold (from 0 to 1). Less more precise. [default: 0.1]
--diff-image Render image to the diff output instead of transparent background. [default: false]
--fail-on-layout Do not compare images and produce output if images layout is different. [default: false]
--help, -h display this help and exit
```
22 changes: 22 additions & 0 deletions scripts/build.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
#!/usr/bin/env bash

package_name=imgdiff

platforms=("windows/amd64" "windows/386" "linux/386" "linux/amd64" "darwin/amd64")

for platform in "${platforms[@]}"
do
platform_split=(${platform//\// })
GOOS=${platform_split[0]}
GOARCH=${platform_split[1]}
output_name=$package_name'-'$GOOS'-'$GOARCH
if [ $GOOS = "windows" ]; then
output_name+='.exe'
fi

env GOOS=$GOOS GOARCH=$GOARCH go build -o $output_name cmd/main.go
if [ $? -ne 0 ]; then
echo 'An error has occurred! Aborting the script execution...'
exit 1
fi
done

0 comments on commit aa53d49

Please sign in to comment.