Зона кода

А давайте немного попрограммируем...

Графическая библиотека pgraph: загрузка изображения из файла

C99Библиотека pgraph

pgraph — это написанная мной на языке C99 примитивная графическая библиотека. Её созданию были посвящены 2 статьи: первая и вторая.

Используя данную библиотеку, можно создавать изображения — прямоугольные области для рисования, состоящие из пикселей. В библиотеке были реализованы функции, позволяющие получать и устанавливать цвета пикселей, рисовать прямые линии, заливать одноцветные области новым цветом и сохранять изображение в файле формата BMP. Однако загрузка изображений из файла предусмотрена не была. Таким образом, библиотека предназначалась для создания изображений "с нуля"; уже созданные ранее в других программах изображения использовать было нельзя.

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

Загрузка изображения из файла: функция load_from_file()

Выполнение загрузки изображения из файла возложим на функцию load_from_file(), имеющую следующий прототип, который мы поместим в заголовочный файл pgraph.h:

image *load_from_file(const char *filename);

Функция load_from_file() получает в качестве параметра адрес текстовой строки, содержащей имя графического файла формата BMP, пытается считать его из файла и сохранить в созданной динамически переменной типа image. В случае успеха функция возвращает адрес созданной переменной. Таким образом, забота об освобождении динамически выделенной под эту переменную памяти ложится "на плечи" пользователя функции. Если же изображения считать не удалось или не получилось сохранить его в переменной, то функция возвращает нулевой адрес.

Данная функция работает исключительно с несжатыми 24-разрядными рисунками, не содержащими палитр. Именно в таком формате сохраняет изображения функция save_to_file(), входящая в библиотеку pgraph. Кстати, рекомендую читателям перед тем, как приступить к рассмотрению кода функции load_from_file(), приведённому ниже, ознакомиться со структурой BMP-файла, который обрабатывает данная функция, и с кодом родственной ей функцией save_to_file(). К слову сказать, первая функция была получена переделкой второй, и в ней использованы переменные, имеющие тот же смысл, что и во второй функции.

Чтение данных из файла будем осуществлять с помощью стандартной библиотечной функции fread(), объявленной в заголовочном файле <stdio.h>, имеющей следующий прототип:

size_t fread(void * restrict buf, size_t size, size_t count, FILE * restrict stream);

Функция fread() осуществляет чтение из потока, адресуемого указателем stream, count значений, каждое из которых имеет размер size, и записывает их в массив, адресуемый указателем buf. Функция возвращает число успешно прочитанных значений. Таким образом, возвращённое значение, отличное от переданного функции числа значений, подлежащих чтению, свидетельствует об ошибке.

Код функции load_from_file() помещён в файл pgraph.c. Вот он:

 1.//Чтение изображения из файла
 2.image *load_from_file(const char *filename)
 3.{
 4.    FILE *fp;
 5.    if (!(fp = fopen(filename, "rb")))
 6.        return 0;
 7.    ushort header[27];
 8.    if (fread(header, 2, 27, fp) != 27)
 9.        return 0;
10.    if (header[0] != 19778 || header[5] != 54 || header[7] != 40
11.                           || header[13] != 1 || header[14] != 24)
12.        return 0;
13.    uint w = *(uint *) (header + 9);
14.    uint h = *(uint *) (header + 11);
15.    char d = w % 4;
16.    uint size = h * (w * 3 + d);
17.    if (*(uint *) (header + 17) != size || *(uint *) (header + 1) != size + 54)
18.        return 0;
19.    if (*(uint *) (header + 3) || *(uint *) (header + 15)
20.                || *(uint *) (header + 19) || *(uint *) (header + 21)
21.                || *(uint *) (header + 23) || *(uint *) (header + 25))
22.        return 0;
23.    image *img = create_image(w, h);
24.    color *pixels = img->pixels;
25.    uint z = 0;
26.    for (uint i = 0; i < h; i++)
27.    {
28.        for (uint j = 0; j < w; j++)
29.        {
30.            color col;
31.            if (fread(&col, 1, 3, fp) != 3)
32.            {
33.                free (img);
34.                return 0;
35.            }
36.            pixels[i * w + j] = (color) {col.blue, col.green, col.red};
37.        }
38.        if (d && (fread(&z, 1, d, fp) != d || z))
39.        {
40.            free (img);
41.            return 0;
42.        }
43.    }
44.    if (feof(fp))
45.    {
46.        free (img);
47.        return 0;
48.    }
49.    fclose(fp);
50.    return img;
51.}

Рассмотрим код данной функции.

В строках 5, 6 осуществляется попытка открыть для чтения двоичный файл, имя которого передано функции, и создать связанный с ним поток. В случае удачи адрес потока присваивается переменной fp, объявленной в строке 4. В противном случае переменная fp получает нулевое значение, после чего в вызывающую функцию возвращается нулевой адрес и на этом выполнение функции load_from_file() завершается.

В 7-й строке создаётся массив header ёмкостью 54 байта, а в следующей строке предпринимается попытка считать в него заголовок графического файла (размер заголовка как раз и должен быть равен 54 байтам). В случае неудачи функция прекращает работу, возвращая нулевой адрес (стр. 9).

Из всей информации, хранящейся в заголовке, нас интересуют только ширина и длина изображения в пикселях. Они записываются в переменные w и h соответственно (см. стр. 13, 14). Далее вычисляются значения переменных d (количество нулевых байтов, требуемых для дополнения горизонтального ряда пикселей) и size (размер части файла, содержащей растр).

Что касается остальной информации, содержащейся в заголовке, то она в нашей функции лишь проверяется на корректность (см. стр. 10-12 и 17-22). Если проверяемые элементы массива header, в котором хранится заголовок, содержат не то, что ожидается, то функция load_from_file() завершает работу, возвращая нулевой адрес.

Особое внимание обратим на строки 19-22, в которых байты заголовка проверяются на наличие в них нулей. Надо заметить, что даже в корректных графических файлах рассматриваемого нами формата в проверяемых байтах могут находиться ненулевые значения. Например, Paint — стандартное приложение Windows, иногда, при сохранении 24-разрядных изображений записывает информацию в байты с номерами 39, 40 и 43, 44 (номера приведены в десятичной системе счисления, нумерация начинается с единицы). Такие файлы, разумеется, не будут обрабатываться функцией load_from_file().

Далее выделяем память под изображение, имеющее размеры, полученные из заголовка файла (стр. 23), для удобства создаём указатель pixels, которому присваиваем адрес массива, в котором будет содержаться информация о цветах пикселей изображения (стр. 24) и объявляем вспомогательную переменную z, инициализируя её нулевым значением (стр. 25). В эту переменную будут считываться байты, которыми дополняются горизонтальные ряды пикселей в случае возникновения такой необходимости.

После этого посредством двух циклов for, один из которых вложен в другой, считываем из файла информацию о цветах пикселей и заполняем ею массив, адресуемый переменной pixels (стр. 26-43). Если одна из попыток получения цвета из файла оканчивается неудачей, сразу же уничтожаем изображение и возвращаем из функции нулевой адрес (см. стр. 31-35). Поскольку цвета каждого пикселя записаны в файле в порядке, обратном тому, в котором они записываются в переменных типа color, мы каждый раз сперва записываем цвет из файла в буферную переменную col, объявленную в строке 30, а после этого в нужном порядке переписываем цвета из col в элемент массива, адресуемого pixels (стр. 36).

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

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

Как мы могли убедиться, функция load_from_file() осуществляет достаточно "жёсткую" проверку обрабатываемого файла на корректность. Даже если некоторые байты, не играющие никакой роли, имеют ненулевые значения, или ненулевые значения имеют байты, дополняющие горизонтальные ряды пикселей, файл считается имеющим неверный формат, и функция возвращает нулевой адрес, свидетельствующий об ошибке. То же самое происходит и в случае, если файл имеет "лишние" завершающие байты.

Так что вполне может произойти ситуация, при которой файл с корректно записанным в него растром может оказаться "забракованным" функцией load_from_file() по причинам, перечисленным в предыдущем абзаце. Решением этой проблемы может быть внесение изменений в код данной функции, ведущих к повышению её "лояльности" к файлам. Эти изменения сводятся к удалению из функции проверок, которые могут, при определённых условиях, считаться избыточными. Читатель без труда сможет, при необходимости, самостоятельно внести эти изменения.

О тестировании

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

Но давайте сделаем проверку чуть более интересной. А именно: после загрузки изображения изменим его и только после этого сохраним в новом файле.

Исходное изображение будет фотографией. К нему мы будем применять достаточно простые преобразования: зеркальное отражение, обращение цветов, обесцвечивание, циклическая перестановка RGB-каналов. Каждое такое преобразование будет реализовано в отдельной функции. Все эти функции поместим в файл pgraph_test.c. А в самом конце этого файла будет располагаться функция main(), из которой данные функции будут вызываться.

Чтобы можно было использовать нашу графическую библиотеку, нужно разместить в начале файла директиву, подключающую заголовочный файл pgraph.h:

#include "pgraph.h"

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

Зеркальное отражение: функция reflection()

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

 1.//Горизонтальное отражение
 2.image *reflection(const image *img)
 3.{
 4.    uint w = img->width, h = img->height;
 5.    image *new_img = create_image(w, h);
 6.    const color *pixels = img->pixels;
 7.    color *new_pixels = new_img->pixels;
 8.    for (uint i = 0; i < h; i++)
 9.        for (uint j = 0; j < w; j++)
10.            new_pixels[i * w + j] = pixels[(i + 1) * w - j];
11.    return new_img;
12.}

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

В 4-й строке сохраняем ширину и высоту исходного изображения в переменных w и h соответственно, а в 5-й динамически создаём новое изображение, имеющее такие же размеры, что и исходное, и помещаем адрес нового в переменную img. В следующих двух строках адреса массивов, содержащих растры изображений, помещаем в отдельные переменные, что позволит для получения цветов пикселей обходиться без обращения к адресам изображений.

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

Последняя инструкция функции reflection() — возвращение адреса созданного изображения (стр. 11).

Обращение цветов: функция negative()

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

 1.//Обращение цветов
 2.image *negative(const image *img)
 3.{
 4.    uint w = img->width, h = img->height;
 5.    image *new_img = create_image(w, h);
 6.    const color *pixels = img->pixels;
 7.    color *new_pixels = new_img->pixels;
 8.    for (uint i = 0; i < h; i++)
 9.        for (uint j = 0; j < w; j++)
10.            new_pixels[i * w + j] = to_color(16777215 - from_color(pixels[i * w + j]));
11.    return new_img;
12.}

Смыслы аргумента функции и возвращаемого значения — те же, что и в предыдущем случае. Более того, как несложно заметить, помимо названия, функция negative() отличается от функции reflection(), только 10-й строкой, в которой формируются цвета пикселей нового изображения. Давайте рассмотрим эту строку подробно.

Сначала мы получаем цвет пикселя исходного изображения и переводим его из формата RGB в формат COLORREF с помощью функции from_color(). Далее выполняем обращение цвета: вычитаем полученное значение из числа 0xffffff (или 16777215 в десятичном формате). Уменьшаемое представляет собой белый цвет, записанный в формате COLORREF. А полученная разность — это обращённый цвет в этом же формате. Остаётся перевести его в формат RGB функцией to_color() и сохранить в качестве нового цвета текущего пикселя нового изображения.

Можно было бы и обойтись без функций from_color() и to_color(), но тогда пришлось бы обрабатывать каждый из трёх каналов по-отдельности (или прибегать к использованию не очень удобных трюков).

Обесцвечивание: функция black_white()

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

 1.//Обесцвечивание
 2.image *black_white(const image *img)
 3.{
 4.    uint w = img->width, h = img->height;
 5.    image *new_img = create_image(w, h);
 6.    const color *pixels = img->pixels;
 7.    color *new_pixels = new_img->pixels;
 8.    for (uint i = 0; i < h; i++)
 9.        for (uint j = 0; j < w; j++)
10.        {
11.            color col  = pixels[i * w + j];
12.            uchar new_col = round(0.3 * col.red + 0.59 * col.green + 0.11 * col.blue);
13.            new_pixels[i * w + j] = (color) {new_col, new_col, new_col};
14.        }
15.    return new_img;
16.}

Как и в предыдущем случае, в силу сходства данной функции с двумя уже описанными, прокомментируем только тело внутреннего цикла (стр. 11-13).

Получаем цвет пикселя исходного изображения (стр. 11) и вычисляем интенсивность серого цвета как линейную комбинацию интенсивностей красного, зелёного и голубого цветов (стр. 12). Весовые коэффициенты в этой линейной комбинации — 0,3, 0,59 и 0,11, стоящие перед интенсивностями перечисленных цветов, отражают восприимчивость человеческого глаза к данным цветам. Таким образом, наибольший вклад в интенсивность серого цвета даёт зелёный цвет, наименьший — голубой, а красный цвет занимает промежуточное положение.

Разумеется, использованная формула общеизвестна; считается, что именно такие коэффициенты способствуют наиболее адекватному представлению цветного изображения в чёрно-белом виде, хотя иногда встречаются и другие наборы коэффициентов.

Все оттенки серого цвета задаются одинаковыми значениями интенсивностей красной, зелёной и голубой компонент. По этой причине, все 3 поля элемента массива, отвечающего за цвет текущего пикселя нового изображения, мы заполняем одним и тем же значением интенсивности серого цвета (стр. 13).

Циклическая перестановка RGB-каналов: функция rgb_to_gbr()

И последняя функция, выполняющая преобразование изображения: rgb_to_gbr(). Вот её код:

 1.//Циклическая перестановка каналов
 2.image *rgb_to_gbr(const image *img)
 3.{
 4.    uint w = img->width, h = img->height;
 5.    image *new_img = create_image(w, h);
 6.    const color *pixels = img->pixels;
 7.    color *new_pixels = new_img->pixels;
 8.    for (uint i = 0; i < h; i++)
 9.        for (uint j = 0; j < w; j++)
10.        {
11.            color col  = pixels[i * w + j];
12.            new_pixels[i * w + j] = (color) {col.green, col.blue, col.red};
13.        }
14.    return new_img;
15.}

От предыдущих функций данная, как обычно, отличается названием и содержимым внутреннего цикла (стр. 11-12). Как несложно заметить, интенсивности красной, зелёной и голубой компонент пикселя нового изображения устанавливаются равными интенсивностям зелёной, голубой и красной компонент соответственно пикселя исходного. Таким образом, цветовые каналы циклически "передают" друг другу свои интенсивности. Очевидно, что если данное преобразование применить к исходному изображению трижды, то результирующее изображение совпадёт с исходным.

Тестирующая программа: функция main()

Перейдём к рассмотрению функции main(), из которой будут вызываться все рассмотренные выше функции, выполняющие преобразования изображений. Ниже расположен её исходный код.

 1.int main()
 2.{
 3.    image *img;
 4.    if (!(img = load_from_file("test.bmp")))
 5.        printf("Не удалось открыть файл");
 6.    else
 7.    {
 8.        image *new_img = reflection(img);
 9.        save_to_file(new_img, "test1.bmp");
10.        free(new_img);
11.        new_img = negative(img);
12.        save_to_file(new_img, "test2.bmp");
13.        free(new_img);
14.        new_img = black_white(img);
15.        save_to_file(new_img, "test3.bmp");
16.        free(new_img);
17.        new_img = rgb_to_gbr(img);
18.        save_to_file(new_img, "test4.bmp");
19.        image *new_img2 = rgb_to_gbr(new_img);
20.        save_to_file(new_img2, "test5.bmp");
21.        free(new_img);
22.        free(new_img2);            
23.    }
24.    free(img);
25.    return 0;
26.}

В функции main() объявляется указатель img на переменную типа image (стр. 3) и осуществляется попытка загрузить из файла test.bmp изображение в созданную динамически переменную данного типа и присвоить её адрес данному указателю (стр. 4, 5). Графический файл должен находиться в корневом каталоге исполняемого файла. В случае невозможности открытия файла выводим соответствующее сообщение на консоль.

Если изображение было успешно загружено, то создаём новые изображения, являющиеся модернизированными версиями исходного с помощью функций reflection(), negative(), black_white(), rgb_to_gbr() и записываем изображения в новые графические файлы (стр. 6-23). Обратите внимание на то, что функция rgb_to_gbr() вызывается дважды (стр. 17 и 19), причём второй раз она обрабатывает уже не исходное изображение, а изображение, сгенерированное ею же в результате первого вызова. Таким образом, последнее изображение содержит вторую версию исходного, полученную циклической перестановкой каналов.

Изображения удаляются, как только становятся ненужными. В результате удаётся ограничиться лишь двумя дополнительными указателями на изображения: new_img и new_img_2.

Перед окончанием работы программы память, выделенная для исходного изображения, освобождается (стр. 24).

В итоге, в случае успешного выполнения программы, в корневой директории исполняемого файла появляются 5 новых графических файлов с именами test1.bmp, test2.bmp, test3.bmp, test4.bmp и test5.bmp.

Результаты тестирования

Прогуливаясь по городу, я сделал цифровой фотоснимок, уменьшил его размеры и сохранил в виде 24-х разрядного BMP-файла, который назвал test.bmp. Изображение, содержащееся в данном файле, я и использовал как исходное. Для печати этого изображения в Сети я перевёл его в формат JPEG.  Внизу Вы видите уменьшенную копию JPEG-файла. Оригинальный JPEG-файл можно получить, щёлкнув по изображению.

Исходное изображение

Исходное изображение

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

Файл test.bmp был помещён в корневую директорию исполняемого файла нашей программы. После выполнения программы в директории появились 5 файлов формата BMP. Все они, как и файл test.bmp, для публикации в Интернете были преобразованы в формат JPEG. Их уменьшенные копии приведены ниже. Щелчки по изображениям приводят к загрузкам оригинальных JPEG-файлов.

Итак, начнём с изображения, полученного горизонтальным зеркальным отражением исходного:

Результат зеркального отражёния

Результат зеркального отражения

Далее рассмотрим негативную версию первоначальной фотографии:

Результат обращения цветов

Результат обращения цветов

А вот чёрно-белая версия исходного изображения:

Результат обесцвечивания

Результат обесцвечивания

Вот, что будет, если в исходном изображении циклически переставить RGB-каналы:

Результат циклической перестановки RGB-каналов

Результат циклической перестановки RGB-каналов

Следующее изображение снова получено циклической перестановкой RGB-каналов, но уже не исходного изображения, а предыдущего:

Результат повторной циклической перестановки RGB-каналов

Результат повторной циклической перестановки RGB-каналов

Как видим, тестирование прошло успешно: мы получили то, чего ожидали.

Заключение

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

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

Скачать исходный код