이걸 왜 하고 있는지 먼저 설명해야 할 것 같다.
원래 천리안 위성사진과 놀기 라는 카테고리를 만들고 천리안 2B호의 공개된 데이터를 가지고 뭔가 좀 해보려고 했는데 하다보니 옆길로 새버려서 그냥 제목과 카테고리를 바꿨다.
천리안 2B호는 정지궤도 기상관측위성으로 한반도 주변을 찍고 있다. 이 데이터는 국립해양위성센터 홈페이지의 다운로드에서 다양한 방법으로 다운받을 수 있는데, 이들은 nc라는 확장자를 쓰는 NetCDF파일로 돼있고 일부 데이터에 대해서는 일반적인 이미지 파일도 있다. 작년 말까지 들었던 딥러닝 교육과정 프로젝트에서 살짝 건드렸는데 아쉬워서 다시 해본다. 같은 홈페이지 위성에 대한 소개 페이지가 있다. 그에 따르면 위성은 370nm ~ 885nm 파장에 걸친 13개의 전자기파 밴드에서 사진을 찍는다.
카테고리: 코드와 함께 설명해보기?¶
일단 카테고리를 만들었는데 이걸 왜 하고 있는 거지? 그냥 내 방식으로 설명을 해보는 것인데... 일단 내가 다시 보기 위해 갈무리를 한다는 의미는 없다. 이렇게 확인할 필요가 없다. 그리고 코딩이나 물리 중 한 쪽에만 익숙한 사람이 쉽게 잘 모르는 개념을 이해할 수 있냐 하면 그것도 아닌 것 같고, 그렇다고 해도 여기에 담긴 코딩이나 물리의 수준이 그다지 높지 않기 때문이다. 그렇다면 마지막으로 양쪽 분야를 조금만 아는 사람들이 남겠다. 그런데 그 사람들이 굳이 이 형식의 글을 재밌게 느낄 이유가 있나?
잘 모르겠는데 일단 이건 써서 올리고 천천히 생각하자. 아무튼 내가 다시 복습한 거 인증하는 의미는 있으니까
자문자답 혹은 질문¶
- 전자기파를 측정하는 기술은 어떤 단계에 있을까? microwave의 경우 horn antenna 같은 것을 이용해서 time-domain 측정을 하고 fourier transform해서 frequency domain으로 바꿔 쓰던데, 이런 것을 가시광선 영역에서 사진을 찍는데 적용한다고 치면 어떤 기술적 문제가 생길까? 아니면 그냥 microwave에서 이미징 센서를 만든다고 하면 어떤 문제가 있을까? 시간? 궤도에 올리기 위한 무게나 크기 문제? 데이터 저장/전송/연산?
- 왜 기상위성의 전자기파 센서들은 죄다 가시광선 영역에 있나?
이것은 결국 햇빛의 주파수 대역을 따라간 것이라고 봐야 할 것 같다. 햇빛과 그것을 반사하는 것들에 대부분의 중요한 정보가 담겨있고, 이외의 경우 그 세기가 부족하거나 해서 이용하기에 어렵겠지.
색과 전자기파의 파장¶
해당 전자기파 범위는 가시광선보다 조금 넓어 적외선과 자외선도 조금 포함하고 있다.
그런데 우리는 왜 색을 세 개의 숫자로 표시할까? 그건 인간의 안구에서 빛을 감지하는 세포(원추세포)의 종류가 셋이기 때문이다. 이들은 다음과 같이 가장 민감한 파장이 다르다.
스펙트럼 역시 색을 표현하는 한 방식이라고 할 수 있다. 이 경우 파장(혹은 진동수)에 따른 빛의 세기를 연속적인 그래프로 나타낸다.
태양광의 색깔은?¶
스펙트럼에서 우리가 아는 3차원 벡터 색상을 얻으려면 스펙트럼과 각 원추세포의 감도를 파장에 대해 적분한 값을 normalize하면 될 것인데, 어떤 값으로 돼있냐에 따라서 적절한 항이 곱해지거나 해야 할 것이다.
수식으로 쓰자면 $sun(\lambda)$ 이 태양빛의 스펙트럼, $s(\lambda), m(\lambda), l(\lambda)$이 각 원추세포의 response라고 하면,
$color_s = \int_{0}^{\infty} sun(\lambda)s(\lambda)d{\lambda}$ 정도의 식을 생각할 수 있는데, 물론 normalization 및 상수가 생략됐다고 봐야겠다.
그러면 간단히 적분을 해서 태양광의 색을 3채널 색으로 바꾸어보자.
태양광의 스펙트럼¶
sunlight spectrum csv 정도로 검색해보니 가장 먼저 나오는 것이 미국 정부 도메인을 쓰는 www.nrel.gov의 한 페이지였다. 여기서 spreadsheet(xsl파일)를 받을 수 있다.
import pandas as pd
sun=pd.read_csv("astmg173.csv", index_col=0)
sun
Etr W*m-2*nm-1 | Global tilt W*m-2*nm-1 | Direct+circumsolar W*m-2*nm-1 | |
---|---|---|---|
Wvlgth nm | |||
280.0 | 0.08200 | 4.730000e-23 | 2.540000e-26 |
280.5 | 0.09900 | 1.230000e-21 | 1.090000e-24 |
281.0 | 0.15000 | 5.690000e-21 | 6.130000e-24 |
281.5 | 0.21200 | 1.570000e-19 | 2.750000e-22 |
282.0 | 0.26700 | 1.190000e-18 | 2.830000e-21 |
... | ... | ... | ... |
3980.0 | 0.00884 | 7.390000e-03 | 7.400000e-03 |
3985.0 | 0.00880 | 7.430000e-03 | 7.450000e-03 |
3990.0 | 0.00878 | 7.370000e-03 | 7.390000e-03 |
3995.0 | 0.00870 | 7.210000e-03 | 7.230000e-03 |
4000.0 | 0.00868 | 7.100000e-03 | 7.120000e-03 |
2002 rows × 3 columns
세 개가 있는데, 일단 Etr column은 우주에서 측정한 값인 것 같다. 그리고 나머지 둘은 위도, 입사각 등에 따라 지구표면에서 잰 것 같다. 그래프를 그려보자.
import matplotlib.pyplot as plt
# rename columns
sun.columns = ["etr", "global_tilt", "direct+circumsolar"]
# plot sunlight spectrum
plt.plot(sun.values)
plt.show()
원추세포의 frequency-response¶
여기서는 위의 그래프와 같은 실제 데이터를 쓸 수도 있지만 간단히 gaussian curve를 적용하도록 하자. 값도 대충 그래프에서 읽어서 한다. 범위는 천리안2B 위성, wavelength step은 위에서 받아온 태양 스펙트럼 데이터에 맞춘다.
import numpy as np
wavelengths = np.arange(370.,885.5,0.5)
stddev=100.0 #standard deviation.
def gaussian(x, peak, stddev=stddev):
return np.exp((peak-x)*(x-peak)/(stddev*stddev))
sensitivity_s=[gaussian(x, 440, 40) for x in wavelengths]
sensitivity_m=[gaussian(x, 530, 60) for x in wavelengths]
sensitivity_l=[gaussian(x, 580, 80) for x in wavelengths]
plt.plot(wavelengths, sensitivity_s)
plt.plot(wavelengths, sensitivity_m)
plt.plot(wavelengths, sensitivity_l)
plt.legend(["S","M","L"])
plt.show()
sun = sun.loc[sun.index<=885]
sun = sun.loc[sun.index>=370]
sun
etr | global_tilt | direct+circumsolar | |
---|---|---|---|
Wvlgth nm | |||
370.0 | 1.290 | 0.755 | 0.517 |
370.5 | 1.170 | 0.683 | 0.468 |
371.0 | 1.180 | 0.693 | 0.476 |
371.5 | 1.220 | 0.721 | 0.496 |
372.0 | 1.140 | 0.674 | 0.465 |
... | ... | ... | ... |
881.0 | 0.920 | 0.909 | 0.846 |
882.0 | 0.944 | 0.932 | 0.868 |
883.0 | 0.939 | 0.929 | 0.865 |
884.0 | 0.944 | 0.933 | 0.869 |
885.0 | 0.955 | 0.944 | 0.879 |
546 rows × 3 columns
그리고 태양 스펙트럼 역시 해당 범위에 맞추고 나머지를 버렸다. 그런데 여기서 문제가 있다. wavelength의 간격의 $0.5nm$이었다가 $1.0nm$으로 바뀐다는 것이다. 별로 정확한 값 구하려고 하는 것은 아니니까 그냥 $0.5nm$ 간격으로 통일하자.
sens=np.column_stack((sensitivity_s, sensitivity_m, sensitivity_l))
cone=pd.DataFrame(data=sens, index=wavelengths, columns=["s", "m", "l"])
cone
s | m | l | |
---|---|---|---|
370.0 | 4.677062e-02 | 8.159878e-04 | 1.017278e-03 |
370.5 | 4.885462e-02 | 8.530127e-04 | 1.051170e-03 |
371.0 | 5.101554e-02 | 8.915937e-04 | 1.086106e-03 |
371.5 | 5.325539e-02 | 9.317903e-04 | 1.122116e-03 |
372.0 | 5.557621e-02 | 9.736638e-04 | 1.159229e-03 |
... | ... | ... | ... |
883.0 | 5.386900e-54 | 9.278968e-16 | 5.888136e-07 |
883.5 | 4.083436e-54 | 8.411715e-16 | 5.615646e-07 |
884.0 | 3.094403e-54 | 7.624460e-16 | 5.355348e-07 |
884.5 | 2.344187e-54 | 6.909924e-16 | 5.106716e-07 |
885.0 | 1.775301e-54 | 6.261483e-16 | 4.869247e-07 |
1031 rows × 3 columns
numerical integration¶
먼저 원추세포 데이터프레임(cone) 기준으로 merge하고 linear interpolate해서 태양 스펙트럼도 $0.5nm$으로 맞추자.
merged_df=cone.join(sun, how='left') #pd.DataFrame.join merges dataframe on indices.
merged_df
s | m | l | etr | global_tilt | direct+circumsolar | |
---|---|---|---|---|---|---|
370.0 | 4.677062e-02 | 8.159878e-04 | 1.017278e-03 | 1.290 | 0.755 | 0.517 |
370.5 | 4.885462e-02 | 8.530127e-04 | 1.051170e-03 | 1.170 | 0.683 | 0.468 |
371.0 | 5.101554e-02 | 8.915937e-04 | 1.086106e-03 | 1.180 | 0.693 | 0.476 |
371.5 | 5.325539e-02 | 9.317903e-04 | 1.122116e-03 | 1.220 | 0.721 | 0.496 |
372.0 | 5.557621e-02 | 9.736638e-04 | 1.159229e-03 | 1.140 | 0.674 | 0.465 |
... | ... | ... | ... | ... | ... | ... |
883.0 | 5.386900e-54 | 9.278968e-16 | 5.888136e-07 | 0.939 | 0.929 | 0.865 |
883.5 | 4.083436e-54 | 8.411715e-16 | 5.615646e-07 | NaN | NaN | NaN |
884.0 | 3.094403e-54 | 7.624460e-16 | 5.355348e-07 | 0.944 | 0.933 | 0.869 |
884.5 | 2.344187e-54 | 6.909924e-16 | 5.106716e-07 | NaN | NaN | NaN |
885.0 | 1.775301e-54 | 6.261483e-16 | 4.869247e-07 | 0.955 | 0.944 | 0.879 |
1031 rows × 6 columns
#linear interpolation
merged_df.interpolate(method='linear', axis=0, inplace=True)
merged_df
s | m | l | etr | global_tilt | direct+circumsolar | |
---|---|---|---|---|---|---|
370.0 | 4.677062e-02 | 8.159878e-04 | 1.017278e-03 | 1.2900 | 0.7550 | 0.517 |
370.5 | 4.885462e-02 | 8.530127e-04 | 1.051170e-03 | 1.1700 | 0.6830 | 0.468 |
371.0 | 5.101554e-02 | 8.915937e-04 | 1.086106e-03 | 1.1800 | 0.6930 | 0.476 |
371.5 | 5.325539e-02 | 9.317903e-04 | 1.122116e-03 | 1.2200 | 0.7210 | 0.496 |
372.0 | 5.557621e-02 | 9.736638e-04 | 1.159229e-03 | 1.1400 | 0.6740 | 0.465 |
... | ... | ... | ... | ... | ... | ... |
883.0 | 5.386900e-54 | 9.278968e-16 | 5.888136e-07 | 0.9390 | 0.9290 | 0.865 |
883.5 | 4.083436e-54 | 8.411715e-16 | 5.615646e-07 | 0.9415 | 0.9310 | 0.867 |
884.0 | 3.094403e-54 | 7.624460e-16 | 5.355348e-07 | 0.9440 | 0.9330 | 0.869 |
884.5 | 2.344187e-54 | 6.909924e-16 | 5.106716e-07 | 0.9495 | 0.9385 | 0.874 |
885.0 | 1.775301e-54 | 6.261483e-16 | 4.869247e-07 | 0.9550 | 0.9440 | 0.879 |
1031 rows × 6 columns
merged_df['etr_s']=merged_df['s']*merged_df['etr']*0.5 #d_wavelength = 0.5nm
merged_df['etr_m']=merged_df['m']*merged_df['etr']*0.5
merged_df['etr_l']=merged_df['l']*merged_df['etr']*0.5
merged_df['direct_s']=merged_df['s']*merged_df['direct+circumsolar']*0.5
merged_df['direct_m']=merged_df['m']*merged_df['direct+circumsolar']*0.5
merged_df['direct_l']=merged_df['l']*merged_df['direct+circumsolar']*0.5
merged_df
s | m | l | etr | global_tilt | direct+circumsolar | etr_s | etr_m | etr_l | direct_s | direct_m | direct_l | |
---|---|---|---|---|---|---|---|---|---|---|---|---|
370.0 | 4.677062e-02 | 8.159878e-04 | 1.017278e-03 | 1.2900 | 0.7550 | 0.517 | 3.016705e-02 | 5.263122e-04 | 6.561442e-04 | 1.209021e-02 | 2.109329e-04 | 2.629663e-04 |
370.5 | 4.885462e-02 | 8.530127e-04 | 1.051170e-03 | 1.1700 | 0.6830 | 0.468 | 2.857995e-02 | 4.990124e-04 | 6.149344e-04 | 1.143198e-02 | 1.996050e-04 | 2.459738e-04 |
371.0 | 5.101554e-02 | 8.915937e-04 | 1.086106e-03 | 1.1800 | 0.6930 | 0.476 | 3.009917e-02 | 5.260403e-04 | 6.408027e-04 | 1.214170e-02 | 2.121993e-04 | 2.584933e-04 |
371.5 | 5.325539e-02 | 9.317903e-04 | 1.122116e-03 | 1.2200 | 0.7210 | 0.496 | 3.248579e-02 | 5.683921e-04 | 6.844908e-04 | 1.320734e-02 | 2.310840e-04 | 2.782848e-04 |
372.0 | 5.557621e-02 | 9.736638e-04 | 1.159229e-03 | 1.1400 | 0.6740 | 0.465 | 3.167844e-02 | 5.549884e-04 | 6.607606e-04 | 1.292147e-02 | 2.263768e-04 | 2.695208e-04 |
... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | ... |
883.0 | 5.386900e-54 | 9.278968e-16 | 5.888136e-07 | 0.9390 | 0.9290 | 0.865 | 2.529149e-54 | 4.356476e-16 | 2.764480e-07 | 2.329834e-54 | 4.013154e-16 | 2.546619e-07 |
883.5 | 4.083436e-54 | 8.411715e-16 | 5.615646e-07 | 0.9415 | 0.9310 | 0.867 | 1.922277e-54 | 3.959815e-16 | 2.643565e-07 | 1.770169e-54 | 3.646478e-16 | 2.434382e-07 |
884.0 | 3.094403e-54 | 7.624460e-16 | 5.355348e-07 | 0.9440 | 0.9330 | 0.869 | 1.460558e-54 | 3.598745e-16 | 2.527724e-07 | 1.344518e-54 | 3.312828e-16 | 2.326899e-07 |
884.5 | 2.344187e-54 | 6.909924e-16 | 5.106716e-07 | 0.9495 | 0.9385 | 0.874 | 1.112903e-54 | 3.280487e-16 | 2.424414e-07 | 1.024410e-54 | 3.019637e-16 | 2.231635e-07 |
885.0 | 1.775301e-54 | 6.261483e-16 | 4.869247e-07 | 0.9550 | 0.9440 | 0.879 | 8.477063e-55 | 2.989858e-16 | 2.325066e-07 | 7.802448e-55 | 2.751922e-16 | 2.140034e-07 |
1031 rows × 12 columns
merged_df.sum()
s 140.874569 m 212.677592 l 283.563991 etr 1557.162500 global_tilt 1292.386000 direct+circumsolar 1139.232500 etr_s 126.342404 etr_m 198.896057 etr_l 251.074416 direct_s 75.990755 direct_m 141.161537 direct_l 185.925174 dtype: float64
결론¶
태양빛의 색 3채널 값은 (normalize를 생략하고) 대기권 밖에서 (126, 199, 251), 지표면에서 (76, 141, 186)라고 할 수도 있다. 그런데 이 색은 우리가 흔히 말하는 RGB 색과는 관계가 없다. 위의 원추세포에서 보다시피 빨간색은 가장 파장이 짧은 'L'원추세포에서도 그 중심과 거리가 좀 있다.
만약 원추세포와 유사한 frequency-responce를 가지는 발광소자를 세 개 박아서 디스플레이 장치를 만든다면 저 값을 입력해서 우리가 실제로 보는 것과 유사한 색깔을 얻을 수 있을 것이다. 그런데 그런 것이 있나?
실제로 디스플레이를 만들 때에는 이런 점을 알아서 잘 고려해서 구현하겠지.