
DearShot은 카메라로 주변 장면을 분석하고, 그 장소에서 참고할 만한 사진 구도를 추천하는 Android 앱이다.
카메라 미리보기와 온디바이스 모델을 연결하면서 고민이 생겼다.
장소를 분석할 수 있게 된 다음에는, 언제 구도 추천을 시작해야 할까?
이번 글에서는 자동 추천을 선택한 이유와, 추천 시작 시점을 판단하는 과정에서 겪은 문제를 정리하려고 한다.
현재는 자동 추천의 시작 조건을 판단하고 준비 신호를 출력하는 단계까지 구현했다. 해당 신호에 프레임 확보와 백엔드 추천 요청을 연결하는 작업은 이후 진행할 예정이다.
가장 간단한 방식은 카메라 화면에 ‘구도 추천받기’ 버튼을 두는 것이다.
사용자가 버튼을 누르면 그 순간의 프레임을 확보하고, 장소와 객체를 분석해 추천을 요청하면 된다. 개발 입장에서도 요청 시점이 명확하고 상태를 관리하기 쉽다.
하지만 내가 만들고 싶은 사용 흐름은 조금 달랐다.
사진을 찍으려고 카메라를 켜고 장면을 비추면, 앱이 주변을 파악한 뒤 어울리는 구도를 제안해주는 모습이었다. 사용자가 촬영 외에도 분석 시작 여부를 판단하고 버튼을 누르는 단계를 거치기보다는, 촬영하려는 행동 안에서 추천까지 이어졌으면 했다.
특히 사진 구도가 익숙하지 않은 사용자는 무엇을 어떻게 비춘 상태에서 추천을 요청해야 하는지도 고민할 수 있다. 이 판단을 앱이 어느 정도 도와주는 편이 더 편리한 경험이 될 것이라고 생각했다.
그래서 초기 방향은 사용자가 장면을 비추면 자동으로 구도를 추천하는 방식으로 잡았다.
물론 자동이라고 무조건 좋은 UX가 되는 것은 아니다. 카메라를 켜자마자 엉뚱한 장면으로 추천하거나, 움직일 때마다 추천이 바뀌면 오히려 방해가 된다. 자동화를 선택한 만큼 앱이 적절한 순간을 기다리는 로직이 필요했다.
처음 카메라를 켰을 때 화면에는 촬영 대상이 아닌 바닥이나 책상이 보일 수 있다. 사용자가 아직 휴대폰을 들어 올리거나 방향을 찾는 중일 수도 있다.
이때 바로 추천하면 사용자가 실제로 찍으려는 장면과 다른 사진을 기준으로 추천할 가능성이 있다.
반대로 계속 분석 결과가 바뀔 때마다 추천하면 요청이 반복되고, 사용자는 어떤 가이드를 따라야 할지 혼란스러워질 수 있다.
처음 생각한 흐름은 다음과 같았다.
카메라 실행
↓
촬영할 장면 탐색
↓
장면이 준비됐는지 판단
↓
추천에 사용할 프레임 확정
↓
추천 요청 1회
↓
선택한 구도 유지이 중 어려웠던 부분은 ‘장면이 준비됐는지 판단’하는 단계였다.
처음에는 다음 조건을 모두 확인하도록 구현했다.
사람이 감지됐는가?
지원하는 장소가 연속해서 인식되는가?
화면이 일정 시간 안정적으로 유지됐는가?
이미 준비 신호를 발생시키지 않았는가?
Places365는 장소 후보를 반환하고, YOLOX는 사람을 포함한 객체의 종류와 위치를 반환한다. 여기에 연속 프레임의 차이를 계산하는 안정 감지기를 추가했다.
각 분석 결과는 서로 다른 주기로 갱신되므로, 오래된 결과를 계속 사용하지 않도록 유효 시간도 확인했다.
논리적으로는 신중한 조건처럼 보였다. 하지만 실제로 테스트해보니 사용자가 추천을 받기 위해 조건을 맞춰줘야 하는 느낌에 가까웠다.
사람이 감지되는 순간에는 화면이 흔들렸고, 화면을 고정했을 때는 장소가 애매하게 나왔다. 로그에서 각각의 성공 결과를 보더라도 같은 시점에 모든 조건이 충족된 것은 아니었다.
분석 모듈이 각각 정상 동작하는 것과, 사용하기 좋은 추천 흐름이 만들어지는 것은 다른 문제였다.
초기 안정 감지는 연속된 이미지를 작은 흑백 이미지로 줄이고, 픽셀 밝기의 평균 차이를 비교하는 방식이었다.
개념적으로는 다음과 같다.
이전 프레임 → 작은 흑백 이미지
현재 프레임 → 작은 흑백 이미지
↓
같은 위치의 밝기 차이 계산
↓
평균 차이가 작으면 안정 후보이 방식은 구현이 단순하고 별도 모델도 필요하지 않았다.
하지만 이미지의 밝기가 달라지는 이유가 카메라 이동만은 아니다. 자동 노출 변화나 피사체 움직임도 영향을 줄 수 있다. 테스트 사진을 모니터에 띄워 휴대폰으로 촬영하는 환경에서는 화면 재촬영에 따른 변화도 고려해야 했다.
실제 로그에는 다음과 같은 값이 나왔다.
AppScene: CAFE, CAFE=75.6%
Stability: difference=0.1117, largeMovement=true, stable=false
Stability: difference=0.0233, largeMovement=false, stable=false
Stability: difference=0.1292, largeMovement=true, stable=false여기서 largeMovement=true는 실제 휴대폰 이동을 측정한 결과가 아니다. 계산한 이미지 차이가 설정한 기준을 넘었다는 의미다.
따라서 이 로그만으로 손떨림, 모니터 깜빡임, 모아레 중 무엇이 원인이었는지 확정할 수는 없었다.
휴대폰을 고정했을 때는 안정 상태도 확인됐다.
Stability: difference=0.0052, duration=3509ms, stable=true문제는 사용자가 항상 휴대폰을 물건에 받쳐놓고 카메라를 사용하지는 않는다는 점이었다.
전체 밝기 변화를 보정하고, 짧은 흔들림을 허용하는 방식도 시도했다. 최근 비교 네 번 중 세 번이 안정적이면 통과하도록 바꾸기도 했다.
하지만 기준값을 계속 높이면 실제 구도가 바뀌는 중에도 안정으로 판단할 수 있다.
이때부터 단순히 수치를 조절하기보다, 무엇을 측정해서 어떤 상황을 구분하려는 것인지 다시 생각하게 됐다.
내가 필요한 것은 정밀한 손떨림 측정이나 촬영 결과의 흔들림 판정이 아니었다.
추천을 시작하기 전에 사용자가 카메라 방향을 대략 정했는지를 알고 싶었다.
이 목적이라면 픽셀 밝기 변화보다 휴대폰 회전을 직접 측정하는 자이로 센서가 더 적합한 근거가 될 수 있다고 판단했다.
동시에 사람 감지도 추천 필수 조건에서 제외했다.
사람이 아직 화면에 없더라도 장소에 맞는 구도를 먼저 보여줄 수 있다. 이후 사용자가 사람을 세우고 가이드를 따라 촬영하면 된다. 객체 결과는 추천에 참고할 정보이지, 추천을 시작할 수 있는 자격 조건일 필요는 없었다.
최종 시작 조건은 다음처럼 단순화했다.
지원하는 장소가 연속해서 확인됨
+
휴대폰의 회전이 잠시 잦아듦
↓
추천 준비 신호 1회Android의 자이로 센서는 X, Y, Z축의 회전 속도를 제공한다. 단위는 rad/s다.
현재 구현에서는 방향별 동작을 구분하기보다 세 축을 합친 회전 크기를 사용했다.
val xd = x.toDouble()
val yd = y.toDouble()
val zd = z.toDouble()
val angularSpeed = sqrt(
xd * xd + yd * yd + zd * zd
)한 번 작은 값이 나왔다고 안정으로 판단하지는 않는다. 최근 약 0.8초의 측정 구간을 확인하도록 했다.
충분한 기록이 모이지 않았으면 WARMING_UP
큰 회전이 감지되면 MOVING으로 판단하고 기록 초기화
구간 대부분의 회전 속도가 낮고 누적 회전량도 작으면 STABLE
유효하지 않은 데이터라면 UNAVAILABLE
순간 회전 속도만 보면 천천히 계속 돌리는 동작이 통과할 수 있다.
따라서 최근 구간의 회전 속도 크기를 시간에 따라 누적한 값도 확인했다.
accumulatedRotation +=
interval.angularSpeed * durationSeconds여기서는 방향을 상쇄하지 않고 크기를 더한다. 왼쪽으로 돌렸다가 오른쪽으로 돌아왔다고 움직임이 없었던 것으로 판단하지 않기 위해서다.
현재 계산은 구간 양 끝 회전 속도 중 큰 값을 사용한 보수적인 근사다. 정확한 자세나 최종 회전 각도를 복원하는 알고리즘은 아니다.
또 센서 이벤트 개수로 시간을 추정하지 않고 SensorEvent.timestamp를 사용했다. 센서 수신 간격이 항상 일정하다고 보장할 수 없기 때문이다.
센서를 처음 다루면서 Android 코드와 계산 로직이 한 클래스에 섞이지 않도록 나눴다.
CameraMotionMonitor
│ Android 센서 이벤트 수신
↓
GyroscopeStabilityDetector
│ 최근 회전 기록으로 판단
↓
CameraMotionState
│ 최신 결과 보관
↓
FrameAnalyzer → RecommendationCoordinatorCameraMotionMonitor는 SensorManager를 통해 센서를 등록하고 해제한다.
GyroscopeStabilityDetector는 숫자와 시각을 입력받아 판단한다. Android 센서 API에 의존하지 않으므로, 고정된 입력으로 움직임을 재현해 테스트하기 쉽다.
FrameAnalyzer에는 모니터 전체 대신 최신 상태를 읽는 함수를 전달했다.
motionStateProvider = motionMonitor::currentState분석기는 필요할 때 최신 결과를 읽는다.
val motion = motionStateProvider()센서가 카메라 프레임마다 실행되는 구조는 아니다. 센서는 독립적으로 상태를 갱신하고, 프레임 분석기가 그 결과를 읽는 구조다.
센서 연결에서는 판단식 외에도 관리할 부분이 있었다.
카메라 세션에 생명주기 Observer를 연결했다.
onResume → 센서 등록, 새 기록으로 시작
onPause → 센서 해제
세션 종료 → Observer와 센서 모두 정리@MainThread를 붙였다고 자동으로 스레드가 바뀌는 것은 아니다. 등록과 해제는 메인 스레드에서 호출하도록 하고, 실행 시 검사도 넣었다.
센서 콜백은 메인 스레드에서 최신 상태를 갱신하고, 카메라 분석은 별도 스레드에서 실행된다.
최신 상태는 불변 객체로 만들고 AtomicReference로 교체하도록 했다. 프레임 분석기가 센서 계산 중인 내부 목록을 직접 읽지 않도록 하기 위해서다.
마지막 결과가 STABLE이었더라도, 센서 이벤트가 끊겼다면 현재도 안정적인지는 알 수 없다.
따라서 마지막 측정 시각을 확인하고 오래된 결과는 사용하지 않도록 했다. 등록 직후 이벤트가 들어오지 않는 경우에도 준비 중 상태로 계속 기다리지 않도록 제한을 뒀다.
현재는 UNAVAILABLE 상태에서 자동 추천을 보류한다. 센서가 없는 기기와 일시적으로 센서가 중지된 상황을 구분한 대체 정책은 추가로 정리할 부분이다.
실제 기기 로그에서는 다음 흐름을 확인했다.
18:27:41.225 AppScene: URBAN_STREET
18:27:42.322 AppScene: URBAN_STREET
18:27:43.053 Recommendation ready: scene=URBAN_STREET첫 도심 인식 로그부터 준비 신호까지 약 1.83초가 걸렸다.
이 값은 한 번 관측한 준비 신호 지연이다. 앱 실행부터 걸린 전체 시간이나 평균 응답 시간은 아니며, 백엔드 추천 완료 시간도 포함하지 않는다.
실제 계산 클래스를 사용한 입력 재현 테스트에서는 다음을 확인했다.
입력 조건 | 결과 |
|---|---|
작은 회전 흔들림 + 지원 장소 | 준비 신호 1회 |
설정한 허용 범위 안의 회전 + 지원 장소 | 준비 신호 1회 |
지속적인 느린 회전 | 준비 신호 없음 |
미지원 장소 | 준비 신호 없음 |
센서 결과가 오래되거나 시각이 잘못된 경우, 중지 이후 새 기록이 충분하지 않은 경우도 별도 테스트했다.
다만 입력 재현 테스트의 성공이 다양한 사용자의 손떨림을 검증했다는 뜻은 아니다. 실제 촬영 상황과 여러 기기에서 기준값을 확인하는 과정은 더 필요하다.
한 가지 의외의 문제는 준비됐다는 사실을 화면에서 알기 어려웠다는 점이다.
현재 준비 신호는 로그에 한 번 출력되고, 같은 세션에서는 추가로 발생하지 않도록 잠긴다. 이후 사진을 바꿔도 다시 출력되지 않기 때문에 테스트하는 입장에서는 반응이 없는 것처럼 느낄 수 있었다.
다음에는 장소 확인 중, 움직임 대기, 추천 준비 완료 같은 상태를 화면에 보여줄 예정이다. 실제 추천 요청과 연결하면 준비 신호 이후 분석 연출과 가이드 표시로 이어지게 된다.
자이로는 회전을 측정하므로, 휴대폰 방향을 유지한 채 옆으로 이동하는 상황을 충분히 감지하지 못한다. 피사체 자체의 움직임이나 이미지의 선명도를 보장해주는 것도 아니다.
그래서 이번 변경을 ‘화면 안정 문제를 완전히 해결했다’고 보기는 어렵다. 추천 시작 목적에 맞춰 판단 근거를 바꾸고, 검증할 수 있는 구조로 정리한 단계라고 생각한다.
필요하다면 이후 저해상도 영상의 특징점 이동을 보조 근거로 추가할 수 있다. 다만 처음부터 모든 방식을 결합하기보다, 실제 사용에서 놓치는 경우를 확인하면서 확장하려고 한다.
앞으로는 준비 신호가 발생한 순간의 프레임을 확보하고, 그 사진의 장소·전체 객체 결과를 묶어 백엔드에 전달할 예정이다.
추천에 사용한 사진과 실시간 화면 표시용 분석 결과도 구분해야 한다. 추천 대기 중에는 장소 키워드와 객체 라벨을 보여주고, 추천이 완료되면 분석 연출을 정리한 뒤 구도 오버레이를 유지하는 흐름을 생각하고 있다.
이번 작업에서 가장 크게 느낀 점은, 조건이 많고 엄격하다고 자동 추천 경험이 좋아지는 것은 아니라는 것이다.
사용자가 무엇을 하려는 순간인지 먼저 정리하고, 그 순간을 판단하는 데 필요한 조건을 고르는 것이 중요했다.
원문: Velog에서 읽기