본문 바로가기
C.W.K.
Stream
Lesson 02 of 05 · published

solvePnP, 카메라를 거꾸로 푼다

~14 min · solvepnp, pnp, reprojection-error, pose

Level 0빈 종이
0 XP0/35 lessons0/12 achievements
0/100 XP to next level100 XP to go0% complete
"진짜 두개골에서 눈이 어디 있는지 알아. 사진에서 어디 찍혔는지도 알고. solvePnP 는 그 사이를 잇는 카메라를 찾아."

solvePnP 가 답하는 질문

Perspective-n-Point, 줄여서 solvePnP 는 앞 레슨을 정확히 뒤집은 거야. 정방향은 자세를 받아 픽셀을 내놨지. 이건 픽셀을 받아 자세를 내놔. 구체적으로 보면 이래. 표준 머리 위에서 위치를 아는 3D 점들이 있고, 그 이목구비가 사진에 찍힌 2D 픽셀도 있어. 솔버는 그 3D 점들을 pinhole 모델로 통과시켰을 때 정확히 그 픽셀에 떨어지게 만드는 회전과 이동을 찾아내. 그 회전이 머리의 방향이야. 눈으로 어림한 게 아니라 계산해서 얻은 투시 상자지.

짝이 연료다

솔버는 을 먹고 돌아. 2D 픽셀 하나와 3D 점 하나를 묶은 짝이야. 3D 쪽은 표준 머리에서 와. 모든 landmark 의 위치를 아는 중립 얼굴이지. 2D 쪽은 검출기(다음 트랙)가 알려줘. 그 landmark 가 이 사진 어디에 나타났는지. 둘은 번호로 묶여. 사진의 33번이 모델의 33번이야. 그래서 검출기 출력과 표준 모델이 같은 번호 체계를 유지해야 해. 한쪽 번호만 바뀌면 모든 짝이 소리 없이 거짓말을 하게 되거든.

reprojection error 를 줄인다

모든 픽셀을 완벽히 맞히는 자세는 대개 없어. 진짜 얼굴은 중립 모델이 아니고 검출도 흔들리니까. 그래서 solvePnP가장 가까운 자세를 찾아. 3D 점을 투영해서 찍힌 자리와 실제로 관측된 자리 사이 거리를 다 더한 값, 그러니까 reprojection error 를 최소로 만드는 거지. 남은 오차는 부산물이 아니라 엔진의 정직 계기야. 작으면 자세가 얼굴을 잘 설명한다는 뜻이고, 크면 무리해서 맞춘 값이니 덜 믿으라는 뜻이야. 솔버가 줄이려는 그 숫자가 나중에 화면에 신뢰도로 뜨는 바로 그 숫자고.

회전에서 상자로

솔버는 회전을 압축된 형태인 회전 벡터로 돌려주고, 그걸 펴면 머리의 상자 방향인 3×3 회전 행렬이 돼. 이 행렬을 사람이 읽는 yaw, pitch, roll 로 바꾸는 건 그 자체로 작은 지뢰밭이야. 순진하게 분해하면 거의 정면인 얼굴에서 값이 엉키거든. 그래서 Loomis 는 머리의 forward 와 up 벡터에서 각도를 읽어. 그 이야기는 두 레슨 뒤에 제대로 하고, 지금은 깔끔한 요약만 챙기자. 짝이 들어가고 자세가 나온다. 오차는 영수증이다.

Code

짝에서 자세로, 그리고 믿음의 눈금까지·python
import cv2
import numpy as np

# 3D model points (canonical head) matched BY INDEX to the 2D landmarks.
object_points = CANONICAL_HEAD[LANDMARK_IDS]      # (N, 3), known on a reference head
image_points  = detected_landmarks[LANDMARK_IDS]  # (N, 2), where they landed in-photo

ok, rvec, tvec = cv2.solvePnP(
    object_points, image_points,
    camera_matrix, dist_coeffs,      # intrinsics; focal comes from the bounded sweep
    flags=cv2.SOLVEPNP_ITERATIVE,
)
R, _ = cv2.Rodrigues(rvec)           # rotation vector -> 3x3 matrix = the head's box

# The honesty meter: reproject the model and measure the leftover pixel gap.
reproj, _ = cv2.projectPoints(object_points, rvec, tvec, camera_matrix, dist_coeffs)
err = np.linalg.norm(reproj.squeeze() - image_points, axis=1).mean()
# small err -> the pose explains the face; large err -> trust this fit less.

External links

Exercise

평평한 카드의 네 모서리를 알고 있고, 그게 사진에 찍힌 네 픽셀도 안다고 해보자. 왜 보통은 그것만으로 카메라 자세 하나가 정해지는지 설명해봐. 그다음 안 정해지는 경우를 찾아봐. 카드를 거의 옆에서 찍었거나 네 모서리가 사진에서 거의 일직선이면 어떻게 될까? 그리고 landmark 가 많고 넓게 퍼질수록 왜 solvePnP 가 안정되는지 연결해봐.
Hint
짝 하나하나가 자세를 조여줘. 넓게 퍼진 짝이 충분하면 필요한 것보다 많이 조여져서 노이즈가 평균으로 씻겨. 반대로 점들이 화면을 충분히 안 덮으면 답이 뭉개져. 옆에서 본 카드나 거의 일직선인 모서리는 깊이를 거의 못 잡아줘서 여러 자세가 비슷하게 찍히거든. 얼굴 landmark 가 안정적인 건 진짜 입체 표면 위에 흩어져 있기 때문이고.

Progress

Progress is local-only — sign in to sync across devices.
이 페이지에서 버그를 발견하셨거나 피드백이 있으세요?문제 신고

댓글 0

🔔 답글 알림 (로그인 필요)
로그인댓글을 남기려면 로그인해 주세요.

아직 댓글이 없어요. 첫 댓글을 남겨보세요.