1. Epipolar geometry

Epipolar geometry는 2 View 비전 (stereo vision)에서의 기하학을 뜻합니다.

  • 두 카메라 중심으 O1, O2라고 할 때, 두 카메라 사이의 시점 변환은 회전 R, 이동 t로 표현할 수 있습니다. 3D 점 P 가 두 카메라의 이미지 평면에 각각 p, p'에 있다.
  • epipolar plane: 점 P, 두 카메라 중심 O1, O2를 포함하는 평면
  • epipolar line: epipolar plane과 각 이미지 평면의 교선
  • epipole: 두 카메라 원점을 있는 선과 이미지 평면이 만나는 점
  • 각 이미지의 점은 반드시 자기 epipolar line위에 있다.
  • baseline: 두 카메라 중심을 잇는 선

 

 

  • epipolar pencil: 3차원 공간의 점 P가 이동함에 따라 base line을 중심으로 회전하고 이때 무수히 생성되는 평면.
  • 생성되는 epipolar line은 항상 epipole과 만나며 부채꼴 형태를 띈다.
  • 왼쪽 이미지의 특정 epipolar line은 펜실 내의 동일한 평면에 속한 오른쪽 이미지의 특정 epipolar line과 일대일 대응 관계를 가진다.

 

2. Fundamental Properties

 

※위 그림들과 노테이션이 다르지만 같은 구조입니다.

한 카메라의 영상 좌표로 대응되는 다른 카메라의 영상 좌표는 없지만 그 영상 좌표가 지나는 epiline은 유일하게 결정될 수 있습니다. (pL → right image plane)

P의 위치가 P1, P2, P3이던 CL의 이미지 평면에 투영되는 P의 위치는 동일합니다.

또한 CL의 이미지 평면에 P1, P2, P3을 투영하면 epipolar line!

 

    1. homography matrix 

p' = H p 

H : p를 p'로 투영시켜 줌(2D → 2D), 한 평면을 다른 평면으로 투영할 때 발생하는 모든 변수 (3 × 3)

p - p'의 관계: p를 회전, 평행 이동, 왜곡 등을 거쳐 다른 평면으로 옮긴 것이 p'. 투영 기하학에서 원점 - 3D 점 P를 잇는 직선상에 놓였다는 공통점

l' 위에 e', p'가 있으므로 위와 같이 표현할 수 있다.

p' = H p 이므로 

이를 fundamental matrix F로 표현할 수 있으므로

 

l' = Fp

  • 투영된 p가 다른 이미지 평면에 그려지는 epipolar line이 되려면 fundamental matrix를 곱한다.

점 p와 l의 관계는 다음과 같이 내적으로 나타낼 수 있다.

l 위에 점 p가 있으므로...

epipolar line l' 위 epipole e' 의 관계는

 

, l' = Fp 이므로

어떠한 점 p에 대해서도 항상 성립해야 하므로, 에피폴 e'과 fundamental matrix F사이에는 다음과 같은 고유한 관계가 도출

모든 에피폴라 선 l'은 e'을 통과, 그런데 l'은 F에 의해 생성되는 벡터들이므로(l' = Fp), e'은 F가 만들어내는 모든 l'과 수직이어야만 한다. 그래서 e'이 영공간(Left Null Space)에 존재하는 벡터!

 

epipolar line l' 위 epipole p' 의 관계는

 

, l' = Fp 이므로

 

: correspondence condition

 

  1. Transpose: p'T Fp = 0
  2. Epipolar lines: l' = FP
  3. Epipole: e'T F = 0

 

    2. Essential and Fundamental matrix

epipolar constraint: O1부터 p까지 vector, O2부터 p'까지 vector, O1부터 O1까지 vector는 모두 coplanar

p̂, p̂'가 normalize coordinates 일 때, 

Rp̂, t, p̂'가 한 평면에 있으므로 coplanar 조건을 적용하여 혼합삼중적 = 0

linear equation으로는

   

, E = t × R

 

E : essential matrix

uncalibrated camera에서는, 

p = K p̂, p' = K p̂'

  

 

 

 

 

 

에 대입하면,

 

 


이므로 (correspondence condition)

 

F : fundamental matrix

fundamental matrix는 각 카메라의 calibration matrix와 두 카메라 사이 상대적인 transition & rotation에 의존

 


Fundamental matrix

 

Essential matrix

카메라 내부 파라미터 행렬 K 가 제거된 상태 (정규화된 이미지 평면)의 매칭 쌍들 사이 기하학적 관계

'스터디 > 투영' 카테고리의 다른 글

카메라 Calibration과 Projection  (0) 2026.03.24

카메라는 3D 공간에서의 데이터를 화면에서 2D 이미지로 변환해줍니다. 

이러한 과정이 어떤 변환을 통해 이뤄질 수 있는 것인지 알아보겠습니다.

 

  • Transformation: 점과 점들의 매칭이 어떻게 되는가
    • rigid transformation: translation, rotation => 크기, 각도가 보존되므로 rigid transformation 이라고 합니다. 

 

1. 2D translation

다음과 같이 표현할 수 있습니다. 

tx, ty를 포함한 벡터를 t벡터라고 하며 얼만큼 움직였나 나타냅니다.

tx는 xi-x의 평균으로, ty 는 yi-y의 평균으로 계산할 수 있습니다.

 

2. 2D rotation

원점 기준, 라이안만큼 회전한 좌표를 말합니다.

x, y 행렬에 곱해진 행렬을 회전 행렬(R)이라고 합니다. 

이 행렬은 아래와 같이 O, O' 지점의 좌표 표현을 통해 간단히 증명할 수 있습니다.  


3. 2D Rigid Linear Transformation

2D의 회전, 이동을 간단히 위와 같이 표현합니다. 

더 간단하게는 R 행렬의 곱, T 행렬의 덧셈으로 표현할 수 있습니다.

Y=RX+T 

 

4. 3D rotation

자, 이제 우리가 원하는 3D 데이터의 회전을 생각해보면, 

축이 뻗어 나가는 방향에 따라 오른손 좌표계 vs 왼손 좌표계로 분류됩니다.

 

사용하는 도구 별로 채택 방식이 다를 수 있으니 유의해야 합니다.

3D 좌표계에서는 x, y, z 축을 기준으로 각각 회전이 가능합니다. 

x축 회전: roll

y축 회전: yaw

z축 회전: pitch

 

 

3D 좌표계에서의 회전도 다음과 같이 R 행렬로서 표현할 수 있습니다. 

정리하면,

R = Rz(theta3) Ry(theta2) Rx(theta1)

 

한가지 더 유의할 점은, 곱하기라고 해서 Rx, Ry, Rz의 곱 순서를 바꾸면 결과값이 달라진다는 것입니다. 

이는 짐벌락 문제로 이어질 수 있고 오일러 각, Quaternion 에 관한 글에서 추후 다루도록 하겠습니다.

 

5. 3D Rigid Linear Transformation

정리하면, 3D 좌표계의 회전, 이동은

 

6. Homogeneous Coordinates

앞에서 계속해서 써왔던 동차좌표입니다.

2D데이터에서 한차원 더 늘리거나 3D데이터의 차원을 늘려 표현했던 것이 이에 해당합니다.

차원을 하나 더 늘리면 곱셈과 덧셈, 스케일 등을 한번에 처리할 수 있어 유용하므로 사용합니다.

 

또한 이는 3D 공간상에서 무한개의 점으로 나타내는 표현을 일직선 상의 점들은 동일하게 표현할 수 있기에 사용합니다.

(x, y) => (x, y, 1) , (x, y) => (wx, wy, w) 단, w가 0이 아닐 때

 

7. 월드 좌표계 => 카메라 좌표계

드디어, 좌표계의 변환을 설명할 수 있게 됐습니다.

 

월드 좌표계는 변하지 않는 좌표계, 카메라 좌표계는 카메라를 원점으로 하는 좌표계로 카메라의 위치에 따라 변하는 좌표계입니다. 3D 좌표계에서 회전, 이동되는 데이터를 계산했던 것처럼, 동일한 데이터를 관측하더라도 좌표계가 변하는 것은 축의 방향이 변하므로 데이터가 이동하는 것처럼 보입니다.

 

즉, 위와 같습니다. 

간단하게 하면, Xc = R Xw + t

Xc = 카메라 좌표 상의 점

Xw = 월드 좌표 상의 점

이를 편하게 [R|t] 라고 하며 카메라의 외부 파라미터(외부 행렬) 이라고 합니다.

 

8. 카메라 좌표계 => 정규화 평면 좌표계

다음과 같은 이미지를 상상하면 편리합니다.

핀홀카메라에서 (렌즈 왜곡 등 고려하지 않는)

실제는 점의 반전 이미지가 이미지 평면에 형성되지만, 수학적 편의에 의해 광학 중심 앞에 이미지 평면이 있는 것처럼!

그러면 점 P는 카메라 좌표계에서 xc, yc, zc 위치에 있고 이미지 평면에 형성된 좌표는 닮음 비에 의해 다음과 같이 계산할 수 있습니다. 카메라 원점에서 이미지 평면 까지의 초점거리는 카메라 마다 다른 초점 거리를 일반화(1) 하여 제외합니다.

 

9. 정규화 평면 좌표계 => 픽셀 좌표계

이미지 센서 픽셀은 정사각형이 아닐수도 있고 fx fy 2개의 다른 초점 거리를 가질수도 있으며 카메라 광학 중심이 이미지 좌표계 중심과 일치하지 않을수도, 카메라 센서의 x, y축 사이에 기울기가 있을수도 있습니다. 이를 고려한 것이 다음과 같습니다.

 

1. 스케일 변환

정규화 평면에서는 m 단위 초점거리 평면이었으므로 이를 pixel로 변환합니다. 동시에 초점거리를 1로 하였던 것을 초점거리를 고려합니다. 

 

곱하면,

 

 

2. 주점 오프셋

이미지 좌표계는 보통 왼쪽 위가 (0, 0) 이므로 cx, cy를 더합니다. 

 

3. skew s

픽셀 x, y 축이 직교하지 않고 기울어진 카메라면, x 방향 위치가 y에 의존합니다.

 

정리하면, 

 

여기서 xn, yn에 곱해지는 행렬을 카메라 내부 파라미터 (내부 행렬)이라고 합니다.

 

 

10. Projection

월드 좌표계에서 픽셀 좌표계로 변환하면,

 

이런 결과를 얻을 수 있습니다.

 

다음 글에선, 2D데이터를 3D로 projection 하는 과정과 더불어 volume rendering에 대해서도 정리해보겠습니다.

'스터디 > 투영' 카테고리의 다른 글

Epipolar geometry와 Fundamental properties  (0) 2026.03.31

오랜 시간 미뤄온 c, c++ 언어 학습 내용을 티스토리에 차근차근 업로드하려고 한다.

 

먼저 구조체 관점에서의 얕은 복사, 깊은 복사가 어떤 차이점이 있을까?

 

  1. 얕은 복사
#include <iostream>
#include <cstring>

using namespace std;

typedef struct Student
{
	float height;
	char* name;
} Student;

int main() {
	
	Student alice, bob;
	char alice_s[] = "alice";
	char bob_s[] = "bob";
	
	alice.name = alice_s;
	alice.height = 160;
	bob.name = bob_s;
	bob.height = 170;

	bob = alice;

	printf("alice name: %s, height: %lf\n", alice.name, alice.height);
	printf("bob name: %s, height: %lf\n", bob.name, bob.height);
	return 0;
}

 

학생들의 키 정보를 가진 구조체를 생성하고 alice, bob 이라는 학생 구조체를 선언했다. 그리고 이름과 키 정보를 저장하고 있는 alice의 정보를 bob 구조체에 복사하려고 한다.

 

= 을 이용하여 bob 구조체 주소에 alice의 구조체 주소를 복사하니 bob 구조체에는 alice의 정보들이 담겼다. 

alice name: alice, height: 160.000000
bob name: alice, height: 160.000000

 

하지만 얕은 복사 시 주의해야 할 점이 있다.

만약 alice의 이름을 chris 수정했다고 해보자.

#include <iostream>
#include <cstring>

using namespace std;

typedef struct Student
{
	float height;
	char* name;
} Student;

int main() {
	
	Student alice, bob;
	char alice_s[] = "alice";
	char bob_s[] = "bob";
	
	alice.name = alice_s;
	alice.height = 160;
	bob.name = bob_s;
	bob.height = 170;

	bob = alice;
	/*printf("address: %p, %p\n", bob, alice);*/
	printf("name address: %p, %p\n", bob.name, alice.name);
	printf("height address: %p, %p\n", bob.height, alice.height);

	printf("alice name: %s, height: %lf\n", alice.name, alice.height);
	printf("bob name: %s, height: %lf\n", bob.name, bob.height);

	strcpy(alice.name, "chris");

	printf("<after mod> alice name: %s, height: %lf\n", alice.name, alice.height);
	printf("<after mod> bob name: %s, height: %lf\n", bob.name, bob.height);
	return 0;
}
name address: 0x7ffdbbdd1992, 0x7ffdbbdd1992
height address: 0x622e4bab02b0, (nil)
alice name: alice, height: 160.000000
bob name: alice, height: 160.000000
<after mod> alice name: chris, height: 160.000000
<after mod> bob name: chris, height: 160.000000

 

구조체의 얕은 복사 결과 alice와 bob의 이름 변수의 주소가 동일해졌고 alice의 이름을 chris로 바꾸니 bob의 이름도 chris가 되어버렸다. 구조체 내의 포인터 변수인 이름 멤버 변수가 값을 복사하지 않고 alice의 이름 주소값을 복사해옴으로써 발생한 문제점이다. 

 

그렇다면, 위의 문제를 해결하려면 어떻게 해야 할까?

 

 

  2. 깊은 복사

#include <iostream>
#include <cstring>

using namespace std;

typedef struct Student
{
	float height;
	char* name;
} Student;

int main() {
	
	Student alice, bob;

	alice.name = (char*)malloc(sizeof(char) * 30);
	bob.name = (char*)malloc(sizeof(char) * 30);

	strcpy(alice.name, "alice");
	alice.height = 160;
	strcpy(bob.name, "bob");
	bob.height = 170;

	bob.height = alice.height;
	strcpy(bob.name, alice.name);

	printf("address: %p, %p\n", bob, alice);
	printf("name address: %p, %p\n", bob.name, alice.name);
	printf("height address: %p, %p\n", bob.height, alice.height);
	
	printf("alice name: %s, height: %lf\n", alice.name, alice.height);
	printf("bob name: %s, height: %lf\n", bob.name, bob.height);

	strcpy(alice.name, "chris");

	printf("<after mod> alice name: %s, height: %lf\n", alice.name, alice.height);
	printf("<after mod> bob name: %s, height: %lf\n", bob.name, bob.height);
	free(alice.name);
	free(bob.name);
	return 0;
}
address: 0x65395ed082e0, 0x65395ed082b0
name address: 0x65395ed082e0, 0x65395ed082b0
height address: 0x65395ed08310, (nil)
alice name: alice, height: 160.000000
bob name: alice, height: 160.000000
<after mod> alice name: chris, height: 160.000000
<after mod> bob name: alice, height: 160.000000

 

Student 구조체의 멤버 변수로 포인터 변수를 그대로 사용하려면 메모리 동적 할당해주고 하나하나 복사하면 된다.

또 다른 방법으로는 이름 멤버 변수를 배열로 바꾸고 하나하나 복사하면 된다. 

 

이런 개념은 확실하게 알아둬야 나중에 고생하지 않는다...!

github로 프로젝트를 관리하기 위해서는 git bash를 이용하여 일련의 명령어들을 입력해주어야 한다.

계속 찾아보기엔 내 시간이 아깝기 때문에 저장해놓고 용이하게 찾아보려고 한다.

 

1. 깃 설정을 바꾸기 위해 다음과 같이 입력한다.

git config --global init.defaultBranch main

위 명령어를 입력하면 기본 브랜치를 master가 아닌 main으로 바뀐다.

 

(과거에는 master를 기본 브랜치로 사용했다고 한다.)

 

2. 이제 현재 위치한 프로젝트 폴더에서 깃 저장소를 만들고 초기화한다.

git init

위 명령어를 입력하면 현재 위치에서 .git 폴더가 생성된다. 이 폴더는 깃이 사용하는 모든 정보(버전 기록 )를 담고 있다. 현재 위치는 깃이 관리하는 프로젝트 디렉토리가 된다. 

 

3. github 계정 정보를 입력한다. 

git config --global user.email "<email_address>"
git config --global user.name "<user_name>"

 

4. 커밋할 파일들을 staging area에 넣는다.

git add <file_location>

 

그리고 아래 명령어를 입력하면 파일이 잘 담겼는지 확인할 수 있다.

git status

 

5. staging area에 있는 파일들로 새로운 버전을 만든다.

git commit -m "<description>"

 

파일의 내용을 만들고 두번째 버전을 만드려면

git add *
git commit -m "<description>"

아래 명령을 입력하면 커밋된 정보를 확인할 수 있다.

git log

위 명령어를 입력하 여러 커밋과 사용자의 이름, 정보가 나온다.

 

파일을 커밋하면 어느 버전이든지 고유의 커밋 식별자를 이용하여 자유롭게 되돌아갈 수 있다. 

git checkout <식별자>

커밋의 식별자는 git log 명령어를 통해 확인할 수 있으며 식별자 앞 7자만 입력하여도 된다.

 

6. 이제 깃의 원격 저장소와 로컬 저장소를 연결한다.

git remote add <원격 저장소를 부르는 별명> <원격 저장소의 주소>

아래 명령어를 입력하면 현재 연결된 원격 저장소를 확인할 수 있다.

git remote -v

아래 명령어는 연락을 끊는다.

git remote remove <원격 저장소의 별명>

 

7. 이제 커밋 파일을 원격 저장소에 올린다.

git push <원격 저장소의 별명> main

 

만약 main이 아닌 다른 브랜치에 올리고 싶다면,

git fetch <원격 저장소 별명>

위 명령어는 원격 저장소(origin)의 최신 브랜치 상태를 로컬에 갱신한다.

git branch --set-upstream-to=origin/<브랜치명> <브랜치명>

위 명령어를 입력하여 기존 <브랜치명>을 <브랜치 별명/브랜치명>에 연결하여 추적 관계를 설정한다.

git branch -vv

이후, 다음 명령어 입력 결과가 <브랜치명> 옆에 <브랜치 별명/브랜치명>이 출력된다면 성공적으로 연결됐다는 의미이다.

 

8. 이제 github에 커밋 파일을 확인할 수 있다.

 

만약 팀원이 github의 파일을 수정했다면 아래 명령어를 통해 파일을 받아올 수 있다.

git clone <원격 저장소 주소>
cd <저장소_이름>
git checkout <커밋해시>

위 명령어를 수행하면 다음과 같은 폴더/ 파일이 생성된다.

 

(현재 디렉토리)
└── 입력한 브랜치 이름/
    ├── .git/
    ├── README.md
    └── other_project_files...


 

8-1. git add로 staging area에 다른 파일을 실수로 올릴때 다음과 같은 명령어를 입력한다.

git restore --staged [제외할 파일 이름]

그럼 add하여 staging area에 올린 수정 작업을 작업 디렉토리에 되돌려 staging area의 파일을 제거한다. 작업 디렉토리의 수정 작업은 그대로 유지된다.

참고로 이 명령어를 사용할 때는 커밋이 하나라도 있어야 한다.
만약 커밋이 하나도 없다면 에러가 발생한다.

다음과 같은 명령어를 입력하면 수정 작업까지 제거되고 이전 상태를 복원한다. 

git restore <제외할 파일 이름>

 

8-2. 다음과 같은 명령어는 커밋한 파일을 취소한다.

git reset --soft HEAD^

 

위 명령어는 가장 최근의 커밋을 취소하며 staging area에는 수정 작업이 그대로 남는다. 

 

커밋과 staging area까지도 취소하고 싶다면 다음과 같은 명령어를 입력한다. 

git reset --mixed HEAD^

위 명령어는 커밋과 staging도 취소한다. 

 

커밋과 staging, 작업 디렉토리의 수정 작업까지도 취소하고 싶다면 다음과 같은 명령어를 입력한다. 

git reset --hard HEAD^

 

특정 커밋 내용을 취소하고 싶다면 다음과 같은 명령어를 입력한다. 

git revert <커밋 해시>

특정 커밋을 취소하기 위해서 새 커밋을 생성한다. 히스토리를 유지하면서 잘못된 커밋만 뮤효화할 때 사용한다. 

 

각종 프로젝트를 맡게 되면서 더이상 이동식 디스크, 로컬 파일로 버전 관리가 벅차서 깃허브를 만들었다.

초창기 SW 개발 공부를 시작했을 때부터 지금까지의 자료를 정리하고 업로드 하는 일이 여간 복잡한게 아니었다!

(모든 것은 과거의 내가 자초한 것.. 이제부턴 미루지 말아야지...!!)

 


Git Hub 연결


git push <remote_name> <branch_name>

<remote_name> : myProject_git

<branch_name> : main

으로 하여 push 명령어를 입력했다.

 

git push myProject_git main
error: src refspec main does not match any
error: failed to push some refs to 'https://github.com/<user_name>/<repository_name>.git'

 

첫번째 에러가 떴다.

 

무슨 말인지 모르겠으니 검색했다.

기본 repository 이름을 master로 바꿨더니 성공했다는 글이 보였다.

 

시도...!

성공은 했지만 원인은 뭔지 모르는 찝찝한 상황...

 

내 기본 remote의 repository 이름은 main인데 master로의 push만 성공한다니

 

로컬 저장소 브랜치명을 확인하기 위해 

# git show-ref 명령어를 입력했다.

아.. master로 되어있다.

 

로컬 저장소 브랜치명을 master에서 main으로 바꿔보았다.

 git branch -m master main

또는 

git config --global init.defaultBranch main

 

 

main으로 잘 바뀌었다.

이제 다시 main에 push를 시도해보았다.

음.. 다른 에러가 발생했다!

 

에러가 fetch하라고 했으므로 fetch도 해보고 pull도 해봤지만 에러는 여전히 발생했다..

갈피를 못잡고 있다가.. 근본적인 원인을 발견했다.

 

처음 repository를 생성할 때, README.txt 파일을 자동으로 생성되게끔 만들었댔다!

그래서 로컬 repository와 remote repository가 일치하지 않아 발생된 에러였다.

이는 기존 데이터 손실을 막기 위해 push에서 에러가 발생한 것이었다,

즉, 애초에 동기화만 잘 해줬어도 끝났을 문제!

 

동기화

#git pull --rebase <repository_name><branch_name>

 

다시 push 시도!

 

음.. 이제 잘된다!

 

결론: 동기화를 잘 하자!

 

이제 스플라인 보간법을 이용하여 점을 잇는 간단한 프로그램을 만들어 보려고 한다.

Qt 프레임워크를 이용, CMake 빌드도구로 만들어보았다.

 

동작:

마우스의 왼쪽 버튼을 누르면 점이 찍히고, 화면에 3개 이상의 점이 찍히면 점을 찍은 순서대로 잇는다.

마우스의 오른쪽 버튼을 누르면 화면에 그려진 그림을 지운다.

 

방법:

QWidget의 멤버 함수인 마우스 이벤트 함수(mousePressEvent)를 오버라이드

QTimer의 멤버 함수인 타이머 함수(timerEvent) 오버라이드

QGraphicsScene 변수를 정의하고 addPixmap 함수 input에 QPixmap 그림을 추가하는 방법으로 마우스가 눌릴 때마다 그려지는 그림 변경

 


.h

멤버 변수는 다음과 같이 정의했다.

bool b_clicked;				// 왼쪽 마우스 클릭 플래그
int index;					// 화면에 찍힌 점의 개수

QPixmap * Pixmap;			// 갱신할 pixmap 이미지
QPoint * Point;				// 점의 위치 저장

QGraphicsView * m_view;		
QGraphicsScene * m_scene;

 

.cpp

생성자에 다음과 같이 m_view, m_scene, 점, 이미지 배열을 초기화 하였다.

MainWindow::MainWindow(QWidget *parent)
    : QMainWindow(parent)
    , ui(new Ui::MainWindow)
{

    index = 0;
    b_clicked = false;
    
    // m_view에 사용할 장면 설정
    this->setCentralWidget(m_view = new QGraphicsView());
    m_scene = new QGraphicsScene;
    m_view->setScene(m_scene);
    m_view->setGeometry(10,30,TILE_WIDTH,TILE_HEIGHT);

    
    // 점, 이미지를 저장할 배열 초기화
    Pixmap = new QPixmap[MAXLENGTH];
    Point = new QPoint[MAXLENGTH];
    QPixmap pixmapWhite(TILE_WIDTH, TILE_HEIGHT);		
    pixmapWhite.fill(Qt::white);					// 띄울 첫 이미지는 하얀 바탕으로 함
    Pixmap[0] = pixmapWhite;

    this->startTimer(0);							// 타이머 시작
}

 

 

마우스가 눌릴 때의 이벤트를 처리하는 함수는 왼쪽과 오른쪽 마우스 클릭을 구분할 수 있다.

왼쪽 마우스가 눌렸을 경우 왼쪽 마우스 클릭 플래그 b_clicked를 true로 하고 점의 개수를 카운트하는 index에 1 증가시킨다. mousePressEvent의 매개 변수 event를 통해 왼쪽 마우스가 눌린 위치를 가져와 점 배열에 저장한다.

void MainWindow::mousePressEvent(QMouseEvent * event)
{
	// 왼쪽 마우스 클릭
    if (event->button() == Qt::LeftButton) 
    {
        b_clicked = true;
        Point[index] = event->pos();
        index++;  

        qDebug() << "window value at (" << point.x() << ", " << point.y() << "): " << point;
    }
    // 오른쪽 마우스 클릭
    else if(event->button() == Qt::RightButton)
    {
        index = 0;			// 점 개수 0으로 초기화

        QPixmap p(TILE_WIDTH, TILE_HEIGHT);
        p.fill(Qt::white);
        Pixmap[0] = p;
        m_scene->addPixmap(Pixmap[0]);
    }
    QWidget::mousePressEvent(event);
}

 

이제 왼쪽 마우스가 클릭 됐음을 인식하여 점과 곡선을 그려주기 위해 timerEvent 함수를 처리할 것이다. (그려지는 동작을 mousePressEvent 함수가 아니라 timerEvent 함수에 처리하는 이유는 추후 다른 옵션을 추가하더라도 timerEvent 함수에서만 수행할 수 있도록 하기 위해서이다. 구현 방법은 다양하므로 각자 상황에 맞는 코드를 짜면 되겠다.) 

void MainWindow::timerEvent(QTimerEvent *)
{
    if(b_clicked)
    {
        QPixmap linePix;
        QImage img = Pixmap[index - 1].toImage();

        // 점 그리기
        linePix = generateDot(img, point, 4);
        // append it to the buffer
        Pixmap[index] = linePix;

        if(index >= 3)
        {
        	// 곡선 그리기
            linePix = generateCurve(img);
        }

        m_scene->addPixmap(linePix);
        b_clicked = false;
    }
}

generateCurve 함수는 최소 점이 3개 이상 찍혔을 때 Spline interpolation에 의해 생성된 스플라인 함수를 그리는 역할이다. 점과 곡선을 그린 후, m_scene에 그린 pixmap을 띄우고 왼쪽 마우스 클릭 플래그를 off 한다.

 

자, 이제  Point에 저장된 점 사이에 생성되는 스플라인 함수를 구해보자. 나의 경우는 재귀함수로 구현했지만, 임베디드 시스템처럼 자원이 제한적인 시스템에서는 재귀함수로 구현하기 보단 배열을 이용하는 방법이 적합할 것이다. 스플라인 함수의 계수를 저장하는 배열을 멤버 변수에 저장하고 계수 저장 배열을 계속해서 검색, 갱신해가며 찾는 방법이 있다.

double * MainWindow::getSplineInterpolation(int n)
{
    if(n == 2)
    {
    	// a_1, b_1, c_1 계산
        double * para = new double[3];
        double dividend = ((double)Point[n - 2].y() - (double)Point[n - 1].y());
        double divisor = (pow((double)Point[n - 2].x(), 2) - pow((double)Point[n - 1].x(), 2) - 2 * Point[n - 1].x() * ((double)Point[n - 2].x() - (double)Point[n - 1].x()));
        if(divisor == 0)
        	divisor = 0.0000001;
        para[0] = (dividend / divisor);

        para[1] = para[0] * (-2) * Point[n - 1].x();
        para[2] = Point[n - 2].y() - para[0] * pow(Point[n - 2].x(), 2) - para[1] * Point[n - 2].x();
        return para;
    }
    else
    {
    	// a_n-1, b_n-1, c_n-1 계산
        double * prev_para = getSplineInterpolation(n - 1);
        double * para = new double[3];

        double dividend = (((double)Point[n - 2].y() - (double)Point[n - 1].y())
        	+ (-2 * prev_para[0] * Point[n - 2].x() - prev_para[1]) * (Point[n - 2].x() - Point[n - 1].x()));
        double divisor = (pow(Point[n - 2].x(), 2) - pow(Point[n - 1].x(), 2) -2 * Point[n - 2].x() * (Point[n - 2].x() - Point[n - 1].x()));
        if(divisor == 0)
        	divisor = 0.0000001;
        para[0] = (dividend / divisor);
        para[1] = 2 * prev_para[0] * Point[n - 2].x() + prev_para[1] -2 * para[0] * Point[n - 2].x();
        para[2] = Point[n - 2].y() - para[0] * pow(Point[n-2].x(), 2) - para[1] * Point[n-2].x();
        return para;
    }
}

앞선 글에서 구한 a_1, b_1, c_1, a_p, b_p, c_p 값이 위와 같음을 이용하였다. (궁금하신 분은 이전의 글을 보면 도움이 될 것이다.) 

getSplineInterpolation 함수 내에서 자기 자신을 다시 호출하고, 현재의 점(n)구간보다 1번 더 이른 시점의 점(n-1)구간의 계수를 prev_para에 저장하여 반환한다. prev_para[0], prev_para [1] 값을 이용하여 계수 a_n, b_n을 계산한다.

 


결과

알고리즘을 검증하기 위해 이미 알려져 있는 점에서 알고리즘을 통해 계산한 다항식의 계를 도출해보았다. 

x y
1 5
2 12
3 23

x구간 [1, 2]에서의 다항식 계

x구간 [2, 3]에서의 다항식 계

 

이미 알려진 점에서의 다항식 계와 일치하였다.


고찰

이렇게 하면, getSplineInterpolation 의 계수를 이용하여 1~n까지의 계수를 구할 수 있을 것이다. 사실, 위와 같은 재귀함수 방법으로 구현하는 것은 상당히 비효율적인 방법이라고 할 수 있다. 1번째 스플라인 함수를 구하기 위해 getSplineInterpolation 함수는 1번 호출되고, 2번째 스플라인 함수를 구하기 위해 getSplineInterpolation 함수는 2번 호출되니 말이다. 그럼 n번째 스플라인 함수는 n번 호출된다. 즉, 동일 작업을 반복하고 있는 꼴이다. 이 반복을 피하기 위해 param[3][n] 변수를 정의하고 1번째 스플라인 함수 계수를 0번째 저장, 2번째 스플라인 함수는 param의 0번째 값을 이용하여 1번째에 저장하면 n번째까지 저장할 수 있다.

 

'스터디 > 보간법' 카테고리의 다른 글

Spline interpolation (스플라인 보간법) 개념  (0) 2025.03.17

+ Recent posts