- 1,获得图片权限,拿到所有图片
- 2,快速分组(基于创建时间,照片尺寸)
- 3,依次两两对比,计算相似度
1.获取图片,并快速分组
// 获取所有图片资源
let allPhotos = PHAsset.fetchAssets(with: .image, options: fetchOptions)
print("找到 \(allPhotos.count) 张照片")
var photoGroups: [String: [PHAsset]] = [:]
let group = DispatchGroup()
// MARK: 第一步 - 基于时间和尺寸快速分组
print("开始基于时间和尺寸进行快速分组...")
for i in 0..<allPhotos.count {
let asset = allPhotos.object(at: i)
group.enter()
processingQueue.async {
defer { group.leave() } // 确保离开组的操作一定会执行
// 获取元数据
let creationDate = asset.creationDate ?? Date()
let deviceInfo = asset.sourceType.rawValue
let size = CGSize(width: asset.pixelWidth, height: asset.pixelHeight)
// 生成组合键(5分钟时间窗口 + 尺寸 + 设备)
let timeInterval = Int(creationDate.timeIntervalSince1970 / 300) // 300秒=5分钟
let groupKey = "\(timeInterval)_\(size.width)_\(size.height)_\(deviceInfo)"
// 线程安全地更新字典
DispatchQueue.global().async {
photoGroups[groupKey] = (photoGroups[groupKey] ?? []) + [asset]
}
}
}
// MARK: 分组完成回调
group.notify(queue: .main) {
self.processPhotoGroups(photoGroups)
}
2.检测相似照片,进行批量相似度对比
2.1 处理传入的照片组,对两张照片进行相似度对比
private func compareSimilarity(asset1: PHAsset, asset2: PHAsset) {
let options = PHImageRequestOptions()
options.deliveryMode = .highQualityFormat
options.isSynchronous = true
var image1: UIImage?
var image2: UIImage?
let group = DispatchGroup()
// 异步获取第一张图片
group.enter()
imageManager.requestImage(for: asset1, targetSize: targetSize, contentMode: .aspectFit, options: options) { image, _ in
image1 = image
group.leave()
}
// 异步获取第二张图片
group.enter()
imageManager.requestImage(for: asset2, targetSize: targetSize, contentMode: .aspectFit, options: options) { image, _ in
image2 = image
group.leave()
}
// 图片获取完成回调
group.notify(queue: .main) {
guard let image1 = image1, let image2 = image2 else {
print("无法获取图片数据,跳过比较")
return
}
// 计算相似度
let similarity = self.calculateImageSimilarity(image1: image1, image2: image2)
print("照片相似度: \(String(format: "%.2f", similarity * 100))%")
// 相似度分级处理
if similarity > 0.75 {
print("发现重复照片!相似度: \(String(format: "%.2f", similarity * 100))%")
self.similarPhotos.append((asset1, asset2, similarity))
if similarity > 0.85 {
print("发现高度相似照片!相似度: \(String(format: "%.2f", similarity * 100))%")
self.highSimilarityPhotos.append((asset1, asset2, similarity))
}
}
}
}
2.2 : 计算两张图片的相似度得分
private func calculateImageSimilarity(image1: UIImage, image2: UIImage) -> Float {
print("开始计算图片相似度...")
guard let cgImage1 = image1.cgImage,
let cgImage2 = image2.cgImage else {
print("无法获取CGImage,返回相似度0")
return 0
}
// 图像预处理流程
let context = CIContext()
let filter = CIFilter(name: "CIPhotoEffectNoir")!
// 灰度化处理第一张图片
filter.setValue(CIImage(cgImage: cgImage1), forKey: kCIInputImageKey)
guard let outputImage1 = filter.outputImage,
let grayImage1 = context.createCGImage(outputImage1, from: outputImage1.extent) else {
print("处理第一张图片失败,返回相似度0")
return 0
}
// 灰度化处理第二张图片
filter.setValue(CIImage(cgImage: cgImage2), forKey: kCIInputImageKey)
guard let outputImage2 = filter.outputImage,
let grayImage2 = context.createCGImage(outputImage2, from: outputImage2.extent) else {
print("处理第二张图片失败,返回相似度0")
return 0
}
// 直方图计算
let hist1 = calculateHistogram(from: grayImage1)
let hist2 = calculateHistogram(from: grayImage2)
// 相关系数计算
let similarity = calculateCorrelation(hist1: hist1, hist2: hist2)
print("图片相似度计算完成: \(String(format: "%.2f", similarity * 100))%")
return similarity
}
2.3: 计算灰度图像的归一化直方图
归一化直方图是将原始直方图的统计结果转换为概率分布的过程,目的是消除图像尺寸或像素总数的影响,使不同图像的直方图具有可比性
private func calculateHistogram(from cgImage: CGImage) -> [Float] {
// 获取像素数据
let width = cgImage.width
let height = cgImage.height
var data = [UInt8](repeating: 0, count: height * width)
// 创建灰度上下文
let context = CGContext(data: &data,
width: width,
height: height,
bitsPerComponent: 8,
bytesPerRow: width,
space: CGColorSpaceCreateDeviceGray(),
bitmapInfo: CGImageAlphaInfo.none.rawValue)
// 绘制图像
context?.draw(cgImage, in: CGRect(x: 0, y: 0, width: width, height: height))
// 统计直方图
var histogram = [Float](repeating: 0, count: 256)
data.forEach { histogram[Int($0)] += 1 }
// 归一化处理
let sum = histogram.reduce(0, +)
return histogram.map { $0 / sum }
}
2.4: 计算两个直方图的相关系数
计算两个直方图的相关系数(通常指皮尔逊相关系数)是衡量它们分布相似性的一种方法,主要用于图像匹配、相似度分析等场景。
private func calculateCorrelation(hist1: [Float], hist2: [Float]) -> Float {
// 计算均值
let mean1 = hist1.reduce(0, +) / Float(hist1.count)
let mean2 = hist2.reduce(0, +) / Float(hist2.count)
// 协方差计算:范围在-1到1之间,更容易比较不同变量对之间的相关性
var numerator: Float = 0
var denominator1: Float = 0
var denominator2: Float = 0
for i in 0..<hist1.count {
let diff1 = hist1[i] - mean1
let diff2 = hist2[i] - mean2
numerator += diff1 * diff2
denominator1 += diff1 * diff1
denominator2 += diff2 * diff2
}
// 相关系数公式
let denominator = sqrt(denominator1 * denominator2)
return denominator != 0 ? numerator / denominator : 0
}
技术指标
-
相似度阈值:
75%: 判定为相似照片
85%: 判定为高相似度照片
-
图像处理:
统一转换为256级灰度图
使用Core Image进行预处理
[图片上传失败...(image-3c19bd-1745551279366)]
- 1,获得图片权限,拿到所有图片
- 2,快速分组(基于创建时间,照片尺寸)
- 3,依次两两对比,计算相似度
1.获取图片,并快速分组
// 获取所有图片资源
let allPhotos = PHAsset.fetchAssets(with: .image, options: fetchOptions)
print("找到 \(allPhotos.count) 张照片")
var photoGroups: [String: [PHAsset]] = [:]
let group = DispatchGroup()
// MARK: 第一步 - 基于时间和尺寸快速分组
print("开始基于时间和尺寸进行快速分组...")
for i in 0..<allPhotos.count {
let asset = allPhotos.object(at: i)
group.enter()
processingQueue.async {
defer { group.leave() } // 确保离开组的操作一定会执行
// 获取元数据
let creationDate = asset.creationDate ?? Date()
let deviceInfo = asset.sourceType.rawValue
let size = CGSize(width: asset.pixelWidth, height: asset.pixelHeight)
// 生成组合键(5分钟时间窗口 + 尺寸 + 设备)
let timeInterval = Int(creationDate.timeIntervalSince1970 / 300) // 300秒=5分钟
let groupKey = "\(timeInterval)_\(size.width)_\(size.height)_\(deviceInfo)"
// 线程安全地更新字典
DispatchQueue.global().async {
photoGroups[groupKey] = (photoGroups[groupKey] ?? []) + [asset]
}
}
}
// MARK: 分组完成回调
group.notify(queue: .main) {
self.processPhotoGroups(photoGroups)
}
2.检测相似照片,进行批量相似度对比
2.1 处理传入的照片组,对两张照片进行相似度对比
private func compareSimilarity(asset1: PHAsset, asset2: PHAsset) {
let options = PHImageRequestOptions()
options.deliveryMode = .highQualityFormat
options.isSynchronous = true
var image1: UIImage?
var image2: UIImage?
let group = DispatchGroup()
// 异步获取第一张图片
group.enter()
imageManager.requestImage(for: asset1, targetSize: targetSize, contentMode: .aspectFit, options: options) { image, _ in
image1 = image
group.leave()
}
// 异步获取第二张图片
group.enter()
imageManager.requestImage(for: asset2, targetSize: targetSize, contentMode: .aspectFit, options: options) { image, _ in
image2 = image
group.leave()
}
// 图片获取完成回调
group.notify(queue: .main) {
guard let image1 = image1, let image2 = image2 else {
print("无法获取图片数据,跳过比较")
return
}
// 计算相似度
let similarity = self.calculateImageSimilarity(image1: image1, image2: image2)
print("照片相似度: \(String(format: "%.2f", similarity * 100))%")
// 相似度分级处理
if similarity > 0.75 {
print("发现重复照片!相似度: \(String(format: "%.2f", similarity * 100))%")
self.similarPhotos.append((asset1, asset2, similarity))
if similarity > 0.85 {
print("发现高度相似照片!相似度: \(String(format: "%.2f", similarity * 100))%")
self.highSimilarityPhotos.append((asset1, asset2, similarity))
}
}
}
}
2.2 : 计算两张图片的相似度得分
private func calculateImageSimilarity(image1: UIImage, image2: UIImage) -> Float {
print("开始计算图片相似度...")
guard let cgImage1 = image1.cgImage,
let cgImage2 = image2.cgImage else {
print("无法获取CGImage,返回相似度0")
return 0
}
// 图像预处理流程
let context = CIContext()
let filter = CIFilter(name: "CIPhotoEffectNoir")!
// 灰度化处理第一张图片
filter.setValue(CIImage(cgImage: cgImage1), forKey: kCIInputImageKey)
guard let outputImage1 = filter.outputImage,
let grayImage1 = context.createCGImage(outputImage1, from: outputImage1.extent) else {
print("处理第一张图片失败,返回相似度0")
return 0
}
// 灰度化处理第二张图片
filter.setValue(CIImage(cgImage: cgImage2), forKey: kCIInputImageKey)
guard let outputImage2 = filter.outputImage,
let grayImage2 = context.createCGImage(outputImage2, from: outputImage2.extent) else {
print("处理第二张图片失败,返回相似度0")
return 0
}
// 直方图计算
let hist1 = calculateHistogram(from: grayImage1)
let hist2 = calculateHistogram(from: grayImage2)
// 相关系数计算
let similarity = calculateCorrelation(hist1: hist1, hist2: hist2)
print("图片相似度计算完成: \(String(format: "%.2f", similarity * 100))%")
return similarity
}
2.3: 计算灰度图像的归一化直方图
归一化直方图是将原始直方图的统计结果转换为概率分布的过程,目的是消除图像尺寸或像素总数的影响,使不同图像的直方图具有可比性
private func calculateHistogram(from cgImage: CGImage) -> [Float] {
// 获取像素数据
let width = cgImage.width
let height = cgImage.height
var data = [UInt8](repeating: 0, count: height * width)
// 创建灰度上下文
let context = CGContext(data: &data,
width: width,
height: height,
bitsPerComponent: 8,
bytesPerRow: width,
space: CGColorSpaceCreateDeviceGray(),
bitmapInfo: CGImageAlphaInfo.none.rawValue)
// 绘制图像
context?.draw(cgImage, in: CGRect(x: 0, y: 0, width: width, height: height))
// 统计直方图
var histogram = [Float](repeating: 0, count: 256)
data.forEach { histogram[Int($0)] += 1 }
// 归一化处理
let sum = histogram.reduce(0, +)
return histogram.map { $0 / sum }
}
2.4: 计算两个直方图的相关系数
计算两个直方图的相关系数(通常指皮尔逊相关系数)是衡量它们分布相似性的一种方法,主要用于图像匹配、相似度分析等场景。
private func calculateCorrelation(hist1: [Float], hist2: [Float]) -> Float {
// 计算均值
let mean1 = hist1.reduce(0, +) / Float(hist1.count)
let mean2 = hist2.reduce(0, +) / Float(hist2.count)
// 协方差计算:范围在-1到1之间,更容易比较不同变量对之间的相关性
var numerator: Float = 0
var denominator1: Float = 0
var denominator2: Float = 0
for i in 0..<hist1.count {
let diff1 = hist1[i] - mean1
let diff2 = hist2[i] - mean2
numerator += diff1 * diff2
denominator1 += diff1 * diff1
denominator2 += diff2 * diff2
}
// 相关系数公式
let denominator = sqrt(denominator1 * denominator2)
return denominator != 0 ? numerator / denominator : 0
}
技术指标
-
相似度阈值:
75%: 判定为相似照片
85%: 判定为高相似度照片
-
图像处理:
统一转换为256级灰度图
使用Core Image进行预处理
Simulator Screenshot - iPhone 15 - 2025-04-25 at 11.22.03.png