Files
to-live-photo/Sources/LivePhotoCore/AIEnhancer/AIEnhancer.swift
empty 3f503c1050 feat: 实现真正的分块处理优化 AI 增强质量
- 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>
2026-01-03 21:04:22 +08:00

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
)
}
}