Загрузка и сохранение изображений в C: оптимальные библиотеки
Для кого эта статья:
- Начинающие программисты, изучающие язык C и обработку изображений
- Профессионалы, желающие улучшить свои навыки работы с графикой на C
Разработчики встраиваемых систем и программисты, интересующиеся оптимизацией памяти и производительности
Работа с изображениями в языке C часто вызывает панический ужас у начинающих программистов. Многие считают, что без использования C++ или более высокоуровневых языков обработка графики — это непреодолимый квест. Однако профессионалы знают: C предоставляет мощные инструменты для эффективной работы с изображениями, позволяя создавать быстрые и легковесные решения. Загрузка и сохранение графических файлов в C требует понимания некоторых ключевых концепций и выбора правильных библиотек, но при правильном подходе это становится рутинной задачей. 🖼️
Осваиваете программирование и хотите углубить знания не только в C, но и получить комплексные навыки разработки? Курс Java-разработки от Skypro — идеальное дополнение к вашему профессиональному набору инструментов. Java, как и C, позволяет эффективно работать с изображениями, но предлагает более высокоуровневый подход. Добавьте к своему арсеналу язык, востребованный на рынке, и станьте универсальным разработчиком, способным решать задачи любой сложности!
Основы работы с изображениями в языке C
Работа с изображениями в C начинается с понимания, что любое изображение — это просто структурированный набор данных. Без встроенных средств для работы с графикой, C требует четкого понимания форматов изображений и способов их представления в памяти.
Фундаментально, цифровое изображение — это двумерный массив пикселей, где каждый пиксель содержит информацию о цвете. В зависимости от формата изображения и глубины цвета, пиксель может занимать от одного бита (черно-белое изображение) до нескольких байтов (полноцветное изображение с альфа-каналом).
| Тип изображения | Бит на пиксель | Описание |
|---|---|---|
| Черно-белое (1-битное) | 1 | Каждый пиксель может быть только черным или белым |
| Оттенки серого | 8 | 256 оттенков серого от черного до белого |
| RGB | 24 | 8 бит на каждый канал (красный, зеленый, синий) |
| RGBA | 32 | RGB + альфа-канал для прозрачности |
При работе с изображениями в C необходимо учитывать следующие аспекты:
- Формат файла — определяет структуру данных и способы сжатия (PNG, JPEG, BMP)
- Размеры изображения — ширина и высота в пикселях
- Глубина цвета — количество бит для хранения цвета одного пикселя
- Порядок байтов — особенно важно при работе с многобайтными значениями
- Управление памятью — выделение и освобождение памяти для данных изображения
Базовая структура для представления изображения в C может выглядеть так:
typedef struct {
unsigned char* data; // Массив пикселей
int width; // Ширина изображения
int height; // Высота изображения
int channels; // Количество каналов (3 для RGB, 4 для RGBA)
} Image;
Важно понимать, что в языке C нет встроенных функций для работы с изображениями. Для чтения и записи файлов изображений требуются либо собственные реализации алгоритмов кодирования и декодирования, либо использование специализированных библиотек.

Библиотеки для управления изображениями в C
Экосистема C предлагает несколько проверенных библиотек для обработки изображений, каждая со своими преимуществами и особенностями. 📚
Александр Петров, руководитель отдела встраиваемых систем
Три года назад мы разрабатывали программное обеспечение для промышленного сканера, работающего на микроконтроллере с ограниченными ресурсами. Требовалось обрабатывать снимки в высоком разрешении, сохраняя их в различных форматах. Попытки использовать тяжеловесные библиотеки с множеством зависимостей приводили к нехватке памяти и непредсказуемому поведению устройства.
Решением стала библиотека stb_image — минималистичное решение, состоящее всего из одного заголовочного файла. Мы смогли интегрировать только нужные функции, что сократило размер кода на 60% по сравнению с libpng и libjpeg. Производительность выросла на 40%, а объем используемой памяти уменьшился вдвое. Сегодня это стандартное решение для всех наших встраиваемых проектов с ограниченными ресурсами.
При выборе библиотеки важно учитывать поддерживаемые форматы, производительность, размер кода и сложность интеграции:
| Библиотека | Поддерживаемые форматы | Особенности | Размер | Лицензия |
|---|---|---|---|---|
| libpng | PNG | Высокая производительность, полная поддержка спецификации PNG | Средний | libpng license (аналог zlib) |
| libjpeg / libjpeg-turbo | JPEG | Стандартная библиотека для JPEG, turbo-версия с SIMD-оптимизациями | Средний | IJG license / BSD |
| stb_image | JPEG, PNG, BMP, TGA, PSD, GIF, HDR, PIC | Минималистичная, "header-only" библиотека | Маленький | Public Domain / MIT |
| FreeImage | BMP, JPEG, TIFF, PNG, и многие другие | Универсальный API для различных форматов | Большой | FIPL (FreeImage Public License) |
| ImageMagick | Более 200 форматов | Полнофункциональное решение для профессиональной обработки | Очень большой | Apache 2.0 |
Для большинства проектов оптимальным выбором являются:
- stb_image — для простых задач и встраиваемых систем с ограниченными ресурсами
- libpng + libjpeg — для проектов, требующих полной поддержки форматов и высокой производительности
- FreeImage — для приложений, работающих с множеством форматов через единый API
Установка библиотек выполняется стандартными средствами управления пакетами. Например, для Ubuntu:
# Для libpng и libjpeg
sudo apt-get install libpng-dev libjpeg-dev
# Для FreeImage
sudo apt-get install libfreeimage-dev
# Для ImageMagick
sudo apt-get install libmagickwand-dev
Для stb_image достаточно скачать единственный заголовочный файл с GitHub и включить его в проект.
Реализация загрузки изображений разных форматов
Разработка кода для загрузки изображений в C требует внимания к деталям и понимания особенностей каждого формата. Рассмотрим наиболее популярные подходы и примеры кода. 🔍
Загрузка изображений с использованием stb_image
Библиотека stb_image предлагает самый простой способ загрузки изображений различных форматов. Достаточно включить один заголовочный файл и определить реализацию перед включением:
#define STB_IMAGE_IMPLEMENTATION
#include "stb_image.h"
// Функция для загрузки изображения
unsigned char* load_image(const char* filename, int* width, int* height, int* channels) {
unsigned char* data = stbi_load(filename, width, height, channels, 0);
if (data == NULL) {
fprintf(stderr, "Ошибка при загрузке изображения: %s\n", stbi_failure_reason());
return NULL;
}
return data;
}
// Пример использования
int main() {
int width, height, channels;
unsigned char* img = load_image("example.jpg", &width, &height, &channels);
if (!img) {
return 1;
}
printf("Изображение загружено: %dx%d, %d каналов\n", width, height, channels);
// Здесь можно обрабатывать изображение
// Освобождение памяти
stbi_image_free(img);
return 0;
}
Преимущество stb_image — универсальность и простота. Одна функция автоматически определяет формат и загружает изображение в единую структуру данных.
Загрузка PNG с использованием libpng
Для более полного контроля над процессом загрузки, особенно при работе с PNG-файлами, libpng предоставляет более детальный API:
#include <png.h>
#include <stdio.h>
#include <stdlib.h>
// Функция загрузки PNG-изображения
unsigned char* load_png(const char* filename, int* width, int* height) {
FILE* fp = fopen(filename, "rb");
if (!fp) {
return NULL;
}
png_structp png = png_create_read_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
if (!png) {
fclose(fp);
return NULL;
}
png_infop info = png_create_info_struct(png);
if (!info) {
png_destroy_read_struct(&png, NULL, NULL);
fclose(fp);
return NULL;
}
if (setjmp(png_jmpbuf(png))) {
png_destroy_read_struct(&png, &info, NULL);
fclose(fp);
return NULL;
}
png_init_io(png, fp);
png_read_info(png, info);
*width = png_get_image_width(png, info);
*height = png_get_image_height(png, info);
png_byte color_type = png_get_color_type(png, info);
png_byte bit_depth = png_get_bit_depth(png, info);
// Преобразование к 8 бит на канал RGBA, если необходимо
if (bit_depth == 16)
png_set_strip_16(png);
if (color_type == PNG_COLOR_TYPE_PALETTE)
png_set_palette_to_rgb(png);
if (color_type == PNG_COLOR_TYPE_GRAY && bit_depth < 8)
png_set_expand_gray_1_2_4_to_8(png);
if (png_get_valid(png, info, PNG_INFO_tRNS))
png_set_tRNS_to_alpha(png);
if (color_type == PNG_COLOR_TYPE_RGB ||
color_type == PNG_COLOR_TYPE_GRAY ||
color_type == PNG_COLOR_TYPE_PALETTE)
png_set_filler(png, 0xFF, PNG_FILLER_AFTER);
if (color_type == PNG_COLOR_TYPE_GRAY ||
color_type == PNG_COLOR_TYPE_GRAY_ALPHA)
png_set_gray_to_rgb(png);
png_read_update_info(png, info);
int rowbytes = png_get_rowbytes(png, info);
unsigned char* image_data = (unsigned char*)malloc(*height * rowbytes);
png_bytep* row_pointers = (png_bytep*)malloc(*height * sizeof(png_bytep));
for (int y = 0; y < *height; y++)
row_pointers[y] = image_data + y * rowbytes;
png_read_image(png, row_pointers);
fclose(fp);
free(row_pointers);
png_destroy_read_struct(&png, &info, NULL);
return image_data;
}
Михаил Соколов, разработчик систем компьютерного зрения
В одном из медицинских проектов мы столкнулись с необходимостью загружать и анализировать рентгеновские снимки высокого разрешения. Изначально мы использовали универсальную библиотеку, но её производительность оказалась недостаточной — на загрузку 20-мегапиксельного изображения уходило до 700 мс.
Переписав код с использованием libjpeg-turbo с ручной оптимизацией буферов и прямым доступом к строкам изображения, мы сократили время загрузки до 150 мс. Ключевым фактором стало использование SIMD-инструкций для параллельной обработки данных и асинхронное чтение блоков изображения с диска.
Эта оптимизация позволила врачам просматривать серии из сотен снимков без ощутимых задержек, что было критично для диагностического процесса. Дополнительным бонусом стало снижение нагрузки на процессор, что позволило выполнять предварительный анализ изображений в фоновом режиме.
При загрузке JPEG-файлов с использованием libjpeg рекомендуется использовать постепенную загрузку для больших изображений:
#include <stdio.h>
#include <stdlib.h>
#include <jpeglib.h>
unsigned char* load_jpeg(const char* filename, int* width, int* height, int* channels) {
struct jpeg_decompress_struct cinfo;
struct jpeg_error_mgr jerr;
FILE* infile = fopen(filename, "rb");
if (!infile) {
return NULL;
}
cinfo.err = jpeg_std_error(&jerr);
jpeg_create_decompress(&cinfo);
jpeg_stdio_src(&cinfo, infile);
jpeg_read_header(&cinfo, TRUE);
jpeg_start_decompress(&cinfo);
*width = cinfo.output_width;
*height = cinfo.output_height;
*channels = cinfo.output_components;
int row_stride = (*width) * (*channels);
unsigned char* buffer = (unsigned char*)malloc((*height) * row_stride);
while (cinfo.output_scanline < cinfo.output_height) {
unsigned char* row_pointer = buffer + (cinfo.output_scanline * row_stride);
jpeg_read_scanlines(&cinfo, &row_pointer, 1);
}
jpeg_finish_decompress(&cinfo);
jpeg_destroy_decompress(&cinfo);
fclose(infile);
return buffer;
}
Для проектов, требующих поддержки множества форматов с единым API, FreeImage предлагает более высокоуровневый подход:
#include <FreeImage.h>
// Необходимо вызвать FreeImage_Initialise() при запуске программы
// и FreeImage_DeInitialise() при завершении
FIBITMAP* load_image_freeimage(const char* filename) {
FREE_IMAGE_FORMAT format = FreeImage_GetFileType(filename, 0);
if (format == FIF_UNKNOWN) {
format = FreeImage_GetFIFFromFilename(filename);
}
if (format == FIF_UNKNOWN) {
return NULL;
}
FIBITMAP* bitmap = FreeImage_Load(format, filename, 0);
if (!bitmap) {
return NULL;
}
// Преобразование к 32-битному RGBA для унификации
FIBITMAP* temp = bitmap;
bitmap = FreeImage_ConvertTo32Bits(temp);
FreeImage_Unload(temp);
return bitmap;
}
При выборе метода загрузки изображений следует учитывать следующие факторы:
- Требуемые форматы — для узкоспециализированных задач лучше использовать специфичные библиотеки (libpng, libjpeg)
- Ресурсы системы — для встраиваемых систем предпочтительнее легковесные решения (stb_image)
- Сложность кода — универсальные API (FreeImage) упрощают поддержку кода
- Производительность — специализированные библиотеки часто быстрее, особенно с дополнительными оптимизациями
Техники сохранения изображений в файлы
Сохранение изображений в файлы требует понимания форматов и их особенностей. Рассмотрим наиболее эффективные подходы для различных библиотек. 💾
Наиболее простой способ сохранения изображений реализован в stbimagewrite:
#define STB_IMAGE_WRITE_IMPLEMENTATION
#include "stb_image_write.h"
int save_image(const char* filename, int width, int height, int channels,
unsigned char* data) {
// Определение формата по расширению
const char* ext = strrchr(filename, '.');
if (!ext) return 0;
if (strcmp(ext, ".png") == 0) {
// Сохранение в PNG (сжатие 8 из 10)
return stbi_write_png(filename, width, height, channels, data, width * channels);
}
else if (strcmp(ext, ".jpg") == 0 || strcmp(ext, ".jpeg") == 0) {
// Сохранение в JPEG (качество 90 из 100)
return stbi_write_jpg(filename, width, height, channels, data, 90);
}
else if (strcmp(ext, ".bmp") == 0) {
return stbi_write_bmp(filename, width, height, channels, data);
}
else if (strcmp(ext, ".tga") == 0) {
return stbi_write_tga(filename, width, height, channels, data);
}
return 0; // Неподдерживаемый формат
}
Для более гибкого контроля при сохранении PNG с использованием libpng:
#include <png.h>
int save_png(const char* filename, int width, int height, int channels,
unsigned char* data, int compression_level) {
FILE *fp = fopen(filename, "wb");
if (!fp) return 0;
png_structp png = png_create_write_struct(PNG_LIBPNG_VER_STRING, NULL, NULL, NULL);
if (!png) {
fclose(fp);
return 0;
}
png_infop info = png_create_info_struct(png);
if (!info) {
png_destroy_write_struct(&png, NULL);
fclose(fp);
return 0;
}
if (setjmp(png_jmpbuf(png))) {
png_destroy_write_struct(&png, &info);
fclose(fp);
return 0;
}
png_init_io(png, fp);
// Установка уровня сжатия (0-9, где 0 – без сжатия, 9 – максимальное сжатие)
png_set_compression_level(png, compression_level);
// Настройка параметров изображения
int color_type = channels == 4 ? PNG_COLOR_TYPE_RGBA : PNG_COLOR_TYPE_RGB;
png_set_IHDR(png, info, width, height, 8, color_type,
PNG_INTERLACE_NONE, PNG_COMPRESSION_TYPE_DEFAULT,
PNG_FILTER_TYPE_DEFAULT);
// Запись заголовка
png_write_info(png, info);
// Подготовка массива указателей на строки изображения
png_bytep* row_pointers = (png_bytep*)malloc(height * sizeof(png_bytep));
int stride = width * channels;
for (int y = 0; y < height; y++) {
row_pointers[y] = data + y * stride;
}
// Запись данных изображения
png_write_image(png, row_pointers);
png_write_end(png, NULL);
// Освобождение ресурсов
free(row_pointers);
png_destroy_write_struct(&png, &info);
fclose(fp);
return 1;
}
Для сохранения JPEG с контролем качества, используя libjpeg:
#include <jpeglib.h>
int save_jpeg(const char* filename, int width, int height, int channels,
unsigned char* data, int quality) {
// JPEG поддерживает только 1 или 3 канала (grayscale или RGB)
if (channels != 1 && channels != 3) return 0;
struct jpeg_compress_struct cinfo;
struct jpeg_error_mgr jerr;
FILE* outfile;
JSAMPROW row_pointer[1];
outfile = fopen(filename, "wb");
if (!outfile) return 0;
cinfo.err = jpeg_std_error(&jerr);
jpeg_create_compress(&cinfo);
jpeg_stdio_dest(&cinfo, outfile);
cinfo.image_width = width;
cinfo.image_height = height;
cinfo.input_components = channels;
cinfo.in_color_space = channels == 3 ? JCS_RGB : JCS_GRAYSCALE;
jpeg_set_defaults(&cinfo);
jpeg_set_quality(&cinfo, quality, TRUE);
jpeg_start_compress(&cinfo, TRUE);
int stride = width * channels;
while (cinfo.next_scanline < cinfo.image_height) {
row_pointer[0] = &data[cinfo.next_scanline * stride];
jpeg_write_scanlines(&cinfo, row_pointer, 1);
}
jpeg_finish_compress(&cinfo);
jpeg_destroy_compress(&cinfo);
fclose(outfile);
return 1;
}
При сохранении изображений важно учитывать следующие аспекты:
- Сжатие и качество — выбор оптимального баланса между размером файла и качеством изображения
- Формат — PNG для изображений с прозрачностью, JPEG для фотографий, BMP для полного сохранения данных
- Каналы — проверка соответствия каналов допустимым для формата (JPEG не поддерживает альфа-канал)
- Метаданные — при необходимости добавление EXIF, ICC-профилей и других метаданных
Для универсального сохранения с поддержкой различных форматов через единый API, FreeImage предлагает простое решение:
#include <FreeImage.h>
int save_image_freeimage(FIBITMAP* bitmap, const char* filename, int flags) {
FREE_IMAGE_FORMAT format = FreeImage_GetFIFFromFilename(filename);
if (format == FIF_UNKNOWN) {
return 0;
}
if (!FreeImage_FIFSupportsWriting(format)) {
return 0;
}
return FreeImage_Save(format, bitmap, filename, flags);
}
// Пример использования
int main() {
FreeImage_Initialise();
FIBITMAP* bitmap = FreeImage_Allocate(800, 600, 24);
// Заполнение bitmap данными...
// Сохранение в JPEG с качеством 90%
save_image_freeimage(bitmap, "output.jpg", JPEG_QUALITYGOOD);
// Сохранение в PNG с максимальным сжатием
save_image_freeimage(bitmap, "output.png", PNG_Z_BEST_COMPRESSION);
FreeImage_Unload(bitmap);
FreeImage_DeInitialise();
return 0;
}
Оптимизация кода при обработке графических данных
Оптимизация кода для работы с изображениями в C требует сочетания алгоритмических улучшений и грамотного управления ресурсами. ⚡
Одним из главных ограничений при работе с изображениями является размер памяти. Рассмотрим техники эффективного использования памяти:
- Строчная обработка — загрузка и обработка изображения построчно, без хранения всего изображения в памяти
- Преобразование на лету — изменение формата или размера изображения в процессе загрузки
- Повторное использование буферов — создание пула буферов для многократного использования
- Управление памятью вручную — предварительное выделение и явное освобождение памяти
Пример оптимизированной загрузки большого изображения порциями:
// Функция для обработки изображения построчно
int process_image_in_chunks(const char* filename,
void (*process_row)(unsigned char* row, int width, int channels)) {
int width, height, channels;
// Открыть файл и прочитать заголовок для получения размеров
FILE* f = fopen(filename, "rb");
if (!f) return 0;
stbi__context s;
stbi__start_file(&s, f);
int result = stbi__jpeg_info(&s, &width, &height, &channels);
if (!result) {
fclose(f);
return 0;
}
// Сбросить позицию файла и инициализировать декодер JPEG
fseek(f, 0, SEEK_SET);
struct jpeg_decompress_struct cinfo;
struct jpeg_error_mgr jerr;
cinfo.err = jpeg_std_error(&jerr);
jpeg_create_decompress(&cinfo);
jpeg_stdio_src(&cinfo, f);
jpeg_read_header(&cinfo, TRUE);
jpeg_start_decompress(&cinfo);
int row_stride = cinfo.output_width * cinfo.output_components;
JSAMPARRAY buffer = (*cinfo.mem->alloc_sarray)
((j_common_ptr) &cinfo, JPOOL_IMAGE, row_stride, 1);
// Обработка по одной строке за раз
while (cinfo.output_scanline < cinfo.output_height) {
jpeg_read_scanlines(&cinfo, buffer, 1);
process_row(buffer[0], cinfo.output_width, cinfo.output_components);
}
jpeg_finish_decompress(&cinfo);
jpeg_destroy_decompress(&cinfo);
fclose(f);
return 1;
}
Для повышения скорости обработки данных в C можно использовать различные техники оптимизации:
| Техника оптимизации | Описание | Применимость | Потенциальный прирост |
|---|---|---|---|
| SIMD инструкции | Использование SSE/AVX/NEON для параллельной обработки пикселей | Фильтры, преобразования цветового пространства | 2x-8x |
| Многопоточность | Распределение обработки между потоками | Большие изображения, независимые операции | Почти линейный рост с числом ядер |
| Кэширование данных | Переупорядочивание доступа для улучшения локальности | Итеративные алгоритмы, обработка плиткой | 1.5x-3x |
| Оптимизация алгоритмов | Снижение вычислительной сложности | Всегда применима | Зависит от алгоритма |
| Оптимизация памяти | Выравнивание данных, минимизация копирования | Всегда применима | 1.2x-2x |
Пример использования SIMD-инструкций для ускорения конвертации RGB в оттенки серого:
#include <immintrin.h> // Для SSE/AVX инструкций
// Неоптимизированная версия
void rgb_to_gray_scalar(unsigned char* rgb, unsigned char* gray, int pixel_count) {
for (int i = 0; i < pixel_count; i++) {
int r = rgb[i*3];
int g = rgb[i*3 + 1];
int b = rgb[i*3 + 2];
// Стандартные коэффициенты для преобразования RGB в оттенки серого
gray[i] = (unsigned char)(0.299f * r + 0.587f * g + 0.114f * b);
}
}
// Оптимизированная версия с использованием SSE
void rgb_to_gray_sse(unsigned char* rgb, unsigned char* gray, int pixel_count) {
// Коэффициенты для преобразования RGB в серый
__m128 coeffs = _mm_setr_ps(0.299f, 0.587f, 0.114f, 0.0f);
int i = 0;
for (; i <= pixel_count – 4; i += 4) {
// Загружаем 12 байтов RGB (4 пикселя)
__m128i rgb0 = _mm_loadu_si128((__m128i*)(rgb + i*3));
__m128i rgb1 = _mm_loadu_si128((__m128i*)(rgb + i*3 + 12));
// Распаковываем и конвертируем в float
__m128i r0g0b0r1 = _mm_unpacklo_epi8(rgb0, _mm_setzero_si128());
__m128i g1b1r2g2 = _mm_unpackhi_epi8(rgb0, _mm_setzero_si128());
__m128i b2r3g3b3 = _mm_unpacklo_epi8(rgb1, _mm_setzero_si128());
// Дальнейшая распаковка и умножение на коэффициенты
// ... (сложная реализация с шафлами и умножениями опущена)
// Запись результата
// ... (упаковка обратно в байты и сохранение)
}
// Обработка оставшихся пикселей
for (; i < pixel_count; i++) {
int r = rgb[i*3];
int g = rgb[i*3 + 1];
int b = rgb[i*3 + 2];
gray[i] = (unsigned char)(0.299f * r + 0.587f * g + 0.114f * b);
}
}
Важные рекомендации для оптимизации кода при работе с изображениями:
- Профилирование — определение узких мест перед оптимизацией
- Выбор подходящих структур данных — использование непрерывных массивов вместо связных структур
- Предварительная обработка — подготовка таблиц поиска для сложных вычислений
- Использование кэша процессора — организация доступа к памяти для максимального использования кэша
- Асинхронные операции — разделение I/O и вычислений для параллельного выполнения
Мощь языка C при работе с изображениями кроется в сочетании низкоуровневого контроля и высокой производительности. Правильный выбор библиотеки — только первый шаг; реальные преимущества приходят с грамотной оптимизацией и пониманием принципов работы с памятью. Независимо от сложности проекта — от встраиваемых систем до высоконагруженных серверов обработки графики — C предоставляет инструменты для создания эффективных решений. Освоив техники, описанные в этой статье, вы сможете разрабатывать код, который не только правильно обрабатывает изображения, но делает это максимально эффективно.
Читайте также
- Библиотека graphics.h: полное руководство для C/C++ разработчиков
- Графические библиотеки C: выбор инструментов для 2D и 3D разработки
- Библиотека graphics.h в C/C++: 15 примеров от новичка до профи
- Настройка графики на C: OpenGL, GLFW, SDL2 для новичков
- Построение графиков функций в C: лучшие библиотеки и примеры
- OpenGL и C: базовые принципы создания 2D и 3D графики
- Графическое программирование на C: точки и координаты как основа
- Графические интерфейсы на C: создание эффективных GUI-приложений
- Основы компьютерной графики на C: от точек и линий к алгоритмам
- Язык C в компьютерной графике: от ASCII-арта до 3D-рендеров