| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186 |
- //
- // 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<ResultType> {
- /// 查找相似图片组
- 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<ResultType>(action: ClassificationAction<ResultType>) 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..<count))
-
- // 创建图片项目数组
- self.imageItems = userAssets.map { asset in
- ImageItem(asset: asset, thumbnailImage: nil, imageInfo: nil)
- }
-
- // 禁用高质量图片缓存以节省内存
- imageManager.allowsCachingHighQualityImages = false
-
- let timeInterval = Date().timeIntervalSince(startTime)
- print("获取相册 \(count) 资产,用时: \(String(format: "%.2f", timeInterval)) 秒")
-
- self.isAssetsLoaded = true
- }
-
- /// 请求资源图像
- /// - Parameters:
- /// - item: 图像项目
- /// - options: 图像请求选项
- /// - Returns: 包含缩略图的图像项目
- internal func requestAssetsImageFor(_ item: ImageItem, options: PHImageRequestOptions) async -> 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)
- }
- }
- }
- }
|