Форум программистов, компьютерный форум, киберфорум
_Ivana
Войти
Регистрация
Восстановить пароль
Рейтинг: 2.50. Голосов: 2.

2D графики - ловля блох и пикселей

Запись от _Ivana размещена 02.09.2014 в 21:49

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

Часто требуется изобразить на экране 2D график некоторой скалярной функции одного аргумента в декартовой системе координат. Эта задача решается через предварительное заполнение массива значений функции на какой-то сетке (массиве) аргумента с необходимым шагом и затем производится отрисовка значений на экране. С другой стороны, нередко в качестве входных данных мы имеем не функцию, а сразу массив ее значений какой-то длины, например прочитанный из wav-файла. Поэтому можно рассматривать задачу построения графика таблично заданной функции в определенном диапазоне индексов таблицы - таким образом мы легко можем делать увеличение/уменьшение масштаба и смещение графика по аргументу функции - надо только передать в нашу процедуру визуализации начальный и конечный индексы массива значений, который мы будем визуализировать. Но область экрана, в котором мы показываем наш график, имеет вполне определенную и конечную ширину и высоту в пикселях. И встает задача размещения произвольного количества значений функции (отсчетов) в заданной ширине/высоте окна в пикселях. Вертикальное масштабирование отсчетов в высоту окна вывода выполняется тривиально и я не буду заострять на нем внимание. А возможные соотношения количества отсчетов и ширины окна - максимально возможного выводимого количества точек сразу дают нам 2 случая, которые мы и рассмотрим.

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

2) Количество отсчетов больше ширины окна в пикселях. В этом случае мы не можем вывести все значения функции, поэтому нужно искать компромисс. Например, один из вариантов визуализации состоит в том, что мы разбиваем весь диапазон выводимых отсчетов на примерно равные группы (фреймы) - по количеству пикселей ширины окна вывода, получаем для каждой координаты окна (горизонтального пикселя) некоторое количество отсчетов (размер фрейма), ищем среди них минимальный и максимальный и рисуем вертикальную линию между масштабированными в окно максимумом и минимумом фрейма на абсциссе данного пикселя, и так для всех пикселей ширины окна. Все бы было хорошо, если бы наша таблица была заполнена значениями гладкой непрерывной функции с малым интервалом дискретизации, но так бывает далеко не всегда, и wav-файл (если конечно в его спектре содержатся частоты не сильно далекие от частоты Найквиста) как раз является примером негладкой функции. Визуально на графике это приводит к "разрывам" между вертикальными линиями соседних фреймов. В принципе это не ошибка визуализации, мы честно рисуем линию от минимума до максимума каждого фрейма, но хочется, чтобы наш график представлял собой непрерывную линию. Одно из решений состоит в том, чтобы устранять разрывы между двумя фреймами, сдвигая соответствующие точка каждого на середину разрыва, например если линия первого фрейма у нас рисуется по вертикали от 1 до 10, а второго - от 20 до 25, то мы должны нарисовать первую линию от 1 до 15, а вторую - от 16 до 25, таким образом видимый разрыв исчезнет и график станет непрерывным. Выглядит это уже лучше, но есть еще один некрасивый момент - если график имеет в каком-то месте достаточно пологий наклон, то при отрисовке это может выглядеть как непрерывная линия двойной толщины. Причины этого понятны - при нашем алгоритме отрисовки вертикальных линий от минимума до максимума и пологом графике мы получаем, что максимум фрейма совпадает с минимумом соседнего, и если эта тенденция сохраняется, то мы визуально видим толстую наклонную линию, хотя она состоит из множества коротких вертикальных линий. Решение этой проблемы достаточно просто - при расчете максимума каждого фрейма, если он не совпадает с минимумом, мы уменьшаем его (уже нормализованное значение в пикселях!) на единицу - таким образом мы избегаем "нахлеста" линий фреймов друг на друга. Конечно, если у нас вдруг случится ситуация, когда все значения следующего фрейма будут строго больше всех значений предыдущего, у нас на графике будет разрыв высотой в один пиксель, но по моему мнению это допустимая плата за решение гораздо более неприятной проблемы. к тому же такая ситуация на практике будет происходить весьма редко. Для полноты картины еще хочется отметить, что стандартная функция LineTo из GDI WinAPI не прорисовывает последний пиксель (то есть, свою финальную точку), поэтому для ее прорисовки ее координату необходимо сместить на 1 пиксель.
Итого, если представить все вышесказанное в виде кода (в данном примере с использованием стандартных функций GDI WinAPI, но это не принципиально), то упрощенная (чтобы не загромождать суть разными аксессуарами) процедура отрисовки выглядит так:
C++
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
void Draw_Graph(HWND hWnd, LPWaveStruct wave)
{
    //for (int i=(*wave).il; i<=(*wave).ir; i++) (*wave).buf[i] = (short int) 32000*sin(0.001*i);
 
    PAINTSTRUCT ps;
    HDC hdc = BeginPaint(hWnd, &ps);
 
    RECT rect; 
    GetClientRect(hWnd, &rect);
    int x = rect.left;
    int samples = (*wave).ir - (*wave).il;
    int pixels = rect.right - rect.left + 1;
    if (samples > pixels) {
        int il = (*wave).il, ir, miny_past, maxy_past;
 
        for (int x=rect.left; x<=rect.right; x++) {
            ir = (*wave).il + (x - rect.left + 1)*samples/pixels;
            int minbuf = (*wave).buf[il], maxbuf = (*wave).buf[il];
            for (int i=il+1; i<=ir; i++) {
                if ((*wave).buf[i] < minbuf) minbuf = (*wave).buf[i];
                if ((*wave).buf[i] > maxbuf) maxbuf = (*wave).buf[i];
            }
            int miny_curr = (minbuf + 32768)*rect.bottom/65535;
            int maxy_curr = (maxbuf + 32768)*rect.bottom/65535;
            if (maxy_curr > miny_curr) maxy_curr--;
 
            if (x != rect.left) {
                int d;
                if ((d = miny_curr - maxy_past - 1) > 0) {
                    maxy_past += d>>1;
                    miny_curr -= (d>>1) + (d&1);
                } else if ((d = miny_past - maxy_curr - 1) > 0) {
                    miny_past -= (d>>1) + (d&1);
                    maxy_curr += d>>1;
                }
                MoveToEx(hdc, x-1, rect.bottom - miny_past, NULL);
                LineTo(hdc, x-1, rect.bottom - (maxy_past + 1));
            }
            if (x == rect.right) {
                MoveToEx(hdc, x, rect.bottom - miny_curr, NULL);
                LineTo(hdc, x, rect.bottom - (maxy_curr + 1));
            }
 
            il = ir + 1;
            miny_past = miny_curr;
            maxy_past = maxy_curr;
        }
    } else {
        int y = rect.bottom - ((*wave).buf[(*wave).il] + 32768)*rect.bottom/65535;
        MoveToEx(hdc, x, y, NULL);
        for (int i=(*wave).il+1; i<=(*wave).ir; i++) {
            x = rect.left + (i-(*wave).il)*pixels/samples;
            y = rect.bottom - ((*wave).buf[i] + 32768)*rect.bottom/65535;
            LineTo(hdc, x, y);
        }
    }
 
    EndPaint(hWnd, &ps);
}
Входные параметры - окно отрисовки графика и указатель на структуру, содержащую массив отсчетов и начальный и конечный индексы для вывода. Внутри никакой плавающей арифметики, только целочисленная - для ускорения работы. Масштабирование по вертикали сделано жестким для 16-битных отсчетов wav-файла, но это легко отредактировать под нужды ситуации.
Размещено в Без категории
Показов 4417 Комментарии 6
Всего комментариев 6
Комментарии
  1. Старый комментарий
    Аватар для Andrej
    А красивые картинки будут? =)
    Запись от Andrej размещена 23.09.2014 в 21:56 Andrej вне форума
  2. Старый комментарий
    Так никто не проявлял интереса, случайно увидел что мою запись прокомментировали. Здесь как-то не принято комментировать блоги, и лично для меня это не мотивирует писать их. Может куда еще попробую, на тот же Хабр, там хоть обсуждения идут и люди читают... А картинки вот - разные зумы/смещения:..... А вот и не вот, сделал несколько картинок, но не нашел как их вставлять в ответ обсуждения в блоге...

    ЗЫ вот здесь можно посмотреть картинки, пока тему не удалили
    Запись от _Ivana размещена 23.09.2014 в 23:49 _Ivana вне форума
    Обновил(-а) _Ivana 24.09.2014 в 00:00
  3. Старый комментарий
    Аватар для Andrej
    Печаль-беда.
    С картинками сразу видно результат - причину для прочтения текста.
    Запись от Andrej размещена 24.09.2014 в 00:03 Andrej вне форума
  4. Старый комментарий
    Нет повода для печали - картинки можно посмотреть в моей отдельной теме с Сишном разделе - пока тема жива
    Запись от _Ivana размещена 24.09.2014 в 00:04 _Ivana вне форума
  5. Старый комментарий
    Картинки в блог можно вставлять из личных альбомов с тегом [img] - попробуйте. Только не забудьте альбомы сделать публичными - а то картинок никто не увидит.
    Запись от Taatshi размещена 24.09.2014 в 02:04 Taatshi вне форума
  6. Старый комментарий
    Вставилось, только оригинальный размер ужался без возможности восстановления.
    Запись от _Ivana размещена 24.09.2014 в 03:25 _Ivana вне форума
    Обновил(-а) _Ivana 24.09.2014 в 03:29
 
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2024, CyberForum.ru