无障碍与用户体验: - 补全 OnboardingView/ProcessingView/ResultView/WallpaperGuideView 无障碍标注 - ContentView 添加后台切换截屏保护(模糊覆盖层) - EditorView iPad 右侧面板改为响应式宽度(minWidth:320, maxWidth:420) - ResultView CelebrationParticles 改用 Task 替代 DispatchQueue 延迟 - DesignSystem accessibility key 从中文改为英文 - EditorView/WallpaperGuideView 生成按钮 accentColor 统一为设计系统 - OnboardingView 第四页添加相册权限预告提示 核心库优化: - LivePhotoLogger 添加日志安全注释 - CacheManager.makeWorkPaths 添加磁盘空间预检查(<500MB 报错) - ImageFormatConverter BGRA/ARGB 转换改用 vImagePermuteChannels 加速 Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
270 lines
8.8 KiB
Swift
270 lines
8.8 KiB
Swift
//
|
|
// ImageFormatConverter.swift
|
|
// LivePhotoCore
|
|
//
|
|
// Utilities for converting between CGImage and CVPixelBuffer formats.
|
|
//
|
|
|
|
import Accelerate
|
|
import CoreGraphics
|
|
import CoreVideo
|
|
import Foundation
|
|
import VideoToolbox
|
|
|
|
/// Utilities for image format conversion
|
|
enum ImageFormatConverter {
|
|
/// Convert CGImage to CVPixelBuffer for Core ML input
|
|
/// - Parameters:
|
|
/// - image: Input CGImage
|
|
/// - pixelFormat: Output pixel format (default BGRA)
|
|
/// - Returns: CVPixelBuffer ready for Core ML
|
|
static func cgImageToPixelBuffer(
|
|
_ image: CGImage,
|
|
pixelFormat: OSType = kCVPixelFormatType_32BGRA
|
|
) throws -> CVPixelBuffer {
|
|
let width = image.width
|
|
let height = image.height
|
|
|
|
// Create pixel buffer
|
|
var pixelBuffer: CVPixelBuffer?
|
|
let attrs: [CFString: Any] = [
|
|
kCVPixelBufferCGImageCompatibilityKey: true,
|
|
kCVPixelBufferCGBitmapContextCompatibilityKey: true,
|
|
kCVPixelBufferMetalCompatibilityKey: true,
|
|
]
|
|
|
|
let status = CVPixelBufferCreate(
|
|
kCFAllocatorDefault,
|
|
width,
|
|
height,
|
|
pixelFormat,
|
|
attrs as CFDictionary,
|
|
&pixelBuffer
|
|
)
|
|
|
|
guard status == kCVReturnSuccess, let buffer = pixelBuffer else {
|
|
throw AIEnhanceError.inputImageInvalid
|
|
}
|
|
|
|
// Lock buffer for writing
|
|
CVPixelBufferLockBaseAddress(buffer, [])
|
|
defer { CVPixelBufferUnlockBaseAddress(buffer, []) }
|
|
|
|
guard let baseAddress = CVPixelBufferGetBaseAddress(buffer) else {
|
|
throw AIEnhanceError.inputImageInvalid
|
|
}
|
|
|
|
let bytesPerRow = CVPixelBufferGetBytesPerRow(buffer)
|
|
let colorSpace = CGColorSpaceCreateDeviceRGB()
|
|
|
|
// Create bitmap context to draw into pixel buffer
|
|
guard
|
|
let context = CGContext(
|
|
data: baseAddress,
|
|
width: width,
|
|
height: height,
|
|
bitsPerComponent: 8,
|
|
bytesPerRow: bytesPerRow,
|
|
space: colorSpace,
|
|
bitmapInfo: CGImageAlphaInfo.premultipliedFirst.rawValue
|
|
| CGBitmapInfo.byteOrder32Little.rawValue
|
|
)
|
|
else {
|
|
throw AIEnhanceError.inputImageInvalid
|
|
}
|
|
|
|
// Draw image into context (this converts the format)
|
|
context.draw(image, in: CGRect(x: 0, y: 0, width: width, height: height))
|
|
|
|
return buffer
|
|
}
|
|
|
|
/// Convert CVPixelBuffer to CGImage
|
|
/// - Parameter pixelBuffer: Input pixel buffer
|
|
/// - Returns: CGImage representation
|
|
static func pixelBufferToCGImage(_ pixelBuffer: CVPixelBuffer) throws -> CGImage {
|
|
var cgImage: CGImage?
|
|
VTCreateCGImageFromCVPixelBuffer(pixelBuffer, options: nil, imageOut: &cgImage)
|
|
|
|
guard let image = cgImage else {
|
|
throw AIEnhanceError.inferenceError("Failed to create CGImage from pixel buffer")
|
|
}
|
|
|
|
return image
|
|
}
|
|
|
|
/// Extract raw RGBA pixel data from CVPixelBuffer
|
|
/// - Parameter pixelBuffer: Input pixel buffer
|
|
/// - Returns: Array of RGBA bytes
|
|
static func pixelBufferToRGBAData(_ pixelBuffer: CVPixelBuffer) throws -> [UInt8] {
|
|
CVPixelBufferLockBaseAddress(pixelBuffer, .readOnly)
|
|
defer { CVPixelBufferUnlockBaseAddress(pixelBuffer, .readOnly) }
|
|
|
|
let width = CVPixelBufferGetWidth(pixelBuffer)
|
|
let height = CVPixelBufferGetHeight(pixelBuffer)
|
|
let bytesPerRow = CVPixelBufferGetBytesPerRow(pixelBuffer)
|
|
|
|
guard let baseAddress = CVPixelBufferGetBaseAddress(pixelBuffer) else {
|
|
throw AIEnhanceError.inferenceError("Cannot access pixel buffer data")
|
|
}
|
|
|
|
let pixelFormat = CVPixelBufferGetPixelFormatType(pixelBuffer)
|
|
|
|
// Handle BGRA format (most common from Core ML)
|
|
if pixelFormat == kCVPixelFormatType_32BGRA {
|
|
return convertBGRAToRGBA(
|
|
baseAddress: baseAddress,
|
|
width: width,
|
|
height: height,
|
|
bytesPerRow: bytesPerRow
|
|
)
|
|
}
|
|
|
|
// Handle RGBA format
|
|
if pixelFormat == kCVPixelFormatType_32RGBA {
|
|
var result = [UInt8](repeating: 0, count: width * height * 4)
|
|
for y in 0..<height {
|
|
let srcRow = baseAddress.advanced(by: y * bytesPerRow)
|
|
let dstOffset = y * width * 4
|
|
memcpy(&result[dstOffset], srcRow, width * 4)
|
|
}
|
|
return result
|
|
}
|
|
|
|
// Handle ARGB format
|
|
if pixelFormat == kCVPixelFormatType_32ARGB {
|
|
return convertARGBToRGBA(
|
|
baseAddress: baseAddress,
|
|
width: width,
|
|
height: height,
|
|
bytesPerRow: bytesPerRow
|
|
)
|
|
}
|
|
|
|
throw AIEnhanceError.inferenceError("Unsupported pixel format: \(pixelFormat)")
|
|
}
|
|
|
|
/// Create CVPixelBuffer from raw RGBA data
|
|
/// - Parameters:
|
|
/// - rgbaData: RGBA pixel data
|
|
/// - width: Image width
|
|
/// - height: Image height
|
|
/// - Returns: CVPixelBuffer
|
|
static func rgbaDataToPixelBuffer(
|
|
_ rgbaData: [UInt8],
|
|
width: Int,
|
|
height: Int
|
|
) throws -> CVPixelBuffer {
|
|
var pixelBuffer: CVPixelBuffer?
|
|
let attrs: [CFString: Any] = [
|
|
kCVPixelBufferCGImageCompatibilityKey: true,
|
|
kCVPixelBufferCGBitmapContextCompatibilityKey: true,
|
|
]
|
|
|
|
let status = CVPixelBufferCreate(
|
|
kCFAllocatorDefault,
|
|
width,
|
|
height,
|
|
kCVPixelFormatType_32BGRA,
|
|
attrs as CFDictionary,
|
|
&pixelBuffer
|
|
)
|
|
|
|
guard status == kCVReturnSuccess, let buffer = pixelBuffer else {
|
|
throw AIEnhanceError.inputImageInvalid
|
|
}
|
|
|
|
CVPixelBufferLockBaseAddress(buffer, [])
|
|
defer { CVPixelBufferUnlockBaseAddress(buffer, []) }
|
|
|
|
guard let baseAddress = CVPixelBufferGetBaseAddress(buffer) else {
|
|
throw AIEnhanceError.inputImageInvalid
|
|
}
|
|
|
|
let bytesPerRow = CVPixelBufferGetBytesPerRow(buffer)
|
|
|
|
// Convert RGBA to BGRA while copying
|
|
for y in 0..<height {
|
|
let dstRow = baseAddress.advanced(by: y * bytesPerRow).assumingMemoryBound(to: UInt8.self)
|
|
let srcOffset = y * width * 4
|
|
|
|
for x in 0..<width {
|
|
let srcIdx = srcOffset + x * 4
|
|
let dstIdx = x * 4
|
|
|
|
// RGBA -> BGRA swap
|
|
dstRow[dstIdx + 0] = rgbaData[srcIdx + 2] // B
|
|
dstRow[dstIdx + 1] = rgbaData[srcIdx + 1] // G
|
|
dstRow[dstIdx + 2] = rgbaData[srcIdx + 0] // R
|
|
dstRow[dstIdx + 3] = rgbaData[srcIdx + 3] // A
|
|
}
|
|
}
|
|
|
|
return buffer
|
|
}
|
|
|
|
// MARK: - Private Helpers
|
|
|
|
private static func convertBGRAToRGBA(
|
|
baseAddress: UnsafeMutableRawPointer,
|
|
width: Int,
|
|
height: Int,
|
|
bytesPerRow: Int
|
|
) -> [UInt8] {
|
|
var result = [UInt8](repeating: 0, count: width * height * 4)
|
|
let dstBytesPerRow = width * 4
|
|
|
|
var srcBuffer = vImage_Buffer(
|
|
data: baseAddress,
|
|
height: vImagePixelCount(height),
|
|
width: vImagePixelCount(width),
|
|
rowBytes: bytesPerRow
|
|
)
|
|
|
|
result.withUnsafeMutableBufferPointer { dstPtr in
|
|
var dstBuffer = vImage_Buffer(
|
|
data: dstPtr.baseAddress!,
|
|
height: vImagePixelCount(height),
|
|
width: vImagePixelCount(width),
|
|
rowBytes: dstBytesPerRow
|
|
)
|
|
// BGRA → RGBA: 通道重排 [2,1,0,3]
|
|
let permuteMap: [UInt8] = [2, 1, 0, 3]
|
|
vImagePermuteChannels_ARGB8888(&srcBuffer, &dstBuffer, permuteMap, vImage_Flags(kvImageNoFlags))
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
private static func convertARGBToRGBA(
|
|
baseAddress: UnsafeMutableRawPointer,
|
|
width: Int,
|
|
height: Int,
|
|
bytesPerRow: Int
|
|
) -> [UInt8] {
|
|
var result = [UInt8](repeating: 0, count: width * height * 4)
|
|
let dstBytesPerRow = width * 4
|
|
|
|
var srcBuffer = vImage_Buffer(
|
|
data: baseAddress,
|
|
height: vImagePixelCount(height),
|
|
width: vImagePixelCount(width),
|
|
rowBytes: bytesPerRow
|
|
)
|
|
|
|
result.withUnsafeMutableBufferPointer { dstPtr in
|
|
var dstBuffer = vImage_Buffer(
|
|
data: dstPtr.baseAddress!,
|
|
height: vImagePixelCount(height),
|
|
width: vImagePixelCount(width),
|
|
rowBytes: dstBytesPerRow
|
|
)
|
|
// ARGB → RGBA: 通道重排 [1,2,3,0]
|
|
let permuteMap: [UInt8] = [1, 2, 3, 0]
|
|
vImagePermuteChannels_ARGB8888(&srcBuffer, &dstBuffer, permuteMap, vImage_Flags(kvImageNoFlags))
|
|
}
|
|
|
|
return result
|
|
}
|
|
}
|