ClassifyPhoto.swift 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428
  1. import Photos
  2. import Vision
  3. class ClassifyPhoto {
  4. struct PhotoSizeInfo {
  5. var totalSize: Int64 = 0
  6. var count: Int = 0
  7. }
  8. struct ClassifiedPhotos {
  9. var screenshots: [PHAsset] = []
  10. var locations: [String: [PHAsset]] = [:] // 按地点分组
  11. var people: [String: [PHAsset]] = [:] // 按人物分组
  12. var similarPhotos: [[PHAsset]] = [] // 存储相似照片组
  13. // 添加容量信息
  14. var screenshotsSize: PhotoSizeInfo = PhotoSizeInfo()
  15. var locationsSize: PhotoSizeInfo = PhotoSizeInfo()
  16. var peopleSize: PhotoSizeInfo = PhotoSizeInfo()
  17. var similarPhotosSize: PhotoSizeInfo = PhotoSizeInfo()
  18. }
  19. func classifyPhotos(
  20. assets: PHFetchResult<PHAsset>,
  21. progressHandler: @escaping (String, Float) -> Void,
  22. completion: @escaping (ClassifiedPhotos) -> Void
  23. ) {
  24. // 在后台队列处理
  25. DispatchQueue.global(qos: .userInitiated).async {
  26. var result = ClassifiedPhotos()
  27. let group = DispatchGroup()
  28. // 开始处理
  29. DispatchQueue.main.async {
  30. progressHandler("正在加载照片...", 0.0)
  31. }
  32. // 1. 检测截图 (占总进度的 20%)
  33. group.enter()
  34. self.fetchScreenshots(from: assets) { screenshots in
  35. result.screenshots = screenshots
  36. DispatchQueue.main.async {
  37. progressHandler("正在检测截图...", 0.2)
  38. }
  39. group.leave()
  40. }
  41. // 2. 检测相似照片 (占总进度的 80%)
  42. group.enter()
  43. self.detectSimilarPhotos(
  44. assets: assets,
  45. progressHandler: { stage, progress in
  46. // 将相似照片检测的进度映射到 20%-100% 的范围
  47. let mappedProgress = 0.2 + (progress * 0.6)
  48. DispatchQueue.main.async {
  49. progressHandler(stage, mappedProgress)
  50. }
  51. }
  52. ) { similarPhotos in
  53. result.similarPhotos = similarPhotos
  54. group.leave()
  55. }
  56. // 3. 按地点分类 (占总进度的 20%)
  57. group.enter()
  58. self.classifyByLocation(assets: assets) { locationGroups in
  59. result.locations = locationGroups
  60. DispatchQueue.main.async {
  61. progressHandler("正在按地点分类...", 0.8)
  62. }
  63. group.leave()
  64. }
  65. // 4. 按人物分类 (占总进度的 20%)
  66. group.enter()
  67. self.classifyByPeople(assets: assets) { peopleGroups in
  68. result.people = peopleGroups
  69. DispatchQueue.main.async {
  70. progressHandler("正在按人物分类...", 1.0)
  71. }
  72. group.leave()
  73. }
  74. // 在所有分类完成后计算大小
  75. group.notify(queue: .main) {
  76. let sizeGroup = DispatchGroup()
  77. // 计算截图大小
  78. sizeGroup.enter()
  79. self.calculateAssetsSize(result.screenshots) { sizeInfo in
  80. result.screenshotsSize = sizeInfo
  81. sizeGroup.leave()
  82. }
  83. // 计算地点照片大小
  84. sizeGroup.enter()
  85. let locationAssets = Array(result.locations.values.flatMap { $0 })
  86. self.calculateAssetsSize(locationAssets) { sizeInfo in
  87. result.locationsSize = sizeInfo
  88. sizeGroup.leave()
  89. }
  90. // 计算人物照片大小
  91. sizeGroup.enter()
  92. let peopleAssets = Array(result.people.values.flatMap { $0 })
  93. self.calculateAssetsSize(peopleAssets) { sizeInfo in
  94. result.peopleSize = sizeInfo
  95. sizeGroup.leave()
  96. }
  97. // 计算相似照片大小
  98. sizeGroup.enter()
  99. let similarAssets = Array(result.similarPhotos.flatMap { $0 })
  100. self.calculateAssetsSize(similarAssets) { sizeInfo in
  101. result.similarPhotosSize = sizeInfo
  102. sizeGroup.leave()
  103. }
  104. // 所有大小计算完成后回调
  105. sizeGroup.notify(queue: .main) {
  106. progressHandler("分类完成", 1.0)
  107. completion(result)
  108. }
  109. }
  110. // // 等待所有处理完成
  111. // group.notify(queue: .main) {
  112. // progressHandler("分类完成", 1.0)
  113. // completion(result)
  114. // }
  115. }
  116. }
  117. private func detectSimilarPhotos(
  118. assets: PHFetchResult<PHAsset>,
  119. progressHandler: @escaping (String, Float) -> Void,
  120. completion: @escaping ([[PHAsset]]) -> Void
  121. ) {
  122. var similarGroups: [[PHAsset]] = []
  123. let group = DispatchGroup()
  124. if #available(iOS 13.0, *) {
  125. var imageFeatures: [(asset: PHAsset, feature: VNFeaturePrintObservation)] = []
  126. // 创建处理队列
  127. let processingQueue = DispatchQueue(label: "com.app.similarPhotos", qos: .userInitiated)
  128. let semaphore = DispatchSemaphore(value: 5)
  129. // 1. 提取所有图片的特征
  130. let totalAssets = assets.count
  131. var processedAssets = 0
  132. progressHandler("正在加载照片...", 0.0)
  133. for i in 0..<assets.count {
  134. let asset = assets[i]
  135. group.enter()
  136. semaphore.wait()
  137. let options = PHImageRequestOptions()
  138. options.deliveryMode = .highQualityFormat
  139. options.isSynchronous = false
  140. options.resizeMode = .exact
  141. PHImageManager.default().requestImage(
  142. for: asset,
  143. targetSize: CGSize(width: 448, height: 448),
  144. contentMode: .aspectFit,
  145. options: options
  146. ) { image, _ in
  147. defer {
  148. semaphore.signal()
  149. }
  150. guard let image = image,
  151. let cgImage = image.cgImage else {
  152. group.leave()
  153. return
  154. }
  155. processingQueue.async {
  156. do {
  157. let requestHandler = VNImageRequestHandler(cgImage: cgImage, options: [:])
  158. let request = VNGenerateImageFeaturePrintRequest()
  159. try requestHandler.perform([request])
  160. if let result = request.results?.first as? VNFeaturePrintObservation {
  161. imageFeatures.append((asset, result))
  162. // 更新特征提取进度
  163. processedAssets += 1
  164. let progress = Float(processedAssets) / Float(totalAssets)
  165. progressHandler("正在提取特征...", progress * 0.6)
  166. }
  167. } catch {
  168. print("特征提取失败: \(error)")
  169. }
  170. group.leave()
  171. }
  172. }
  173. }
  174. // 2. 比较特征相似度并分组
  175. group.notify(queue: processingQueue) {
  176. progressHandler("正在比较相似度...", 0.6)
  177. // 近似度
  178. let similarityThreshold: Float = 0.7
  179. var processedComparisons = 0
  180. let totalComparisons = (imageFeatures.count * (imageFeatures.count - 1)) / 2
  181. var processedIndices = Set<Int>()
  182. for i in 0..<imageFeatures.count {
  183. if processedIndices.contains(i) { continue }
  184. var similarGroup: [PHAsset] = [imageFeatures[i].asset]
  185. processedIndices.insert(i)
  186. for j in (i + 1)..<imageFeatures.count {
  187. if processedIndices.contains(j) { continue }
  188. do {
  189. var distance: Float = 0
  190. try imageFeatures[i].feature.computeDistance(&distance, to: imageFeatures[j].feature)
  191. let similarity = 1 - distance
  192. if similarity >= similarityThreshold {
  193. similarGroup.append(imageFeatures[j].asset)
  194. processedIndices.insert(j)
  195. }
  196. // 更新比较进度
  197. processedComparisons += 1
  198. let compareProgress = Float(processedComparisons) / Float(totalComparisons)
  199. progressHandler("正在比较相似度...", 0.6 + compareProgress * 0.4)
  200. } catch {
  201. print("相似度计算失败: \(error)")
  202. }
  203. }
  204. if similarGroup.count > 1 {
  205. similarGroups.append(similarGroup)
  206. }
  207. }
  208. // 按照照片数量降序排序
  209. similarGroups.sort { $0.count > $1.count }
  210. DispatchQueue.main.async {
  211. completion(similarGroups)
  212. }
  213. }
  214. }
  215. }
  216. // 按地点分类
  217. private func classifyByLocation(assets: PHFetchResult<PHAsset>,
  218. completion: @escaping ([String: [PHAsset]]) -> Void) {
  219. var locationGroups: [String: [PHAsset]] = [:]
  220. let group = DispatchGroup()
  221. let geocodeQueue = DispatchQueue(label: "com.app.geocoding")
  222. let semaphore = DispatchSemaphore(value: 10) // 限制并发请求数
  223. assets.enumerateObjects { asset, _, _ in
  224. if let location = asset.location {
  225. group.enter()
  226. semaphore.wait()
  227. geocodeQueue.async {
  228. let geocoder = CLGeocoder()
  229. geocoder.reverseGeocodeLocation(location) { placemarks, error in
  230. defer {
  231. semaphore.signal()
  232. group.leave()
  233. }
  234. if let placemark = placemarks?.first {
  235. let locationName = self.formatLocationName(placemark)
  236. DispatchQueue.main.async {
  237. if locationGroups[locationName] == nil {
  238. locationGroups[locationName] = []
  239. }
  240. locationGroups[locationName]?.append(asset)
  241. }
  242. }
  243. }
  244. }
  245. }
  246. }
  247. // 等待所有地理编码完成后回调
  248. group.notify(queue: .main) {
  249. completion(locationGroups)
  250. }
  251. }
  252. // 格式化地点名称(只返回城市名)
  253. private func formatLocationName(_ placemark: CLPlacemark) -> String {
  254. if let city = placemark.locality {
  255. return city
  256. }
  257. return "其他"
  258. }
  259. // 按人物分类
  260. private func classifyByPeople(assets: PHFetchResult<PHAsset>,
  261. completion: @escaping ([String: [PHAsset]]) -> Void) {
  262. var peopleGroups: [String: [PHAsset]] = [:]
  263. let group = DispatchGroup()
  264. // 创建一个数组来存储检测到人脸的照片
  265. var facesArray: [PHAsset] = []
  266. // 遍历所有照片
  267. assets.enumerateObjects { asset, _, _ in
  268. group.enter()
  269. // 获取照片的缩略图进行人脸检测
  270. let options = PHImageRequestOptions()
  271. options.isSynchronous = false
  272. options.deliveryMode = .fastFormat
  273. PHImageManager.default().requestImage(
  274. for: asset,
  275. targetSize: CGSize(width: 500, height: 500), // 使用较小的尺寸提高性能
  276. contentMode: .aspectFit,
  277. options: options
  278. ) { image, _ in
  279. guard let image = image else {
  280. group.leave()
  281. return
  282. }
  283. // 使用 Vision 框架检测人脸
  284. guard let ciImage = CIImage(image: image) else {
  285. group.leave()
  286. return
  287. }
  288. let request = VNDetectFaceRectanglesRequest()
  289. let handler = VNImageRequestHandler(ciImage: ciImage)
  290. do {
  291. try handler.perform([request])
  292. if let results = request.results, !results.isEmpty {
  293. // 检测到人脸,添加到数组
  294. DispatchQueue.main.async {
  295. facesArray.append(asset)
  296. }
  297. }
  298. } catch {
  299. print("人脸检测失败: \(error)")
  300. }
  301. group.leave()
  302. }
  303. }
  304. // 等待所有检测完成后更新结果
  305. group.notify(queue: .main) {
  306. if !facesArray.isEmpty {
  307. peopleGroups["包含人脸的照片"] = facesArray
  308. }
  309. completion(peopleGroups)
  310. }
  311. }
  312. // 识别截图
  313. private func fetchScreenshots(from assets: PHFetchResult<PHAsset>,
  314. completion: @escaping ([PHAsset]) -> Void) {
  315. var screenshots: [PHAsset] = []
  316. // 获取系统的截图智能相册
  317. let screenshotAlbums = PHAssetCollection.fetchAssetCollections(
  318. with: .smartAlbum,
  319. subtype: .smartAlbumScreenshots,
  320. options: nil
  321. )
  322. // 从截图相册中获取所有截图
  323. screenshotAlbums.enumerateObjects { collection, _, _ in
  324. let fetchOptions = PHFetchOptions()
  325. let screenshotAssets = PHAsset.fetchAssets(in: collection, options: fetchOptions)
  326. screenshotAssets.enumerateObjects { asset, _, _ in
  327. screenshots.append(asset)
  328. }
  329. }
  330. completion(screenshots)
  331. }
  332. }
  333. extension ClassifyPhoto {
  334. // 获取资源大小的辅助方法
  335. private func getAssetSize(_ asset: PHAsset, completion: @escaping (Int64) -> Void) {
  336. let resources = PHAssetResource.assetResources(for: asset)
  337. if let resource = resources.first {
  338. var size: Int64 = 0
  339. if let unsignedInt64 = resource.value(forKey: "fileSize") as? CLong {
  340. size = Int64(unsignedInt64)
  341. }
  342. completion(size)
  343. } else {
  344. completion(0)
  345. }
  346. }
  347. // 计算资产组的总大小
  348. private func calculateAssetsSize(_ assets: [PHAsset], completion: @escaping (PhotoSizeInfo) -> Void) {
  349. let group = DispatchGroup()
  350. var totalSize: Int64 = 0
  351. for asset in assets {
  352. group.enter()
  353. getAssetSize(asset) { size in
  354. totalSize += size
  355. group.leave()
  356. }
  357. }
  358. group.notify(queue: .main) {
  359. completion(PhotoSizeInfo(totalSize: totalSize, count: assets.count))
  360. }
  361. }
  362. }