Сеанс передачи видео звуком через воду с разоблачением

«Господь всемогущий! Кажется я только что убил мистера Мэя! … Но как бы то ни было, продолжим» © Дж. Кларксон

В этой статье я расскажу, как передать видео (ну, почти видео) при помощи звука через воду, используя обычный ноутбук, кусок провода, два джека 3.5 мм и две пьезо пищалки. А так же объясню почему и как это работает, расскажу забавную историю про то, как мы это придумали. А в качестве вишенки на торт, к статье прилагается проект на C# с исходниками, чтобы все, кому интересно, сами могли попробовать, ведь научное знание проверяемо, не так ли?


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

Подводный GPS с нуля за год
Подводный GPS: продолжение
Навигация под водой: пеленгуй — не пеленгуй, обречен ты на успех
К вопросу о влиянии цианобактерий на речевые функции президента

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

Предельные значения скоростей для самых современных гидроакустических модемов находятся очень далеко от того, чтобы ими можно было передавать видео. Насколько мне известно, рекорд принадлежит компании EvoLogics и составляет 62.5 kbps с заявленной максимальной дистанцией 300 метров. Более того, слова о невозможности передачи видео звуком через воду (на разумные дистанции) как раз и принадлежат Константину Георгиевичу, основателю и руководителю EvoLogics.

В бытность мою научным сотрудником НИИ Гидросвязи, тогда еще абсолютно несмышленым, мне хотелось великих свершений, побед на севере и на юге, большого разрыхления почв, (нет, мне и сейчас их хочется, но тогда я совсем был не отягощен багажом опыта и знаний и все казалось почти магическим и сказочным). В нашем тогдашнем коллективе (часть которого является моим настоящим) мы часто фантазировали на тему каких-нибудь нереальных гидроакустических проектов, рылись на свалке складе и пробовали использовать подряд всякие артефакты великой древней цивилизации, частью которой и является тот НИИ, попутно пытаясь постичь дао гидроакустической связи.

Погружение в те воспоминания вызывает во мне противоречивые чувства. Тогда казалось, ничто и никто не могло нас остановить: мы выбили у директора китайский фрезерный станок для макетирования изделий, собирали нормобарические корпуса из голландских водопроводных труб Van De Lande, производителю которых даже писали письмо на тему: «А не проверяли ли вы случайно, какое внешнее давление выдерживают ваши трубы?». За собственные средства собирали макеты в контейнерах для завтраков и втайне от руководства самовольно выезжали на испытания, по коллегам и родственникам собирая ледобуры, санки, даже в складчину покупали китайскую ПВХ лодку в Ашане. Оглядываясь назад, я чувствую как сердце мое наполняется ужасом, ностальгией и трепетом.

Справедливости ради стоит отметить, что все это время мы получали огромную поддержку от некоторых наших руководителей — словом и делом, а в последствие все наши поделки легализовались в ОКР (имеется в виду Опытно-Конструкторская Работа, а не Обсессивно-Компульсивное Расстройство), который даже был представлен на международном военно-морском салоне в 2013 году. Да-да, мы возили на салон наши водопроводные трубы, выкрашенные StDmitirev собственноручно в ярко-оранжевый! Вот они, в чемоданчиках:

-7jkii7tkq9yhpbwuvb1dw6ah2g.jpeg

Как-то раз мой друг и коллега StDmitirev в разгар беседы о спектрах и спекрограммах произнес сокраментальную фразу:

«А вот, было бы прикольно сделать такую систему: подводник сидит в подводной лодке и смотрит в монитор, на котором плавно движется спектрограмма, на которой как пальцем другого подводника по запотевшему окну другой подводной лодки написаны буквы и цифры».

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

Сейчас уже сложно вспомнить (дело было в далеком 2012 году). В моем распоряжении был рабочий компьютер с веб-камерой, разные артефакты-антенны и специальное «ведро гидроакустическое повышающее» (ВГ-1-П) с водой. Повышающим его назвали из-за того, что я всякому начальству показывал в нем работу разных макетов оборудования, что привело к моему повышению до старшего научного сотрудника.

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

Итак, рассказываю, как на духу — как передать видео через воду:

Как сформировать сигнал?


Мы помним, что идея основывается на «рисовании на спектрограмме», то есть, передаваемая картинка — это есть спектрограмма сигнала. Для преобразования сигнала из временной области в частотную и обратно удобно применять (ну, например) преобразование Фурье, точнее быстрое преобразование Фурье, для краткости называемое БПФ или, что более привычно FFT (Fast Fourier Transform).

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

l4wi3t5t4mk3vq5zo_-bmau-dne.png

Допустим, что размер окна FFT у нас равен N и есть массив размером N. Если считать его спектром сигнала, то его нулевой элемент соответствует нулевой частоте (постоянка), а отсчет с индексом N-1 соответствует частоте дискретизации Sample Rate. Нужно выбрать такие размеры кадра изображения и размер окна FFT, чтобы с одной стороны все это хоть как-то было похоже на видео (передача одного кадра занимала бы разумное время), а с другой — используемая полоса частот была адекватной в принципе и адекватной доступному оборудованию. Теперь, если с какого-нибудь понравившегося отсчета мы (снизу вверх на схеме) впишем значения яркостей столбца картинки (Frame coloumn), а потом выполним обратное FFT, то на выходе получим сигнал, кодирующий один столбец изображения. Теперь нам остается таким же образом сформировать сигналы для остальных столбцов изображения и поочередно излучить их при помощи звуковой карты.

Стоит отметить что FFT на выходе дает массив комплексных значений, так вот наш сигнал — это вещественная часть. Само собой, получившийся сигнал по столбцам приводится к 16-ти битным знаковым целым (в таком виде обычно хранят цифровой звуковой сигнал) и нормализуется.

На самом деле в начале картинки я еще вписываю несколько столбцов максимальной яркости, в последствии, на стороне приемника это позволит определить АЧХ приемопередающего тракта (и канала передачи), которая будучи инвертирована и немного сглажена поможет нам улучшить принимаемый кадр.

На мой взгляд проще всего продемонстрировать устройство передатчика куском кода, вот он (метод Encode класса Encoder):

public double[] Encode(Bitmap source)
        {
            Bitmap frame;

            if (source.PixelFormat != System.Drawing.Imaging.PixelFormat.Format8bppIndexed)
                frame = Grayscale.CommonAlgorithms.RMY.Apply(source);
            else
                frame = source;

            if (!frame.Size.Equals(frameSize))
                frame = resizer.Apply(frame);       

            double[] samples = new double[fftSize * frameSize.Width];
            alglib.complex[] slice = new alglib.complex[fftSize];
            double maxSlice;
            int sampleIndex = 0;

            int colsCount = frameSize.Width;
            int startRow = startLine;
            int endRow = startRow + frameSize.Height;

            for (int x = 0; x < colsCount; x++)
            {
                for (int y = startRow; y < endRow; y++)
                    slice[y].x = (frame.GetPixel(x, frameSize.Height - (y - startRow) - 1).R / 255.0) * short.MaxValue;

                for (int y = 0; y < fftSize; y++)
                    slice[y].x *= randomizerMask[y];

                alglib.fftc1dinv(ref slice);

                maxSlice = double.MinValue;
                for (int y = 0; y < slice.Length; y++)
                    if (Math.Abs(slice[y].x) > maxSlice)
                        maxSlice = Math.Abs(slice[y].x);
                
                for (int i = 0; i < slice.Length; i++)
                {
                    samples[sampleIndex] = (short)Math.Round(slice[i].x * short.MaxValue / maxSlice);
                    sampleIndex++;
                }    
            }

            return samples;
        }

Код, естественно, ни на что не претендует и писался впопыхах чисто для демонстрации.

Так что на счет скорости передачи?


И как ее оценить? Нам удалось (со зла не со зла) около двух месяцев сохранять интригу и некоторые наши старшие товарищи и руководители в свободное время умудрялись исписывать кучу бумаги, прикидывая как могла получиться ТАКАЯ бешеная скорость передачи.

Например, если частота дискретизации 96 кГц, а размер окна FFT мы примем равным 512, на вход передатчику будем подавать картинки размером 120×120 пикселей (8 бит на пиксель), то время, которое потребуется для передачи одного кадра изображения составит:

120×512/96000 = 0.64 секунды

Битовая скорость вроде бы должна составить:

120×120*8 / 0.64 = 180 000 бит в секунду!

Сын директора в то время восторгался — да это же можно уже Интернет-протоколы использовать! Это же прорыв!

Как я покажу ниже, очень легко попасть в такое заблуждение. Что же тут не так? Ведь все так просто и изящно!
На самом же деле подобный расчет скорости неприменим к данному методу, так же, как например, он неприменим к аналоговому телевизионному сигналу, сколько там бит на пиксель? =) А как на счет простейшего детекторного приемника? =))
Описанный метод передачи по сути АНАЛОГОВЫЙ и понятия «бит» и «пиксель» к нему не применимы — можно в той же картинке, теоретически, взять не 8 бит на яркость пикселя, а 16 и «скорость» автоматически возрастет двукратно.

Самое время показать самые первые результаты нашего «прорыва»:

a5jipvfgywxfxpy7p9jwl34v-ze.gif

Картинка выше была получена нами зимой 2012 года на реке Пичуга. Дистанция передачи составила 700 метров. Да, увы, мой дорогой читатель, это совсем не HD и даже не тянет на самый позорный CamRip. Не помню уже кто, но кто-то очень точно подметил, что все наши «видео» похожи на передачу сигналов о помощи с погибающей планеты.

Что примечательно, с натяжкой этот можно характеризовать как некое подобие OFDM — данные передаются на ортогональных поднесущих, что означает хорошую устойчивость к тональным и другим узкополосным помехам — в этом случае искажаются отдельные «строки» картинки. Импульсная же помеха — наоборот, искажает один или группу столбцов. Характерная «полосатость» картинок вызвана т.н. частотно-селективным замиранием вследствие многолучевого распространения, но об этом я расскажу как-нибудь в другой раз.

Как устроен приемник?


Сразу оговорюсь, что для того, чтобы попробовать этот метод в ведре или даже в небольшом бассейне, будет вполне достаточно двух часовых пьез (такие круглые) с припаянным к ним разъемом для звуковой карты. Для передатчика можно взять достаточно длинный (2–3–4–5 метров) и неэкранированный кабель, загерметизировав сам пьезоэлемент цапон-лаком или небольшим слоем герметика — на несколько раз точно хватит. Получившуюся гидроакустическую антенну (не, ну, а что?) вставляем в разъем для наушников.

На фото внизу разные пьезы, оказавшиеся под рукой на момент написания статьи. Все показанные пьезоэлементы вполне годятся для «попробовать» и обычно есть в любом помойке радиомагазине. Пятак не обладает пьезоэффектом и присутствует на картинке для масштаба.

5vr6xqryjht0yd5tqz7sdvmbd3a.jpeg

Для приемника лучше взять экранированный микрофонный кабель с таким же разъемом и замазанной герметиком или лаком пьезой на конце. Эту антенну вставляем в разъем для микрофона.

Для экспериментов на водоеме в качестве передатчика лучше взять какое-нибудь пьезокольцо и подавать на него усиленный (усилителя на TDA2030 с правильно намотанным трансформатором хватит на несколько сотен метров в хорошем водоеме или можно намотать ещё 5 витков) сигнал. Для приемника в этом случае тоже потребуется предусилитель и желательно полосовой фильтр. Если читателям будет интересно более подробно узнать про это — скажите об этом в комментариях и мы постараемся сделать статью, посвященную созданию усилителей мощности, предусилителей и антенн для гидроакустической связи.

Итак, вернемся к приемнику, точнее к его софтовой части


Самое главное в связи — это синхронизация и определение наличия полезного сигнала. В нашем примере детектирование производится по энергии в полосе: определяются места где она резко возрастает (начало кадра) и где резко спадает (конец кадра), с условием что от фронта до спада должно быть не меньше длительности кадра.

При всей своей простоте работает на удивление неплохо.
Данные со звуковой карты собираются по FFTSize отсчетов, над ними сразу выполняется FFT и в виде отдельных «слайсов» они хранятся, дожидаясь того момента, когда будут обработаны процедурой поиска, вот ее код (метод Search в классе Receiver):

private void Search()
        {
            int sliceIndex = 0;
            int frameWidth = encoder.FrameSize.Width;
            int minSlicesToSearch = Convert.ToInt32((frameWidth + 5) * 2);
            int sliceSize = encoder.FFTSize;
            double weight;
            int lastRisePosition = 0;
            int prevRisePosition = 0;
            
            while ((slices.Count > minSlicesToSearch) && (sliceIndex < slices.Count))
            {
                weight = 0.0;
                for (int i = 0; i < sliceSize; i++)
                    weight += Math.Abs(slices[sliceIndex][i]);

                double ratio = weight / previousWeight;

                if ((ratio >= risePeekRatio) && (sliceIndex - prevRisePosition > frameWidth))
                {
                    prevRisePosition = lastRisePosition;
                    lastRisePosition = sliceIndex;

                    if (lastRisePosition + (frameWidth + 5) < slices.Count)
                    {
                        double[][] samples = new double[frameWidth + 5][];
                        for (int i = 0; i < frameWidth + 5; i++)
                        {
                            samples[i] = new double[sliceSize];
                            Array.Copy(slices[lastRisePosition + i], samples[i], sliceSize);
                        }

                        slices.RemoveRange(0, sliceIndex);
                        lastRisePosition = 0;

                        if (FrameReceived != null)
                            FrameReceived(this, new FrameReceivedEventArgs(encoder.DecodeEx(samples, 5)));                            

                        lastRisePosition = sliceIndex;
                    }
                    
                }

                sliceIndex++;
                previousWeight = weight;
            }

            Interlocked.Decrement(ref isSearching);
        }

А вот кусок кода, который отвечает за декодирование картинки (Encoder.DecodeEx):

public Bitmap Decode(double[] samples, int measureCols)
        {
            int colCount = samples.Length / fftSize;
            if (colCount == frameSize.Width + measureCols)
            {
                int rowCount = frameSize.Height;
                Bitmap temp = new Bitmap(colCount, rowCount);

                double[] slice = new double[fftSize];
                alglib.complex[] sliceC = new alglib.complex[fftSize];
                int samplesCount = 0;
                byte component;

                int decodeStart = startLine;
                int decodeEnd = startLine + rowCount;

                double maxSlice;

                for (int x = 0; x < colCount; x++)
                {
                    for (int y = 0; y < fftSize; y++)
                    {
                        slice[y] = samples[samplesCount];
                        samplesCount++;
                    }

                    alglib.fftr1d(slice, out sliceC);

                    maxSlice = double.MinValue;
                    for (int y = decodeStart; y < decodeEnd; y++)
                        if (alglib.math.abscomplex(sliceC[y].x) > maxSlice)
                            maxSlice = alglib.math.abscomplex(sliceC[y].x);

                    int offset = temp.Height + decodeStart - 1;

                    for (int y = decodeStart; y < decodeEnd; y++)
                    {
                        component = (byte)(255.0 * alglib.math.abscomplex(sliceC[y].x) / maxSlice);
                        temp.SetPixel(x, offset - y, Color.FromArgb(component, component, component));
                    }
                }
                return temp;

            }
            else
            {
                throw new ApplicationException("Specified array length error");
            }
        }

А сейчас предлагаю посмотреть на результаты экспериментов по передаче «видео», проводившихся в разное время в разных водоемах.
Обе картинки (ниже) были записаны на международном военно-морском салоне в СПб в 2013 году на нашем (тогда) стенде через два ноутбука и аквариум.
Разобрать, что написано на бейдже не представляется возможным

xpwxsqvxyyqwrzrjh0n5wr0tm-q.gif

w79yzbz7caic4fre7xftuh_tq00.gif

А вот два «видео» записанных нами в одном из заливов Ладожского Озера в Карелии, они являются своего рода рекордом для данного метода (просто дальше мы никогда не пробовали и вряд ли будем) — первое из них получено на дистанции 500, а второе аж 1000 метров:

Передача видео через воду, дистанция 500 м (файл 8.7 мБ)

ekc_svi18z7oupqlmckhziofpy8.gif

Поскольку «видео» писалось в реальном времени при помощи веб-камеры, то в кадр попадали разные странные вещи. Будет очень интересно, если кто-нибудь угадает и напишет в комментарии, что находится на заднем плане в последнем «видео»).

В подтверждение того, что метод давным-давно опубликован — наша статья аж за 2013 год

Для захвата изображения с веб-камеры я использовал замечательную библиотеку AForge.

Функции работы с комплексными числами и FFT используются из прекрасной библиотеки AlgLib.

И, как я и обещал, весь проект на C# (VS2012) прилагается к статье в качестве материала для «домашней» работы. Для удобства отдельно лежит проект и бинарные файлы.
В демке предусмотрена возможность изменения (перемещения) занимаемой полосы частот, а также гамма-коррекция выходного кадра (все можно менять в реальном времени).

P.S.


Я давно не брал в руки C# и очень сложно найти время в рабочем графике, поэтому заранее извиняюсь за сумбурность и поспешность кода.

P.P. S.


Кусок провода, два джека и две пьезы к статье не прикладываю — на всех не хватит.

Errata и Appendix


— в некоторых звуковых картах на входе есть ФНЧ который трагически обрезает все выше ~15 кГц (зачем???)

— по умолчанию демо-проект работает с частотой дискретизации 96 кГц, но не все современные звуковые карты ее поддерживают (Почему???). Если оборудование не может 96 кГц то нужно установить в настройках 48 кГц, если нет, то 44100 уж точно поддерживается везде, однако, длительность передачи одного кадра будет соответственно больше

Вот список ноутбуков и звуковых карт которые можно считать оборудованием юного гидроакустика:

  • Lenovo ideapad Y510P со звуком JBL
  • Asus N55S
  • Asus K501U
  • внешняя звуковая карта Sound Blaster X-Fi Surround 5.1 (model no. SB 1095)

© Geektimes