Привет, глубокоуважаемые!



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

Если в вашей душе от этих слов что-то колыхнулось, добро пожаловать под кат, в темные воды нашего пруда!




«Самый лучший отдых — растолковывать общеизвестные истины.» (С) АБС, Полдень XXII век


Прелюдия


Основное правило клуба свидетелей гидроакустики состоит в том, что видео при помощи гидроакустики на более-менее значимое расстояние (больше нескольких метров) в среднем водоеме передать нельзя, и всегда будет нельзя.
На это есть серьезные причины — канал связи с очень низкой пропускной способностью, низкой скоростью распространения сигнала (в воде всего 1500 м/с) и высокой вероятностью ошибки. Доступная полоса частот составляет всего несколько десятков килогерц.
Но и это еще не все — если, условно говоря, сигнал на частотах порядка 10 кГц распространяется в воде на дистанции порядка 8-10 км, то на частоте 20 кГц — уже на 3-5 км, и чем выше частота, тем сильнее затухание. Например, наши самые маленькие в мире модемы uWAVE работают в полосе 20-30 кГц и передают данные на скорости 78 бит/с на 1000 метров, а RedLINE с полосой 5-15 на 8000 метров. Рекорд же среди коммерческих устройств принадлежит EvoLogics — 68 кБит на 300 метров.
Физику, увы, нельзя обмануть и с ней нельзя договориться — можно передавать или очень медленно и помехозащищенно, или быстро, но на небольшие расстояния.
Однако, в некоторых случаях можно «срезать некоторые углы», какие углы мы срежем в этот раз — ниже.

Что мы будем делать сегодня и что для этого нужно?


В прошлых статьях мы уже передавали «видео» звуком через воду, напомню, что там кадр «рисовался на спектре», то есть спектр, а точнее спектрограмма сигнала являлась картинкой.
Позже мы изготовляли простые гидроакустические антенны из мусора и делали простейший гидроакустический модем. Там же мы изготавливали предусилитель для антенны (проект печатной платы для самостоятельного изготовления ЛУТ-ом по-прежнему лежит здесь).

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

Итак, подытожим, оформим в список, что нам потребуется:

— пара гидроакустических антенн из пьезопищалок
предусилитель, изготовляемый ЛУТ-ом
исходный код проекта на C#
— пара свинцовых АКБ на 12 вольт
— усилитель на TDA, я взял такой всего за 50 рублей на али

Немного теории


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


На картинке изображены два колебания, сдвинутые друг относительно друга на Pi/2 — то есть синусная и косинусная фаза. А если частота ровно в четыре раза меньше частоты дискретизации, то на период приходится всего 4 сэмпла.
Внимательный хабрачитатель конечно заметил, что оба сигнала сдвинуты на Pi/4. При таком сдвиге сигнал принимает только два значения: v2/2 и -v2/2.
И даже не важны конкретные значения, важно что можно использовать только знаки: «+» и «-».

Теперь мы можем синусную фазу представить как последовательность знаков «+» «+» «-» «-», а косинусную как «+» «-» «-» «+».

Под спойлером повторяем работу детектора:
Пусть входной сигнал лежит в буфере sn, у нас есть два кольцевых буфера усреднения для синусной и косинусной фазы — bs и bc размером N. Указатели головы и хвоста у них общие — bH и bT. В начальный момент времени bH = N-1, bT = 0. Счетчик циклов усреденения C = 0.
Берем из входного буфера по 4 сэмпла и складываем согласно последовательностям знаков.

a = sn(i)
bs(bH) = a
bc(bH) = a
s1 = s1 + a - bs(bT)
s2 = s2 + a - bc(bT)
bH = (bH + 1) % N
bT = (bT + 1) % N

a = sn(i+1)
bs(bH) = a
bc(bH) = -a
s1 = s1 + a - bs(bT)
s2 = s2 - a - bc(bT)
bH = (bH + 1) % N
bT = (bT + 1) % N

a = sn(i+2)
bs(bH) = -a
bc(bH) = -a
s1 = s1 - a - bs(bT)
s2 = s2 - a - bc(bT)
bH = (bH + 1) % N
bT = (bT + 1) % N

a = sn(i+3)
bs(bH) = -a
bc(bH) = a
s1 = s1 - a - bs(bT)
s2 = s2 + a - bc(bT)
bH = (bH + 1) % N
bT = (bT + 1) % N


После каждой обработанной четверки сэмплов проверяем счетчик циклов усреднения и если он перевалил за N, то вычисляем амплитуду cA несущей:

if ++cycle >= N
cA = sqrt(s1 * s1 + s2 * s2)
cycle = 0
end



Вот этот метод мы берем за основу, он будет отвечать за «синхронизацию».
Теперь разберемся, как кодируется изображение. Предлагаю использовать амплитудную манипуляцию. Манипуляция — это когда сигнал разбивается на равные отрезки, называемые чипами или символами, и какой-либо варьируемый параметр (в нашем случае амплитуда) сохраняется на длине чипа.
Если, к примеру, мы можем варьировать амплитуду в пределах от 0 до 32767 (16-битные сэмплы), а нам нужно передавать 255 значений яркости пикселей, то на единицу изменения яркости пикселя, амплитуда чипа будет меняться на 32768/255=128.
Еще один важный параметр — длина чипа, начнем с одного периода несущей — четыре сэмпла в нашем случае.
Значит картинка будет передаваться попиксельно, каждый пиксель длится 4 сэмпла, а амплитуда на этом периоде будет равна b[x,y] * 128, где b[x,y] — значение яркости пикселя с координатами x и у в изображении b.

Давайте прикинем, какая получится скорость передачи.
В примере я использовал размер кадра 120х120 пикселей. Это значит, что для передачи одного кадра нам потребуется

120х120х4 = 57600 сэмплов,

Если частота дискретизации 96 кГц, то передача одного кадра займет время:

57600/96000 = 0.6 секунды

Очевидно, что нам потребуется какая-то пауза, некий защитный интервал, чтобы детектор мог определить начало следующего кадра. Из гуманных соображений, предположим, что нам хватит 0.1 секунды, в течении которой затухнут все эхо (на самом деле нет). Тогда, в конце концов скорость передачи получится:

1/(0.6 + 0.1) = 1.428 кадра в секунду.

Очень легко здесь совершить ошибку и попробовать посчитать скорость в битах в секунду. Смотрите, какая невероятная получается скорость передачи:

120*120*8/1.428 = 80 627 бит/с

Но что будет, если у меня не 8-битные пиксели, а 16-ти битные?

120*120*16/1.428 = 161344 бит/с

Подвох здесь состоит в том, что опять же, данный метод передачи нельзя назвать цифровым, и понятие битовой скорости для него не вполне валидно.
Попробуйте посчитать битовую скорость передачи для аналогового телевизионного сигнала. А для детекторного приемника? :)

Вот так, например, будет выглядить кусок сигнала, передающий яркости 10-ти пикселей, значения которых меняются поочередно: 1 2 1 2 1 2 1 2 1 2


Теперь рассмотрим, как это работает в примере. Методы Encode и Decode живут в классе Encoder и отвечают за модуляцию и демодуляцию изображения:

public double[] Encode(Bitmap source, double carrier, int pSize, int interframePauseMs)
        {
            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);

            int cols = frameSize.Width;
            int rows = frameSize.Height;

            int col = 0;
            int row = 0;

            double delta = Math.PI * 2 * carrier / sampleRate;
            double alpha = 0;
            double phase = 0;

            double pxAmplitude = 0;
            double chipLimit = Math.PI * 2 * chipSize;
            double pLimit = Math.PI * 2;

            List<double> samples = new List<double>();            

            bool isFinished = false;

            for (int i = 0; i < pSize; i++)
            {
                alpha = Math.Sin(phase);
                phase += delta;

                if (phase >= pLimit)
                {
                    phase -= pLimit;                    
                }

                samples.Add(alpha * short.MaxValue);
            }

            while (!isFinished)
            {
                alpha = Math.Sin(phase);
                phase += delta;

                if (phase >= chipLimit)
                {
                    phase -= chipLimit;
                    pxAmplitude = (((double)frame.GetPixel(col, row).R) / 255.0) * short.MaxValue;

                    if (++col >= cols)
                    {
                        if (++row >= rows)
                            isFinished = true;
                        else
                            col = 0;
                    }
                }

                samples.Add(alpha * pxAmplitude);                
            }

            if (interframePauseMs > 0)
            {
                samples.AddRange(new double[(int)((((double)interframePauseMs) / 1000.0) * (double)sampleRate)]);
            }

            return samples.ToArray();
        }


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

public Bitmap Decode(double[] samples, double carrier, int pSize)
        {
            int cols = frameSize.Width;
            int rows = frameSize.Height;

            int col = 0;
            int row = 0;

            Bitmap result = new Bitmap(cols, rows);
           
            double delta = Math.PI * 2 * carrier / sampleRate;
            double alpha = 0;
            double phase = 0;
                        
            double chipLimit = Math.PI * 2 * chipSize;            
            double chipAmplitude = 0;
            double maxAmplitude = WaveUtils.GetMaxAmplitude(samples);
            double pxMax = -maxAmplitude;
            double pxMin = maxAmplitude;            

            double smp;

            for (int i = pSize; (i < samples.Length) && (row < rows); i++)
            {
                alpha = Math.Sin(phase);                
                phase += delta;
                
                if (phase >= chipLimit)
                {
                    phase -= chipLimit;
                    chipAmplitude = (Math.Max(Math.Abs(pxMax), Math.Abs(pxMin)) / maxAmplitude);
                    pxMin = maxAmplitude;
                    pxMax = -maxAmplitude;
                    var gs = Convert.ToByte(chipAmplitude * 255);

                    result.SetPixel(col, row, Color.FromArgb(255, gs, gs, gs));

                    if (++col >= cols)
                    {
                        col = 0;
                        row++;
                    }                   
                }
                else
                {
                    smp = samples[i] * alpha;
                    if (smp > pxMax)
                        pxMax = smp;
                    if (smp < pxMin)
                        pxMin = smp;
                }
            }

            return result;
        }


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

Сам же поиск сигнала (детектирование, синхронизация) происходят также, как и в нашем простейшем гидроакустическом модеме, с той лишь разницей, что здесь я вынес его в отдельный класс FsBy4CarrierDetector для разнообразия.
Вся несложная магия происходит в методе bool ProcessSample(short a)


public bool ProcessSample(short a)
        {
            bool result = false;

            if (smpCount == 0)
            {
                ring1[ringHead] = a;
                ring2[ringHead] = a;
                s1 += a - ring1[ringTail];
                s2 += a - ring2[ringTail];
            }
            else if (smpCount == 1)
            {
                ring1[ringHead] = a;
                ring2[ringHead] = -a;
                s1 += a - ring1[ringTail];
                s2 += - a - ring2[ringTail];
            }
            else if (smpCount == 2)
            {
                ring1[ringHead] = -a;
                ring2[ringHead] = -a;
                s1 += -a - ring1[ringTail];
                s2 += -a - ring2[ringTail];
            }
            else if (smpCount == 3)
            {
                ring1[ringHead] = -a;
                ring2[ringHead] = a;
                s1 += -a - ring1[ringTail];
                s2 += a - ring2[ringTail];
            }
                                                        
            ringHead = (ringHead + 1) % ringSize;
            ringTail = (ringTail + 1) % ringSize;

            if (++smpCount >= 4)
            {
                smpCount = 0;
                if (++cycle >= ringSize)
                {
                    s = Math.Sqrt(s1 * s1 + s2 * s2) / ringSize;                  
                    cycle = 0;                    
                    result = (s - sPrev) >= Threshold;                    
                    sPrev = s;
                }
            }

            return result;
        }


Он вызывается на каждом входящем сэмпле и возвращает true в случае детектирования несущей.

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

Теперь, после того, как мы кратко рассмотрели как это все работает, давайте перейдем к самой вкусной части: что из этого всего можно получить.

Немного практики


Для начала проверим как все работает без гидроакустического канала — просто приложив приемную и передающую антенны друг к другу.
Сперва на картинке побольше (240х120), чтобы хоть что-то можно было разобрать:


А потом побыстрее, чтобы было больше жизни больше похоже на видео:


Вроде бы недурно? Но не торопимся с выводами, и идем в плавательный бассейн:


И вот здесь, как я и обещал в заголовке мы собственными глазами увидим эхо:


Как тебе такое, Илон Маск? вам такое HD? Почему же так получается?
А все очень просто — эхо по сути — это задержанные копии исходного сигнала, лихо интерферирующие с ним в точке приема, складывающиеся в разной фазе и дают такую картинку. Так как мы и передаем картинку — то в итоге и получаем много много картинок наложенных друг на друга с разной амлитудой. Все это в конечном счете приводит к размытию и размножению.

Забегая назад, давайте проверим все на модельной крупной картинке. Я взял рандомную фотографию:


Смодулировал ее, потом добавил эхо и немного шума, затем подверг декодированию, и да — результат напоминает то, что мы получили в бассейне:


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

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


В апреле мы улучили момент и съездили с макетом на пруд и побаловаись еще и там:




Результат мало чем отличается от результатов, полученных в бассейне:




И сразу для сравнения предыдущий метод:


А вот и собранные из сохраненных кадров gif-анимации, метод 1:


И метод 2, который мы и обсуждаем в этой статье:


В заключение


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

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

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

Если читатель просто хочет попробовать (даже на воздухе при помощи микрофона и динамиков), то вот ссылки сразу на релизы:
Метод-1
Метод-2 (из этой статьи)

P.S.

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

P.S./2

Сразу отвечу на частый вопрос: для рыб и прочих морских обитателей на таких детских мощностях это все просто незаметно.

Комментарии (24)


  1. yulai-b
    28.05.2019 07:48
    +2

    Нельзя ли использовать для передачи сигнала магнитное поле? Ферритовую антенну, например.


    1. Javian
      28.05.2019 08:13
      +1

      Сверхдлинные волны. Только антенна некомпактная, а скорости не выше.
      Магнитная для передатчика не пригодна.
      habr.com/ru/post/158161
      habr.com/ru/post/158273

      Сверхдлинные волны — v = 3—30 кГц (? = 10—100 км).
      Имеют свойство проникать вглубь толщи воды до 20 м и в связи с этим применяются для связи с подводными лодками, причем, лодке не обязательно всплывать на эту глубину, достаточно выкинуть радио буй до этого уровня.


  1. sixeL
    28.05.2019 10:33
    +1

    Какие будут отличия при использовании частотной модуляции?


    1. AlekDikarev Автор
      28.05.2019 10:36

      В статье используется манипуляция. Можно и частотную, но тогда приемник будет устроен гораздо сложнее, я не пробовал, но почти уверен что будет плюс-минус так же


      1. StDmitriev
        28.05.2019 21:26
        +1

        Манипуляция. Причём сознанием.


  1. KamAdm
    28.05.2019 10:44

    Я таки не понял. Получаемы сигнал никак не фильтруется от шумов?


    1. AlekDikarev Автор
      28.05.2019 10:55

      В предусилителе есть фильтр 8-ого порядка на полосу 5-35 кГц. Цифровым образом фильтровать смысла особого нет


  1. xFFFF
    28.05.2019 11:31

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


    1. KamAdm
      28.05.2019 11:56

      Тоже об этом подумал.


    1. AlekDikarev Автор
      28.05.2019 12:32

      Здесь вероятность ошибки примерно равна 1 ), демодулятор — мозг, он очень недурно распознает сильно побитую картинку. Дело не в АМ, ее вполне годно применяют для передачи голоса: habr.com/ru/post/407143 (опять же, «речное» эхо воспринимается как приятная реверберация концертного зала). Тут суть в том, что канал с низкой пропускной способностью, и если пытаться передавать данные (хоть какой модуляцией) с большой скоростью — все побъется и коррекция ошибок тоже не спасет.


  1. drWhy
    28.05.2019 12:00

    … собственными глазами увидим эхо:
    При приёме аналогового телевидения на кусок провода вместо антенны картинка похожа. Можно отслеживать перемещение человека в помещении по перемещению одного из повторов изображения. Собственно, радиолокация, прародительница ТВ.
    И метод 2, который мы и обсуждаем в этой статье:
    Желательно пересмотреть схему передачи кадровых/строчных синхроимпульсов. Когда амплитуда принятого шума превышает порог, шум считается синхроимпульсом и дёргается по вертикали/горизонтали весь кадр, что сильно мешает восприятию и так зашумлённой картинки. Может быть потеряно несколько важных кадров, если решите когда-нибудь использовать наработки всерьёз.
    В эфирном телевидении такая ситуация редкость, там обычно размах синхроимпульсов с запасом выше размаха сигнала, а вот в аналоговом видеонаблюдении на коаксиальном кабеле в условиях помех наводки на экран кабеля могут быть в несколько раз выше сигнала.
    для рыб и прочих морских обитателей на таких детских мощностях это все просто незаметно.
    Безопасно может быть, но не незаметно. Рыбы общаются в том же диапазоне волн, использую невысокие мощности передачи и чувствительные приёмники (боковую линию и резонатор в виде плавательного пузыря). Хищные рыбы благодаря этому выжывают, находя добычу в мутной воде (пассивно). Конечно, восстановить картинку аналогового видеосигнала они не смогут (как и мы на слух, для этого кадровый буфер нужен), но дискотеку вашу, скорее всего, слышат.
    или напрасно, и тогда это нужно немедленно прекратить.
    Прекращать немедленно не нужно, интересно же.


    1. Peter03
      29.05.2019 00:01
      +1

      А как тогда рыбы себя чувствуют когда киты поют на 180 дБ?


      1. drWhy
        29.05.2019 00:20

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


  1. slovak
    28.05.2019 23:48

    Очень ждем обратной связи от читателей, так как очень важно понимать, что ты делаешь что-то не напрасно


    Чем диктовано исключительное использование С#?
    В контексте потенциальных клиентов из отрасли робототехники — не самый лучший выбор.
    Дешевый модем достаточный для проброса «консоли» под воду с открытым исходным кодом на С/С++ и биндингами на Python должен обеспечить стабильный спрос. Пока же поголовно плавают на тизере (шнурок с витой парой за роботом).

    >> WinExe

    Стоит вспомнить о пользователях Linux, которых имхо большинство в тех же робо кругах.
    А у Вас все проекты на шарпах. Я в курсе про .NET core, не минусуйте пожалуйста, но все же есть ли возможность собрать всё под Linux?

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


    1. AlekDikarev Автор
      29.05.2019 00:01
      +1

      Я пишу или на ANSI C под DSP, или на шарпе под PC. Собственно код из статей три с половиной строки и перевести его куда хочется желающий сможет без проблем. Другое дело что он просто для демонстрации и на скорую руку. В реальной жизни для передачи данных через воду все гораздо сложнее


  1. AVDerov
    29.05.2019 13:19

    Добрый день!

    EvoLogics — 68 кБит на 300 метров.

    Если не секрет, за счет чего расширили полосу передатчика, несколько излучателей или просто продавливают мощностью?


    1. AlekDikarev Автор
      29.05.2019 13:25

      На сколько мне известно там одна антенна. Что конкретно они там делают — лучше узнавать у первоисточника


      1. drWhy
        29.05.2019 14:33

        Насколько я понимаю, перегрузить водный «эфир» гораздо проще. Что будет, если попытаться одновременно передавать описанной методикой два видеопотока?
        Т.е. больше одной пары модемов в одной акватории могут работать только за счёт разделения по коду? И практически работа всегда идёт под шумами, аналог шумоподобных сигналов в радиосвязи?


        1. AlekDikarev Автор
          29.05.2019 16:42

          Если передавать два видео они смешаются, одна картинка на другую в зависимости от времени прихода начала. Будут проблемы с синхронизацией. Что касаемо модемов — то разделение по коду гарантирует только неприем в другом кодовом канале. Сам по себе широкополосный сигнал хоть и кодово разделённый будет очень серьёзной помехой.


          1. drWhy
            29.05.2019 16:54
            +1

            То есть разделение только по частоте/времени и надеемся, что никто из соседей не использует джаммер забьёт весь канал.


            1. AlekDikarev Автор
              29.05.2019 17:50
              +1

              Если совсем грубо — да, есть конечно ньюансы, но там на отдельную статью объяснять. Ну и джаммер для гидроакустической связи милое и элементарное дело. Если речь идёт только не о военных которые давят мощу на фоне которой джаммер будет иметь бледный вид. Хотя опять же смотря где ставить


            1. StDmitriev
              29.05.2019 18:24
              +1

              Примерно так) Представляете какой ужас бы был, если бы и в радиосвязи доступная полоса была с пару десятков килогерц?)


  1. AVDerov
    31.05.2019 00:43

    Александр, вы не пробовали тестировать свои алгоритмы при помощи моделирования функции канала (AT, bellhop и тому подобные коды)? На ваш взгляд это имеет смысл?


    1. AlekDikarev Автор
      31.05.2019 10:33

      «Мои алгоритмы» звучит очень пафосно. Все алгоритмы давно известны. Bellhop-ом мой коллега пользовался. Там модель распространения, оно все зависит например, от профиля дна и ВРСЗ, ни того ни другого обычно нет.