Преобразуем изображение в звук — что можно услышать?

Привет Хабр.

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

kistiptqvnn-pmxshurxrawkqze.png

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

Преобразуем 2D в 1D


Первая очевидная задача, которая нас ожидает — это преобразовать двухмерное «плоское» изображение в «одномерную» звуковую волну. Как подсказали в комментариях к той статье, для этого удобно воспользоваться кривой Гильберта.
kkn92oz48liofrdsozg5jamskcq.gif
Она по своей сути похожа на фрактал, и идея в том, что при увеличении разрешения изображения, относительное расположение объектов не меняется (если объект был в верхнем левом углу картинки, то он останется там же). Различные размерности кривых Гильберта могут дать нам разные изображения: 32×32 для N=5, 64×64 для N=6, и так далее. «Обходя» изображение по этой кривой, мы получаем линию, одномерный объект.

Следующий вопрос это размер картинки. Интуитивно хочется взять изображение побольше, но тут есть большое «но»: даже картинка 512×512, это 262144 точек. Если преобразовать каждую точку в звуковой импульс, то при частоте дискретизации 44100, мы получим последовательность длиной в целых 6 секунд, а это слишком долго — изображения должны обновляться быстро, например с использованием web-камеры. Делать частоту дискретизации выше бесмысленно, мы получим ультразвуковые частоты, неслышимые ухом (хотя для совы или летучей мыши может и пойдет). В итоге методом научного тыка было выбрано разрешение 128×128, которое даст импульсы длиной 0.37c — с одной стороны, это достаточно быстро чтобы ориентироваться в реальном времени, с другой вполне достаточно, чтобы уловить на слух какие-то изменения в форме сигнала.

Обработка изображения


Первым шагом мы загружаем изображение, преобразуем его в ч/б и масштабируем до нужного размера. Размер изображения зависит от размерности кривой Гильберта.

from PIL import Image
from hilbertcurve.hilbertcurve import HilbertCurve
import numpy as np
from scipy.signal import butter, filtfilt


# Create Hilbert curve
dimension = 7
hilbert = HilbertCurve(dimension, n=2)
print("Hilbert curve dimension:", dimension)  # Maximum distance along curve
print("Max_dist:", hilbert.max_h)  # Maximum distance along curve
print("Max_coord:", hilbert.max_x)  # Maximum coordinate value in any dimension

# Load PIL image
f_name = "image01.png"
img = Image.open(f_name)
width, height = img.size
out_size = hilbert_curve.max_x + 1
if width != out_size:
    img = img.resize((out_size, out_size), Image.ANTIALIAS)

# Get image as grayscale numpy array
img_grayscale = img.convert(mode='L')
img_data = np.array(img_grayscale)


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

width, height = img_grayscale.size

sound_data = np.zeros(width*height)
for ii in range(width*height):
    coord_x, coord_y = hilbert_curve.coordinates_from_distance(ii)
    pixel_l = img_data[coord_x][coord_y]

    # Inverse colors (paper-like, white = 0, black = 255)
    pixel_l = 255 - pixel_l

    # 0..255 => 0..32768
    ampl = pixel_l*32

    sound_data[ii] = ampl


Из кода, надеюсь, все понятно. Функция coordinates_from_distance делает за нас всю работу по преобразованию координат (х, у) в расстояние на кривой Гильберта, значение яркости L мы инвертируем и преобразуем в цвет.

Это еще не все. Т.к. на изображении могут быть большие блоки одного цвета, это может привести к появлению в звуке «dc-компоненты» — длинного ряда отличных от нуля значений, например [100,100,100,…]. Чтобы их убрать, применим к нашему массиву high-pass filter (фильтр Баттерворта) с частотой среза 50Гц (совпадение с частотой сети случайно). Синтез фильтров есть в библиотеке scipy, которым мы и воспользуемся.

def butter_highpass(cutoff, fs, order=5):
    nyq = 0.5 * fs
    normal_cutoff = cutoff / nyq
    b, a = butter(order, normal_cutoff, btype='high', analog=False)
    return b, a

def butter_highpass_filter(data, cutoff, fs, order=5):
    b, a = butter_highpass(cutoff, fs, order)
    y = filtfilt(b, a, data)
    return y

# Apply high pass filter to remove dc component

cutoff_hz = 50
sample_rate = 44100
order = 5
wav_data = butter_highpass_filter(sound_data, cutoff_hz, sample_rate, order)


Последним шагом сохраним изображение. Т.к. длина одного импульса короткая, мы повторяем его 10 раз, это будет на слух более приближено к реальному повторяющемуся изображению, например с веб-камеры.

# Clip data to int16 range
sound_output = np.clip(wav_data, -32000, 32000).astype(np.int16)

# Save
repeat = 10
sound_output_ntimes = np.tile(sound_output, repeat)
wav_name = "ouput.wav"
scipy.io.wavfile.write(wav_name, sample_rate, sound_output_ntimes)


Результаты


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

Тест-1

ftte9mjfgwj6nib_lj76pfrqmre.png

Изображению соответствует такой звуковой сигнал:
bdgwquzss88fzb1s8hzyydf-das.png

WAV: cloud.mail.ru/public/nt2R/2kwBvyRup

Тест-2

vrjt6ufndqzfdsywsgrpbnwlqiw.png

Идея этого теста — сравнить «звучание» объекта другой формы. Звуковой сигнал:
6zbq9axxuoknevsj_iuxc94unre.png

WAV: cloud.mail.ru/public/2rLu/4fCNRxCG2

Можно заметить, что звучание действительно другое, и на слух разница есть.

Тест-3

cvcp3h7itiq8srj-rs_j6ntb6lk.png

Идея теста — проверить объект меньшего размера. Звуковой сигнал:
ykrax3udam03yphwwtlouygkgdk.png

WAV: cloud.mail.ru/public/5GLV/2HoCHvoaY

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

Заключение


Данный тест, разумеется, не диссертация, а просто proof of concept, сделанный за несколько часов свободного времени. Но даже так, оно в принципе работает, и разницу ощущать на слух вполне реально. Я не знаю, можно ли научиться ориентироваться в пространстве по таким звукам, гипотетически наверно можно после некоторой тренировки. Хотя тут огромное поле для улучшений и экспериментов, например, можно использовать стереозвук, что позволит лучше разделять объекты с разных сторон, можно экспериментировать с другими способами конвертации изображения в звук, например, кодировать цвет разными частотами, и пр. И наконец, перспективным тут является использование 3d-камер, способных воспринимать глубину (увы, такой камеры в наличии нет). Кстати, с помощью несложного кода на OpenCV, вышеприведенный алгоритм можно адаптировать к использованию web-камеры, что позволит экспериментировать с динамическими изображениями.

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

© Habrahabr.ru