blob: aa1a791694cd2868bfb5da34c8f352eb35c1ab2d [file] [log] [blame]
package main
import (
"bytes"
"context"
"image"
"image/png"
"io"
"os"
"path/filepath"
"strings"
"github.com/spf13/cobra"
"go.skia.org/infra/go/skerr"
"go.skia.org/infra/go/util"
"go.skia.org/infra/gold-client/go/imgmatching"
"go.skia.org/infra/gold-client/go/imgmatching/exact"
"go.skia.org/infra/gold-client/go/imgmatching/fuzzy"
"go.skia.org/infra/gold-client/go/imgmatching/positive_if_only_image"
"go.skia.org/infra/gold-client/go/imgmatching/sample_area"
"go.skia.org/infra/gold-client/go/imgmatching/sobel"
)
// matchEnv provides the environment for the match command.
type matchEnv struct {
algorithmName string
parameters []string
}
// getMatchCmd returns the definition of the match command.
func getMatchCmd() *cobra.Command {
env := &matchEnv{}
cmd := &cobra.Command{
Use: "match",
Short: "Runs an image matching algorithm against two images on disk",
Long: `
Takes a (potentially non-exact) image matching algorithm (and any algorithm-specific parameters)
and executes it against the two given images on disk.
Reports whether or not the specified algorithm considers the two images to be equivalent.
This command is intended for experimenting with various combinations of non-exact image matching
algorithms and parameters.
`,
Args: cobra.ExactArgs(2), // Takes exactly two images as positional arguments.
Run: env.runMatchCmd,
}
cmd.Flags().StringVar(&env.algorithmName, "algorithm", "", "Image matching algorithm (e.g. exact, fuzzy, sobel).")
cmd.Flags().StringArrayVar(&env.parameters, "parameter", []string{}, "Any number of algorithm-specific parameters represented as name:value pairs (e.g. sobel_edge_threshold:10).")
must(cmd.MarkFlagRequired("algorithm"))
return cmd
}
func (m *matchEnv) runMatchCmd(cmd *cobra.Command, args []string) {
ctx := cmd.Context()
m.Match(ctx, args[0], args[1])
}
// Match instantiates the specified image matching algorithm and runs it against two images.
func (m *matchEnv) Match(ctx context.Context, leftFile, rightFile string) {
// Load input images.
leftImage, err := loadPng(leftFile)
ifErrLogExit(ctx, err)
rightImage, err := loadPng(rightFile)
ifErrLogExit(ctx, err)
// Instantiate the specified algorithm.
optionalKeys, err := makeOptionalKeys(m.algorithmName, m.parameters)
ifErrLogExit(ctx, err)
algorithmName, matcher, err := imgmatching.MakeMatcher(optionalKeys)
ifErrLogExit(ctx, err)
// Run the algorithm against the two input images.
imagesMatch := matcher.Match(leftImage, rightImage)
// Report the algorithm's boolean output.
if imagesMatch {
logInfo(ctx, "Images match.\n")
} else {
logInfo(ctx, "Images do not match.\n")
}
// Print out algorithm-specific debug information.
switch algorithmName {
case imgmatching.ExactMatching:
printOutExactDebugInfo(ctx, matcher.(*exact.Matcher))
case imgmatching.FuzzyMatching:
printOutFuzzyDebugInfo(ctx, matcher.(*fuzzy.Matcher))
case imgmatching.PositiveIfOnlyImageMatching:
printOutPositiveIfOnlyImageDebugInfo(ctx, matcher.(*positive_if_only_image.Matcher))
case imgmatching.SampleAreaMatching:
printOutSampleAreaDebugInfo(ctx, matcher.(*sample_area.Matcher))
case imgmatching.SobelFuzzyMatching:
err := printOutSobelDebugInfo(ctx, matcher.(*sobel.Matcher))
ifErrLogExit(ctx, err)
}
exitProcess(ctx, 0)
}
// loadPng loads a PNG image from disk.
func loadPng(fileName string) (image.Image, error) {
// Load the image and save the bytes because we need to return them.
imgBytes, err := os.ReadFile(fileName)
if err != nil {
return nil, skerr.Wrapf(err, "loading file %s", fileName)
}
img, err := png.Decode(bytes.NewReader(imgBytes))
if err != nil {
return nil, skerr.Wrapf(err, "decoding PNG file %s", fileName)
}
return img, nil
}
// makeOptionalKeys turns the specified algorithm name and parameters into the equivalent map of
// optional keys that function imgmatching.MakeMatcher() expects.
func makeOptionalKeys(algorithmName string, parameters []string) (map[string]string, error) {
keys := map[string]string{
imgmatching.AlgorithmNameOptKey: algorithmName,
}
for _, pair := range parameters {
split := strings.SplitN(pair, ":", 2)
if len(split) != 2 {
return nil, skerr.Fmt("parameter %q must be a key:value pair", pair)
}
keys[split[0]] = split[1]
}
return keys, nil
}
// printOutExactDebugInfo prints out stats debug info reported by the given exact.Matcher.
func printOutExactDebugInfo(ctx context.Context, matcher *exact.Matcher) {
printDebugInfoItem(ctx, "Last different pixel found", matcher.LastDifferentPixelFound())
}
// printOutFuzzyDebugInfo prints out stats reported by the given fuzzy.Matcher.
func printOutFuzzyDebugInfo(ctx context.Context, matcher *fuzzy.Matcher) {
printDebugInfoItem(ctx, "Number of different pixels", matcher.NumDifferentPixels())
printDebugInfoItem(ctx, "Maximum delta", matcher.MaxPixelDelta())
printDebugInfoItem(ctx, "Pixel comparison method", matcher.PixelComparisonMethod())
}
// printOutPositiveIfOnlyImageDebugInfo prints out stats reported by the given
// positive_if_only_image.Matcher.
func printOutPositiveIfOnlyImageDebugInfo(ctx context.Context, matcher *positive_if_only_image.Matcher) {
printDebugInfoItem(ctx, "Last different pixel found", matcher.LastDifferentPixelFound())
}
// printOutSampleAreaDebugInfo prints out stats reported by the given
// sample_area.Matcher.
func printOutSampleAreaDebugInfo(ctx context.Context, matcher *sample_area.Matcher) {
printDebugInfoItem(ctx, "Failed sample area", matcher.FailedSampleArea())
printDebugInfoItem(ctx, "Number of different pixels", matcher.NumDifferentPixels())
printDebugInfoItem(ctx, "Sample area width too small", matcher.SampleAreaWidthTooSmall())
printDebugInfoItem(ctx, "Sample area width too large", matcher.SampleAreaWidthTooLarge())
printDebugInfoItem(ctx, "Max different pixels per area out of range", matcher.MaxDifferentPixelsPerAreaOutOfRange())
printDebugInfoItem(ctx, "Sample area pixel delta threshold out of range", matcher.SampleAreaChannelDeltaThresholdOutOfRange())
}
// printOutSobelDebugInfo writes intermediate images generated by the given sobel.Matcher to a
// temporary directory and prints out the resulting paths. It also prints out the stats reported by
// the embedded fuzzy.Matcher.
func printOutSobelDebugInfo(ctx context.Context, matcher *sobel.Matcher) error {
tempDir, err := os.MkdirTemp("", "goldctl-*")
if err != nil {
return skerr.Wrap(err)
}
writeToDiskAndPrintOut := func(ctx context.Context, msg, filename string, img image.Image) error {
p := filepath.Join(tempDir, filename)
err := writePngToTmp(img, p)
if err != nil {
return skerr.Wrap(err)
}
printDebugInfoItem(ctx, msg, p)
return nil
}
err = writeToDiskAndPrintOut(ctx, "Sobel operator output (left image)", "sobel-output.png", matcher.SobelOutput())
if err != nil {
return skerr.Wrap(err)
}
err = writeToDiskAndPrintOut(ctx, "Left image with edges removed", "image1-no-edges.png", matcher.ExpectedImageWithEdgesRemoved())
if err != nil {
return skerr.Wrap(err)
}
err = writeToDiskAndPrintOut(ctx, "Right image with edges removed", "image2-no-edges.png", matcher.ActualImageWithEdgesRemoved())
if err != nil {
return skerr.Wrap(err)
}
printOutFuzzyDebugInfo(ctx, &matcher.Matcher)
return nil
}
// writePngToTmp takes an image, saves it to disk as a PNG image with the given filename.
func writePngToTmp(img image.Image, filename string) error {
err := util.WithWriteFile(filename, func(writer io.Writer) error {
err := png.Encode(writer, img)
if err != nil {
return skerr.Wrap(err)
}
return nil
})
if err != nil {
return skerr.Wrap(err)
}
return nil
}
// printDebugInfoItem prints out the given debug name/value pair. The name is left-padded so that
// multiple calls to this function produce vertically aligned output that is easier to read.
func printDebugInfoItem(ctx context.Context, name string, value interface{}) {
logInfof(ctx, "%36s: %v\n", name, value)
}