Принимаем и декодируем аналоговое ТВ с помощью SDR и Python

в 11:25, , рубрики: gnu radio, PAL, python, SDR, SECAM, декодирование, Научно-популярное, прием, Программирование, старое железо, ТВ, Электроника для начинающих

Сегодня мы продолжим тему SDR-приема и обработки сигналов. Приемом аналогового ТВ я заинтересовался совершенно случайно, после вопроса одного из читателей. Однако это оказалось не так просто, из-за банального отсутствия образцов сигнала — во многих местах аналоговое ТВ уже отключено. Читатель даже прислал запись с RTL-SDR, однако ширина записи у RTL порядка 2МГц, в то время как полоса ТВ-сигнала занимает около 8МГц, и на записи было ничего не понятно. В итоге, тема была надолго заброшена, и наконец, только сейчас, в очередную поездку к родственникам я взял с собой SDRPlay, и настроившись на частоты ТВ-каналов, увидел на экране искомый сигнал.

Небольшая программа на Python, и все работает:

Принимаем и декодируем аналоговое ТВ с помощью SDR и Python - 1

Для тех, кому интересны подробности, продолжение под катом.

Теория

В те давние послевоенные годы, когда о цифровой передаче сигналов знали только в секретных лабораториях, но люди уже хотели смотреть ТВ, существовали три конкурирующих аналоговых стандарта. Первым был американский NTSC (National Television System Committee), который разрабатывался еще с 40х годов, был «заточен» под американскую частоту сети 60Гц и имел вертикальное разрешение всего лишь в 486 строк. Чуть позже в Германии стал разрабатываться стандарт PAL (Phase Alternating Line), который был немного лучше американского (разрешение «целых» 576 строк и ориентированность на европейскую частоту сети 50Гц), и еще чуть позже появился французский SECAM (Séquentiel couleur à mémoire). В нем были устранены некоторые недостатки PAL, которые касались передачи цвета, ну и есть версия, что принятие двух стандартов также было политическим решением, чтобы жители одних стран не могли смотреть передачи из стран других (до единого Евросоюза и Шенгена было еще около 50 лет). Так или иначе, но весь мир оказался разделен примерно так:

Принимаем и декодируем аналоговое ТВ с помощью SDR и Python - 2

Т.к. Хабр все-таки сайт русскоязычный, то в дальнейшем мы будем рассматривать именно SECAM, хотя если кто-то пришлет образец сигнала PAL, тоже было бы интересно.

Спектр SECAM, если верить старинным свиткам, выглядит следующим образом:

Принимаем и декодируем аналоговое ТВ с помощью SDR и Python - 3

Слева, на частоте F0, находится амплитудно-модулированный яркостный (L) сигнал. Это фактически черно-белое изображение, которое до сих пор может быть показано на старом теплом и ламповом черно-белом ТВ. Проблема Legacy и наличия у пользователей старых девайсов существовала уже тогда, так что цветовой канал был добавлен отдельно, без потери совместимости со старыми телеприемниками. Два канала цвета передавались поочередно в частотной модуляции на частотах 4.25 и 4.406МГц. И наконец, еще выше по частоте отдельно передавался звук, также в частотной модуляции.

Кстати, с приемом ТВ в Петербурге есть забавный момент. Как отрапортовали российские СМИ, аналоговое ТВ было отключено еще в октябре:

Принимаем и декодируем аналоговое ТВ с помощью SDR и Python - 4

Однако это касается лишь государственных каналов, коммерческие никто не принуждает отключать свое вещание. По крайней мере, на момент написания статьи (декабрь 2019) примерно 5-6 каналов еще доступны в «аналоге» прямо в центре Питера. Но сколько это продлится, неизвестно, так что желающим записать «для истории» образцы сигналов наверное все-таки стоит поторопиться.

Наконец, настала пора включить SDR и посмотреть, что мы имеем в реале:

Принимаем и декодируем аналоговое ТВ с помощью SDR и Python - 5

Звуковой канал сложности не представляет, на него можно просто навестись «мышкой» в HDSDR, выбрать FM с шириной полосы порядка 50КГц и послушать. Мы начнем декодирование с канала яркостного, это позволит нам получить готовую «картинку».

Декодирование

Как было написано выше, сигналы яркости передаются в АМ. Чтобы не писать декодер самостоятельно, воспользуемся GNU Radio — перенесем спектр на нулевую частоту, запустим АМ-декодер и сохраним результат в файл.

Принимаем и декодируем аналоговое ТВ с помощью SDR и Python - 6

Теперь мы можем открыть сохраненный файл в Python:

import numpy as np
import matplotlib.pyplot as plt


lum_data = np.fromfile("pal_lum.raw", dtype='int32')
lum_data = -lum_data - 4700

fs = 9000000//2
x_time = np.linspace(0, len(lum_data)/fs, num=len(lum_data))
plt.plot(x_time, lum_data)

Мы видим на экране последовательность из 4х кадров.

Принимаем и декодируем аналоговое ТВ с помощью SDR и Python - 7

Длина одного кадра 0.02с — это как раз 1/50 — кратно частоте сети 50Гц, сигналы которой служат в качестве «тактового генератора» (не забываем, что сигнал аналоговый). За каждый кадр передается 320 строк — развертка у нас черезстрочная, так что итоговая частота кадров составляет 25Гц.

Посмотрим отдельные строки подробнее:

Принимаем и декодируем аналоговое ТВ с помощью SDR и Python - 8

Как можно видеть, началу каждой строки соответствует «синхроимпульс», затем размах сигнала соответствует текущим значениям яркости в данной строке. Все довольно просто, и вероятно, практически без изменений такой сигнал и подавался на электронно-лучевую трубку ТВ.

Остальное дело техники. Создаем в памяти изображение и копируем в него два кадра, т.к. развертка у нас черезстрочная. Размах сигнала не превышает +200, что позволяет нам записать эти значения напрямую как цвета RGB.

# Output image
frame_size = fs*1//50

img_x, img_y = 320, 650
img_size = (img_y, img_x, 3)
img_data = np.zeros(img_size, dtype=np.uint8)
img_data.fill(255)

frame_num = 0
# Frame #1
pos_x, pos_y = 0, 0
for px in range(frame_num*frame_size, (frame_num+1)*frame_size):
    val = lum_data[px]

    if val < 0: val = 0
    if val > 255: val = 255
    img_data[pos_y][pos_x] = (0, val, 0)
    pos_x += 1
    if lum_data[px] <= 0 and lum_data[px+1] > 0:
        pos_x = 0
        pos_y += 2

print("Scan lines 1:", pos_y)

# Frame #2
pos_x, pos_y = 0, 0
for px in range((frame_num+1)*frame_size, (frame_num+2)*frame_size):
    val = lum_data[px]

    if val < 0: val = 0
    if val > 255: val = 255
    img_data[pos_y+1][pos_x] = (0, val, 0)
    pos_x += 1
    if lum_data[px] <= 0 and lum_data[px+1] > 0:
        pos_x = 0
        pos_y += 2

img_resized = cv2.resize(img_data, dsize=(3*img_x, img_y), interpolation=cv2.INTER_CUBIC)
plt.imshow(img_resized, interpolation='nearest')

Как можно видеть, я использую переход через ноль для детектирования начала новой строки. Картинка оказалась сжатой по вертикали, это зависит в данном случае от частоты дискретизации SDR, в итоге я просто сделал ресайз в ширину.

Окончательный результат на анимации из 10 кадров (больше не принимает файловый архив хабра):

Принимаем и декодируем аналоговое ТВ с помощью SDR и Python - 9

Заключение

Подобные стандарты интересно анализировать, т.к. во-первых, они довольно-таки простые для реализации, во-вторых, их изучение представляет еще и отчасти исторический интерес. Разумеется, цели сделать полноценный софтовый ТВ-тюнер у меня не было, так что код приведен в минимально-работоспособном виде.

Если оценки статьи будут положительны, во второй части можно будет рассмотреть работу с цветом, и вывести полноценную цветную картинку.

Для желающих поэкспериментировать самостоятельно, IQ-файл можно скачать по ссылке.

Всем удачных экспериментов.

Автор: DmitrySpb79

Источник

* - обязательные к заполнению поля


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js