- TiledImageProcessor 重写:将大图拆分为 512×512 重叠 tiles - 64px 重叠区域 + 线性权重混合,消除拼接接缝 - AIEnhancer 自动选择处理器:大图用 TiledImageProcessor,小图用 WholeImageProcessor - 信息损失从 ~86% 降至 0%(1080×1920 图像不再压缩到 288×512) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
247 lines
7.5 KiB
Swift
247 lines
7.5 KiB
Swift
//
|
|
// AIEnhancer.swift
|
|
// LivePhotoCore
|
|
//
|
|
// AI super-resolution enhancement using Real-ESRGAN Core ML model.
|
|
//
|
|
|
|
import CoreGraphics
|
|
import CoreML
|
|
import Foundation
|
|
import os
|
|
|
|
// MARK: - Configuration
|
|
|
|
/// AI enhancement configuration
|
|
public struct AIEnhanceConfig: Codable, Sendable, Hashable {
|
|
/// Enable AI super-resolution
|
|
public var enabled: Bool
|
|
|
|
public init(enabled: Bool = false) {
|
|
self.enabled = enabled
|
|
}
|
|
|
|
/// Disabled configuration
|
|
public static let disabled = AIEnhanceConfig(enabled: false)
|
|
|
|
/// Standard configuration
|
|
public static let standard = AIEnhanceConfig(enabled: true)
|
|
}
|
|
|
|
// MARK: - Result
|
|
|
|
/// AI enhancement result
|
|
public struct AIEnhanceResult: Sendable {
|
|
/// Enhanced image
|
|
public let enhancedImage: CGImage
|
|
|
|
/// Original image size
|
|
public let originalSize: CGSize
|
|
|
|
/// Enhanced image size
|
|
public let enhancedSize: CGSize
|
|
|
|
/// Processing time in milliseconds
|
|
public let processingTimeMs: Double
|
|
}
|
|
|
|
// MARK: - Errors
|
|
|
|
/// AI enhancement error types
|
|
public enum AIEnhanceError: Error, Sendable, LocalizedError {
|
|
case modelNotFound
|
|
case modelLoadFailed(String)
|
|
case inputImageInvalid
|
|
case inferenceError(String)
|
|
case memoryPressure
|
|
case cancelled
|
|
case deviceNotSupported
|
|
|
|
public var errorDescription: String? {
|
|
switch self {
|
|
case .modelNotFound:
|
|
return "AI model file not found in bundle"
|
|
case let .modelLoadFailed(reason):
|
|
return "Failed to load AI model: \(reason)"
|
|
case .inputImageInvalid:
|
|
return "Input image is invalid or cannot be processed"
|
|
case let .inferenceError(reason):
|
|
return "AI inference failed: \(reason)"
|
|
case .memoryPressure:
|
|
return "Not enough memory for AI processing"
|
|
case .cancelled:
|
|
return "AI enhancement was cancelled"
|
|
case .deviceNotSupported:
|
|
return "Device does not support AI enhancement"
|
|
}
|
|
}
|
|
}
|
|
|
|
// MARK: - Progress
|
|
|
|
/// Progress callback for AI enhancement
|
|
/// - Parameter progress: Value from 0.0 to 1.0
|
|
public typealias AIEnhanceProgress = @Sendable (Double) -> Void
|
|
|
|
// MARK: - Main Actor
|
|
|
|
/// AI enhancement actor for super-resolution processing
|
|
public actor AIEnhancer {
|
|
private let config: AIEnhanceConfig
|
|
private var processor: RealESRGANProcessor?
|
|
private let logger = Logger(subsystem: "LivePhotoCore", category: "AIEnhancer")
|
|
|
|
/// Scale factor (4 for Real-ESRGAN x4plus)
|
|
public static let scaleFactor: Int = 4
|
|
|
|
/// Initialize with configuration
|
|
public init(config: AIEnhanceConfig = .standard) {
|
|
self.config = config
|
|
}
|
|
|
|
// MARK: - Device Capability
|
|
|
|
/// Check if AI enhancement is available on this device
|
|
public static func isAvailable() -> Bool {
|
|
// Require iOS 17+
|
|
guard #available(iOS 17.0, *) else {
|
|
return false
|
|
}
|
|
|
|
// Check device memory (require at least 4GB)
|
|
let totalMemory = ProcessInfo.processInfo.physicalMemory
|
|
let memoryGB = Double(totalMemory) / (1024 * 1024 * 1024)
|
|
guard memoryGB >= 4.0 else {
|
|
return false
|
|
}
|
|
|
|
// Neural Engine is available on A12+ (iPhone XS and later)
|
|
// iOS 17 requirement ensures A12+ is present
|
|
return true
|
|
}
|
|
|
|
// MARK: - Model Download (ODR)
|
|
|
|
/// Check if AI model needs to be downloaded
|
|
public static func needsDownload() async -> Bool {
|
|
let available = await ODRManager.shared.isModelAvailable()
|
|
return !available
|
|
}
|
|
|
|
/// Get current model download state
|
|
public static func getDownloadState() async -> ModelDownloadState {
|
|
await ODRManager.shared.getDownloadState()
|
|
}
|
|
|
|
/// Download AI model with progress callback
|
|
/// - Parameter progress: Progress callback (0.0 to 1.0)
|
|
public static func downloadModel(progress: @escaping @Sendable (Double) -> Void) async throws {
|
|
try await ODRManager.shared.downloadModel(progress: progress)
|
|
}
|
|
|
|
/// Release ODR resources when AI enhancement is no longer needed
|
|
public static func releaseModelResources() async {
|
|
await ODRManager.shared.releaseResources()
|
|
}
|
|
|
|
// MARK: - Model Management
|
|
|
|
/// Preload the model (call during app launch or settings change)
|
|
public func preloadModel() async throws {
|
|
guard AIEnhancer.isAvailable() else {
|
|
throw AIEnhanceError.deviceNotSupported
|
|
}
|
|
|
|
guard processor == nil else {
|
|
logger.debug("Model already loaded")
|
|
return
|
|
}
|
|
|
|
logger.info("Preloading Real-ESRGAN model...")
|
|
|
|
processor = RealESRGANProcessor()
|
|
try await processor?.loadModel()
|
|
|
|
logger.info("Model preloaded successfully")
|
|
}
|
|
|
|
/// Release model from memory
|
|
public func unloadModel() async {
|
|
await processor?.unloadModel()
|
|
processor = nil
|
|
logger.info("Model unloaded")
|
|
}
|
|
|
|
// MARK: - Enhancement
|
|
|
|
/// Enhance a single image with AI super-resolution
|
|
/// - Parameters:
|
|
/// - image: Input CGImage to enhance
|
|
/// - progress: Optional progress callback (0.0 to 1.0)
|
|
/// - Returns: Enhanced result with metadata
|
|
public func enhance(
|
|
image: CGImage,
|
|
progress: AIEnhanceProgress? = nil
|
|
) async throws -> AIEnhanceResult {
|
|
guard config.enabled else {
|
|
throw AIEnhanceError.inputImageInvalid
|
|
}
|
|
|
|
guard AIEnhancer.isAvailable() else {
|
|
throw AIEnhanceError.deviceNotSupported
|
|
}
|
|
|
|
let startTime = CFAbsoluteTimeGetCurrent()
|
|
let originalSize = CGSize(width: image.width, height: image.height)
|
|
|
|
logger.info("Starting AI enhancement: \(image.width)x\(image.height)")
|
|
|
|
// Ensure model is loaded
|
|
if processor == nil {
|
|
try await preloadModel()
|
|
}
|
|
|
|
guard let processor = processor else {
|
|
throw AIEnhanceError.modelNotFound
|
|
}
|
|
|
|
// Choose processor based on image size
|
|
// - Small images (≤ 512x512): use WholeImageProcessor (faster, single inference)
|
|
// - Large images (> 512 in either dimension): use TiledImageProcessor (preserves detail)
|
|
let usesTiling = image.width > RealESRGANProcessor.inputSize || image.height > RealESRGANProcessor.inputSize
|
|
|
|
let enhancedImage: CGImage
|
|
if usesTiling {
|
|
logger.info("Using tiled processing for large image")
|
|
let tiledProcessor = TiledImageProcessor()
|
|
enhancedImage = try await tiledProcessor.processImage(
|
|
image,
|
|
processor: processor,
|
|
progress: progress
|
|
)
|
|
} else {
|
|
logger.info("Using whole image processing for small image")
|
|
let wholeImageProcessor = WholeImageProcessor()
|
|
enhancedImage = try await wholeImageProcessor.processImage(
|
|
image,
|
|
processor: processor,
|
|
progress: progress
|
|
)
|
|
}
|
|
|
|
let processingTime = (CFAbsoluteTimeGetCurrent() - startTime) * 1000
|
|
let enhancedSize = CGSize(width: enhancedImage.width, height: enhancedImage.height)
|
|
|
|
logger.info(
|
|
"AI enhancement complete: \(Int(originalSize.width))x\(Int(originalSize.height)) -> \(Int(enhancedSize.width))x\(Int(enhancedSize.height)) in \(Int(processingTime))ms"
|
|
)
|
|
|
|
return AIEnhanceResult(
|
|
enhancedImage: enhancedImage,
|
|
originalSize: originalSize,
|
|
enhancedSize: enhancedSize,
|
|
processingTimeMs: processingTime
|
|
)
|
|
}
|
|
}
|