Графический редактор

В данной статье рассматривается создание простейшего графического редактора.
Наш редактор будет обладать самым минимальным функционалом: карандаш, кисть, ластик, рисование простейших фигур (линия, прямоугольник, овал), выбор цвета. Выбор цвета как из минимального набора в цветовой панели, так и из расширенного набора цветов.
Кроме того, можно будет загружать изображения, редактировать их и сохранять. Работа с изображениями в формате jpg и png.
Создание простейшего графического редактора
Цель: Изучение компонентов Image, ScrollBox, компонетов-диалогов, а также обработки событий мыши.
Краткие теоретические сведения.
Компонент Image представляют собой некоторую ограниченную поверхность с канвой, на которую можно заносить изображения. Свойства и методы работы с канвой описаны в Л/р№4.
Компонент Image используется для отображения графики: пиктограмм, битовых матриц и метафайлов. Для этого служит его свойство Picture — объект типа TPicture. Если установить свойство AutoSize в true, то размер компонента Image будет автоматически подгоняться под размер помещенной в него картинки. Если же свойство AutoSize установлено в false, то изображение может не поместиться в компонент или, наоборот, площадь компонента может оказаться много больше площади изображения. Другое свойство — Stretch позволяет подгонять не компонент под размер рисунка, а рисунок под размер компонента
Свойство — Center, установленное в true, центрирует изображение на площади Image, если размер компонента больше размера рисунка.
Рассмотрим еще одно свойство — Transparent (прозрачность). Если Transparent равно true, то изображение в Image становится прозрачным. Это можно использовать для наложения изображений друг на друга.
ScrollBox — панель с прокруткой. Этот компонент предназначен для создания области, в которой могут размещаться компоненты, занимающие площадь большую, чем сам ScrollBox. Если размеры ScrollBox меньше, чем размещенные компоненты, то появляются полосы прокрутки, которые позволяют пользователю перемещаться по всем размещенным в ScrollBox компонентам.
Свойства компонентов OpenPictureDialog и SavePictureDialog ничем не отличаются от свойств компонентов OpenDialog и SaveDialog. Единственное отличие — заданное значение по умолчанию свойства Filter в OpenPictureDialog и SavePictureDialog. В этих компонентах заданы следующие фильтры:
Простой графический редактор с использованием OpenCV
В этой статье я расскажу, как достаточно быстро и просто написать редактор изображений на C++ с использованием библиотеки компьютерного зрения opencv. Реализованы такие эффекты, как насыщенность, экспозиция, резкость, контрастность и другие. Никакой магии!

Внимание! Под катом много графики и кода.
Итак, начнем…
Насыщенность
Ингредиенты:
— система цветности HSV,
— функция разбиения на слои «split»,
— функция объединения слоев «merge».
Для изменения насыщенности изображение преобразуется в систему цветности HSV и разбивается на слои. К значениям слоя «Sature» прибавляется шаг. Слои объединяются. Все просто:

Экспозиция
График функции x + sin(x * 0.01255) * step * 10
Функция в основном затрагивает середину диапазона.
Оттенок
Ингредиенты:
— система цветности RGB,
— функция «split», «merge» и «LUT»,
— гистограммы, преобразованные функцией экспозиции, для красного, синего и зеленого каналов,
— защита от переполнения значений гистограммы.
Параметр оттенка характеризует наличие в изображении зеленого и пурпурного цвета. В системе цветности RGB можно управлять зеленым слоем, но при этом нужно не забывать компенсировать падение яркости другим двумя слоями. Для преобразования красного и синего слоев используется положительная гамма-функция экспозиции, для зеленого – отрицательная.

Цветовая температура
Ингредиенты: те же, что и в оттенке, но гистограммы для красного и зеленого положительные, а для синего слоя двойная отрицательная.
Цветовая температура характеризует наличие в изображении желтого и синего цветов. Значит будем «крутить» синий.

Свет и тени
Ингредиенты:
— система цветности HSV,
— функция «split», «merge», «LUT»,
— гистограмма теней, преобразованная функцией (0.36811145*e)^(-(x^1.7))*0.2x*step,
— гистограмма светов, преобразованная функцией (0.36811145*e)^(-(256 — x)^1.7)*0.2(256-x)*step,
— защита от переполнения значений гистограммы.
Параметр «свет» характеризует яркость светлых областей изображения, а параметр «тени» — яркость темных областей. Преобразовывать будем канал яркостей.
<img src https://habrastorage.org/r/w780q1/files/993/f4b/384/993f4b38469a4cc2b34de7b91a2198b8.jpg» data-src=»https://habrastorage.org/files/993/f4b/384/993f4b38469a4cc2b34de7b91a2198b8.jpg» data-blurred=»true»/>» alt=«image»/>
На графике функция преобразования теней обозначается красной линией, функция света – зеленой.


Контраст
Ингредиенты:
— система цветности RGB,
— функция «split», «merge», «LUT»,
— уровень контраста «(100+step)/100»,
— гистограмма контрастности, полученная из формулы ((x/255 – 0.5)*constrastLevel + 0.5)*255.
Контраст определяется в разности яркостей. Т.е. для увеличения контраста нам нужно раздвинуть диапазон яркостей от центра к краям. Преобразование выполняется для всех слоев.

Красная линия – повышенный контраст, зеленая – пониженный.

Резкость
Ингредиенты:
— функция размытия «blur»,
— матрица свертки, с рассчитанными коэффициентами,
— функция преобразования матрицей свертки «filter2D»,
— копия изображения.
Резкость (четкость) определяется выделением отдельных элементов, их контуров. Величина, обратная резкости – размытость.
В opencv для размытия изображения используем функцию blur, принимающую в качестве параметров исходное изображение, выходное изображение, и размер матрицы размытия. От размера матрицы размытия и зависит сила размытия. Этот размер должен быть четным, чтобы не указывать вручную центр матрицы.
Четкость в opencv проще всего повысить с помощью матрицы свертки, используя специальную для этого матрицу. Функция «filter2D», которая принимает исходное изображение, результирующее изображение, количество бит на значение матрицы свертки, матрицу свертки, выполняет непосредственно преобразование. Итак, как будет выглядеть метод повышения/понижения четкости.
6.1 Уроки OpenGL. Создание растрового редактора на OpenGL + C#.
2. Изучение модели работы программы. Описание создаваемых классов и методов их взаимодействий.
Итак, начнем мы с определения того, что понимается под растровым рисунком.
Как известно, растровое изображение – это структура данных, представленная таким образом, что основывается на сетке, состоящей из пикселей (точек). Как правило, эта структура имеет прямоугольную форму и визуализируется на экране компьютера. Создание растрового редактора сводится к разработке такой программы, которая будет снабжена набором инструментов для редактирования растровой структуры данных. Все эти элементы основаны на математических алгоритмах компьютерной графики.
Разрабатываемый нами графический редактор будет работать по следующему принципу:
Пользователь работает с программой: основываясь на визуализированных с помощью оболочки программы данных, он выполняет определенные действия – выбирает инструменты, рисует в рабочей области рисунка, управляет слоями. Все эти действия, основанные на обработке карты сообщений от операционной системы оболочкой программы, которая обрабатывает их и вызывает методы класса anEngine. Этот управляющий класс отвечает за основные просчеты и визуализацию – он управляет созданием слоев, через него идет обновление информации в слоях, калькулируется результирующее изображение и т.д. Этот класс мы будет называть «движком» нашего графического редактора.
Отсюда же идет управление кистями. Для хранения кистей будет использоваться отдельный класс.
Сейчас мы реализуем не все функции, а лишь самые главные, которые должны обеспечить работоспособность программы – основа оболочки, класс движка, класс слоев, класс кистей. Работать на самом деле будет только один слой, одна кисть и алгоритмы инициализации OpenGL (в оболочке), а также визуализации.
Большую часть мы усложним и доработаем в последствии. Схема работы программы так же представлена на рисунке 1.
Рисунок 1. Схема работы разрабатываемого растрового редактора.
Теперь когда наши цели определены и оглашена схема работы программы, мы перейдем к написанию её основы.
3. Создание базовой оболочки программы.
- Меню программы. На данный момент мы создадим 3 раздела меню: Файл, Рисование и Слои (их подменю можно увидеть на рисунке 3).
Рисунок 3. Примеры создаваемых меню. - Панель инструментов. Ширина панели 44 пикселя. Создайте на будущее заготовки 3-х кнопок. Эту панель в последующих частях главы мы заполним кнопками для установки режимов рисования кистей, рисования геометрических объектов и т.д.
- Элемент SimpleOpenGLControl. Расположите его так, как показано на рисунке, затем свойство name установите равным AnT. Здесь будет проходить основной render.
- Здесь находится элемент CheckedListBox. В следующей части главы мы реализуем систему слоев, которые будут активно участвовать в визуализации. В этом элементе будет отображаться список наших слоев. Оболочка также получит функции для редактирования параметров слоев.
- Здесь будет находиться панель инструментов. Пока что мы расположим на ней 2 кнопки. В дальнейшем кнопки этой панели будут отвечать за операции над слоями.
Когда элементы размещены, коснемся на минуту кода оболочки, после чего перейдем к созданию дополнительных классов.
Не забудьте подключить необходимые пространства имен:
Конструктор формы выглядит следующим образом:
Далее добавим код объявления экземпляра класса движка растрового редактора:
Когда вы добавите этот код, он выделится, как будто содержит ошибку – в принципе, так оно и есть, так как мы еще не добавили код самого класса anEngine. Но сначала мы завершим код оболочки, а потом уже перейдем к функциям дополнительных классов (иначе мы очень быстро запутаемся).
Теперь добавьте один таймер, назовите его RenderTimer. Затем добавьте обработку события загрузки формы.
Функция, вызываемая событием загрузки формы (чтобы ее добавить, щелкните двойным щелчком левой клавиши мыши по заголовку окна или перейдите через свойства формы приложения к списку доступных событий (event) и добавьте двойным щелчком в пустой области обработку события Load).
Эта функция будет содержать код инициализации OpenGL. Ее код представлен ниже:
Как видно из кода, мы создаем 2D ортогональную проекцию, причем в отличие от кода, который мы использовали в предыдущих главах, здесь мы устанавливаем проекцию таким образом, что размер видимой области в проекции будет равен размерам элемента AnT. Другими словами, координата X на элементе AnT будет равна координате X видимой части в координатной системе OpenGL. Но координатная ось Y направлена в противоположенную сторону, поэтому координата Y в координатной системе OpenGL будет равна AnT.Height – Y’ (Y’ – координата Y на элементе AnT).
После настройки проекции мы инициализируем экземпляр движка растрового редактора и активируем таймер.
В настройках таймера добавьте обработчик события Tick, либо выделив элемент таймер (далее перейти к его свойствам – меню Event, затем двойной щелчок на пустой строке справа от события event), либо просто двойным щелчком на элементе RenderTimer.
В свойствах таймера установите интервал равным 30 миллисекундам.
Код функции обработчика события таймера и функции визуализации будут выглядеть следующим образом:
Когда мы создавали программу, визуализирующую график функции, мы уже назначали объекту AnT обработку события движения мыши. Аналогичные действия необходимо совершить и здесь.
Код функции-обработчика будет выглядеть следующим образом:
С кодом оболочки для данной главы все. Теперь необходимо как минимум добавить класс anEngine и реализовать в нем методы и конструктор, иначе созданный нами на данном этапе код так и останется непригодным для компилирования.
Здесь немного коснемся того, что же происходит. С инициализацией OpenGL в функции Form1_Load, созданием счетчиков и назначением обработчиков событий – все понятно (иначе вы не выполняли предыдущие главы).
Функция обработки движения мыши проверяет, нажата ли левая клавиша мыши. Если да, то будет вызываться метод рисования кистью (Drawing), который мы далее реализуем в классе anEngine. В этот метод в качестве параметров передаются значения координаты на элементе AnT — X и AnT.Height – Y (так как оси Y у нас направлены в противоположенные стороны, мы это уже обсудили в настройках инициализации OpenGL).
Функция Drawing выполняет стандартные операции очистки буфера глубины и цвета, очищение объектно-видовой матрицы, после чего вызывается функция, отвечающая за визуализацию рисунка.
Вот и всё.
Теперь прейдем к созданию «ядра» или «движка» нашей программы.
4. Создание дополнительных классов и их базовых методов.
Добавьте в программу новый класс. Мы уже делали это в главе 1.3.
Назовите класс anEngine. Для него будет создана заготовка класса и отдельный файл с исходным кодом, называемый anEngine.cs. На самом деле здесь мы разместим еще два дополнительных класса и даже начнем с них, чтобы избежать путаницы. Сгенерированный код класса будет выглядеть следующим образом:
Приступим к его обновлению: первым делом добавим все пространства имен, что и в файле Form1.cs, чтобы в будущем не столкнуться с тем, что какое-либо пространство имен не подключено.
Подключаемые пространства имен:
Теперь переключимся на добавление в исходный код еще двух классов: anLayer и anBrush.
Начнем с anBrush. Разместите код этого класса перед кодом заготовки класса anEngine. Пока он будет не очень разнообразным: вся его функциональность будет сводиться к хранения маски кисти. Причем на данном этапе (создания заготовки всех классов и настройки их работоспособности) мы занесем в конструктор стандартную кисть в виде крестика.
Код класса anBrush:
Как видите, код данного класса пока очень прост. Но он нам необходим, чтобы сразу заложить алгоритм рисования кистью в классе anLayer.
Кисть, которую мы задаем по умолчанию кодом:
Легче представить и понять смысл этого кода, изучив рисунок полученного массива чисел (рис. 4).
Рисунок 4. Принцип, по которому задается маска кисти.
Теперь рассмотрим класс anLayer. Его задачей будет хранение данных закрашенных пикселей одного слоя изображения. Помимо хранения графических данных, слой также содержит алгоритм рисования на основе выбранной кисти: когда пользователь пытается что-либо нарисовать на окне, зажав левую клавишу мыши и перемещая курсор по элементу AnT, оболочка получает событие мыши, которое обрабатывается (корректируется координата Y) и передается в экземпляр класса anEngine с помощью функции Drawing. Этот код мы уже рассмотрели.
Когда будет вызываться функция Drawing, класс anEngine будет определять выбранную на данный момент кисть (пока что она у нас только одна), определять активный слой (слой пока что будет работать тоже только один, но в следующей части этой главы мы полностью доработаем систему слоев, кистей и функцию оболочки).
Дальше у необходимого слоя будет вызываться функция, отвечающая за рисование, где будут определяться различные факторы (например, ограничивающие рисунок по краям изображения, для того чтобы в процессе не выйти за границы рисования), а затем будет происходить изменение цветов пикселей маской кисти.
Код этого класса на данный момент будет выглядеть следующим образом:
Код очень подробно прокомментирован и довольно прост. Поясним лишь функцию рисования: в нее передаются в качестве параметров кисть и координаты, в которых должно пройти рисование.
Так как кисть в ширину и высоту может быть больше одного пикселя, нам необходимо определить середину кисти (для этого мы получаем ее ширину и высоту, через свойства Width и Heigth элемента myBrush (который, кстати, является public элементом).
Далее мы вычитаем из координаты, где должно произойти рисование, половину ширины кисти для оси X и половину высоты для оси Y. При этом мы можем получить отрицательные значения, если, например, кисть будет шириной 10 пикселей, нам, следовательно, надо будет сместиться на 5 пикселей влево, чтобы начать алгоритм рисования кистью. Но что если при этом координата X точки, где должно пройти рисование будет равна, к примеру, 2? Тогда мы получим отрицательно индекс элемента массива, откуда должно начаться рисование и получим ошибку.
Мы назовем это выходом за границу массива слева. Также выйти можно за границу справа, т.е. индексы получатся превышающими количество объявленных элементов в массиве.
Теперь остается проанализировать код класса anEngine. В нем мы заранее объявим некоторые методы, которые пока не будут задействованы, но они нам еще точно пригодятся.
Код класса anEngine:
Как видите, некоторые функции объявлены, но не несут никакой функциональной нагрузки.
Визуализация реализована так же, как просто вызов визуализации одного единственного слоя.
В дальнейшем мы реализуем алгоритм объединения всех слоев в одном для функции SwapImage, который будет генерировать единственный итоговый слой с учетом иерархии и настроек видимости, а потом уже будет визуализирован.
Но даже такой, на первый взгляд, недописанный код уже создает минимальный объем объектной модели программы, который заставит ее работать. На рисунке 5 вы можете видеть пример работы программы.
Рисунок 5. Проверка работы основы 2D растрового редактора.
Откомпелировав приложение, вы также можете удостовериться в работе функции рисования.
Теперь можно перейти к более точной реализации системы классов, кистей и визуализации, а также подключить все созданные элементы управления на оболочке программы.
Примечания
Если вы используете Visual Studio 2010, то вам необходимо добавить строку: