코딩 더 매트릭스 - 6장 기저 (1)

좌표 표현, 일차종속, 일차독립, 기저, 기저변경

2018년 12월 17일

필립 클라인의 저서 코딩 더 매트릭스 6장 기저


  1. 좌표 표현을 선형결합, 행렬-벡터 곱셈으로 표현해 봅니다.
  2. 좌표 표현을 이미지 손실 압축에 적용해 봅니다.
  3. 생성자 집합을 찾기 위한 알고리즘 Grow와 Shrink를 살펴봅니다.
  4. 최소 스패닝 트리를 이해하고 Grow와 Shrink 알고리즘으로 최소 스패닝 트리를 찾아 봅니다.
  5. Superfluous-Vector 정리를 이해합니다.
  6. 일차종속과 일차독립을 이해합니다.
  7. 기저와 기저변경을 이해합니다.
  8. 카메라 좌표계를 이해하고 공간상의 점을 이미지 평면 상에 매핑하는 원근감을 이해합니다.





좌표 표현

2차원 평면에서 좌표 (1,2)(1,2)를 다음과 같이 선형결합, 행렬-벡터 곱셈으로 나타낼 수 있습니다.

1[10]+2[01]=[1001][12]1\begin{bmatrix} 1 \\ 0 \end{bmatrix} + 2\begin{bmatrix} 0 \\ 1 \end{bmatrix} = \begin{bmatrix} 1 & 0 \\ 0 & 1 \end{bmatrix} \begin{bmatrix} 1 \\ 2 \end{bmatrix}

좌표축이 a1,...,ana_1, ..., a_n이라 할때 열벡터 a1,...,ana_1, ..., a_n들로 구성된 행렬을 A=[a1...an]A = \begin{bmatrix}a_1 & ... & a_n\end{bmatrix}와 같이 쓸 수 있습니다.

Au=vAu = v라 할때 uua1,...,ana_1, ..., a_n에 대한 vv의 좌표 표현입니다. uu는 행렬방정식 Au=vAu = v의 해이기도 합니다. 벡터공간 VVSpan {a1,...,an}Span\text{ }\{a_1, ..., a_n\}일때 AA의 열들은 벡터공간 VV의 생성자들이고 vVv \in V입니다. 따라서 방정식 Au=vAu = v는 적어도 하나의 해를 가집니다.





이미지 손실압축

100 X 200 의 이미지를 압축하고자 합니다. 다음과 같은 방법들을 고려해볼 수 있습니다.

  1. 벡터를 k-스파스 벡터로 100개의 엔트리를 가진 이미지의 한 벡터에 대해 가장 큰 k개 엔트리를 제외한 나머지 벡터를 0으로 만들어 스파스한 벡터를 저장하는 방법입니다. e.g.[200,30,75,50]>[200,0,75,0]e.g. [200,30,75,50] -> [200,0,75,0]
  2. 이미지 벡터를 좌표 표현 생성자 벡터 aR100a \in \Bbb{R}^{100} 을 최소 갯수로 찾아 생성자 벡터들과 좌표 표현을 저장합니다. 이때 모든 이미지 벡터들을 생성자벡터와 좌표 표현의 선형결합으로 표현할 수 있어야 합니다. 만약 최소로 필요한 생성자 벡터가 50개라면 좌표 표현은 50 X 200개만 있어도 됩니다. 하지만 생성자 벡터의 최소 개수가 크면 효율이 없습니다.
  3. 앞의 두 전략의 조합

    • 먼저 최소 갯수의 생성자 {a1,...,an:aiR100}\{ a_1, ..., a_n : a_i \in \Bbb{R}^{100}\}와 각각의 이미지 벡터에 대한 좌표 표현 uu를 찾습니다.
    • uRnu \in \Bbb{R}^n를 스파스한 벡터 u~\tilde{u}로 대체하고 u~\tilde{u}를 저장합니다.
    • 압축된 이미지를 복원시 u~\tilde{u}a1,...,ana_1, ..., a_n의 선형결합을 계산합니다.





생성자 집합을 찾기 위한 두 개의 Greedy 알고리즘

이미지 손실압축에서 최소로 필요한 생성자 a1,...,ana_1, ..., a_n는 어떻게 찾을 수 있을까요? 두 개의 Greedy 알고리즘을 살펴 보겠습니다. 다음에 오는 코드들은 의사코드 입니다.

def Grow(V):
	B = 0
	repeat while possible
		find a vector v in V that is not in Span B, and put it in B

def Shrink(V):
	B = some finite set of vectors that spans V
	repeat while possible
		find a vector v in V such that Span {B - v} = V, and remove v from V

Grow 알고리즘은 벡터공간 VV의 생성자 집합 BB를 찾을때까지 추가를 반복합니다. 만약 더이상 추가할 생성자 벡터가 없다면 알고리즘을 종료합니다. Shrink 알고리즘은 생성자 집합 BB에서 더이상 제거할 벡터가 없을때 까지 제거를 반복합니다. 물론 두 알고리즘 모두 집합 BB는 반드시 VV를 생성해야 합니다.





최소 스패닝 트리 (Minimum Spanning Tree)

포켓몬스터의 배경이었던 관동지방 전체 도시에 전력을 공급한다고 생각해보겠습니다.

위의 그래프에서 노드는 도시, 엣지는 도시 사이에 설치가능한 전력 공급망, 엣지의 가중치는 설치 비용을 뜻합니다. 만약 노드들 사이의 경로가 2개 이상이라면 이 그래프는 사이클을 포함한다고 합니다. 예를 들어 무지개시티보라시티를 잇는 경로는 2개 이상이므로 사이클을 포함합니다.

스패닝 트리는 사이클을 포함하지 않고 최소 경로만 선택한 그래프를 뜻합니다. Spanning 이라는 단어에서 유추할 수 있듯 최소로 선택한 엣지들 만으로 모든 노드들을 연결할 수 있습니다. 아래 그래프들은 스패닝 트리입니다.

한편 그래프의 왼편과 오른편은 노드들을 공유하고 있지 않으므로 (서로 분리돼있음) 그래프는 두 개의 서브그래프로 이루어져 있다고 말할 수 있습니다.

최소 스패닝 트리는 가중치의 합이 최소인 스패닝 트리입니다. Grow 알고리즘과 Shrink 알고리즘으로 최소 스패닝 트리를 직접 찾아보겠습니다. 우선 엣지들을 다음과 같이 표로 정리해 볼 수 있습니다.

회색시티 달맞이산 태초마을 블루시티 무지개시티 갈색시티 보라시티
{회색시티,달맞이산} 1 1
{회색시티,태초마을} 1 1
{달맞이산,태초마을} 1 1
{블루시티,무지개시티} 1 1
{블루시티,갈색시티} 1 1
{무지개시티,갈색시티} 1 1
{무지개시티,보라시티} 1 1
{갈색시티,보라시티} 1 1

표의 행은 두 노드를 연결하는 엣지의 GF2 상의 벡터로 만들 수 있습니다. 예를 들면 회색시티와 달맞이산을 연결한 엣지는 [1,1,0,0,0,0,0][1,1,0,0,0,0,0] 입니다. 각각의 엣지들을 GF2 상의 벡터로 표현했을때

회색시티 달맞이산 태초마을 블루시티 무지개시티 갈색시티 보라시티
1 1
1 1
1 1

위의 벡터들을 모두 계수 1에 대해 선형결합하면 무지개시티에서 보라시티를 잇는 엣지를 만들 수 있습니다. 따라서 어떤 벡터가 다른 벡터들의 생성에 속하는지 쉽게 알아낼 수 있습니다. 이를 활용해 Grow 알고리즘으로 그래프의 최소 스패닝 트리를 찾아보겠습니다.

from GF2 import one
from vec import Vec
from vecutil import zero_vec

Gray = '회색시티'
Moon = '달맞이산'
Green = '태초마을'
Blue = '블루시티'
Rainbow = '무지개시티'
Brown = '갈색시티'
Purple = '보라시티'

D = {Gray, Moon, Green, Blue, Rainbow, Brown, Purple}

graph = {
    (Gray, Moon): { 'vec': Vec(D, {Gray: one, Moon: one}), 'cost': 7 },
    (Gray, Green): { 'vec': Vec(D, {Gray: one, Green: one}), 'cost': 2 },
    (Moon, Green): { 'vec': Vec(D, {Moon: one, Green: one}), 'cost': 9 },
    (Blue, Rainbow): { 'vec': Vec(D, {Blue: one, Rainbow: one}), 'cost': 5 },
    (Blue, Brown): { 'vec': Vec(D, {Blue: one, Brown: one}), 'cost': 3 },
    (Rainbow, Brown): { 'vec': Vec(D, {Rainbow: one, Brown: one}), 'cost': 4 },
    (Rainbow, Purple): { 'vec': Vec(D, {Rainbow: one, Purple: one}), 'cost': 8 },
    (Brown, Purple): { 'vec': Vec(D, {Brown: one, Purple: one}), 'cost': 6 },
}

def recursion(v, target, B):
    if v == target:
        return True
    for i, l in enumerate(B):
        if recursion(v + l, target, B[i+1:]):
            return True

def grow():
    B = []
    res = []
    initial_vec = zero_vec(D)
    keys = sorted(graph.keys(), key=lambda x: graph[x]['cost'])

    for k in keys:
        if not recursion(initial_vec, graph[k]['vec'], B):
            B.append(graph[k]['vec'])
            res.append(k)
    return res

res = grow()
for r in res:
    print(r)

>>>
('회색시티', '태초마을')
('블루시티', '갈색시티')
('무지개시티', '갈색시티')
('갈색시티', '보라시티')
('회색시티', '달맞이산')

프로시저 grow는 생성자 집합 B를 찾을 때까지 벡터공간 graph의 모든 벡터에 대해 반복합니다. 이때 반복순서는 벡터들의 가중치의 오름차순입니다. 각 벡터에 대해 재귀적으로 벡터가 집합 B의 생성에 속하는지 판단합니다. 생성에 속하지 않는다면 B에 추가합니다.

다음은 Shrink 알고리즘을 이용해 최소 스패닝 트리를 찾는 코드입니다.

def shrink():
    B = [ item['vec'] for k, item in graph.items() ]
    res = list(graph.keys())
    initial_vec = zero_vec(D)
    keys = sorted(graph.keys(), key=lambda x: graph[x]['cost'], reverse=True)

    for k in keys:
        if recursion(initial_vec, graph[k]['vec'], [ v for v in B if v != graph[k]['vec'] ]):
            B.remove(graph[k]['vec'])
            res.remove(k)
    return res

res = shrink()
for r in res:
    print(r)

>>>
('회색시티', '달맞이산')
('회색시티', '태초마을')
('블루시티', '갈색시티')
('무지개시티', '갈색시티')
('갈색시티', '보라시티')

프로시저 shrink는 생성자 집합 B를 찾을 때까지 벡터공간 graph의 모든 벡터에 대해 반복합니다. 이때 반복순서는 벡터들의 가중치의 내림차순입니다. 각 벡터에 대해 재귀적으로 벡터가 집합 B의 생성에 속하는지 판단합니다. 생성에 속한다면 B에서 제거합니다.

Grow와 Shrink 모두 다음 그래프를 최소 스패닝 트리로 찾아냅니다.





Superfluous-Vector 정리

임의의 집합 SS와 임의의 벡터 vSv \in S에 대해 만약 vvSS내의 다른 벡터들의 선형결합으로 표현될 수 있다면 Span (S{v})=Span SSpan\text{ }(S - \{v\}) = Span\text{ }S입니다. 이를 증명해보겠습니다.

S={v1,v2,...,vn}S = \{v_1, v_2, ..., v_n\}이라 하고 vnv_n을 다른 벡터들의 선형결합으로 표현될 수 있다고 하겠습니다.

vn=α1v1+α2v2+...+αn1vn1v_n = \alpha_1 v_1 + \alpha_2 v_2 + ... + \alpha_{n-1} v_{n-1}

Span SSpan\text{ }S내의 모든 벡터 vv는 다음과 같이 표현할 수 있습니다.

v=β1v1+β2v2+...+βnvnv = \beta_1 v_1 + \beta_2 v_2 + ... + \beta_n v_n

vnv_n에 관한 식을 위 식에 대입하면

v=β1v1+β2v2+...+βn1vn1+βn(α1v1+α2v2+...+αn1vn1)=(β1+βnα1)v1+(β2+βnα2)v2+...+(βn1+βnαn1)vn1\begin{aligned} v & = \beta_1 v_1 + \beta_2 v_2 + ... + \beta_{n-1} v_{n-1} + \beta_n (\alpha_1 v_1 + \alpha_2 v_2 + ... + \alpha_{n-1} v_{n-1}) \\ & = (\beta_1 + \beta_n \alpha_1)v_1 + (\beta_2 + \beta_n \alpha_2)v_2 + ... + (\beta_{n-1} + \beta_n \alpha_{n-1})v_{n-1} \end{aligned}

따라서 Span SSpan\text{ }S내의 임의의 벡터는 Span (S{vn})Span\text{ }(S - \{v_n\})내에서도 존재합니다.





일차종속, 일차독립

a1v1+...+anvn=0a_1v_1 + ... + a_n v_n = 0 식을 벡터 [v1,...,vn][v_1, ..., v_n]들의 자명하지 않은 결합으로 표현할 수 있다면(계수 a1,...,ana_1, ..., a_n 중 적어도 하나가 0이 아니라면) 벡터들은 일차종속입니다. a1v1+...+anvn=0a_1v_1 + ... + a_n v_n = 0 식에서 anvna_n v_n을 우변으로 넘기면 anvn=(a1v1+...+an1vn1)a_n v_n = -(a_1 v_1 + ... + a_{n-1} v_{n-1}) 이므로 anvna_n v_n는 다른 벡터들의 선형결합으로 표현할 수 있습니다.

a1v1+...+anvn=0a_1v_1 + ... + a_n v_n = 0이 오직 자명한 결합으로 표현할 수 있다면(계수 a1,...,ana_1, ..., a_n 이 모두 0이라면) 일차독립입니다.

일차독립 집합의 부분집합은 일차독립입니다. 이 명제는 만약 부분집합이 일차종속이면 모집합이 일차종속이라는 명제의 대우입니다. 이것이 참이라는 것은 쉽게 알 수 있습니다. 집합 SS, TT가 있을때 STS \in T이고 S={s1,s2,...,sn}S = \{s_1, s_2, ..., s_n\}, T={s1,s2,...,sn,t1,...,tm}T = \{s_1, s_2, ..., s_n, t_1, ..., t_m\}라 하겠습니다. SS가 일차종속이라면 a1s1+...+ansn=0a_1s_1 + ... + a_n s_n = 0을 만족하는 계수 a1,...,ana_1, ..., a_n 중 적어도 하나는 0이 아닙니다. 따라서 a1s1+...+ansn+0t1+...+0tn=0a_1s_1 + ... + a_n s_n + 0t_1 + ... + 0t_n = 0 식이 성립하고 이는 집합 TT가 일차종속이라는 것을 의미합니다.





기저

벡터공간 VV에 대해 VV생성자들로 구성된 일차독립 집합을 기저라 합니다. 따라서 VV의 벡터들의 집합 BB가 다음 두 성질을 만족하면 BBVV의 기저입니다.

  • Span B=VSpan\text{ }B = V
  • BB는 일차독립

예를 들어 벡터공간 R3\Bbb{R}^3에 대한 기저 중 하나는 [1,0,0],[0,1,0],[0,0,1][1,0,0], [0,1,0], [0,0,1]입니다. 모든 벡터 [x,y,z]R3[x,y,z] \in \Bbb{R}^3x[1,0,0]+y[0,1,0]+z[0,0,1]x[1,0,0] + y[0,1,0] + z[0,0,1] 로 표현 가능하므로 [1,0,0],[0,1,0],[0,0,1][1,0,0], [0,1,0], [0,0,1]R3\Bbb{R}^3 를 생성합니다. 세 백터 중 어느 것도 나머지 두 벡터의 선형결합으로 표현불가능하므로 일차독립입니다. 따라서 [1,0,0],[0,1,0],[0,0,1][1,0,0], [0,1,0], [0,0,1]R3\Bbb{R}^3의 기저입니다.

[1,1,1],[1,1,0],[0,1,1][1,1,1], [1,1,0], [0,1,1] 역시 R3\Bbb{R}^3의 기저입니다. 이미 알고있는 기저 [1,0,0],[0,1,0],[0,0,1][1,0,0], [0,1,0], [0,0,1][1,1,1],[1,1,0],[0,1,1][1,1,1], [1,1,0], [0,1,1]의 생성에 있으면 [1,1,1],[1,1,0],[0,1,1][1,1,1], [1,1,0], [0,1,1]R3\Bbb{R}^3을 생성합니다.

[1,0,0]=[1,1,1][0,1,1][0,1,0]=[1,1,0]+[0,1,1][1,1,1][0,0,1]=[1,1,1][1,1,0]\begin{aligned} & [1,0,0] = [1,1,1] - [0,1,1] \\ & [0,1,0] = [1,1,0] + [0,1,1] - [1,1,1] \\ & [0,0,1] = [1,1,1] - [1,1,0] \end{aligned}

[1,1,1],[1,1,0],[0,1,1][1,1,1], [1,1,0], [0,1,1]은 세 백터 중 어느 것도 나머지 두 벡터의 선형결합으로 표현불가능 하다면 일차독립입니다. [1,1,1]=α[1,1,0]+β[0,1,1][1,1,1] = \alpha [1,1,0] + \beta [0,1,1] 에서 첫번째 엔트리가 1이려면 α\alpha가 1이어야 합니다. 그리고 세번째 엔트리가 1이려면 β\beta가 1이어야 합니다. 하지만 α\alpha, β\beta가 모두 1일때 두번째 엔트리가 2가 되므로 [1,1,1][1,1,1][1,1,0],[0,1,1][1,1,0], [0,1,1]의 선형결합으로 표현할 수 없습니다. 나머지 두개의 벡터들에 대해서도 선형결합으로 표현 불가능합니다. 따라서 세 백터는 일차독립이고 R3\Bbb{R}^3의 기저입니다.

벡터공간 VV의 임의의 벡터 vv에 대해 VV의 기저 벡터들에 의한 표현은 오직 하나만 존재합니다. 두 개가 존재한다고 가정하고 이를 증명해보겠습니다. 기저 벡터가 a1,...,ana_1, ..., a_n일때

v=α1a1+...+αnan=β1a1+...+βnanv = \alpha_1a_1 + ... + \alpha_n a_n = \beta_1a_1 + ... + \beta_n a_n

로 두 개의 표현이 존재합니다. 위 식은 다음과 같이 정리할 수 있습니다.

0=(α1β1)a1+...+(αnβn)an0 = (\alpha_1 - \beta_1)a_1 + ... + (\alpha_n - \beta_n)a_n

a1,...,ana_1, ..., a_n은 기저이므로 계수 α1β1,...,αnβn\alpha_1 - \beta_1, ..., \alpha_n - \beta_n은 모두 0입니다. 따라서 αi=βi\alpha_i = \beta_i이고 vv의 기저에 대한 표현은 오직 하나만 존재합니다.





기저변경

하나의 기저에 대한 어떤 벡터의 좌표 표현을 또 다른 기저에 대한 동일한 벡터의 좌표 표현으로 바꿀 수 있습니다. 이를 기저변경이라 합니다. 필드 FF상의 벡터공간 VV에 대해 a1,...,ana_1, ..., a_n이 기저를 형성한다고 할 때 다음과 같이 함수 f:FnVf: F^n \to V를 정의하겠습니다.

f([x1,...,xn])=a1x1+...+anxnf([x_1, ..., x_n]) = a_1x_1 + ... + a_n x_n

즉 함수 ff는 어떤 벡터의 a1,...,ana_1, ..., a_n에 대한 표현을 벡터 그 자체로 매핑합니다. 어떤 벡터의 기저에 대한 표현은 오직 하나이므로 ff는 전단사 함수이고 가역적입니다. 함수 ff에 대응하는 행렬 AA가 있다면 f(u)=Au=vf(u) = Au = v는 가역적이므로 매핑된 벡터 vv를 이용해 좌표 표현 u=A1vu = A^{-1}v 를 구할 수 있습니다.

벡터 공간 VV에 대해 a1,...,ana_1, ..., a_n이 하나의 기저를 형성하고 b1,...,bmb_1, ..., b_m이 다른 기저를 형성할때 함수 f:FnVf: F^n \to Vg:FmVg: F^m \to V를 다음과 같이 정의하겠습니다.

f([x1,...,xn])=a1x1+...+anxng([y1,...,ym])=b1y1+...+bmym\begin{aligned} f([x_1, ..., x_n]) = a_1x_1 + ... + a_n x_n \\ g([y_1, ..., y_m]) = b_1y_1 + ... + b_m y_m \end{aligned}

VV의 벡터 vv에 대해 v=f(x)=g(y)v = f(x) = g(y) 는 벡터 vv를 서로 다른 기저와 좌표 표현으로 표현할 수 있음을 뜻합니다. 함수의 가역성을 이용해 합성함수 g1fg^{-1} \circ f 를 정의해보겠습니다. 함수의 정의역은 FnF^n이고 공역은 FmF^m입니다. 이는 기저 a1,...,ana_1, ..., a_n에 대한 좌표 표현 xx를 기저 b1,...,bnb_1, ..., b_n에 대한 좌표 표현으로 변경할 수 있음을 뜻합니다. ( FnVFmF^n \to V \to F^m )

f(x)=g(y)f(x) = g(y)a1,...,ana_1, ..., a_n을 열벡터로 갖는 행렬 AAb1,...,bmb_1, ..., b_m을 열벡터로 갖는 행렬 BB, 좌표 표현 xx, yy를 이용해 Ax=ByAx = By로 바꿔 쓸 수 있습니다. BB는 역행렬이 존재하므로 B1Ax=yB^{-1}Ax = y가 성립합니다. 따라서 a1,...,ana_1, ..., a_n 기저에 대한 좌표 표현 xx에 행렬 B1AB^{-1}A 를 곱함으로써 b1,...,bmb_1, ..., b_m 에 대한 좌표 표현 yy를 얻을 수 있습니다. 이때 행렬 B1AB^{-1}A는 기저변경 행렬입니다.





원근감 렌더링

3차원 공간상의 물체를 카메라로 찍으려 합니다. 아래는 물체에 반사된 빛이 카메라의 이미지 센서 어레이를 통과하는 모습을 단순화한 그림입니다.

이미지 센서 어레이의 왼쪽 위 모서리를 (0,0)이라 하면 다람쥐의 코에서 반사된 빛은 (1,2)를 통과합니다.

공간의 한 점 pp를 이미지 센서 어레이 평면에 대응하는 점 qq로 매핑하는 함수가 필요합니다. 간단하게 이런 함수를 표현할 수 있는 특별히 편리한 기저가 있습니다. 이것을 카메라 좌표계라고 합니다.

원점은 카메라의 중심이고 기저벡터 a1a_1은 왼쪽 위 모서리에서 오른쪽 모서리로 수평방향으로 향하는 벡터입니다. a2a_2는 왼쪽 위 모서리에서 왼쪽 아래 모서리로 수직방향으로 향하는 벡터입니다. a3a_3는 원점에서 왼쪽 위 모서리로 향하는 벡터입니다. 이제 공간의 점 pp와 대응하는 점 qq를 다음 그림에서 살펴보겠습니다.

물체의 옆면에서 바라본 위 그림에서 기저벡터 a1a_1은 우리가 바라보는 방향의 벡터이므로 보이지 않습니다. 다람쥐의 코 pp를 카메라 좌표계 기저들로 선형결합 하면 p=x1a1+x2a2+x3a3p = x_1a_1 + x_2a_2 + x3a_3입니다. 이에 대응하는 점 qq는 삼각형의 닮음비를 이용하면 q=x1x3a1+x2x3a2+x3x3a3q = \frac{x_1}{x_3}a_1 + \frac{x_2}{x_3}a_2 + \frac{x_3}{x_3}a_3 입니다.

간단히 아래 그림의 정육면체를 카메라 좌표계를 이용해 그려보겠습니다.

from vecutil import list2vec

L = [
    [0,0,0],
    [1,0,0],
    [0,1,0],
    [1,1,0],
    [0,0,1],
    [1,0,1],
    [0,1,1],
    [1,1,1]
]
corners = [ list2vec(v) for v in L ]

corners는 모서리들을 카메라 좌표계에 대한 좌표 표현으로 저장합니다.

# 볼록결합으로 모든 좌표의 점들을 벡터화
def line_segment(pt1, pt2, samples=100):
    return [ (i / samples) * pt1 + (1 - i / samples) * pt2 for i in range(samples + 1) ]

line_segments = [
    line_segment(corners[i], corners[j]) for i, j in
    [ (0,1), (2,3), (0,2), (1,3), (4,5), (6,7), (4,6), (5,7), (0,4), (1,5), (2,6), (3,7) ]
]
pts = sum(line_segments, [])

프로시저 line_segment는 두 모서리를 인수로 받아 두 모서리 사이의 점 100개를 반환합니다.

# 카메라 중심을 원점으로 두기위해 평행이동
shifted_pts = [ v + list2vec([ 1,1,8 ]) for v in pts ]

# 기저
cb = [
    list2vec([ 1 / 100, 0, 0 ]),
    list2vec([ 0, 1 / 100, 0 ]),
    list2vec([ 0, 0, 1 ])
]

# 좌표 표현 찾기
def vec2rep(cb, v):
    return list2vec([ v[i] / cb[i][i] for i in v.D ])

# 이미지 평면으로 투영
def scale_down(x):
    return list2vec([ x[0] / x[2], x[1] / x[2], 1 ])

reps = [ vec2rep(cb, v) for v in shifted_pts ]
in_camera_plane = [ scale_down(u) for u in reps ]

카메라 좌표상 카메라 중심을 원점으로 두기 위해 모든 점들을 [1,1,8][1, 1, 8]만큼 평행이동 합니다. 카메라 좌표의 기저는 [1100,0,0],[0,1100,0],[0,0,1][\frac{1}{100},0,0], [0,\frac{1}{100},0], [0,0,1]입니다. 프로시저 vec2rep는 점들을 기저에 대한 좌표 표현으로 변환해줍니다. scale_down은 공간의 점을 대응하는 이미지 센서 어레이로 매핑합니다.

def pixel(v):
    return (v[0], v[1])

pixels = [ pixel(u) for u in in_camera_plane ]
plot(pixels, 30, 1)