Всё об устройстве и работе SSTV с примерами на Python

от автора

SSTV (slow-scan television) — телевидение с медленной разверткой, узкополосный формат передачи данных, позволяющий передавать изображения через эфир. В этой статье будут рассмотрены подробности кодирования, декодирования SSTV-сигнала.

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

Введение

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

Технология SSTV базируется на схожих с классическим телесигналом принципах.

Телевидение, как таковое, подразумевает, что происходит передача изображения; в частности в обычном телесигнале изображение передается покадрово, по 25 кадров в секунду.

Каждый кадр, в свою очередь, передается построчно. В строках содержится информация о яркости цветовых каналов (в частности яркость и цветоразностный сигнал при передаче YCbCr).

Для передачи всей этой информации требуется достаточно широкая полоса пропускания сигнала, а также высокочастотное декодирование. К примеру, для SECAM необходима полоса шириной 6.5 МГц.

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

Необходимо упомянуть, что технология SSTV использовалась на заре освоения космоса, когда технологии еще не позволяли передавать и обрабатывать широкополосные сигналы; например фотография обратной (темной) стороны Луны была передана посредством телевидения с медленной разверткой.

С тех пор принципы SSTV не претерпели фундаментальных изменений, изменился формат и способы кодирования строк и цветности.

На момент написания статьи технология активно используется радиолюбителями.

Области применения

SSTV в основном используется радиолюбителями, когда романтика телеграфной и телефонной передачи надоедает и хочется попробовать чего-то нового и необычного; например попытаться увидеться с корреспондентом, находящегося за тысячи километров по ту сторону радиоэфира, либо же отправить ему через эфир свою QSL-карточку (открытка с рапортом, диапазоном и позывными, подтверждающая факт проведения связи в эфире). Также популярен  прием изображений с Международной Космической Станции на диапазоне 2м (144 МГц); передачи, как правило, приурочены к праздничным и памятным событиям.

Также, технология SSTV может быть использована как средство передачи данных последнего шанса, в экстренных ситуациях, когда, например, необходимо передать карты, схемы, фотографии, и т.д., а использование других каналов связи сильно затруднено и/или невозможно.

Принципы работы SSTV

В основе формирования SSTV сигнала лежит использование FSK (Frequency Shift Keying) — частотной манипуляции. Данные определяются тоном (частотой) сигнала, меняющегося во времени; при этом частотная манипуляция обладает свойствами помехоустойчивой, т.к. помехи влияют на амплитуду несущего сигнала, а не на его частоту.

Кодирование цвета

При кодировании изображения, последнее разбивается на отдельные цветовые каналы яркости цвета или цветоразностного сигнала. Далее, в зависимости от яркости канала формируется тон, высота которого пропорционально яркости канала; чем ярче — тем выше тон.

В SSTV яркость канала разбивается на 255 значений, соответственно одна цветовая строка будет содержать в себе тональности с шагом, кратным 1/255. Например широкополосный формат SSTV имеет диапазон тонов от 1.5 КГц до 2.3 КГц, таким образом, получается диапазон в 800 Гц (2300 — 1500 = 800) и 3.137 Гц (800 / 255 = 3.137) на одну ступень яркости.

В случае узкополосных форматов, диапазон частот составляет 256 Гц (от 2.044 КГц до 2.3 КГц), соответственно одному шагу яркости соответствует 1 Гц.

Рисунок 1: Соотношение частоты яркости сигнала.

На рисунке 1 приведен наглядный пример соотношения частот и яркости канала.

Структура сигнала

Сигнал SSTV состоит из:

  1. заголовка, по которому приемник определяет начало и формат передаваемого сигнала, относительно которого будет происходить декодирование строк;

  2. импульса синхронизации строки;

  3. последовательно закодированных строк изображения.

Заголовок сигнала

Заголовок сигнала в свою очередь состоит из серии калибровочных импульсов и VIS-кода.

Рисунок 2: Структура калибровочных импульсов.

Сигнал VIS (Vertical Interval Signaling) включает в себя стартовый импульс, кодовые биты, определяющие формат, завершающийся стоп-битом.
На рисунке 3 приведена структура VIS-сигнала.

Рисунок 3: Структура VIS-кода.

Как видно из рисунка, сигнал формируется сначала из двух тонов 1.9 КГц, длительностью по 0.3 сек, разделенные тоном в 1.2 КГц, длительностью 0.01 сек.

Далее кодируются последовательность из 8 бит, определяющая формат видеосигнала. Нулю соответствует тон в 1.3 КГц, единице 1.1 КГц.

В зависимости от типа сигнала, узкополосный или широкополосный, длительность бита равняется 0.022 сек и 0.03 сек соответственно.

Биты VIS-кода определяют тип формата и его свойства

Таблица 1: Расшифровка VIS-кода.

MSB

LSB

Значение

P

6

5

4

3

2

1

0

0

0

Цветное видео

0

1

ЧБ, красный канал

1

0

ЧБ, зеленый канал

1

1

ЧБ, синий канал

0

Ширина кадра 128/160 пикселей

1

Ширина кадра 256/320 пикселей

0

Высота кадра 128/120 строк

1

Высота кадра 256/240 строк

0

0

0

Признак формата Robot

1

0

0

Признак форматов AVT, Scottie DX

1

1

Признак формата PD

*

Бит четности

Первые 4 бита определяют цветовые свойства формата, следующие 3 бита унифицируют формат.

Последний 8-й бит — бит четности, который принимает значение 1, если количество информационных бит нечетное.

На данный момент предел в 8 бит уже практически исчерпан и в некоторых форматах этот код расширен до 16 бит.

Примеры кодов некоторых форматов:
Martin 1: 00101100
Robot 36: 00001000
PD50: 01011101
Scottie DX: 01001100

Примеры 16-и битных кодов:
MMSSTV ML180: 0000010100100011
MMSSTV MP320: 0000101000100011

Кодирование строк

При кодировании строк, исходное изображение раскладывается на каналы цветности.

У черно-белого изображения — это канал яркости. Цветные изображения состоят из компонентов красного, зеленого и синих цветов (RGB), либо из яркости и цветоразностных составляющих по синему и красному цветам.

В случае передачи изображения в формате RGB последовательно передается каждый цветовой канал (могут передаваться как в порядке R, G, B, так и B, G, R).

В зависимости от формата, под каждый канал может быть определено разное время передачи. Так, например, в формате Robot выделено вдвое меньшее время, чем под канал яркости; таким образом, при потере цветности, изображение будет черно-белым.

Рисунок 4: Общий вид кодирования строки формата Robot.

Определение начала строки осуществляется добавлением в сигнал импульсов синхронизации. В большинстве форматов импульс синхронизации добавляется перед началом строки.

Каналы цветности также могут быть отделены импульсами синхронизации. Так, например, на рисунке 4 показана структура формата Robot, между каналами Y и Cb (и между каналами Cb и Cr) расположен сигнал длительностью 4 миллисекунды.

Каждый формат кодирования изображения может иметь свой порядок синхронизации строк и каналов; например в семействе форматов Martin каналы передаются в формате GBR, при этом каждый канал отделен от другого импульсом; в семействе MMSSTV каналы передаются неразрывно, а формат AVT вообще не имеет синхроимпульсов в строке.

Узкополосный SSTV

Узкополосные (narrow) форматы SSTV отличаются от широкополосных (wide) тем, что имеют полосу частот для передачи изображения в 256 Гц (от 2.04 КГц до 2.3 КГц).

Рисунок 5: Заголовок узкополосного SSTV.

Перед передачей VIS-кода формируется заголовок длительностью 400 миллисекунд, содержащий в себе 2 пары тонов в 1.9 КГц и 2.3 КГц, длительностью 100 миллисекунд каждый. По этим сигналам на принимающей стороне происходит определение типа передачи.

Следующей особенностью узкополосного формата является способ формирования VIS-кода. Код записывается 4-я группами по 6 бит каждая. Кодируемые значения представлены в таблице 2. Дополнительная информация о свойствах формата в биты VIS-кода не вносится.

Таблица 2: группы данных VIS-кода узкополосного SSTV

Группа

Биты

1

101101

2

010101

3

код_формата

4

010101 xor код_формата

VIS-код для узкополосного формата MMSSTV MP73-N (идентификатор формата равен значению 2) имеет вид 101101-010101-000010-010111

В остальном принципы передачи изображения идентичны широкополосным SSTV.

Форматы SSTV

Перечень наиболее распространенных широкополосных SSTV-форматов используемых в любительской радиосвязи.

Таблица 3: Wide SSTV форматы

Формат

Каналы

Время кадра

Высота

Ширина

Amiga Video Transceiver 90

RGB

98

240

320

Martin 1

GBR

114

256

320

Martin 2

GBR

58

256

320

MMSSTV MR73

YCbCr

73

256

320

MMSSTV MR90

YCbCr

90

256

320

MMSSTV MR115

YCbCr

115

256

320

MMSSTV MR140

YCbCr

140

256

320

MMSSTV MR175

YCbCr

175

256

320

MMSSTV MP73

YCbCr

73

256

320

MMSSTV MP115

YCbCr

115

256

320

MMSSTV MP140

YCbCr

140

256

320

MMSSTV MP175

YCbCr

175

256

320

MMSSTV ML180

YCbCr

180

256

320

MMSSTV ML240

YCbCr

240

256

320

MMSSTV ML280

YCbCr

280

256

320

MMSSTV ML320

YCbCr

320

256

320

P3

RGB

203

496

640

P5

RGB

305

496

640

P7

RGB

406

496

640

PD50

YCbCr

50

256

320

PD90

YCbCr

90

256

320

PD120

YCbCr

126

496

640

PD160

YCbCr

161

400

512

PD180

YCbCr

187

496

640

PD240

YCbCr

248

496

640

PD290

YCbCr

289

616

800

Robot 24

YCbCr

24

240

320

Robot 36

YCbCr

36

240

320

Robot 72

YCbCr

72

240

320

Robot B&W 8

BW

8

120

160

Robot B&W 12

BW

12

120

160

SC2 60

RGB

62

256

320

SC2 120

RGB

122

256

320

SC2 180

RGB

182

256

320

Scottie 1

GBR

110

256

320

Scottie 2

GBR

71

256

320

Scottie DX

GBR

269

256

320

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

Таблица 4: Wide SSTV форматы

Формат

Каналы

Время кадра

Высота

Ширина

MMSSTV MP73-N

YCbCr

73

256

320

MMSSTV MP110-N

YCbCr

110

256

320

MMSSTV MP140-N

YCbCr

140

256

320

MMSSTV MC110-N

RGB

110

256

320

MMSSTV MC140-N

RGB

140

256

320

MMSSTV MC180-N

RGB

180

256

320

Как видно из таблиц 3 и 4, в большинстве форматов числовое значение в имени кода соответствует времени передачи кадра или очень близкое к нему.

Кодирование SSTV сигнала

Пример кодирования изображения в SSTV сигнал на языке python.

Для работы примера потребуются библиотеки:

numpy~=2.2.3 pillow~=11.1.0 scipy~=1.15.2 

Установка через pip:

pip install numpy~=2.2.3 pillow~=11.1.0 scipy~=1.15.2 

В качестве примера взят формат Robot 72.

Параметры формата:

  • Ширина полосы: широкополосный (1.5-2.3 КГц)

  • Количество строк: 240

  • Размер строки: 320

  • Цветопередача: YCbCr

  • Время кадра: 72 секунды

Импорт:

import math import statistics import typing from itertools import chain  import numpy as np from PIL import Image from scipy.io.wavfile import write 

Объявление типов (опционально):

Signal = typing.List[float] SignalGen = typing.Generator[float, None, None] Color = int  Tone = typing.NamedTuple("Tone", [("freq", typing.Union[int, typing.Tuple[int, int]]), ("time", float)]) Channel = typing.NamedTuple("Channel", [("id", typing.Union[int, typing.Tuple[int, int]]), ("time", float)]) 

Частота дискретизации выходного звукового файла:

SAMPLE_RATE = 11025 

Вспомогательные функции:

def yield_tones(tones) -> SignalGen:    for tone in tones:        yield (tone.freq, tone.time)  FREQ_LOW = 1500 FREQ_HIGH = 2300  def color_to_freq(color: Color) -> float:    return color * (FREQ_HIGH - FREQ_LOW) / 255 + FREQ_LOW 

FREQ_LOW — нижняя частота кодирования цвета, FREQ_HIGH — верхняя соответственно.

Параметр color — значение яркости цветового канала, лежащее в диапазоне от 0 до 255.

Определение SSTV-заголовка:

HEADER_WIDE = [    Tone(1900, 0.100000),    Tone(1500, 0.100000),    Tone(1900, 0.100000),    Tone(1500, 0.100000),    Tone(2300, 0.100000),    Tone(1500, 0.100000),    Tone(2300, 0.100000),    Tone(1500, 0.100000), ]  def encode_header() -> SignalGen:    yield from yield_tones(HEADER_WIDE) 

HEADER_WIDE — последовательность тонов, согласно спецификации к формату, где первым параметром задается частота в герцах, а вторым — продолжительность сигнала в секундах (значение 0.100000 соответствует 100 миллисекундам).

Определение VIS-кода и калибровочного сигнала:

VIS_CODE = 12  BIT_1_WIDE_FREQ = 1100 BIT_0_WIDE_FREQ = 1300  VIS_WIDE_BIT_SIZE = 0.030000 VIS_BIT_TONE_WIDE = Tone((BIT_0_WIDE_FREQ, BIT_1_WIDE_FREQ), VIS_WIDE_BIT_SIZE) VIS_BIT_TONE_MEDIAN_WIDE = Tone(statistics.median([BIT_0_WIDE_FREQ, BIT_1_WIDE_FREQ]), VIS_WIDE_BIT_SIZE)  CALIBRATION_WIDE = [    Tone(1900, 0.300000),    Tone(1200, 0.010000),    Tone(1900, 0.300000),    Tone(1200, 0.030000), ]  def encode_vis() -> SignalGen:    fsk_len = 8    vis = VIS_CODE    vis |= (vis.bit_count() & 1) << fsk_len - 1  # Parity bit     yield from yield_tones(CALIBRATION_WIDE)     value = vis    for _ in range(fsk_len):        yield (VIS_BIT_TONE_WIDE.freq[value & 1], VIS_BIT_TONE_WIDE.time)        value >>= 1     yield (VIS_BIT_TONE_MEDIAN_WIDE.freq, VIS_BIT_TONE_MEDIAN_WIDE.time) 

Формату Robot 72 соответствует VIS-код со значением 12; количество бит, необходимых для кодирования VIS-кода, составляет 8 бит (значение переменной fsk_len).

Для кодирования бит используются частоты 1.1 КГц для логических единиц и 1.3 КГц для нулей соответственно (значения BIT_1_WIDE_FREQ, BIT_0_WIDE_FREQ).

Функция encode_vis определяет количество бит в VIS-коде и выравнивает четность исходного числа, выставляя 8 бит.

VIS_BIT_TONE_MEDIAN_WIDE — сигнал завершения блока с VIS-кодом.

Определение параметров кодирования изображения

COLOR = "YCbCr"  LINE_WIDTH = 320 LINE_COUNT = 240 

Формат Robot 72 использует цветоразностное кодирование. Размер кадра 320*240. Эти значения используются для кадрирования исходного изображения, а также для получения значения цветов каналов согласно цветовой схеме из COLOR.

Определение продолжительности импульсов синхронизации и длительности импульсов цветопередачи:

SCAN_TIME = 0.138000  PIXEL_TIME = SCAN_TIME / LINE_WIDTH HALF_SCAN_TIME = SCAN_TIME / 2 HALF_PIXEL_TIME = HALF_SCAN_TIME / LINE_WIDTH  SYNC_PULSE = 0.009000 SYNC_PORCH = 0.003000 SEP_PULSE = 0.004500 SEP_PORCH = 0.001500 

SCAN_TIME — длительность одной строки изображения. Исходя из этого определяется длительность сигнала на один пиксел изображения. Т.к. у формата Robot 72 значения обоих цветоразностных сигналов передаются за то же время, за которое передается строка с яркостью, необходимо определить половину времени на пиксел.

Значения длительности импульсов синхронизации и импульсов-разделителей взяты из спецификации к формату.

Определение структуры кодирования формата:

TIMING_SEQUENCE = [    Tone(1200, SYNC_PULSE),    Tone(1500, SYNC_PORCH),    Channel(0, PIXEL_TIME),    Tone(1500, SEP_PULSE),    Tone(1900, SEP_PORCH),    Channel(1, HALF_PIXEL_TIME),    Tone(1500, SEP_PULSE),    Tone(1900, SEP_PORCH),    Channel(2, HALF_PIXEL_TIME), ] 

Список TIMING_SEQUENCE описывает последовательность кодирования данных одной строки изображения. Перед кодированием первого канала записываются сигналы синхронизации (1.2 и 1.5 КГц). Перед последующими каналами записываются импульсы-разделители (1.5 и 1.9 КГц). Как можно заметить, для последующих каналов длительность пикселов определяется значением HALF_PIXEL_TIME.

Определение генераторной функции, переводящей изображение в последовательность тонов, согласно TIMING_SEQUENCE:

def encode_image_data(image) -> SignalGen:    height = LINE_COUNT    width = LINE_WIDTH     pixels = image.convert(COLOR).resize((width, height), Image.Resampling.LANCZOS).load()     y = 0    while y < height:        odd_line = y % 2         for tone in TIMING_SEQUENCE:            if isinstance(tone, Tone):                yield (freq, tone.time)             elif isinstance(tone, Channel):                for px in range(width):                    pixel = pixels[px, y]  # RGB order                    pixel = (pixel[0], pixel[2], pixel[1])  # YUV order                     yield (color_to_freq(pixel[_id]), tone.time)         y += 1 

Функция encode_image_data последовательно обходит по строкам в изображении и выполняет перевод каждого пиксела в строке согласно TIMING_SEQUENCE.

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

Определение функции преобразования изображения в набор сэмплов, готовых к записи в wav-файл:

def encode(image) -> SignalGen:    spms = SAMPLE_RATE / 1000    offset = 0    samples = 0    factor = math.pi * 2 / SAMPLE_RATE  # math.tau -- 2pi    sample = 0     generators = chain(        encode_header(),        encode_vis(),        encode_image_data(image),    )     for freq, sec in generators:        samples += spms * sec * 1000        tx = int(samples)        freq_factor = freq * factor         for sample in range(tx):            yield math.sin(math.fmod(sample * freq_factor + offset, math.tau))         offset += (sample + 1) * freq_factor        samples -= tx 

В функции encode формируется сигнал на основе функции синуса (sin), происходит расчет фазы и выдается последовательность дискретных значений от -1 до 1 с частотой дискретизации SAMPLE_RATE.

Открытие файла с изображением и сохранение результирующего сигнала в wav-файл:

with Image.open("color-bars.png") as im:    amplitude = np.iinfo(np.int16).max    tones = np.fromiter(encode(im), dtype=np.float32) * amplitude  write("robot72-example.wav", SAMPLE_RATE, tones.astype(np.int16)) 

Исходное изображение из файла color-bars.png и запись в целевой файл с именем robot72-example.wav.

В качестве тестового изображения можно воспользоваться настроечной ТВ-таблицей:

Рисунок 6: Настроечная телевизионная таблица.

Спектрограмма получившегося аудиофайла (общий вид):

Рисунок 7: Общий вид спектра сигнала.

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

Спектрограмма начала файла:

Рисунок 8: Заголовок SSTV, VIS-код и начальные строки SSTV-сигнала.

Здесь можно увидеть ярко выраженные тона синхронизации длительностью 0.1 сек, импульсы калибровки и биты VIS-кода. Далее идут тона, соответствующие данным цветовых каналов.

Спектрограмма каналов:

Рисунок 9: Спектр первых строк SSTV-сигнала.

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

Декодирование SSTV сигнала

Импорт:

import statistics import typing from functools import reduce  import numpy as np from PIL import Image from scipy.io.wavfile import read from scipy.signal.windows import hann 

Определение типов:

Signal = typing.List[float] SignalGen = typing.Generator[float, None, None] Color = int  Bit = typing.Literal[0, 1] BitGen = typing.Generator[Bit, None, None]  Tone = typing.NamedTuple("Tone", [("freq", typing.Union[int, typing.Tuple[int, int]]), ("time", float)]) ToneSlice = typing.Tuple[slice, float] ToneSlices = typing.List[ToneSlice] Channel = typing.NamedTuple("Channel", [("id", typing.Union[int, typing.Tuple[int, int]]), ("time", float)]) 

Вспомогательные функции для работы с сигналом:

def bits_to_int(bits: typing.List[Bit]) -> int:    return reduce(lambda value, bit: (value << 1) | (bit & 1), bits[::-1])  def barycentric_peak_interp(bins, x):    y1 = bins[x] if x <= 0 else bins[x - 1]    y3 = bins[x] if x + 1 >= len(bins) else bins[x + 1]     denom = y3 + bins[x] + y1    if denom == 0:        return 0     return (y3 - y1) / denom + x  def peak_fft_freq(signal: Signal, sample_rate: float) -> float:    windowed_data = signal * hann(len(signal))    fft = np.abs(np.fft.rfft(windowed_data))     # Get index of bin with the highest magnitude    x = np.argmax(fft)    # Interpolated peak frequency    peak = barycentric_peak_interp(fft, x)     # Return frequency in hz    return peak * sample_rate / len(windowed_data) 

bits_to_int — функция свертки списка битов в число;

barycentric_peak_interp — функция интерполяции барицентрическим полиномом;

peak_fft_freq — функция определения частоты с максимальной амплитудой в отрезке сигнала (используется преобразование Фурье с оконной функцией Хеннинга).

Определение параметров формата Robot 72:

SAMPLE_RATE = None  FREQ_LOW = 1500 FREQ_HIGH = 2300  FREQ_SYNC_PULSE = 1200 FREQ_SYNC_PORCH = 1500 FREQ_SYNC_MEDIAN = statistics.median([FREQ_SYNC_PULSE, FREQ_SYNC_PORCH])  WINDOW_FACTOR = 4.88  VIS_CODE = 12  BIT_1_WIDE_FREQ = 1100 BIT_0_WIDE_FREQ = 1300  VIS_WIDE_BIT_SIZE = 0.030000 VIS_BIT_TONE_WIDE = Tone((BIT_0_WIDE_FREQ, BIT_1_WIDE_FREQ), VIS_WIDE_BIT_SIZE) VIS_BIT_TONE_MEDIAN_WIDE = Tone(statistics.median([BIT_0_WIDE_FREQ, BIT_1_WIDE_FREQ]), VIS_WIDE_BIT_SIZE)  CALIBRATION_WIDE = [    Tone(1900, 0.300000),    Tone(1200, 0.010000),    Tone(1900, 0.300000),    Tone(1200, 0.030000), ]  COLOR = "YCbCr"  LINE_WIDTH = 320 LINE_COUNT = 240  SCAN_TIME = 0.138000  PIXEL_TIME = SCAN_TIME / LINE_WIDTH HALF_SCAN_TIME = SCAN_TIME / 2 HALF_PIXEL_TIME = HALF_SCAN_TIME / LINE_WIDTH  SYNC_PULSE = 0.009000 SYNC_PORCH = 0.003000 SEP_PULSE = 0.004500 SEP_PORCH = 0.001500  CHAN_TIME = SEP_PULSE + SCAN_TIME HALF_CHAN_TIME = SEP_PULSE + HALF_SCAN_TIME  CHANNELS = 3 CHAN_SYNC = 0  CHAN_OFFSETS = [SYNC_PULSE + SYNC_PORCH] CHAN_OFFSETS.append(CHAN_OFFSETS[0] + CHAN_TIME + SEP_PORCH) CHAN_OFFSETS.append(CHAN_OFFSETS[1] + HALF_CHAN_TIME + SEP_PORCH)  LINE_TIME = CHAN_OFFSETS[2] + HALF_SCAN_TIME 

Вспомогательные функции декодирования сигналов:

def freq_to_color(freq: float) -> Color:    lum = int(round((freq - FREQ_LOW) / ((FREQ_HIGH - FREQ_LOW) / 255)))    return min(max(lum, 0), 255)  def read_bits(signal: Signal,              bit_count: int, bit_time: float,              freq_true: float, freq_false: float,              sample_rate: float,              offset: int = 0) -> BitGen:    bit_threshold = statistics.median([freq_true, freq_false])    bit_size = round(bit_time * sample_rate)     for bit_idx in range(bit_count):        bit_offset = offset + bit_idx * bit_size        section = signal[bit_offset:bit_offset + bit_size]        freq = peak_fft_freq(section, sample_rate)        yield int(freq <= bit_threshold)  def tones_to_slices(tones: typing.Iterable[Tone],                    sample_rate: float,                    window_size: typing.Optional[float] = None) -> typing.Tuple[int, ToneSlices]:    # The margin of error created here will be negligible when decoding the    # vis due to each bit having a length of 30ms. We fix this error margin    # when decoding the image by aligning each sync pulse    slices = []    time_acc = 0    for it in tones:        area = slice(            round(time_acc * sample_rate),            round((time_acc + (window_size or it.time)) * sample_rate)        )        slices.append((area, it.freq))        time_acc += it.time     return (round(time_acc * sample_rate), slices)  def match_frequencies(signal: Signal, slices: typing.List[typing.Tuple[slice, float]],                      sample_rate: float, threshold: float = 50.0) -> bool:    # Check they're the correct frequencies    return all(abs(peak_fft_freq(signal[part], sample_rate) - freq) < threshold for part, freq in slices) 

freq_to_color — функция переводящая частоту тона в соответствующее значение цветового канала;

read_bits — функция переводящая последовательность тонов в последовательность бит;

tones_to_slices — утилитарная функция, отображение списка тонов в список слайсов сигнала;

match_frequencies — функция-предикат, определяющая соответствие сигнала указанному списку тонов, если удалось найти все тона в сигнале, возвращает истинно (параметр threshold — порог расхождения частот, по умолчанию 50 Гц).

Поиск заголовка SSTV-сигнала:

def find_header(        header: typing.Iterable[Tone],        signal: Signal,        threshold: float = 50.0,        stride_time: float = 0.002,        window_size: float = 0.010 ) -> typing.Optional[typing.Tuple[int, int]]:    stride_len = round(stride_time * SAMPLE_RATE)     header_size, slices = tones_to_slices(header, SAMPLE_RATE, window_size)     for curr_sample in range(0, len(signal), stride_len):        if curr_sample + header_size >= len(signal):            continue         search_area = signal[curr_sample:curr_sample + header_size]         if match_frequencies(search_area, slices, SAMPLE_RATE, threshold=threshold):            return curr_sample, header_size     return None 

Функция find_header последовательно проходит по сигналу signal и определяет вхождение последовательности тонов из параметра header, в качестве результата выдает кортеж с данными о том, на каком семпле был найден заголовок и какая его продолжительность.

Декодирование VIS-кода:

def decode_vis(signal: Signal, vis_start: int, bit_time: float = VIS_WIDE_BIT_SIZE) -> int:    """Decodes the vis from the audio data and returns the SSTV mode"""    bit_count = 8    vis_bits = list(        read_bits(            signal, bit_count, bit_time,            freq_true=BIT_1_WIDE_FREQ, freq_false=BIT_0_WIDE_FREQ,            sample_rate=SAMPLE_RATE, offset=vis_start        )    )     # Check for even parity in last bit    vis = vis_bits[:bit_count]    if sum(vis) % 2:        raise ValueError("Error decoding VIS header (invalid parity bit)")     vis_value = bits_to_int(vis[:-1])    if vis_value != VIS_CODE:        raise ValueError(f"SSTV mode is unsupported (VIS: {vis_value})")     return vis_value 

Функция decode_vis переводит последовательность тонов в нули и единицы, дополнительно проверяя бит четности. В рамках примера используется в демонстрационных целях, т.к. результат не используется, а только проверяется, что код соответствует формату Robot 72.

Синхронизация:

def align_sync(signal: Signal, align_start: int, start_of_sync: bool = True):    sync_window = round(SYNC_PULSE * 1.4 * SAMPLE_RATE)    align_stop = len(signal) - sync_window     if align_stop <= align_start:        return None     current_sample = align_start    for current_sample in range(align_start, align_stop):        search_section = signal[current_sample:current_sample + sync_window]         if peak_fft_freq(search_section, SAMPLE_RATE) > FREQ_SYNC_MEDIAN:            break     end_sync = current_sample + sync_window // 2     if start_of_sync:        return end_sync - round(SYNC_PULSE * SAMPLE_RATE)    else:        return end_sync 

Функция align_sync определяет точное нахождение импульса синхронизации в сигнале. Возвращаемое значение — номер сэмпла в исходном сигнале.

Алгоритм работы:

  1. выбрать часть данных из сигнала;

  2. найти в этом участке сигнала частоту, соответствующую частоте синхроимпульса.

Перевод данных строк в изображение:

def decode_image_data(signal: Signal, image_start: int) -> typing.List[typing.List[typing.List[int]]]:    image_data = [[[0 for _ in range(LINE_WIDTH)] for _ in range(CHANNELS)] for _ in range(LINE_COUNT)]     seq_start = image_start    for line in range(LINE_COUNT):        for chan in range(CHANNELS):            if chan == CHAN_SYNC:                if line > 0 or chan > 0:                    # Set base offset to the next line                    seq_start += round(LINE_TIME * SAMPLE_RATE)                 # Align to start of sync pulse                seq_start = align_sync(signal, seq_start)                if seq_start is None:                    return image_data             pixel_time = PIXEL_TIME             if chan > 0:                pixel_time = HALF_PIXEL_TIME             centre_window_time = (pixel_time * WINDOW_FACTOR) / 2            pixel_window = round(centre_window_time * 2 * SAMPLE_RATE)             for px in range(LINE_WIDTH):                chan_offset = CHAN_OFFSETS[chan]                 px_pos = round(seq_start + (chan_offset + px * pixel_time - centre_window_time) * SAMPLE_RATE)                px_end = px_pos + pixel_window                 if px_end >= len(signal):                    return image_data                 pixel_area = signal[px_pos:px_end]                freq = peak_fft_freq(pixel_area, SAMPLE_RATE)                 image_data[line][chan][px] = freq_to_color(freq)     return image_data  def draw_image(image_data: typing.List[typing.List[typing.List[int]]]) -> Image:    image = Image.new(COLOR, (LINE_WIDTH, LINE_COUNT))    pixel_data = image.load()     for y in range(LINE_COUNT):        for x in range(LINE_WIDTH):            pixel_data[x, y] = (image_data[y][0][x], image_data[y][2][x], image_data[y][1][x])     image = image.convert("RGB")    return image 

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

Функция draw_image преобразует данные из decode_image_data в финальное графическое изображение в формате RGB.

Значение параметра WINDOW_FACTOR подбирается эмпирически.

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

def decode(signal: Signal) -> Image:    if not (header := find_header(CALIBRATION_WIDE, signal)):        return None     hdr_start, hdr_len = header    hdr_end = hdr_start + hdr_len     print("Header start:", hdr_start)    print("Header end:", hdr_end)     bit_time = VIS_WIDE_BIT_SIZE     vis = decode_vis(signal, hdr_end)     print("VIS code:", vis)     if vis != VIS_CODE:        raise ValueError(f"Unsupported VIS code {vis}")     bit_len = 8 + 1  # 8 bits + 1 stub    vis_len = bit_time * bit_len * SAMPLE_RATE    vis_end = hdr_start + vis_len + hdr_len     image_data = decode_image_data(signal, round(vis_end))     return draw_image(image_data)  if __name__ == '__main__':    SAMPLE_RATE, signal = read("examples/robot72-example.wav")     img = decode(signal)    img.save("examples/robot72-example-out.png") 

Фцнкция decode собирает в себя всю вышеописанную логику воедино: поиск SSTV-заголовка, извлечение VIS-кода и декодирование сигнала в картинку.

Рисунок 10: изображение, полученное путем декодирования исходного сигнала.

На рисунке 10 приведен результат декодирования SSTV сигнала в формате Robot 72.

Примеры изображений

SSTV-сигналы, принятые R9FEU на диапазоне 20м.

Заключение

В статье был рассмотрен формат передачи изображений SSTV, принципы его работы, разобраны механизмы кодирования и декодирования сигнала на примере формата Robot 72.

Ссылки

  1. Исходный код кодировщика Robot 72

  2. Исходный код декодера Robot 72

  3. Исходный код родительской программы, с которой были сформированы примеры

  4. Исходный код программы, на базе которой были реализованы остальные форматы

  5. Основной опорный документ по SSTV

  6. Исходный код программы MMSSTV

  7. Галерея изображений


ссылка на оригинал статьи https://habr.com/ru/articles/914448/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *