blob: 0e63c10d2abb041dd8d1fd992339b239cb4a1cd1 [file] [log] [blame]
package dynamicdiff
import (
"fmt"
"image"
"path/filepath"
"strings"
"go.skia.org/infra/go/sklog"
"go.skia.org/infra/go/util"
"go.skia.org/infra/golden/go/diff"
"go.skia.org/infra/golden/go/diffstore"
)
type DynamicDiffMetrics struct {
// NumDiffPixels is the number of static pixels that are different.
NumDiffPixels int `json:"numDiffPixels"`
// PixelDiffPercent is the percentage of static pixels that are different.
PixelDiffPercent float32 `json:"pixelDiffPercent"`
// MaxRGBDiffs contains the maximum difference of each channel.
MaxRGBDiffs []int `json:"maxRGBDiffs"`
// NumStaticPixels is the total number of static pixels.
NumStaticPixels int `json:"numStaticPixels"`
// NumDynamicPixels is the total number of dynamic pixels. Note that
// NumStaticPixels + NumDynamicPixels = number of total pixels.
NumDynamicPixels int `json:"numDynamicPixels"`
}
// PixelDiffStoreMapper implements the diffstore.DiffStoreMapper interface.
// It uses instances of
// of an imageID is: runID/{nopatch/withpatch}/rank/URLfilename. A runID has the
// format userID-timeStamp.
type PixelDiffStoreMapper struct {
util.LRUCodec
}
// NewPixelDiffStoreMapper returns a new instance of PixelDiffStoreMapper with
// a codec that encodes/decodes instance of DynamicDiffMetrics to/from JSON.
func NewPixelDiffStoreMapper(diffInstance interface{}) diffstore.DiffStoreMapper {
return PixelDiffStoreMapper{LRUCodec: util.JSONCodec(&DynamicDiffMetrics{})}
}
// DiffFn implements the diffstore.DiffStoreMapper interface.
func (g PixelDiffStoreMapper) DiffFn(leftImg *image.NRGBA, rightImg *image.NRGBA) (interface{}, *image.NRGBA) {
return DynamicContentDiff(leftImg, rightImg)
}
// DiffID implements the diffstore.DiffStoreMapper interface.
func (p PixelDiffStoreMapper) DiffID(leftImgID, rightImgID string) string {
// Return a string containing the common runID, rank and URL of the two image paths.
path := strings.Split(leftImgID, "/")
return strings.Join([]string{path[0], path[2], path[3]}, ":")
}
// SplitDiffID implements the diffstore.DiffStoreMapper interface.
func (p PixelDiffStoreMapper) SplitDiffID(diffID string) (string, string) {
path := strings.Split(diffID, ":")
return filepath.Join(path[0], "nopatch", path[1], path[2]),
filepath.Join(path[0], "withpatch", path[1], path[2])
}
// DiffPath implements the diffstore.DiffStoreMapper interface.
func (p PixelDiffStoreMapper) DiffPath(leftImgID, rightImgID string) string {
path := strings.Split(leftImgID, "/")
imageName := path[0] + "/" + path[3]
return fmt.Sprintf("%s.%s", imageName, diffstore.IMG_EXTENSION)
}
// ImagePaths implements the diffstore.DiffStoreMapper interface.
func (p PixelDiffStoreMapper) ImagePaths(imageID string) (string, string, string) {
localPath := fmt.Sprintf("%s.%s", imageID, diffstore.IMG_EXTENSION)
path := strings.Split(imageID, "/")
runID := strings.Split(path[0], "-")
timeStamp := runID[1]
datePath := filepath.Join(timeStamp[0:4], timeStamp[4:6], timeStamp[6:8], timeStamp[8:10])
gsPath := filepath.Join(datePath, localPath)
return localPath, "", gsPath
}
// IsValidDiffImgID implements the diffstore.DiffStoreMapper interface.
func (p PixelDiffStoreMapper) IsValidDiffImgID(diffImgID string) bool {
path := strings.Split(diffImgID, "/")
return len(path) == 2
}
// IsValidImgID implements the diffstore.DiffStoreMapper interface.
func (p PixelDiffStoreMapper) IsValidImgID(imgID string) bool {
path := strings.Split(imgID, "/")
return len(path) == 4
}
// DynamicContentDiff is a function that calculates the DiffMetrics and diff
// image for the provided images, taking into account that pixels with dynamic
// content are marked cyan and removing such pixels from the calculations. The
// images are assumed to have the same dimensions.
func DynamicContentDiff(left, right *image.NRGBA) (*DynamicDiffMetrics, *image.NRGBA) {
bounds := left.Bounds()
resultImg := image.NewNRGBA(image.Rect(0, 0, bounds.Dx(), bounds.Dy()))
// Pix is a []uint8 of R, G, B, A, R, G, B, A, ... values.
p1 := left.Pix
p2 := right.Pix
numStaticPixels := 0
numDynamicPixels := 0
numDiffPixels := 0
maxRGBDiffs := make([]int, 3)
// Each pixel consists of 4 values (R, G, B, A). Alpha is ignored for diff
// purposes.
for i := 0; i < len(p1); i += 4 {
r, g, b := p1[i+0], p1[i+1], p1[i+2]
R, G, B := p2[i+0], p2[i+1], p2[i+2]
// Ignore pixels with dynamic content, mark the pixel in the diff image as
// dynamic, and increment the count of dynamic pixels.
if isDynamicContentPixel(r, g, b) || isDynamicContentPixel(R, G, B) {
copy(resultImg.Pix[i:], []uint8{0, 255, 255, 255})
numDynamicPixels++
continue
}
// Increment the count of static pixels.
numStaticPixels++
// If the pixels do not have the same RGB values, update the diff metrics
// and the diff image.
if r != R || g != G || b != B {
numDiffPixels++
dr := util.AbsInt(int(r) - int(R))
dg := util.AbsInt(int(g) - int(G))
db := util.AbsInt(int(b) - int(B))
maxRGBDiffs[0] = util.MaxInt(dr, maxRGBDiffs[0])
maxRGBDiffs[1] = util.MaxInt(dg, maxRGBDiffs[1])
maxRGBDiffs[2] = util.MaxInt(db, maxRGBDiffs[2])
copy(resultImg.Pix[i:], diff.PixelDiffColor[deltaOffset(dr+dg+db)])
}
}
return &DynamicDiffMetrics{
NumDiffPixels: numDiffPixels,
PixelDiffPercent: diff.GetPixelDiffPercent(numDiffPixels, numStaticPixels),
MaxRGBDiffs: maxRGBDiffs,
NumStaticPixels: numStaticPixels,
NumDynamicPixels: numDynamicPixels,
}, resultImg
}
// If the pixel is cyan, it contains dynamic content. This reflects the current
// behavior of the CT screenshot benchmark when the dynamic content detection
// flag is enabled.
func isDynamicContentPixel(red, green, blue uint8) bool {
return red == 0 && green == 255 && blue == 255
}
// If the pixels don't have the same value, the minimum value that can be passed
// to this function is 1 and the maximum is 255*3 = 765. We must convert the
// range [1, 765] to the range [1, 7] in order to select the correct offset
// into the diff.PixelDiffColor slice. To convert a number n from range [x, y]
// to [a, b], we use the following formula:
// (b - a)(n - x)
// f(n) = -------------- + a
// y - x
func deltaOffset(n int) int {
ret := 6*(n-1)/764 + 1
if ret < 1 || ret > 7 {
sklog.Fatalf("Input out of range [1, 765]: %d", n)
}
return ret - 1
}