Golang图像处理测试指南

Golang图像处理测试指南 我创建了一个输出图像的包,现在我想为该包创建一系列测试。我知道,测试本应在更早的时候创建,而不是在我几乎完成包的时候才做,但事已至此。

那么,我的问题是:当输出是图像时,你如何创建测试?当然,我可以比较存储的正确图像和输出图像的字节,如果它们匹配,一切就正常。

但是,根据我最初的测试,每个新创建的图像(在我的例子中是PNG格式)都与之前创建的图像不同。当我用meld比较图像时,看起来文件中有很多变化,不仅仅是元数据,比如创建日期等。不过,这些图像在视觉上是相同的,而这正是我想要测试的部分,即视觉部分。

对我来说,这听起来像是其他人也一定遇到过的问题?那么,你如何为图像输出创建测试呢?

我的包具体做什么并不重要,但作为参考,它创建国际象棋棋盘的图像:GitHub - Hultan/chessImager: ChessImager 是一个 Go 包,它根据 FEN 字符串创建国际象棋棋盘的图像。它具有高度可配置性,因此你可以创建看起来完全符合你期望的棋盘图像。


更多关于Golang图像处理测试指南的实战教程也可以访问 https://www.itying.com/category-94-b0.html

6 回复

嗯,你可能是对的,等我完成几个测试后我们就能知道了。也许有些情况下我需要近似匹配。

更多关于Golang图像处理测试指南的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


当然可以,但是如果你修改了代码中的任何内容,你也应该修改所有的测试。 例如,你不能升级你生成图片的组件。

我可能在最初的测试中做了些蠢事。现在,第二次创建后,我没有在图像中看到任何变化。

所以,看起来可以逐字节地比较图像。

有趣的是,我认为在我的情况下,我只需要检查完全匹配。因此,我认为直接逐字节比较应该比在每个像素上进行大量数学运算更快。

我不需要知道是否接近,它要么匹配,要么不匹配。

你好,你好吗? 我将一个服务从Java迁移到了Go,该服务负责转换不同格式的图像。 为了比较结果,我们编写了一个小脚本,从两个服务获取图像并进行比较。 这里有一些技术可以参考:

如何用Go比较图像?

在图像处理测试中,直接比较字节确实不可靠,因为PNG编码器可能产生不同的字节输出(时间戳、压缩参数等)。以下是几种实用的测试方法:

1. 像素级比较(推荐)

比较图像的实际像素数据,忽略元数据:

import (
    "image"
    "testing"
    
    _ "image/png"
    "os"
)

func TestImagePixels(t *testing.T) {
    // 生成测试图像
    got := generateChessBoard()
    
    // 加载预期图像
    wantFile, err := os.Open("testdata/expected.png")
    if err != nil {
        t.Fatalf("无法打开预期图像: %v", err)
    }
    defer wantFile.Close()
    
    want, _, err := image.Decode(wantFile)
    if err != nil {
        t.Fatalf("无法解码预期图像: %v", err)
    }
    
    // 比较尺寸
    if got.Bounds() != want.Bounds() {
        t.Errorf("图像尺寸不匹配: got %v, want %v", got.Bounds(), want.Bounds())
    }
    
    // 比较每个像素
    bounds := got.Bounds()
    for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
        for x := bounds.Min.X; x < bounds.Max.X; x++ {
            gR, gG, gB, gA := got.At(x, y).RGBA()
            wR, wG, wB, wA := want.At(x, y).RGBA()
            
            if gR != wR || gG != wG || gB != wB || gA != wA {
                t.Errorf("像素不匹配 at (%d, %d): got (%d,%d,%d,%d), want (%d,%d,%d,%d)",
                    x, y, gR, gG, gB, gA, wR, wG, wB, wA)
                return
            }
        }
    }
}

2. 使用go-cmp进行灵活比较

import (
    "testing"
    "github.com/google/go-cmp/cmp"
    "image/color"
)

func TestImageWithGoCmp(t *testing.T) {
    got := generateChessBoard()
    want := loadExpectedImage()
    
    // 自定义比较选项
    opts := []cmp.Option{
        cmp.Comparer(func(x, y color.Color) bool {
            xr, xg, xb, xa := x.RGBA()
            yr, yg, yb, ya := y.RGBA()
            return xr == yr && xg == yg && xb == yb && xa == ya
        }),
        cmp.AllowUnexported(image.RGBA{}),
    }
    
    if diff := cmp.Diff(want, got, opts...); diff != "" {
        t.Errorf("图像不匹配 (-want +got):\n%s", diff)
    }
}

3. 哈希比较(快速但可能误判)

import (
    "crypto/md5"
    "image"
    "testing"
)

func TestImageHash(t *testing.T) {
    got := generateChessBoard()
    want := loadExpectedImage()
    
    // 计算像素数据的哈希
    gotHash := hashImage(got)
    wantHash := hashImage(want)
    
    if gotHash != wantHash {
        t.Errorf("图像哈希不匹配: got %x, want %x", gotHash, wantHash)
    }
}

func hashImage(img image.Image) [16]byte {
    bounds := img.Bounds()
    hash := md5.New()
    
    for y := bounds.Min.Y; y < bounds.Max.Y; y++ {
        for x := bounds.Min.X; x < bounds.Max.X; x++ {
            r, g, b, a := img.At(x, y).RGBA()
            hash.Write([]byte{
                byte(r >> 8), byte(r & 0xFF),
                byte(g >> 8), byte(g & 0xFF),
                byte(b >> 8), byte(b & 0xFF),
                byte(a >> 8), byte(a & 0xFF),
            })
        }
    }
    
    var result [16]byte
    copy(result[:], hash.Sum(nil))
    return result
}

4. 针对国际象棋棋盘的特定测试

func TestChessBoardSpecific(t *testing.T) {
    board := generateChessBoard()
    bounds := board.Bounds()
    
    // 测试棋盘格颜色
    // 假设棋盘是8x8,每个格子50x50像素
    cellSize := 50
    
    // 测试a1应该是深色格子
    a1Color := board.At(cellSize/2, bounds.Max.Y-cellSize/2)
    expectedDark := color.RGBA{181, 136, 99, 255} // 典型深色格子
    
    if !colorsEqual(a1Color, expectedDark) {
        t.Errorf("a1格子颜色错误: got %v, want %v", a1Color, expectedDark)
    }
    
    // 测试h8应该是深色格子
    h8Color := board.At(bounds.Max.X-cellSize/2, cellSize/2)
    if !colorsEqual(h8Color, expectedDark) {
        t.Errorf("h8格子颜色错误: got %v, want %v", h8Color, expectedDark)
    }
}

func colorsEqual(c1, c2 color.Color) bool {
    r1, g1, b1, a1 := c1.RGBA()
    r2, g2, b2, a2 := c2.RGBA()
    return r1 == r2 && g1 == g2 && b1 == b2 && a1 == a2
}

5. 黄金文件测试模式

// testdata/golden_test.go
package chessimager_test

import (
    "flag"
    "image/png"
    "os"
    "path/filepath"
    "testing"
)

var update = flag.Bool("update", false, "更新黄金文件")

func TestGoldenImages(t *testing.T) {
    testCases := []struct {
        name     string
        fen      string
        filename string
    }{
        {"起始位置", "rnbqkbnr/pppppppp/8/8/8/8/PPPPPPPP/RNBQKBNR", "start_position.png"},
        {"王车易位后", "r3k2r/pppppppp/8/8/8/8/PPPPPPPP/R3K2R", "castled.png"},
    }
    
    for _, tc := range testCases {
        t.Run(tc.name, func(t *testing.T) {
            // 生成图像
            img := generateBoardFromFEN(tc.fen)
            
            goldenPath := filepath.Join("testdata", tc.filename)
            
            if *update {
                // 更新黄金文件
                f, err := os.Create(goldenPath)
                if err != nil {
                    t.Fatal(err)
                }
                defer f.Close()
                png.Encode(f, img)
                return
            }
            
            // 与黄金文件比较
            compareWithGolden(t, img, goldenPath)
        })
    }
}

对于你的国际象棋图像包,建议结合使用像素级比较和特定领域测试。像素比较确保视觉一致性,特定测试验证棋盘布局的正确性。使用-update标志可以方便地更新测试数据。

回到顶部