Анализ рядов NDVI при помощи разложения в ряд Фурье

Классический анализ данных ДЗЗ происходит по на основе обработки яркостей пикселей в нескольких спектральных каналах. Другой подход связан с анализом динамики яркостей в течении длительгого периода. Суть этих методов основана на том, что различные классы Земной поверхности (здесь, в первую очередь, речь идет о растительности) обладают различными фенологическими характеристиками. Эти характеристики порождают различия во временных рядах NDVI, поэтому вместо анализа яркостей пикселей на отдельных снимках можно анализировать динамику NDVI.

Пример такого подхода приводится в Lhermitte S. et al. Hierarchical image segmentation based on similarity of NDVI time series //Remote Sensing of Environment. – 2008. – Т. 112. – №. 2. – С. 506-521. В статье предлагается найти разложение в ряд Фурье для временного ряда NDVI. Коэффициенты ряда Фурье однозначно описывают исходный ряд и поэтому классификацию Земной поверхности можно можно произвести на базе этих коэффициентов.

Ниже производится эта процедура в очень упрощенном виде, поскольку в цитируемой работе анализируется не только временные, но и пространственные вариации NDVI. Здесь в качестве данных для анализа используется продукт MOD13 (16-ти дневные композиты).

Эта статья строится следующим образом:

  1. Сначала идет краткая теория по преобразованию Фурье: дается определение прямого и обратного преобразования Фурье, некоторые свойства.
  2. Во втором разделе показываются некоторые типичные приемы и особенности работы с преобразованием Фурье при анализе временных рядов. Эта часть не касается непосредственно нашей задачи --- классификации данных ДЗЗ по их временной динамике, но раздел показывает основы, которые необходимы для дальнейшего понимания.
  3. В последнем разделе показано, как коэффициенты разложения могут быть использованы для классификации данных ДЗЗ.

Немного теории по преобразованию Фурье

Вводная часть

Дана функция $f(x)$, определенная на интервале $x\in (-\pi, pi]$. Известно, что ее можно разложить в ряд, состоящий из тригонометрических функций (ограничения на $f(x)$ очень слабые, на практике любая функция будет им удовлетворять):

$$ f(x) = \frac{a_0}2 + \sum_{n=0}^\infty (a_n\cos nx + b_n \sin nx) $$

Легко показать, что коэффициенты ряда могут быть вычисленны по формулам:

$$ a_n = \frac1{\pi} \int_{-\pi}^\pi f(x) \cos nx dx, $$

для $n \ge 0$ и $$ b_n = \frac1{\pi} \int_{-\pi}^\pi f(x) \sin nx dx $$

для $n >0$.

Исходный интервал $x\in (-\pi, \pi]$ может быть легко промасштабирован на произвольный симметричный отрезов $[-L, L]$. Кроме того, формула разложения часто записывается в более простом виде в комплексной форме: $$ f(x) = \sum_{-\infty}^{\infty} c_n e^{in\pi x / L}, x\in [-L, L]. $$ (Последнее выражение базируется на формуле Эйлера: $e^{i\phi} = \cos \phi + i \sin \phi$. Нужно обратить внимание на то, что хотя теперь ряд Фурье стал комплексным, сама функция $f(x)$ по прежнему остается действительной).

Преобразование Фурье

Преобразование Фурье -- это преобразование функции, заданное на всей числовой оси $x\in (-\pi, \pi)$ следующим образом:

$$ F(k) = \frac{1}{\sqrt{2\pi}} \int_{-\infty}^{\infty} e^{-i kx} f(x) dx. $$

Обратное преобразование (переход от $F(k)$ к $f(x)$) задается формулой:

$$ f(x) = \frac{1}{\sqrt{2\pi}} \int_{-\infty}^{\infty} e^{-i kx} F(k) dk. $$

Таким образом преобразование Фурье возвращает результат, представленный в виде комплексных чисел. При анализе сигналов при помощи преобразования Фурье важную роль играют понятия амплитуды (модуля) и частоты (аргумента) комплексного числа.

Для вычисления коэффициентов преобразования Фурье существует классический алгоритм, который носит название "Быстрое преборазование Фурье". Вот его основные особенности:

  1. Низкая вычислительная стоимость ($O(N\log N)$).
  2. Алгоритм ищет разложение на отрезке $[-L, L]$. Как следствие, подразумевается функция, разложение которой вычисляется, должна быть периодической с периодом $2L$.

Основные приемы анализа временных рядов с ипользованием преобразования Фурье

Подготовка данных


In [1]:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt

from scipy.fftpack import fft, ifft, fftshift, fftfreq

from tools import get_modis_ndvi_frame

Чтение NDVI, разложение в ряд Фурье

Прочитаем данные по торфяной воде (три географически различные точки) и построим график изменения:


In [2]:
water_files = ['data/MODIS/point_1_NDVI.csv', 'data/MODIS/point_2_NDVI.csv', 'data/MODIS/point_3_NDVI.csv']

water_frames = map(get_modis_ndvi_frame, water_files)
plot(water_frames[0].DayNum, water_frames[0].NDVI, 
     water_frames[1].DayNum, water_frames[1].NDVI, 
     water_frames[2].DayNum, water_frames[2].NDVI)


Out[2]:
[<matplotlib.lines.Line2D at 0x7f5ae8031b10>,
 <matplotlib.lines.Line2D at 0x7f5ae8031d10>,
 <matplotlib.lines.Line2D at 0x7f5ae8044410>]

Откровенно говоря я не ожидал, что вода будет так сильно меняться в годичном цикле. (Возможно, я перепутал названия классов, т.к. данные по NDVI были получены из продуктов MODIS, а названия классов при интерпретации Landsat. Возможно, дело в том, что в ячейка растра MODIS хранит "интегральный" сигнал нескольких "реальных" классов. Может быть, вода "цветет". Короче, много чего может быть, но для нижеследующих примеров это не так важно -- важно показать принцип работы.)

Найдем разложение в ряд Фурье для первой точки и выведем первые 5 коэффициентов разложения:


In [3]:
water0 = water_frames[0].NDVI
water0_fft = fft(water0)
water0_fft[:5]


Out[3]:
array([ 152649.00000000    +0.j        ,   -1178.39940456 -6213.16061453j,
          5496.13482940  -679.98871301j, -104265.44564268+34133.60326204j,
         -5928.63805986 +3034.28066771j])

Как видим, коэффициенты являются комплексными числами. Произведем обратное преобразование Фурье, убедимся, что на выходе мы получаем первоначальный ряд:


In [4]:
print np.array(water0[:5])  # Исходный сигнал
print ifft(fft(water0))[:5]  # Преобразование Фурье (туда-сюда)
print ifft(water0_fft).real[:5]  # Действительная часть сигнала


[-100 -246  -75   -4  -59]
[-100. -3.95432479e-14j -246. +3.29527066e-13j  -75. +1.18629744e-13j
   -4. +4.49330146e-14j  -59. +8.31218664e-14j]
[-100. -246.  -75.   -4.  -59.]

Здесь видно, что на самом деле после обратного преобразования Фурье получаются комплексные числа, но мнимая часть их очень близка к нулю (появление ненулевой комплексной части объясняется погрешностями вычислений). Поэтому эти мнимые части можно отбросить:


In [5]:
plot(water0)


Out[5]:
[<matplotlib.lines.Line2D at 0x7f5ae3a94cd0>]

In [6]:
water0_fft = fft(water0)
water0_restored = ifft(water0_fft).real
plot(water0_restored)


Out[6]:
[<matplotlib.lines.Line2D at 0x7f5ae39d7950>]

Коэффициенты, получаемые быстрым преобразованием Фурье, амплитуда сигнала и его энергия.

Функция fft возвращает "сдвинутые" вдоль числовой оси коэффициенты (подробнее об этом см. http://docs.scipy.org/doc/numpy/reference/routines.fft.html#background-information): в элементе water0_fft[0] хранится константа (среднее значение сигнала), water0_fft[1:n/2] коэффициенты, соответствующие положительным частотам, а water0_fft[n/2+1:] -- коэффициенты, соответствующие отрицательным частотам. Для того, чтобы получить коэффициенты, следующие в "естественном" порядке, можно воспользоваться функцией fftshift, которая переупорядочивает эти коэффициенты. Альтернативный путь -- воспользоваться функцией fftfreq, которая, наборот, подстраивает массив частот под порядок коэффициентов.

Пусть a -- исходный сигнал, зависящий от времени, A = fft(A), тогда np.abs(A) возвращает амплитудный спектр сигнала, np.abs(A)**2 -- энергетический спект сигнала, а np.angle(A) -- фазовый спектр.

(Хорошее практическое введение в тему есть тут: http://habrahabr.ru/post/112068/, правда оно рассматривает MATLAB в качестве языка реализации).

Построим амплитудный и энергетический спектры:


In [7]:
# Амплитудный спектр
freq = fftfreq(water0.shape[0])
water0_fft = fft(water0)
idx = argsort(freq)  # индексы, чтобы частоты на графике шли по возрастанию
plot(freq[idx], abs(water0_fft)[idx], '-o')


Out[7]:
[<matplotlib.lines.Line2D at 0x7f5ae390ea90>]

In [8]:
# Энергетический спектр
plot(freq[idx], abs(water0_fft[idx])**2, '-o')


Out[8]:
[<matplotlib.lines.Line2D at 0x7f5ae38c6f50>]

Удаление шумов из данных

После построения энергетического спектра становится очевидным, что почити вся энергия сигнала содержится в паре-тройке компонент (гармоник). В данном случае гармоники с высокой энергией -- это $A_0$ (константа), а также $A_{3}$ и $A_{-3}$ (в силу симметрии эти два коэффициента -- одна и та же гармоника). Эти гармоники соответствуют выраженным частотам, так, например, гармоники с индексом "3" ($A_{3}$ и $A_{-3}$) сигнализируют о том, что в наших данных присутствуют три хорошо выраженных "волны" (цикла).

Остальные гармоники несут очень незначительную энергию и могут быть опущены. Поэтому "занулим" все коэффициенты разложения, кроме $A_0$, $A_{3}$ и $A_{-3}$, и восстановим сигнал из полученной последовательности. (Для простоты реализации обнулим все коэффициенты, модуль которых меньше определенного порога).


In [9]:
# Зануляем
water0_fft = fft(water0)
water0_fft[abs(water0_fft)< 50000] = 0
# Востанавливаем
water0_filt = ifft(water0_fft).real
plot(water0_filt)


Out[9]:
[<matplotlib.lines.Line2D at 0x7f5ae3807390>]

На самом деле, учитывая, что мы оставили только три коэффициента ($A_0$, $A_3$, $A_{-3}$, причем, в силу симметрии, по сути имеем только два коэффициента), мы получили на выходе синусоиду, которая приближает наш сигнал: $A_0 + A_3\sin(...)$. Покажем для наглядности на одном графике исходный и восстановленный сигнал:


In [10]:
t = range(len(water0))
plot(t, water0, t, water0_filt)


Out[10]:
[<matplotlib.lines.Line2D at 0x7f5ae3737e90>,
 <matplotlib.lines.Line2D at 0x7f5ae37460d0>]

Если оставить большее количество коэффициентов, получим иное приближение:


In [11]:
# Зануляем
water0_fft = fft(water0)
water0_fft[abs(water0_fft)< 20000] = 0
# Востанавливаем
water0_filt = ifft(water0_fft).real
plot(t, water0, t, water0_filt)


Out[11]:
[<matplotlib.lines.Line2D at 0x7f5ae3678810>,
 <matplotlib.lines.Line2D at 0x7f5ae3678a10>]

Сегментация на базе коэффициентов разложения в ряд Фурье

Как было сказано выше, идея, лежащая в основе, -- проста: основную форму кривой описывают коэффициенты разложения, которые несут наибольшую энергию, остальные коэффициенты описывают "второстепенные" компоненты сигнала. Поэтому можно выделить несколько основных коэффициентов и использовать их в качестве признаков при классификации.

Проверим это на практике:

  1. Возьмем ряды NDVI, соотвествующие нескольким различным классам (раздел "Чтение данных NDVI").
  2. Для каждого временного ряда произведем преобразование Фурье и найдем коффициенты разложений, несущие наибольую энергию ($k$ коэффициентов) -- раздел "Преобразование Фурье и основные гармоники".
  3. Оценим возможность классификации данных, используя выделенные коэффициенты в качестве признаков (раздел "Классификация").

Чтение данных NDVI

Темная вода


In [12]:
water_files = ['data/MODIS/point_1_NDVI.csv', 'data/MODIS/point_2_NDVI.csv', 'data/MODIS/point_3_NDVI.csv']

water_frames = map(get_modis_ndvi_frame, water_files)
plot(water_frames[0].DayNum, water_frames[0].NDVI,
     water_frames[1].DayNum, water_frames[1].NDVI, 
     water_frames[2].DayNum, water_frames[2].NDVI)


Out[12]:
[<matplotlib.lines.Line2D at 0x7f5ae35c22d0>,
 <matplotlib.lines.Line2D at 0x7f5ae35c24d0>,
 <matplotlib.lines.Line2D at 0x7f5ae35c2b90>]

Сосново-кустарничково-сфагновое олиготрофное болото (густой рям)


In [13]:
pine_swamp_files = ['data/MODIS/point_4_NDVI.csv', 'data/MODIS/point_5_NDVI.csv', 'data/MODIS/point_6_NDVI.csv']

pine_swamp_frames = map(get_modis_ndvi_frame, pine_swamp_files)
plot(pine_swamp_frames[0].DayNum, pine_swamp_frames[0].NDVI, 
     pine_swamp_frames[1].DayNum, pine_swamp_frames[1].NDVI, 
     pine_swamp_frames[2].DayNum, pine_swamp_frames[2].NDVI)


Out[13]:
[<matplotlib.lines.Line2D at 0x7f5ae34ff110>,
 <matplotlib.lines.Line2D at 0x7f5ae34ff310>,
 <matplotlib.lines.Line2D at 0x7f5ae34ff9d0>]

Осоково-(C. rostrata) сфагновое мезоолиготрофное болото или олиготрофное пушицево-сфагновое болото


In [14]:
rostrata_files = ['data/MODIS/point_7_NDVI.csv', 'data/MODIS/point_8_NDVI.csv', 'data/MODIS/point_9_NDVI.csv']

rostrata_frames = map(get_modis_ndvi_frame, rostrata_files)
plot(rostrata_frames[0].DayNum, rostrata_frames[0].NDVI, 
     rostrata_frames[1].DayNum, rostrata_frames[1].NDVI, 
     rostrata_frames[2].DayNum, rostrata_frames[2].NDVI)


Out[14]:
[<matplotlib.lines.Line2D at 0x7f5ae3443650>,
 <matplotlib.lines.Line2D at 0x7f5ae3443850>,
 <matplotlib.lines.Line2D at 0x7f5ae3443f10>]

Сосняки-зеленомошники


In [15]:
pine_files = ['data/MODIS/point_10_NDVI.csv', 'data/MODIS/point_11_NDVI.csv', 'data/MODIS/point_12_NDVI.csv']

pine_frames = map(get_modis_ndvi_frame, pine_files)
plot(pine_frames[0].DayNum, pine_frames[0].NDVI, 
     pine_frames[1].DayNum, pine_frames[1].NDVI, 
     pine_frames[2].DayNum, pine_frames[2].NDVI)


Out[15]:
[<matplotlib.lines.Line2D at 0x7f5ae330c390>,
 <matplotlib.lines.Line2D at 0x7f5ae330c590>,
 <matplotlib.lines.Line2D at 0x7f5ae330cc50>]

Сравнение

Возьмем по одному представителю каждого класса и поместим на общий график, чтобы увидеть сходства и различия в поведении NDVI.


In [16]:
fig = plt.figure(figsize=(12,6), dpi=100)
plt.plot(water_frames[0].DayNum, water_frames[0].NDVI, label='Water')
plt.plot(pine_swamp_frames[0].DayNum, pine_swamp_frames[0].NDVI, label='Pine+swamp')
plt.plot(rostrata_frames[0].DayNum, rostrata_frames[0].NDVI, label='Rostrata')
plt.plot(pine_frames[0].DayNum, pine_frames[0].NDVI, label='Pine')
plt.legend(loc=4)
plt.show()


  1. Как видим, единственный объект, который хорошо отличается от остальных -- вода. Водные объекты имеют более низкий NDVI и более короткий "вегетационный период", если так вообще можно говорить о водных объектах :). У всех остальных объектов вегетация начинается и заканчивается приблизительно в одно время.

  2. У сосняков относительно высокий NDVI в зимний период.

  3. Отличается ли один тип болота от другого, сказать сложно: график осокового болота лежит чуть ниже, чем график сосново-кустарничкого болота, но насколько это различие значимо статистически по одной точке не скажешь -- нужно привлечь больше статистики.

Поместим все имеющиеся точки болот на один график:


In [17]:
fig = plt.figure(figsize=(12,6), dpi=100)
plt.plot(pine_swamp_frames[0].DayNum, pine_swamp_frames[0].NDVI, 'r', label='Pine+swamp')
plt.plot(pine_swamp_frames[1].DayNum, pine_swamp_frames[1].NDVI, 'r')
plt.plot(pine_swamp_frames[2].DayNum, pine_swamp_frames[2].NDVI, 'r')
plt.plot(rostrata_frames[0].DayNum, rostrata_frames[0].NDVI, 'b', label='Rostrata')
plt.plot(rostrata_frames[1].DayNum, rostrata_frames[1].NDVI, 'b')
plt.plot(rostrata_frames[2].DayNum, rostrata_frames[2].NDVI, 'b')
plt.legend(loc=1)
plt.show()


Видимо, основное отличие их состоит в том, что в зимний период сосновое болото имеет более высокий NDVI.

Преобразование Фурье и основные гармоники

Для работы над разными рядами нам обязательно нужно, чтобы данные были за один и тот же период времени. Убедимся в этом:


In [18]:
for num in [0, 1, 2]:
    for frames in [water_frames, pine_swamp_frames, 
                  rostrata_frames, pine_frames]:
        print np.all(water_frames[0].DayNum == frames[1].DayNum),


True True True True True True True True True True True True

Темная вода

Выполним преобразование Фурье


In [19]:
water0 = water_frames[0].NDVI
water1 = water_frames[1].NDVI
water2 = water_frames[2].NDVI

water0_fft = fft(water0)
water1_fft = fft(water1)
water2_fft = fft(water2)

Построим графики амплитудных спектров


In [20]:
freq = fftfreq(water0.shape[0])
idx = argsort(freq)  # индексы, чтобы частоты на графике шли по возрастанию

fig = plt.figure(figsize=(12,6), dpi=100)
plt.plot(freq[idx], abs(water0_fft[idx]), '-o', label='0')
plt.plot(freq[idx], abs(water1_fft[idx]), '-o', label='1')
plt.plot(freq[idx], abs(water2_fft[idx]), '-o', label='2')
plt.legend(loc=1)
plt.show()


Подберем порог обнуления коэффициентов разложения, при котором сохряняются основные особенности сигнала и запомним оставшиеся коэффициенты:


In [21]:
water0_fft = fft(water0)
water1_fft = fft(water1)
water2_fft = fft(water2)

tol = 20000
water0_fft[abs(water0_fft)< tol] = 0
water1_fft[abs(water1_fft)< tol] = 0
water2_fft[abs(water2_fft)< tol] = 0

water0_filt = ifft(water0_fft).real
water1_filt = ifft(water1_fft).real
water2_filt = ifft(water2_fft).real

# Графики
fig = plt.figure(figsize=(12,6), dpi=100)
# plt.plot(water0, 'b-', label='0')
# plt.plot(water1, 'g-', label='1')
# plt.plot(water2, 'r-', label='2')
plt.plot(water0_filt, 'b-o', label='0 filt')
plt.plot(water1_filt, 'g-o', label='1 filt')
plt.plot(water2_filt, 'r-o', label='2 filt')
plt.legend(loc=1)
plt.show()


Частоты, на которых остались ненулевые коэффициенты:


In [22]:
print freq[water0_fft != 0]
print freq[water0_fft != 0]
print freq[water0_fft != 0]

print freq[0], freq[3], freq[9]


[ 0.          0.04347826  0.13043478 -0.13043478 -0.04347826]
[ 0.          0.04347826  0.13043478 -0.13043478 -0.04347826]
[ 0.          0.04347826  0.13043478 -0.13043478 -0.04347826]
0.0 0.0434782608696 0.130434782609

С учетом симметриии, получаем, что форму этих кривых определяют в основном три коэффициента: $A_0$, $A_3$, $A_9$

Сосново-кустарничково-сфагновое олиготрофное болото (густой рям)

Повторим процедуру, которую мы произвели с водой, на остальных данных


In [23]:
pine_swamp0 = pine_swamp_frames[0].NDVI
pine_swamp1 = pine_swamp_frames[1].NDVI
pine_swamp2 = pine_swamp_frames[2].NDVI

pine_swamp0_fft = fft(pine_swamp0)
pine_swamp1_fft = fft(pine_swamp1)
pine_swamp2_fft = fft(pine_swamp2)

fig = plt.figure(figsize=(12,6), dpi=100)
plt.plot(freq[idx], abs(pine_swamp0_fft[idx]), '-o', label='0')
plt.plot(freq[idx], abs(pine_swamp1_fft[idx]), '-o', label='1')
plt.plot(freq[idx], abs(pine_swamp2_fft[idx]), '-o', label='2')
plt.legend(loc=1)
plt.show()



In [24]:
pine_swamp0_fft = fft(pine_swamp0)
pine_swamp1_fft = fft(pine_swamp1)
pine_swamp2_fft = fft(pine_swamp2)

tol = 20000
pine_swamp0_fft[abs(pine_swamp0_fft)< tol] = 0
pine_swamp1_fft[abs(pine_swamp1_fft)< tol] = 0
pine_swamp2_fft[abs(pine_swamp2_fft)< tol] = 0

pine_swamp0_filt = ifft(pine_swamp0_fft).real
pine_swamp1_filt = ifft(pine_swamp1_fft).real
pine_swamp2_filt = ifft(pine_swamp2_fft).real

# Графики
fig = plt.figure(figsize=(12,6), dpi=100)
# plt.plot(water0, 'b-', label='0')
# plt.plot(water1, 'g-', label='1')
# plt.plot(water2, 'r-', label='2')
plt.plot(pine_swamp0_filt, 'b-o', label='0 filt')
plt.plot(pine_swamp1_filt, 'g-o', label='1 filt')
plt.plot(pine_swamp2_filt, 'r-o', label='2 filt')
plt.legend(loc=1)
plt.show()

print freq[pine_swamp0_fft != 0]
print freq[pine_swamp1_fft != 0]
print freq[pine_swamp1_fft != 0]

print freq[0], freq[3], freq[9], freq[12]


[ 0.          0.04347826  0.13043478  0.17391304 -0.17391304 -0.13043478
 -0.04347826]
[ 0.          0.04347826  0.13043478 -0.13043478 -0.04347826]
[ 0.          0.04347826  0.13043478 -0.13043478 -0.04347826]
0.0 0.0434782608696 0.130434782609 0.173913043478

Здесь фигурируют 4 незануленных коэффициента: $A_0$, $A_3$, $A_9$, $A_{12}$. (При пороге в 16000 появляется 5-й коэф. $A_6$, но он приводит к противоестественной "волне" на макушке, т.е. в середине лета)

Осоково-(C. rostrata) сфагновое мезоолиготрофное болото или олиготрофное пушицево-сфагновое болото


In [25]:
rostrata0 = rostrata_frames[0].NDVI
rostrata1 = rostrata_frames[1].NDVI
rostrata2 = rostrata_frames[2].NDVI

rostrata0_fft = fft(rostrata0)
rostrata1_fft = fft(rostrata1)
rostrata2_fft = fft(rostrata2)

fig = plt.figure(figsize=(12,6), dpi=100)
plt.plot(freq[idx], abs(rostrata0_fft[idx]), '-o', label='0')
plt.plot(freq[idx], abs(rostrata1_fft[idx]), '-o', label='1')
plt.plot(freq[idx], abs(rostrata2_fft[idx]), '-o', label='2')
plt.legend(loc=1)
plt.show()



In [26]:
rostrata0_fft = fft(rostrata0)
rostrata1_fft = fft(rostrata1)
rostrata2_fft = fft(rostrata2)

tol = 20000
rostrata0_fft[abs(rostrata0_fft)< tol] = 0
rostrata1_fft[abs(rostrata1_fft)< tol] = 0
rostrata2_fft[abs(rostrata2_fft)< tol] = 0

rostrata0_filt = ifft(rostrata0_fft).real
rostrata1_filt = ifft(rostrata1_fft).real
rostrata2_filt = ifft(rostrata2_fft).real

# Графики
fig = plt.figure(figsize=(12,6), dpi=100)
# plt.plot(water0, 'b-', label='0')
# plt.plot(water1, 'g-', label='1')
# plt.plot(water2, 'r-', label='2')
plt.plot(rostrata0_filt, 'b-o', label='0 filt')
plt.plot(rostrata1_filt, 'g-o', label='1 filt')
plt.plot(rostrata2_filt, 'r-o', label='2 filt')
plt.legend(loc=1)
plt.show()

print freq[rostrata0_fft != 0]
print freq[rostrata1_fft != 0]
print freq[rostrata2_fft != 0]

print freq[0], freq[3], freq[9], freq[12]


[ 0.          0.04347826  0.13043478  0.17391304 -0.17391304 -0.13043478
 -0.04347826]
[ 0.          0.04347826  0.13043478  0.17391304 -0.17391304 -0.13043478
 -0.04347826]
[ 0.          0.04347826  0.13043478  0.17391304 -0.17391304 -0.13043478
 -0.04347826]
0.0 0.0434782608696 0.130434782609 0.173913043478

Сосняки-зеленомошники


In [27]:
pine0 = pine_frames[0].NDVI
pine1 = pine_frames[1].NDVI
pine2 = pine_frames[2].NDVI

pine0_fft = fft(pine0)
pine1_fft = fft(pine1)
pine2_fft = fft(pine2)

fig = plt.figure(figsize=(12,6), dpi=100)
plt.plot(freq[idx], abs(pine0_fft[idx]), '-o', label='0')
plt.plot(freq[idx], abs(pine1_fft[idx]), '-o', label='1')
plt.plot(freq[idx], abs(pine2_fft[idx]), '-o', label='2')
plt.legend(loc=1)
plt.show()



In [28]:
pine0_fft = fft(pine0)
pine1_fft = fft(pine1)
pine2_fft = fft(pine2)

tol = 20000
pine0_fft[abs(pine0_fft)< tol] = 0
pine1_fft[abs(pine1_fft)< tol] = 0
pine2_fft[abs(pine2_fft)< tol] = 0

pine0_filt = ifft(pine0_fft).real
pine1_filt = ifft(pine1_fft).real
pine2_filt = ifft(pine2_fft).real

# Графики
fig = plt.figure(figsize=(12,6), dpi=100)
# plt.plot(water0, 'b-', label='0')
# plt.plot(water1, 'g-', label='1')
# plt.plot(water2, 'r-', label='2')
plt.plot(pine0_filt, 'b-o', label='0 filt')
plt.plot(pine1_filt, 'g-o', label='1 filt')
plt.plot(pine2_filt, 'r-o', label='2 filt')
plt.legend(loc=1)
plt.show()

print freq[pine0_fft != 0]
print freq[pine1_fft != 0]
print freq[pine2_fft != 0]

print freq[0], freq[3], freq[9], freq[12]


[ 0.          0.04347826  0.13043478 -0.13043478 -0.04347826]
[ 0.          0.04347826  0.13043478 -0.13043478 -0.04347826]
[ 0.          0.04347826  0.13043478  0.17391304 -0.17391304 -0.13043478
 -0.04347826]
0.0 0.0434782608696 0.130434782609 0.173913043478

Краткие выводы по коэффициентам

Как видно из графиков, форму кривых более-менее точно описывают в основном коэффициэты $A_0$, $A_3$, $A_9$, $A_{12}$ (для некоторых кривых достаточно и меньшего количества коэффициетов, но мы выберем те, что описывают все кривые, а не только часть из них).

Построим на одном графике отфильтрованные сигналы с этими коэффициентами (на примере одной точки из каждого класса).


In [29]:
water0_fft = fft(water0)
pine_swamp0_fft = fft(pine_swamp0)
rostrata0_fft = fft(rostrata0)
pine0_fft = fft(pine0)

# Обнулим коэффициенты
size = water0_fft.shape
idx = [0, 3, 9, 12, -3, -9, -12]  # Индексы коэффициентов, которые не зануляем

water0_fft_upd = np.zeros(size, dtype=complex)
pine_swamp0_fft_upd = np.zeros(size, dtype=complex)
rostrata0_fft_upd = np.zeros(size, dtype=complex)
pine0_fft_upd = np.zeros(size, dtype=complex)
for t in idx:
    water0_fft_upd[t] = water0_fft[t]
    pine_swamp0_fft_upd[t] = pine_swamp0_fft[t]
    rostrata0_fft_upd[t] = rostrata0_fft[t]
    pine0_fft_upd[t] = pine0_fft[t]
    
    
# Обратное преобразование
water0_filt = ifft(water0_fft_upd).real
pine_swamp0_filt = ifft(pine_swamp0_fft_upd).real
rostrata0_filt = ifft(rostrata0_fft_upd).real
pine0_filt = ifft(pine0_fft_upd).real

fig = plt.figure(figsize=(12,6), dpi=100)
plt.plot(water0_filt, '-o', label='water')
plt.plot(pine_swamp0_filt, label='pine_swamp')
plt.plot(rostrata0_filt, '-o', label='rostrata')
plt.plot(pine0_filt, label='pine')
plt.legend(loc=3)
plt.show()


Как видим, на этих графиках прослеживаются основные тенденции, которые были подмечены на "сырых" NDVI:

  1. Водные объекты имеют более низкий NDVI и более короткий "вегетационный период". У всех остальных объектов вегетация начинается и заканчивается приблизительно в одно время.

  2. У сосняков относительно высокий NDVI в зимний период.

  3. Сосновые и осоковые болота очень похожи друг на друга. Наиболее характерное отличие -- в зимний период сосновое болото имеет более высокий NDVI, чем осоковое.

Классификация

Выше мы выделили четыре коэффициента, которые можно использовать для классификации $A_0$, $A_3$, $A_9$, $A_{12}$. Но в данном случае у нас имеется всего двенадцать различных точек, поэтому нет смысла использовать семь независимых переменных, так как размерность пространства окажется излишне велика. Размерность семь получается в силу того, что коэффициенты представляют собой комплексные числа (1 + 2 + 2 + 2).

Поэтому искусственно снизим размерность до трех -- возмем в качестве объясняющих переменных два коэффициента с наибольшей энергией ($A_0$ и $A_3$).

Построим графики точкек:


In [30]:
result = []
for data in [water0, water1, water2,
             pine_swamp0, pine_swamp1, pine_swamp2,
             rostrata0, rostrata1, rostrata2,
             pine0, pine1, pine2]:
    f = fft(data)
    result.append([f[0].real, f[3].real, f[3].imag])

result = pd.DataFrame(result, columns=['x1', 'x2', 'x3'])
# Нормализуем для удобства отображения
# result = (result - result.mean())/(result.max() - result.min())

In [31]:
# Тут я пытался построить график в 3d, но он вышел не читаемый
# поэтому я перешел на 2d -- только по первым двум координатам

# from mpl_toolkits.mplot3d import Axes3D
# fig = plt.figure()
# ax = fig.add_subplot(111, projection='3d')
# ax.scatter(result.x1[:3], result.x2[:3], result.x3[:3], marker = '*', s=36) # water
# ax.scatter(result.x1[3:6], result.x2[3:6], result.x3[3:6], marker = '^', s=36) # pine_swamp
# ax.scatter(result.x1[6:9], result.x2[6:9], result.x3[6:9], marker = '>', s=36) # rostr
# ax.scatter(result.x1[9:12], result.x2[9:12], result.x3[9:12], marker = 'v', s=36) # pine
# plt.show()

fig = plt.figure()
plot(result.x1[:3], result.x2[:3], marker = '*') # water
plot(result.x1[3:6], result.x2[3:6], marker = '^') # pine_swamp
plot(result.x1[6:9], result.x2[6:9], marker = '>') # rostr
plot(result.x1[9:12], result.x2[9:12], marker = 'v') # pine
plt.show()


Ввывод

Из графика видно, что точки разных классов лежат хотя и в близких, но все же различных областях пространства. Это позволяет сделать предположение о том, что метод, в основе которого лежат процедуры анализа гармоник, может с успехом использоваться для сегментации ДЗЗ.