// // PhotosImageClassifier.swift // PhotoClassifierKit // // Created by Groot on 2025/4/23. // Copyright © 2025 GrootTech. All rights reserved. // import Photos import PhotosUI import UIKit import CoreLocation import Vision /// 照片图像分类器,用于对相册中的图片进行智能分类 public class PhotosImageClassifier { public typealias BatchProgressCompletion = (ClassificationProgress, [ImageItem]) async -> Void /// 分类器配置选项 public struct Configuration: Codable { /// 每批处理的图片数量 public var batchSize: Int = 200 /// 最大并发处理数量(使用Vision框架时不应大于5) public var maxConcurrentProcessing: Int = 4 /// 相似度判定阈值(0.0-1.0),值越大表示要求越相似 public var similarityThreshold: CGFloat = 0.7 } /// 分类行为类型 enum ClassificationAction { /// 查找相似图片组 case similarGroups(with: [ImageItem]) /// 查找包含人物的图片 case peopleImages(with: [ImageItem]) /// 查找屏幕截图 case screenshotImages(with: [ImageItem]) /// 查找模糊图片 case blurryImages(with: [ImageItem]) } /// 单例实例 public static let shared = PhotosImageClassifier() private let imageManager = PHCachingImageManager() private let prefetchImageSize: CGSize = .init(width: 64, height: 64) internal lazy var imageRequestOptions: PHImageRequestOptions = { let requestOptions = PHImageRequestOptions() requestOptions.isSynchronous = false requestOptions.deliveryMode = .highQualityFormat requestOptions.resizeMode = .fast return requestOptions }() // MARK: - 公开属性 /// 分类配置 private(set) public var configuration: Configuration = .init() /// 是否已加载完所有资源项目 private(set) public var isAssetsLoaded: Bool = false /// 是否已完成全部相册分类 private(set) public var isClassificationFinished: Bool = false // MARK: - 私有属性 /// 存储所有加载完成的资源项目(带缩略图) private var imageItems: [ImageItem] = [] /// 私有初始化方法,加载所有图像资产 private init() { fetchAllImageAssets() } /// 执行指定的分类操作 /// - Parameter action: 分类操作类型 /// - Returns: 分类结果 func perform(action: ClassificationAction) async -> ResultType? { var result: ResultType? = nil switch action { case .similarGroups(let items): result = PhotosImageClassifyFilter.filterSimilarImages(items: items, threshold: configuration.similarityThreshold) as? ResultType case .peopleImages(let items): result = PhotosImageClassifyFilter.filterPeopleImages(items: items) as? ResultType case .screenshotImages(let items): result = PhotosImageClassifyFilter.filterScreenshotImages(items: items) as? ResultType case .blurryImages(let items): result = PhotosImageClassifyFilter.filterBlurryImages(items: items) as? ResultType } return result } /// 按批次 /// 开始分类过程,返回完整分类结果 /// - Parameter completion: 完成回调,返回处理后的图像项目和是否完成标志 public func startClassification(with types: [PhotoImageClassifyType] = PhotoImageClassifyType.all, completion: @escaping BatchProgressCompletion) async { await processBatches(with: types, items: imageItems, batchCompletionHandler: completion) } /// 按批次 /// 开始分类过程,分批次返回分类结果 /// - Parameter completion: 完成回调,当前进度以及分类结果 /// 每个`PhotoImageClassifiedResult`为当前批次分类结果 public func startClassification(with types: [PhotoImageClassifyType] = PhotoImageClassifyType.all, completion: @escaping (ClassificationProgress, PhotoImageClassifiedResult) async -> Void) async { await processBatches(with: types, items: imageItems) { [weak self] progress, items in guard let self = self else { return } let result = PhotoImageClassifiedResult( similarGroups: types.contains(.similar) ? await self.perform(action: .similarGroups(with: items)) : nil, peopleImages: types.contains(.people) ? await self.perform(action: .peopleImages(with: items)) : nil, screenshotImages: types.contains(.screenshot) ? await self.perform(action: .screenshotImages(with: items)) : nil, blurryImages: types.contains(.blurry) ? await self.perform(action: .blurryImages(with: items)) : nil ) self.isClassificationFinished = progress.isCompleted await completion(progress, result) } } /// 设置分类器配置 /// - Parameter configuration: 分类器配置选项 /// - Note: 必须在调用`startClassification`方法前进行配置 public func configure(with configuration: Configuration) { self.configuration = configuration } /// 重置 public func reset() { self.isAssetsLoaded = false self.isClassificationFinished = false self.imageItems = [] self.configuration = .init() } } // MARK: - 私有方法 extension PhotosImageClassifier { // MARK: - 资源获取 /// 获取所有图像资源 private func fetchAllImageAssets() { let startTime = Date() // 配置获取选项 let fetchOptions = PHFetchOptions() fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)] fetchOptions.includeHiddenAssets = false // 排除隐藏资源 fetchOptions.includeAllBurstAssets = false // 排除连拍照片 // 获取所有图片资源 let fetchResult = PHAsset.fetchAssets(with: .image, options: fetchOptions) // 批量获取资源对象 let count = fetchResult.count let userAssets = fetchResult.objects(at: IndexSet(integersIn: 0.. ImageItem { var mutableItem = item return await withCheckedContinuation { continuation in self.imageManager.requestImage(for: mutableItem.asset, targetSize: self.prefetchImageSize, contentMode: .aspectFit, options: options) { image, _ in if let image = image { mutableItem.thumbnailImage = image } continuation.resume(returning: mutableItem) } } } }