문제 원인: 너무 많은 메모리 사용
이 이벤트(네 컷 영상 추출하러 가기)를 누르면 크게 3가지 일련의 작업을 처리한다.
- 각각 4개의 영상에서 프레임별 이미지 추출 (1초 당, 24개의 이미지) 후 CGImage로 저장 ⇒ [[CGImage]]
- 각 영역별 이미지를 하나의 이미지로 합성
(CGImage[0][0], CGImage[0][1],CGImage[0][2],CGImage[0][3]) ⇒ 합성된 CGImage[0] - 이미지를 영상으로 변환
문제의 원인: 영상을 이미지 배열로 변환하는 부분
⇒ 여기에서 갑자기 메모리를 2GB나 사용하는 것을 확인
ExtractService의 extractFrameImages() 메서드
struct AVAssetContainer:Identifiable {
var id: String
let idx: Int
let minDuration:Float
let originalAssetURL:String
}
class ExtractService{
var avAssetContainers: [AVAssetContainer] = []
var minDuration: Double = 0.47
var frameCounts:Int{ avAssetContainers.count }
private let fps:Double = 24
func extractFrameImages() async throws -> [[CGImage]] {
guard !avAssetContainers.isEmpty else { throw ExtractError.emptyContainer }
let imageDatas: [[CGImage]] = try await withThrowingTaskGroup(of: (Int,[CGImage]).self) { taskGroup in
for (offset,v) in avAssetContainers.enumerated(){
taskGroup.addTask {[minDuration,fps] in
let asset = AVAsset(url: URL(string: v.originalAssetURL)!)
let generator = AVAssetImageGenerator(asset: asset)
generator.appliesPreferredTrackTransform = true
generator.requestedTimeToleranceBefore = .init(seconds: Double(1 / (fps * 2)), preferredTimescale: 600)
generator.requestedTimeToleranceAfter = .init(seconds: Double(1 / (fps * 2)), preferredTimescale: 600)
var imageDatas:[CGImage] = []
var lastImage: CGImage!
let time = CMTime(seconds: 0, preferredTimescale: 600)
let imgContain: (image: CGImage, actualTime: CMTime) = try await generator.image(at: time)
imageDatas.append(imgContain.image)
lastImage = imgContain.image
for idx in (1..<Int(minDuration * fps)){
let time = CMTime(seconds: Double(idx) / 24, preferredTimescale: 600)
let imgContain = try? await generator.image(at: time)
if let imgContain{
imageDatas.append(imgContain.image)
lastImage = imgContain.image
}
else{ imageDatas.append(lastImage) }
}
return (offset,imageDatas)
}
}
var imageContainers: [[CGImage]] = Array(repeating:[], count: frameCounts)
for try await imageDatas in taskGroup{ imageContainers[imageDatas.0] = imageDatas.1 }
return imageContainers
}
return imageDatas
}
}
⇒ Concurrency 병렬 처리를 이용한다.
- 각각의
AVAssetContainer
를 병렬로 처리한다. - 각 Task는
AVAssetContainer
는 영상의 주소를 통해 AVAsset을 가져온다. - 가장 짧은 영상을 기준으로 프레임 이미지 배열의 length를 정한다.
- 각 Asset을
AVAssetImageGenerator
를 이용하여 프레임 단위의 CGImage를 가져온다. - 각 Task 별로 생성된 CGImage 배열을 하나로 합쳐 2차원 배열을 만든다.
CGImage[4][N]
➕ CPU 이용률
➕ 작업 속도
# 해결방법 1 - 이미지를 파일에 담기
✅ extractFrameImages()
로직을 각각의 이미지를 tempDirectory
에 저장해서 이미지를 저장한 경로를 반환한다.
실행 결과
메모리 사용량
✅ 2.5GB ⇒ 160MB → 90% 이상의 메모리 효율 상승
메서드 실행 시간
❌ 1521ms ⇒ 7955ms → 실행 시간 4배 이상 증가…
➕ 디스크 사용량과 CPU 사용량
# 해결방법 2 - DownSampling 이용하기
기존에 작성한 코드를 보면 AVAssetImageGenerator
를 통해 이미지를 가져오면 그 이미지를 그대로 쓰고 있었다..!
class ExtractService{
func extractFrameImages() async throws -> [[CGImage]] {
...
let imageDatas: [[CGImage]] = try await withThrowingTaskGroup(of: (Int,[CGImage]).self) { taskGroup in
...
let imgContain: (image: CGImage, actualTime: CMTime) = try await generator.image(at: time)
imageDatas.append(imgContain.image)
lastImage = imgContain.image
}
}
⇒ imgContain.image
의 CGImage의 크기를 줄이자
⇒ 어차피 한 영역만 차지하기 때문에 큰 픽셀의 이미지일 필요는 없다.
#1 CIImage를 이용해 이미지 줄이기
class ExtractService{
func extractFrameImages() async throws -> [[CGImage]] {
...
let imageDatas: [[CGImage]] = try await withThrowingTaskGroup(of: (Int,[CGImage]).self) { taskGroup in
...
let imgContain: (image: CGImage, actualTime: CMTime) = try await generator.image(at: time)
let downImage = self.downSample(image: imgContain.image)
imageDatas.append(downImage)
lastImage = downImage
}
}
}
extension ExtractService {
func downSample(image: CGImage) -> CGImage {
let ciImage = CIImage(cgImage: image)
let targetWidth : CGFloat = 360
let scale = targetWidth / ciImage.extent.width
let targetHeight = ciImage.extent.height * scale
let scaleSize = CGSize(width: targetWidth, height: targetHeight)
let transformedCIImage: CIImage = ciImage.transformed(by: .init(scaleX: scale, y: scale), highQualityDownsample: true)
let context = CIContext()
let afterDownsamplingImage: CGGimage = context.createCGImage(transformedCIImage, from: .init(origin: .zero, size: scaleSize))!
return afterDownsamplingImage
}
}
⇒ 목표 너비를 360 픽셀로 지정한다.
⇒ CIImage는 AffineTransform을 할 수 있으므로 scale 배율 변화하는 transform
메서드를 적용해서 배율을 줄인다.
⇒ 이 너비로 줄이기 위한 Scale을 지정해서 적용한다.
#1 실행 결과
메모리 사용량
✅ 2.5GB ⇒ 267MB → 88% 이상의 메모리 효율 상승
메서드 실행시간
⚠️ 1521ms ⇒ 3304ms → 실행 시간 2배 이상 증가…
➕ 이미지 압축률 ⇒ ✅ 약 90% 이상 DownSampling 달성
➕ CPU 사용량
#2 vImage를 이용해 이미지 줄이기
💡 GPT가 갑자기 이런 방법을 쓰라고 멋대로 적었다..!
Accelerate라는 프레임워크를 더 찾아봤으며, 이 프레임워크는 CPU에 직접 대규모 연산을 빠르게 처리하는 명령어를 통해 속도를 향상시키는데 사용한다고 한다…
⇒ 대규모 연산 중 백터(배열)과 관련된 처리를 빠르게 할 수 있다.
✅ Down Sampling과 관련된 Article이 있었고 여기 나온 코드들을 약간 수정해서 적용했다.
Building a Basic Image-Processing Workflow | Apple Developer Documentation
Resize an image with vImage.
developer.apple.com
▶️ 적용 코드
class ExtractService{
func extractFrameImages() async throws -> [[CGImage]] {
...
let imageDatas: [[CGImage]] = try await withThrowingTaskGroup(of: (Int,[CGImage]).self) { taskGroup in
...
let imgContain: (image: CGImage, actualTime: CMTime) = try await generator.image(at: time)
let downImage = self.downsampleVImage(image: imgContain.image)
imageDatas.append(downImage)
lastImage = downImage
}
}
}
extension ExtractService {
func downsampleVImage(image: CGImage, targetSize: CGSize = .init(width: 300, height: 300*1.77)) -> CGImage {
guard let format = vImage_CGImageFormat(
bitsPerComponent: 8,
bitsPerPixel: 32,
colorSpace: image.colorSpace ?? CGColorSpaceCreateDeviceRGB(),
bitmapInfo: CGBitmapInfo(rawValue: CGImageAlphaInfo.noneSkipLast.rawValue),
renderingIntent: .defaultIntent
) else {
return image
}
do {
var sourceBuffer = try vImage_Buffer(cgImage: image, format: format)
var destinationBuffer = vImage_Buffer()
defer {
free(destinationBuffer.data) // 메모리를 직접 해제해줘야함!!
free(sourceBuffer.data) // 메모리를 직접 해제해줘야함!!
}
// 이미지 크기 조절 알고리즘
let scaleX = targetSize.width / CGFloat(image.width)
let scaleY = targetSize.height / CGFloat(image.height)
let scale = min(scaleX, scaleY)
let destWidth = Int(CGFloat(image.width) * scale)
let destHeight = Int(CGFloat(image.height) * scale)
let bytesPerPixel = 4 // rgba를 대응하기 1바이트당 하나씩
destinationBuffer.width = UInt(destWidth)
destinationBuffer.height = UInt(destHeight)
destinationBuffer.rowBytes = destWidth * bytesPerPixel // 한 열의 bytes
destinationBuffer.data = malloc(destHeight * destinationBuffer.rowBytes) // 데이터 할당한다.
// openCV에서 scaling을 해주는 것과 비슷한 메서드
vImageScale_ARGB8888(&sourceBuffer, &destinationBuffer, nil, vImage_Flags(kvImageHighQualityResampling))
let destCGImage = try destinationBuffer.createCGImage(format: format)
return destCGImage
} catch {
return image
}
}
}
#2 실행 결과
메모리 사용량
✅ 2.5GB ⇒ 126.7 → 약 95%의 메모리 사용량 감소
실행 시간
✅ 1521ms ⇒ 1458ms → 실행 시간 감소
ps. 현재 기기의 상태에 따라 다를 수 있다… 암튼, 실행 시간이 거의 차이가 없다..!
➕ 이미지 압축률 ⇒ ✅ 약 90% 이상 DownSampling 달성
➕ CPU 사용량
결론
⇒ 모든 기기에 vImage를 사용해 압축시키고 이미지 배열을 반환하는 방법 채택
Why?
- iOS 4.0 부터 지원하는 프레임워크, 앱 버전은 16.0 이상으로 모든 사용자가 Accelerate 프레임워크를 쓸 수 있다.
- 기존 방식보다 실행시간이 초 단위가 아닌 정도의 오차범위 내에 오히려 감소하는 모습을 보여줌…
- 디스크에 이미지를 저장하는 방식보다 메모리 효율이 더 좋음
실행 비교 표
원본 이미지 배열 | 디스크에 이미지 저장 | CIImage DownSample 배열 | vImage DownSample 배열 | |
메모리 사용량 | 2.5GB | 131mb | 267mb | 126mb |
실행 시간 | 1521ms | 7955ms | 3304ms | 1458ms |
CPU 사용량 | 262 | 326% | 120% | 5% |
'이모저모 > AVFoundation' 카테고리의 다른 글
iOS 앨범 이미지 Fetch - PHCachingImageManager로 성능 향상 (0) | 2025.03.24 |
---|---|
12주차. 멀티미디어 시스템 & 스트리밍 (0) | 2024.07.28 |
11주차. 오디오 압축 및 처리 (0) | 2024.07.23 |
9주차. 동영상 압축기술(1) (0) | 2024.07.07 |
7주차. 영상처리(Image processing) (0) | 2024.07.07 |
댓글