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

«Самый лучший отдых — растолковывать общеизвестные истины.» (С) АБС, Полдень 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. При таком сдвиге сигнал принимает только два значения: √2/2 и -√2/2.
И даже не важны конкретные значения, важно что можно использовать только знаки: «+» и «-».
Теперь мы можем синусную фазу представить как последовательность знаков «+» «+» «-» «-», а косинусную как «+» «-» «-» «+».
Под спойлером повторяем работу детектора:
Пусть входной сигнал лежит в буфере sn, у нас есть два кольцевых буфера усреднения для синусной и косинусной фазы — bs и bc размером N. Указатели головы и хвоста у них общие — bH и bT. В начальный момент времени bH = N-1, bT = 0. Счетчик циклов усреденения C = 0.
Берем из входного буфера по 4 сэмпла и складываем согласно последовательностям знаков.
После каждой обработанной четверки сэмплов проверяем счетчик циклов усреднения и если он перевалил за N, то вычисляем амплитуду cA несущей:
Берем из входного буфера по 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), чтобы хоть что-то можно было разобрать:

А потом побыстрее, чтобы было

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

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

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

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

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

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


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


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

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

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

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