PhotosImageClassifier.swift 7.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186
  1. //
  2. // PhotosImageClassifier.swift
  3. // PhotoClassifierKit
  4. //
  5. // Created by Groot on 2025/4/23.
  6. // Copyright © 2025 GrootTech. All rights reserved.
  7. //
  8. import Photos
  9. import PhotosUI
  10. import UIKit
  11. import CoreLocation
  12. import Vision
  13. /// 照片图像分类器,用于对相册中的图片进行智能分类
  14. public class PhotosImageClassifier {
  15. public typealias BatchProgressCompletion = (ClassificationProgress, [ImageItem]) async -> Void
  16. /// 分类器配置选项
  17. public struct Configuration: Codable {
  18. /// 每批处理的图片数量
  19. public var batchSize: Int = 200
  20. /// 最大并发处理数量(使用Vision框架时不应大于5)
  21. public var maxConcurrentProcessing: Int = 4
  22. /// 相似度判定阈值(0.0-1.0),值越大表示要求越相似
  23. public var similarityThreshold: CGFloat = 0.7
  24. }
  25. /// 分类行为类型
  26. enum ClassificationAction<ResultType> {
  27. /// 查找相似图片组
  28. case similarGroups(with: [ImageItem])
  29. /// 查找包含人物的图片
  30. case peopleImages(with: [ImageItem])
  31. /// 查找屏幕截图
  32. case screenshotImages(with: [ImageItem])
  33. /// 查找模糊图片
  34. case blurryImages(with: [ImageItem])
  35. }
  36. /// 单例实例
  37. public static let shared = PhotosImageClassifier()
  38. private let imageManager = PHCachingImageManager()
  39. private let prefetchImageSize: CGSize = .init(width: 64, height: 64)
  40. internal lazy var imageRequestOptions: PHImageRequestOptions = {
  41. let requestOptions = PHImageRequestOptions()
  42. requestOptions.isSynchronous = false
  43. requestOptions.deliveryMode = .highQualityFormat
  44. requestOptions.resizeMode = .fast
  45. return requestOptions
  46. }()
  47. // MARK: - 公开属性
  48. /// 分类配置
  49. private(set) public var configuration: Configuration = .init()
  50. /// 是否已加载完所有资源项目
  51. private(set) public var isAssetsLoaded: Bool = false
  52. /// 是否已完成全部相册分类
  53. private(set) public var isClassificationFinished: Bool = false
  54. // MARK: - 私有属性
  55. /// 存储所有加载完成的资源项目(带缩略图)
  56. private var imageItems: [ImageItem] = []
  57. /// 私有初始化方法,加载所有图像资产
  58. private init() {
  59. fetchAllImageAssets()
  60. }
  61. /// 执行指定的分类操作
  62. /// - Parameter action: 分类操作类型
  63. /// - Returns: 分类结果
  64. func perform<ResultType>(action: ClassificationAction<ResultType>) async -> ResultType? {
  65. var result: ResultType? = nil
  66. switch action {
  67. case .similarGroups(let items):
  68. result = PhotosImageClassifyFilter.filterSimilarImages(items: items, threshold: configuration.similarityThreshold) as? ResultType
  69. case .peopleImages(let items):
  70. result = PhotosImageClassifyFilter.filterPeopleImages(items: items) as? ResultType
  71. case .screenshotImages(let items):
  72. result = PhotosImageClassifyFilter.filterScreenshotImages(items: items) as? ResultType
  73. case .blurryImages(let items):
  74. result = PhotosImageClassifyFilter.filterBlurryImages(items: items) as? ResultType
  75. }
  76. return result
  77. }
  78. /// 按批次
  79. /// 开始分类过程,返回完整分类结果
  80. /// - Parameter completion: 完成回调,返回处理后的图像项目和是否完成标志
  81. public func startClassification(with types: [PhotoImageClassifyType] = PhotoImageClassifyType.all, completion: @escaping BatchProgressCompletion) async {
  82. await processBatches(with: types, items: imageItems, batchCompletionHandler: completion)
  83. }
  84. /// 按批次
  85. /// 开始分类过程,分批次返回分类结果
  86. /// - Parameter completion: 完成回调,当前进度以及分类结果
  87. /// 每个`PhotoImageClassifiedResult`为当前批次分类结果
  88. public func startClassification(with types: [PhotoImageClassifyType] = PhotoImageClassifyType.all, completion: @escaping (ClassificationProgress, PhotoImageClassifiedResult) async -> Void) async {
  89. await processBatches(with: types, items: imageItems) { [weak self] progress, items in
  90. guard let self = self else { return }
  91. let result = PhotoImageClassifiedResult(
  92. similarGroups: types.contains(.similar) ? await self.perform(action: .similarGroups(with: items)) : nil,
  93. peopleImages: types.contains(.people) ? await self.perform(action: .peopleImages(with: items)) : nil,
  94. screenshotImages: types.contains(.screenshot) ? await self.perform(action: .screenshotImages(with: items)) : nil,
  95. blurryImages: types.contains(.blurry) ? await self.perform(action: .blurryImages(with: items)) : nil
  96. )
  97. self.isClassificationFinished = progress.isCompleted
  98. await completion(progress, result)
  99. }
  100. }
  101. /// 设置分类器配置
  102. /// - Parameter configuration: 分类器配置选项
  103. /// - Note: 必须在调用`startClassification`方法前进行配置
  104. public func configure(with configuration: Configuration) {
  105. self.configuration = configuration
  106. }
  107. /// 重置
  108. public func reset() {
  109. self.isAssetsLoaded = false
  110. self.isClassificationFinished = false
  111. self.imageItems = []
  112. self.configuration = .init()
  113. }
  114. }
  115. // MARK: - 私有方法
  116. extension PhotosImageClassifier {
  117. // MARK: - 资源获取
  118. /// 获取所有图像资源
  119. private func fetchAllImageAssets() {
  120. let startTime = Date()
  121. // 配置获取选项
  122. let fetchOptions = PHFetchOptions()
  123. fetchOptions.sortDescriptors = [NSSortDescriptor(key: "creationDate", ascending: false)]
  124. fetchOptions.includeHiddenAssets = false // 排除隐藏资源
  125. fetchOptions.includeAllBurstAssets = false // 排除连拍照片
  126. // 获取所有图片资源
  127. let fetchResult = PHAsset.fetchAssets(with: .image, options: fetchOptions)
  128. // 批量获取资源对象
  129. let count = fetchResult.count
  130. let userAssets = fetchResult.objects(at: IndexSet(integersIn: 0..<count))
  131. // 创建图片项目数组
  132. self.imageItems = userAssets.map { asset in
  133. ImageItem(asset: asset, thumbnailImage: nil, imageInfo: nil)
  134. }
  135. // 禁用高质量图片缓存以节省内存
  136. imageManager.allowsCachingHighQualityImages = false
  137. let timeInterval = Date().timeIntervalSince(startTime)
  138. print("获取相册 \(count) 资产,用时: \(String(format: "%.2f", timeInterval)) 秒")
  139. self.isAssetsLoaded = true
  140. }
  141. /// 请求资源图像
  142. /// - Parameters:
  143. /// - item: 图像项目
  144. /// - options: 图像请求选项
  145. /// - Returns: 包含缩略图的图像项目
  146. internal func requestAssetsImageFor(_ item: ImageItem, options: PHImageRequestOptions) async -> ImageItem {
  147. var mutableItem = item
  148. return await withCheckedContinuation { continuation in
  149. self.imageManager.requestImage(for: mutableItem.asset, targetSize: self.prefetchImageSize, contentMode: .aspectFit, options: options) { image, _ in
  150. if let image = image {
  151. mutableItem.thumbnailImage = image
  152. }
  153. continuation.resume(returning: mutableItem)
  154. }
  155. }
  156. }
  157. }