iOS清理照片

  • 1,获得图片权限,拿到所有图片
  • 2,快速分组(基于创建时间,照片尺寸)
  • 3,依次两两对比,计算相似度

Demo : https://github.com/xu1Peng/Cleaner

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,依次两两对比,计算相似度

Demo : https://github.com/xu1Peng/Cleaner

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
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容