iOS 카메라 개발기 - 설정은 먹은 척한다
24MP를 달라고 했더니 에러 없이 12MP가 왔고, 1.46× 줌은 5.6MP를 늘린 사진이었다. 값이 들어갔다고 적용까지 된 건 아니었다.
egg3분 읽기
지난 편에서 전면 커넥션이 videoRotationAngle = 90을 조용히 무시했다. 이번 편은 같은 일이 해상도와 줌에서도 일어난 이야기다.
측정 기기는 이번에도 iPhone 17 Pro, iOS 26.6.1 한 대다.
24MP를 달라고 했더니 12MP가 왔다
지원 목록에 5712×4284가 있어서 maxPhotoDimensions에 넣었다. 출력 객체도 받아들였고 캡처 설정에도 같은 값이 들어갔다. 그런데 찍고 나서 resolved 값을 찍어보니 이랬다.
maxPhotoDimensions -> 5712x4284 (supported: 4032x3024, 5712x4284)
capture maxPhotoDimensions 5712x4284
resolved 4032x3024 output.max 5712x4284
에러도 경고도 없다. output.maxPhotoDimensions는 24MP 값을 그대로 들고 있으니 설정만 보면 성공이다. 실제로 나온 크기는 AVCaptureResolvedPhotoSettings.photoDimensions에만 있었고 거기는 12MP였다.
파일 크기를 보기 전까지는 성공한 줄 알았다. 48MP는 이 구성에서 지원 목록에 아예 없었다.
원인은 아직 못 좁혔다. 후보는 셋이다.
photoQualityPrioritization = .balanced(가장 유력하다고 본다)- ProRAW 활성화
- 비압축 420f 버퍼로 받기
하나씩 끄면서 resolved 값을 찍어보면 나올 텐데 아직 거기까지 못 했다. 확인하면 이 글에 덧붙이겠다.
그 뒤로는 설정을 넣으면 resolved 쪽을 같이 찍는다. 캡처가 시작될 때 델리게이트로 한 줄이면 된다.
func photoOutput(_ output: AVCapturePhotoOutput,
willBeginCaptureFor resolved: AVCaptureResolvedPhotoSettings) {
let d = resolved.photoDimensions
print("resolved \(d.width)x\(d.height) output.max \(output.maxPhotoDimensions.width)x\(output.maxPhotoDimensions.height)")
}
1.46×는 12MP가 아니었다
1× 위의 줌이 디지털 크롭이라는 건 알았다. 몰랐던 건 크롭한 뒤 단순히 늘리는 건지, 뭔가 더 해주는 건지였다.
그래서 가설 이미지를 만들어 비교했다. 1.46×(35mm 상당)로 한 장, 같은 장면을 1×로 한 장 찍는다. 1× 사진의 가운데를 잘라 bicubic으로 키웠다. 이게 “그냥 늘렸다”는 가설 이미지다.
실제 1.46× 사진과 가설 이미지를 공간 주파수 스펙트럼으로 비교하니 전 구간이 0.03~0.13 decade 안에서 겹쳤다. 크롭의 한계 주파수 위쪽에서도 격차가 없었으니 더해진 디테일은 없는 셈이다.
다시 말해 1.46× 사진은 실질 약 5.6MP를 12MP로 늘린 사진이다. 12MP를 1.46의 제곱으로 나누면 그 숫자가 나온다.
이 방법은 줌 말고도 쓸 데가 많은 것 같다. 같은 장면 두 장을 찍고 “디지털이면 이렇게 나와야 한다”는 가설 이미지를 합성하고 스펙트럼을 겹쳐 본다. 광학인지 디지털인지 헷갈릴 때 꽤 쓸만할 듯 싶다.
35mm 환산은 24 곱하기 배율
EXIF의 FocalLengthIn35mmFilm은 1×에서 24, 1.46×에서 35로 찍힌다. 정확히 1.5×로 찍으면 36이 적힌다. 기준 24mm에 배율을 곱한 선형 계산이다.
그래서 28mm, 35mm 같은 프리셋은 배율로 정하면 표기가 어긋난다. 목표 mm / 1× 렌즈 mm로 거꾸로 계산해야 EXIF에 원하는 숫자가 찍힌다. 35mm 프리셋이 1.46×라는 애매한 숫자가 된 것도 이 때문.
화각에서 계산하는 방법도 해봤다. 36 / (2·tan(fov/2))로 구한 값은 13.5, 23.6, 102.8mm로 애플 표기와 1~3mm씩 어긋났다. 화면 라벨은 계산을 포기하고 표기값 표로 맞췄다.
세션 프리셋에 사진까지 깎였다
프리뷰를 가볍게 하려고 세션 프리셋을 .hd1920x1080으로 뒀더니 정지 사진도 1080p로 나왔다. 프리셋은 프리뷰만이 아니라 세션 전체에 걸린다.
.photo로 두고 deliversPreviewSizedOutputBuffers로 프리뷰 버퍼만 작게 받으니 12MP 촬영과 30fps 프리뷰가 같이 됐다. 저조도에서는 카메라가 알아서 24fps로 내려간다.
이름이 비슷한 두 플래그
isFastCapturePrioritizationEnabled는 연타 중에 화질을 낮춰 속도를 얻는 플래그다. isResponsiveCaptureEnabled는 화질에 영향이 없다.
이름만 보면 둘 다 “빠르게”라서 같이 켜기 쉬운데, 앞의 것은 화질을 내주는 플래그라 알고 켜야 한다.
장치가 준 값을 되돌려줬더니 죽었다
화이트 밸런스를 고정하려고 device.deviceWhiteBalanceGains를 읽어서 temperatureAndTintValues(for:)에 그대로 넘겼다. 장치가 준 값을 장치에 되돌려주는 거라 안전할 줄 알았다.
AWB가 수렴하는 도중에는 범위 밖 값이 들어올 때가 있었고 NSRangeException으로 앱이 죽었다. 읽은 값도 1.0부터 maxWhiteBalanceGain 사이로 잘라야 한다.
let top = device.maxWhiteBalanceGain
func clamp(_ v: Float) -> Float { min(max(v, 1), top) }
var g = device.deviceWhiteBalanceGains
g.redGain = clamp(g.redGain)
g.greenGain = clamp(g.greenGain)
g.blueGain = clamp(g.blueGain)
let tt = device.temperatureAndTintValues(for: g)
해상도도 프리셋도 에러 한 줄 없이 내가 넣은 것과 다른 값으로 돌고 있었다. 24MP가 안 나오는 원인은 아직 숙제로 남았으니 확인하는 대로 덧붙여야겠다.
다음 편은 저장이다. 사진 앱이 DNG를 받아주지 않아 요청 전체가 날아가던 이야기.
Filed under 개발