如何比较两个UIImage对象

76

我正在开发一个应用程序。在其中我使用了 imageviews。在更改 UIImageview 图像之前,我需要将该图像放入 UIimage 对象中,并与另一个 UIImage 对象进行比较,以查找它们是否相同。请告诉我如何实现。


接受的答案在这个讨论串中。 - Mathew Varghese
你应该维护应用程序状态并拥有数据模型。这样,你就能了解应用程序中正在发生的事情。 - pronebird
19个回答

137

一种方法是先将它们转换为图像数据,然后进行比较。

- (BOOL)image:(UIImage *)image1 isEqualTo:(UIImage *)image2
{
    NSData *data1 = UIImagePNGRepresentation(image1);
    NSData *data2 = UIImagePNGRepresentation(image2);

    return [data1 isEqual:data2];
}

7
这最好作为一个类别来实现。 - abbood
1
这里涉及一个编程问题:该使用 isEqualToData: 还是 isEqual:?这是一个合理的问题——我不确定 isEqual: 在处理 NSData 对象时是否会失败,但是使用专门为该类设计的条件测试似乎是合乎逻辑的选择,对吗? - toblerpwn
UIImagePNGRepresentation在处理图像时存在缺陷,因为它没有考虑方向。两个方向不同但逻辑上相同的图像可能无法通过此检查。 - Micah Hainline
如果其中一个图像被保存为CoreData(作为NSData),而另一个图像是imageNamed,则不起作用。 - Sam
10
比较相当耗费时间。 - tounaobun
显示剩余3条评论

30

一个Swift实现,来自@Simon的答案:

func image(image1: UIImage, isEqualTo image2: UIImage) -> Bool {
    let data1: NSData = UIImagePNGRepresentation(image1)!
    let data2: NSData = UIImagePNGRepresentation(image2)!
    return data1.isEqual(data2)
}

或者根据 @nhgrif 的建议扩展 UIImage:

import UIKit

extension UIImage {

    func isEqualToImage(image: UIImage) -> Bool {
        let data1: NSData = UIImagePNGRepresentation(self)!
        let data2: NSData = UIImagePNGRepresentation(image)!
        return data1.isEqual(data2)
    }

}

是的,我同意,但我只是从另一个答案中翻译代码。 - Mark Tickner
更新答案以包括您的建议示例 @nhgrif - Mark Tickner
2
现在我唯一的评论是,我们应该返回 false ,而不是强制解包和崩溃,如果任何一个为 nilguard let data1 = UIImagePNGRepresentation(self),data2 = UIImagePNGRepresentation(image) else { return false } return data1 == data2 - nhgrif
在 Swift 4 中需要这样编写代码: let data1: NSData = UIImagePNGRepresentation(image1)! as NSData - Shivam Pokhriyal
1
通过将其转换为Swift 4,更新了此答案。 - Greg Hilston

18

更新了 Mark Tickner 的解决方案以适应 Swift 4

import UIKit

extension UIImage {

    func isEqualToImage(_ image: UIImage) -> Bool {
        let data1 = self.pngData()
        let data2 = image.pngData()
        return data1 == data2
    }

}

这两个变量可能有些多余,但它们可能有助于向新手解释。可以缩短为:

import UIKit

extension UIImage {

    func isEqualToImage(_ image: UIImage) -> Bool {
        return self.pngData() == image.pngData()
    }

}

@YogeshPatel 我无法发表评论,你需要自己尝试一下。如果你成功了,请在这里更新我们! - Greg Hilston
2
我已经尝试过了,但对我没有用。这就是为什么我告诉你的原因。你能帮我吗? - Yogesh Patel

10

当使用[UIImage imageNamed:]时,我们可以使用isEqual:来比较,否则我们可以比较数据。


这不是一个非常安全的解决方案,特别是在 UITableView 中使用它时。我建议使用 @Simon 的解决方案。 - Sebyddd
@Sebyddd 真的吗?你有尝试在渲染UITableViewCells时使用Simon的解决方案吗?那个比较操作非常昂贵...所以滚动会非常卡顿...真实的故事。 - abbood
@abbood 是的,这可能会更昂贵,但从我的测试来看,这个方法并不总是返回正确的结果,原因不明。如果两个 NSObjects 指向相同的内存地址,则它们被认为是相等的。这就是 isEqual 的作用。因此,imageNamed 在不同的内存地址返回一个新实例。它并不是非常稳定的。 - Sebyddd
@Sebyddd isEqual: 的默认实现是从 NSObject 继承而来的,它是一个简单的引用比较。然而,一些对象会覆盖这个默认实现。例如,我知道其中一个是 NSString,它的 isEqual: 在功能上等同于 isEqualToString:,两者都是值比较,而不是引用比较。至于 UIImage 是否重写了继承的方法 isEqual,我无法评论。 - nhgrif

8
正确的答案取决于“你想进行什么样的比较?”。
  1. 最简单的方法就是比较数据。
  2. 如果您想知道“是否从一个本地文件创建了图像”- 您可以使用-isEqual:(但有一种危险的方法,因为我不确定,如果出于某种原因清除了图像缓存会发生什么情况)。
  3. 提供每像素比较的难度更大(当然,系统会花更多时间)。由于法律原因,我无法提供公司库中的代码:(

但是,您可以在facebook的ios-snapshot-test-case项目中检查好的示例:链接指向所需文件。您可以使用性能测试来测量处理时间。

出于伟大的正义,我将在下面复制那里的代码:

- (BOOL)fb_compareWithImage:(UIImage *)image tolerance:(CGFloat)tolerance
{
  NSAssert(CGSizeEqualToSize(self.size, image.size), @"Images must be same size.");

  CGSize referenceImageSize = CGSizeMake(CGImageGetWidth(self.CGImage), CGImageGetHeight(self.CGImage));
  CGSize imageSize = CGSizeMake(CGImageGetWidth(image.CGImage), CGImageGetHeight(image.CGImage));

  // The images have the equal size, so we could use the smallest amount of bytes because of byte padding
  size_t minBytesPerRow = MIN(CGImageGetBytesPerRow(self.CGImage), CGImageGetBytesPerRow(image.CGImage));
  size_t referenceImageSizeBytes = referenceImageSize.height * minBytesPerRow;
  void *referenceImagePixels = calloc(1, referenceImageSizeBytes);
  void *imagePixels = calloc(1, referenceImageSizeBytes);

  if (!referenceImagePixels || !imagePixels) {
    free(referenceImagePixels);
    free(imagePixels);
    return NO;
  }

  CGContextRef referenceImageContext = CGBitmapContextCreate(referenceImagePixels,
                                                             referenceImageSize.width,
                                                             referenceImageSize.height,
                                                             CGImageGetBitsPerComponent(self.CGImage),
                                                             minBytesPerRow,
                                                             CGImageGetColorSpace(self.CGImage),
                                                             (CGBitmapInfo)kCGImageAlphaPremultipliedLast
                                                             );
  CGContextRef imageContext = CGBitmapContextCreate(imagePixels,
                                                    imageSize.width,
                                                    imageSize.height,
                                                    CGImageGetBitsPerComponent(image.CGImage),
                                                    minBytesPerRow,
                                                    CGImageGetColorSpace(image.CGImage),
                                                    (CGBitmapInfo)kCGImageAlphaPremultipliedLast
                                                    );

  if (!referenceImageContext || !imageContext) {
    CGContextRelease(referenceImageContext);
    CGContextRelease(imageContext);
    free(referenceImagePixels);
    free(imagePixels);
    return NO;
  }

  CGContextDrawImage(referenceImageContext, CGRectMake(0, 0, referenceImageSize.width, referenceImageSize.height), self.CGImage);
  CGContextDrawImage(imageContext, CGRectMake(0, 0, imageSize.width, imageSize.height), image.CGImage);

  CGContextRelease(referenceImageContext);
  CGContextRelease(imageContext);

  BOOL imageEqual = YES;

  // Do a fast compare if we can
  if (tolerance == 0) {
    imageEqual = (memcmp(referenceImagePixels, imagePixels, referenceImageSizeBytes) == 0);
  } else {
    // Go through each pixel in turn and see if it is different
    const NSInteger pixelCount = referenceImageSize.width * referenceImageSize.height;

    FBComparePixel *p1 = referenceImagePixels;
    FBComparePixel *p2 = imagePixels;

    NSInteger numDiffPixels = 0;
    for (int n = 0; n < pixelCount; ++n) {
      // If this pixel is different, increment the pixel diff count and see
      // if we have hit our limit.
      if (p1->raw != p2->raw) {
        numDiffPixels ++;

        CGFloat percent = (CGFloat)numDiffPixels / pixelCount;
        if (percent > tolerance) {
          imageEqual = NO;
          break;
        }
      }

      p1++;
      p2++;
    }
  }

  free(referenceImagePixels);
  free(imagePixels);

  return imageEqual;
}

7

我倾向于使用(Swift)的解决方案

import UIKit

func ==(lhs: UIImage, rhs: UIImage) -> Bool
{
  guard let data1 = UIImagePNGRepresentation(lhs),
            data2 = UIImagePNGRepresentation(rhs)
    else { return false }

  return data1.isEqual(data2)
}

使用默认的isEqual实现对我也没有起作用,这是唯一对我有效的解决方案。 - inexcitus

7

将图像转换为JPG / PNG,或依赖可访问性标识符要么是昂贵的操作,要么容易出现故障和失败。

在这里,我遵循苹果提供的建议,位于以下链接

isEqual(:)方法是唯一可靠的方法来确定两个图像是否包含相同的图像数据。您创建的图像对象可能彼此不同,即使您使用相同的缓存图像数据进行初始化也是如此。唯一确定它们的等式是使用 isEqual() 方法,它比较实际的图像数据。程序清单1说明了比较图像的正确和错误方式。

为了简化事情,我创建了以下扩展程序,用于执行比较,以便避免转换第一个图像的问题:

import UIKit

extension UIImage {
    func isEqual(to image: UIImage) -> Bool {
        return isEqual(image)
    }
}

有了这个,我现在可以设置一个示例来比较一对图像:

let imageA = UIImage(named: "a")!
let imageB = UIImage(named: "b")!
let imageC = UIImage(named: "a")!

print(imageA.isEqual(to: imageA)) // true
print(imageA.isEqual(to: imageC)) // true
print(imageA.isEqual(to: imageB)) // false

9
尽管苹果公司的文档如此描述,但我发现在 Obj-C 中 isEqual 方法并不能正确比较两个 UIImage 实例的数据,即使这两个实例包含着相同的数据,也会返回 false。将 UIImage 实例转换成 PNG 图像,并比较它们的原始数据(就像 Simon 的回答中所做的那样),是我发现的唯一正确比较图片的方法。 - BinaryNate

7

Facebook的比较算法的Swift 4.x版本:

/// Value in range 0...100 %
typealias Percentage = Float

// See: https://github.com/facebookarchive/ios-snapshot-test-case/blob/master/FBSnapshotTestCase/Categories/UIImage%2BCompare.m
private func compare(tolerance: Percentage, expected: Data, observed: Data) throws -> Bool {
    guard let expectedUIImage = UIImage(data: expected), let observedUIImage = UIImage(data: observed) else {
        throw Error.unableToGetUIImageFromData
    }
    guard let expectedCGImage = expectedUIImage.cgImage, let observedCGImage = observedUIImage.cgImage else {
        throw Error.unableToGetCGImageFromData
    }
    guard let expectedColorSpace = expectedCGImage.colorSpace, let observedColorSpace = observedCGImage.colorSpace else {
        throw Error.unableToGetColorSpaceFromCGImage
    }
    if expectedCGImage.width != observedCGImage.width || expectedCGImage.height != observedCGImage.height {
        throw Error.imagesHasDifferentSizes
    }
    let imageSize = CGSize(width: expectedCGImage.width, height: expectedCGImage.height)
    let numberOfPixels = Int(imageSize.width * imageSize.height)

    // Checking that our `UInt32` buffer has same number of bytes as image has.
    let bytesPerRow = min(expectedCGImage.bytesPerRow, observedCGImage.bytesPerRow)
    assert(MemoryLayout<UInt32>.stride == bytesPerRow / Int(imageSize.width))

    let expectedPixels = UnsafeMutablePointer<UInt32>.allocate(capacity: numberOfPixels)
    let observedPixels = UnsafeMutablePointer<UInt32>.allocate(capacity: numberOfPixels)

    let expectedPixelsRaw = UnsafeMutableRawPointer(expectedPixels)
    let observedPixelsRaw = UnsafeMutableRawPointer(observedPixels)

    let bitmapInfo = CGBitmapInfo(rawValue: CGImageAlphaInfo.premultipliedLast.rawValue)
    guard let expectedContext = CGContext(data: expectedPixelsRaw, width: Int(imageSize.width), height: Int(imageSize.height),
                                          bitsPerComponent: expectedCGImage.bitsPerComponent, bytesPerRow: bytesPerRow,
                                          space: expectedColorSpace, bitmapInfo: bitmapInfo.rawValue) else {
        expectedPixels.deallocate()
        observedPixels.deallocate()
        throw Error.unableToInitializeContext
    }
    guard let observedContext = CGContext(data: observedPixelsRaw, width: Int(imageSize.width), height: Int(imageSize.height),
                                          bitsPerComponent: observedCGImage.bitsPerComponent, bytesPerRow: bytesPerRow,
                                          space: observedColorSpace, bitmapInfo: bitmapInfo.rawValue) else {
        expectedPixels.deallocate()
        observedPixels.deallocate()
        throw Error.unableToInitializeContext
    }

    expectedContext.draw(expectedCGImage, in: CGRect(origin: .zero, size: imageSize))
    observedContext.draw(observedCGImage, in: CGRect(origin: .zero, size: imageSize))

    let expectedBuffer = UnsafeBufferPointer(start: expectedPixels, count: numberOfPixels)
    let observedBuffer = UnsafeBufferPointer(start: observedPixels, count: numberOfPixels)

    var isEqual = true
    if tolerance == 0 {
        isEqual = expectedBuffer.elementsEqual(observedBuffer)
    } else {
        // Go through each pixel in turn and see if it is different
        var numDiffPixels = 0
        for pixel in 0 ..< numberOfPixels where expectedBuffer[pixel] != observedBuffer[pixel] {
            // If this pixel is different, increment the pixel diff count and see if we have hit our limit.
            numDiffPixels += 1
            let percentage = 100 * Float(numDiffPixels) / Float(numberOfPixels)
            if percentage > tolerance {
                isEqual = false
                break
            }
        }
    }

    expectedPixels.deallocate()
    observedPixels.deallocate()

    return isEqual
}

6

我对Mark的回答进行了一些修改,并使用了Data和elementsEqual而不是NSData和isEqual。

extension UIImage {
    func isEqual(to image: UIImage) -> Bool {
        guard let data1: Data = UIImagePNGRepresentation(self),
            let data2: Data = UIImagePNGRepresentation(image) else {
                return false
        }
        return data1.elementsEqual(data2)
    }
}

5

Swift 3

有两种方法。如下:

1)使用isEqual()函数。

 self.image?.isEqual(UIImage(named: "add-image"))

2) 使用 accessibilityIdentifier

将 accessibilityIdentifier 设置为图片名称。

myImageView.image?.accessibilityIdentifier = "add-image"

然后使用以下代码。
extension UIImageView
{
func getFileName() -> String? {
// First set accessibilityIdentifier of image before calling.
let imgName = self.image?.accessibilityIdentifier
return imgName
}
}

最后,方法调用的识别方式

myImageView.getFileName()

myImageView.getFileName() -> 总是返回 Nil.. 我已经设置了可访问标识符,但这个方法 getFileName 总是返回 nil。 - Yash Bedi
抱歉,阿卜杜勒,这些答案在Swift 3.0 iOS 10.x中对我都不起作用。 - user3069232
@YashBedi 在viewdidload中添加这个myImageView.image?.accessibilityIdentifier = "add-image"。 - Abdul Hameed

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接