【问题标题】:In .Net is System.Drawing.Image.Save deterministic?在 .Net 中 System.Drawing.Image.Save 是确定性的吗?
【发布时间】:2015-07-08 19:18:55
【问题描述】:

我正在尝试通过它们的字节内容比较两个图像。但是,它们不匹配。

两个图像都是从同一个源图像生成的,使用相同的方法和相同的参数。我猜测图像生成中的某些内容或我转换为字节数组的方式不是确定性的。有谁知道非确定性行为发生在哪里以及我是否可以轻松地为我的单元测试强制执行确定性行为?

我的测试类中的这个方法将图像转换为字节数组 - image.Save 是确定性的吗? memStream.ToArray() 是确定性的吗?

private static byte[] ImageToByteArray(Image image)
{
    byte[] actualBytes;
    using (MemoryStream memStream = new MemoryStream())
    {
        image.Save(memStream, ImageFormat.Bmp);
        actualBytes = memStream.ToArray();
    }
    return actualBytes;
}

这是失败的单元测试 - TestImageLandscapeDesertResized_300_300 是使用 ImageHelper.ResizeImage(testImageLandscape, 300, 300)TestImageLandscapeDesert 生成的,然后在加载到项目的资源文件之前保存到文件中。如果我的代码中的所有调用都基于我的输入参数是确定性的,那么这个测试应该通过。

public void ResizeImage_Landscape_SmallerLandscape()
{
    Image testImageLandscape = Resources.TestImageLandscapeDesert;
    Image expectedImage = Resources.TestImageLandscapeDesertResized_300_300;
    byte[] expectedBytes = ImageToByteArray(expectedImage);
    byte[] actualBytes;
    using (Image resizedImage = ImageHelper.ResizeImage(testImageLandscape, 300, 300))
    {
        actualBytes = ImageToByteArray(resizedImage);
    }
    Assert.IsTrue(expectedBytes.SequenceEqual(actualBytes));
}

正在测试的方法 - 此方法将缩小输入图像,使其高度和宽度小于maxHeightmaxWidth,保留现有的纵横比。一些图形调用可能是不确定的,我无法从 Microsoft 有限的文档中看出。

public static Image ResizeImage(Image image, int maxWidth, int maxHeight)
{
    decimal width = image.Width;
    decimal height = image.Height;
    decimal newWidth;
    decimal newHeight;

    //Calculate new width and height
    if (width > maxWidth || height > maxHeight)
    {
        // need to preserve the original aspect ratio
        decimal originalAspectRatio = width / height;

        decimal widthReductionFactor = maxWidth / width;
        decimal heightReductionFactor = maxHeight / height;

        if (widthReductionFactor < heightReductionFactor)
        {
            newWidth = maxWidth;
            newHeight = newWidth / originalAspectRatio;
        }
        else
        {
            newHeight = maxHeight;
            newWidth = newHeight * originalAspectRatio;
        }
    }

    else
        //Return a copy of the image if smaller than allowed width and height
        return new Bitmap(image);

    //Resize image
    Bitmap bitmap = new Bitmap((int)newWidth, (int)newHeight, PixelFormat.Format48bppRgb);
    Graphics graphic = Graphics.FromImage(bitmap);
    graphic.InterpolationMode = InterpolationMode.HighQualityBicubic;
    graphic.DrawImage(image, 0, 0, (int)newWidth, (int)newHeight);
    graphic.Dispose();

    return bitmap;
}

【问题讨论】:

  • 你可以从你的类中提取位图/图形(例如,使用适配器),这样你就可以测试你的实际逻辑和调用而不是图形组件本身(你不需要测试 - 我们已经知道图形可以正常工作)。
  • 该代码已经使用了相当长的一段时间,但我们没有进行太多的单元测试。我知道这是不好的做法,但我现在正试图通过创建单元测试来纠正这种情况。因此,在不更改被测代码的情况下,您是否建议我使用 MS Fakes 来填充图形调用并验证它们是使用预期参数调用的。
  • 您可以检查的一件事是 ImageToByteArray 在同一图像上多次调用时是否返回相同的内容。这不是我通常这样做的方式,即在两者上使用 GetPixel 并比较值。它可能会慢一点,但您可以准确报告图像的不同之处。
  • @mikez 我用相同的源图像调用了ImageToByteArray 方法 5 次,间隔 17 秒。将第一个结果与每个后续结果进行比较表明,所有 5 个调用都返回了相同的值。非确定性行为必须在被测方法中发生。
  • 我注意到您使用PixelFormat.Format48bppRgb 创建位图,但随后您将其保存为 .bmp。这可能会导致意外行为......当您以这种方式保存图像时,图像的位深度将减少到 24bpp。也许这会影响您的参考图像的保存方式?

标签: c# image unit-testing deterministic


【解决方案1】:

这最终奏效了。我不知道这对于单元测试是否是一个好主意,但是由于 GDI+ 逻辑是不确定的(或者我的逻辑与它交互),这似乎是最好的方法。

我使用 MS Fakes Shimming 功能填充依赖调用并验证预期值是否传递给被调用的方法。然后我调用本机方法以获取待测方法的其余部分所需的功能。最后,我验证了返回图像的一些属性。

不过,我更愿意将预期输出与实际输出进行直接比较...

[TestMethod]
[TestCategory("ImageHelper")]
[TestCategory("ResizeImage")]
public void ResizeImage_LandscapeTooLarge_SmallerLandscape()
{
    Image testImageLandscape = Resources.TestImageLandscapeDesert;

    const int HEIGHT = 300;
    const int WIDTH = 300;
    const int EXPECTED_WIDTH = WIDTH;
    const int EXPECTED_HEIGHT = (int)(EXPECTED_WIDTH / (1024m / 768m));
    const PixelFormat EXPECTED_FORMAT = PixelFormat.Format48bppRgb;
    bool calledBitMapConstructor = false;
    bool calledGraphicsFromImage = false;
    bool calledGraphicsDrawImage = false;

    using (ShimsContext.Create())
    {
        ShimBitmap.ConstructorInt32Int32PixelFormat = (instance, w, h, f) => {
            calledBitMapConstructor = true;
            Assert.AreEqual(EXPECTED_WIDTH, w);
            Assert.AreEqual(EXPECTED_HEIGHT, h);
            Assert.AreEqual(EXPECTED_FORMAT, f);
            ShimsContext.ExecuteWithoutShims(() => {
                ConstructorInfo constructor = typeof(Bitmap).GetConstructor(new[] { typeof(int), typeof(int), typeof(PixelFormat) });
                Assert.IsNotNull(constructor);
                constructor.Invoke(instance, new object[] { w, h, f });
            });
        };
        ShimGraphics.FromImageImage = i => {
            calledGraphicsFromImage = true;
            Assert.IsNotNull(i);
            return ShimsContext.ExecuteWithoutShims(() => Graphics.FromImage(i));
        };
        ShimGraphics.AllInstances.DrawImageImageInt32Int32Int32Int32 = (instance, i, x, y, w, h) => {
            calledGraphicsDrawImage = true;
            Assert.IsNotNull(i);
            Assert.AreEqual(0, x);
            Assert.AreEqual(0, y);
            Assert.AreEqual(EXPECTED_WIDTH, w);
            Assert.AreEqual(EXPECTED_HEIGHT, h);
            ShimsContext.ExecuteWithoutShims(() => instance.DrawImage(i, x, y, w, h));
        };
        using (Image resizedImage = ImageHelper.ResizeImage(testImageLandscape, HEIGHT, WIDTH))
        {
            Assert.IsNotNull(resizedImage);
            Assert.AreEqual(EXPECTED_WIDTH, resizedImage.Size.Width);
            Assert.AreEqual(EXPECTED_HEIGHT, resizedImage.Size.Height);
            Assert.AreEqual(EXPECTED_FORMAT, resizedImage.PixelFormat);
        }
    }

    Assert.IsTrue(calledBitMapConstructor);
    Assert.IsTrue(calledGraphicsFromImage);
    Assert.IsTrue(calledGraphicsDrawImage);
}

【讨论】:

    【解决方案2】:

    这个有点晚了,但添加这个以防它帮助任何人。在我的单元测试中,这可靠地比较了我使用 GDI+ 动态生成的图像。

    private static bool CompareImages(string source, string expected)
    {
        var image1 = new Bitmap($".\\{source}");
        var image2 = new Bitmap($".\\Expected\\{expected}");
    
        var converter = new ImageConverter();
        var image1Bytes = (byte[])converter.ConvertTo(image1, typeof(byte[]));
        var image2Bytes = (byte[])converter.ConvertTo(image2, typeof(byte[]));
    
        // ReSharper disable AssignNullToNotNullAttribute
        var same = image1Bytes.SequenceEqual(image2Bytes);
        // ReSharper enable AssignNullToNotNullAttribute
    
        return same;
    }
    

    【讨论】:

      猜你喜欢
      • 2017-01-13
      • 1970-01-01
      • 2011-12-31
      • 2012-05-13
      • 2016-10-20
      • 1970-01-01
      • 1970-01-01
      • 2016-05-12
      相关资源
      最近更新 更多