Introduction
In this article I will show you how to make a software like Photoshop except simpler, smaller and easier to use. You can load an image, change it into many color spaces (RGB, HSV, YCrCb), adjust brightness/contrast, do some filter (Noise, Blur ...), equalize histogram, view image information and so on. Image after processing can be saved with an optional quality.
This small software was written using Microsoft Visual Studio 2010 and OpenCV library (version 2.4.1). To use the code, you must have some basic knowledge in image processing, know how to use MFC in Visual Studio as well as OpenCV.
Using the Code
I divide this program into 3 parts: image processing core class, image preview class and main processing.
The Image processing core class contains many processing functions like adjust brightness, add noise, detect edge, rotate image and so on. All these functions have a form like this:
void Brightness(cv::Mat &src, cv::Mat &dst, int val)
Where src
is source image, dst
is image after processing and val
is value for a specific function. If We name the header file the ImgProcessing, then in the implementation file, the function above looks like this:
void ImgProcessing::Brightness(Mat &src, Mat &dst, int val)
{
if(src.channels() == 1)
{
for(int i = 0; i < src.rows; i++)
for(int j = 0; j < src.cols; j++)
dst.at<uchar>(i,j) = saturate_cast<uchar>(src.at<uchar>(i,j) + val);
}
if(src.channels() == 3)
{
for(int i = 0; i < src.rows; i++)
for(int j = 0; j < src.cols; j++)
for(int k = 0; k < 3; k++)
dst.at<Vec3b>(i,j)[k] = saturate_cast<uchar>(src.at<Vec3b>(i,j)[k] + val);
}
}
The image preview class is inherited from CDialog class. This class has unity functions that allow us to change parameters and preview what image should be like after processing.
Two images above show cases of previewing (noise and distort image). In fact, there are many cases need to be previewed before any decision was made. Because each function has its own type and its own parameters, so the number of dialog preview will be nearly equal to the number of processing function! That means we must create a lot of dialog and spend more time to write the code. It is not a good choice. To avoid creating many preview dialogs and writing more code, we create just only one and give it some options. The appearance and the returned value of preview dialog will depends on what the main process told it to do.
For example, suppose we are processing the brightness of an image, when the brightness adjustment menu is clicked, the image preview dialog will appear. And the OnInitDialog function will be defined as the following:
BOOL ImgPreview::OnInitDialog()
{
this->slider1.SetRange(0, 450);
this->slider1.SetPos(225);
this->slider2.SetRange(0, 450);
this->slider2.SetPos(225);
this->slider3.SetRange(0, 450);
this->slider3.SetPos(225);
this->slider2.EnableWindow(false);
this->slider3.EnableWindow(false);
switch(type)
{
case m_brightness:
{
this->param1.SetWindowTextA("Brightness");
}
break;
.....
}
return true;
}
When we change the position of slider on image preview dialog, the brightness of the image must be changed and displayed on the picture box. There are ways to display an image on controls in MFC (eg. picture control). In this article, I use a special unity function of MFC to force image to display. So let's create an Picture Control or arbitrary control you like. Change the ID of control to IDC_STATIC_IMG_PREVIEW
and when the dialog is initialized, put the following code to OnInitDialog
function:
BOOL ImgPreview::OnInitDialog()
{
cv::namedWindow("Image Preview", 1);
HWND hWnd = (HWND) cvGetWindowHandle("Image Preview");
HWND hParent = ::GetParent(hWnd);
::SetParent(hWnd, GetDlgItem(IDC_STATIC_IMG_PREVIEW)->m_hWnd);
::ShowWindow(hParent, SW_HIDE);
return true;
}
Until now, each time we call the cv::imshow("Image Preview", image)
function, It will force the picture control to show the image. Now, the code when changing slider position will be as following:
void ImgPreview::OnNMCustomdrawSlider1(NMHDR *pNMHDR, LRESULT *pResult)
{
LPNMCUSTOMDRAW pNMCD = reinterpret_cast<LPNMCUSTOMDRAW>(pNMHDR);
cv::Mat img_dst = img_display.clone();
switch(type)
{
case m_brightness:
{
i_param1 = slider1.GetPos() - (int)slider1.GetRangeMax()/2;
l_disp1.SetWindowTextA(ToString(i_param1));
process.Brightness(img_display, img_dst, i_param1);
cv::imshow("Image Preview", img_dst);
}
}}
img_display
is an image which resized from source image to be smaller to display on preview dialog.
If the OK button is pressed, the is_ok
member of ImgPreview class will be set to true to let the main process know that everything seems alright. Otherwise, it will be set to false (This is one way two dialogs communicate).
The main process is based on MFC dialog with a menu system, buttons and other controls. Let's create a menu system like this:
And then add event handler to each menu. The invert event handler
looks like this:
void CMyPhotoEditorDlg::OnUpdateAdjustmentsInvert(CCmdUI *pCmdUI)
{
process.Invert(src, src);
ImageDisplay(src);
.....
}
Where process
is an instance of ImgProcessing
class, and ImageDisplay()
is a function forced image to display in MFC control.
If menu that process an image need to be previewed, menu event handlers will look this:
void CMyPhotoEditorDlg::OnUpdateAdjustmentsBrightness(CCmdUI *pCmdUI)
{
ImgPreview dlg;
dlg.SetType(m_brightness);
dlg.src = src;
dlg.DoModal();
if(dlg.is_ok)
{
process.Brightness(src, src, dlg.i_param1);
ImageDisplay(src);
.....
}
}
Another important task is to validate menu system. If not, some errors may occur. For example, if we try to convert image that are in grayscale mode to hsv mode or if we try to modify image that has not been loaded (ie empty image), program may crash. Validate menu function is as below:
void CMyPhotoEditorDlg::ValidateMenu()
{
CMenu *m_file = GetMenu();
m_file->GetSubMenu(0);
CMenu *m_image = GetMenu();
m_image->GetSubMenu(1);
....
if(!is_load)
{
m_file->EnableMenuItem(ID_FILE_SAVE1, MF_DISABLED|MF_GRAYED);
m_image->EnableMenuItem(ID_MODE_GRAYSCALE, MF_DISABLED|MF_GRAYED);
....
}
else
{
m_file->EnableMenuItem(ID_FILE_SAVE1, MF_ENABLED);
m_image->EnableMenuItem(ID_MODE_GRAYSCALE, MF_ENABLED);
....
}
if(is_gray)
{
m_image->EnableMenuItem(ID_MODE_HSV, MF_DISABLED|MF_GRAYED);
m_image->EnableMenuItem(ID_MODE_YCrCb, MF_DISABLED|MF_GRAYED);
m_image->EnableMenuItem(ID_MODE_RGBCOLOR, MF_DISABLED|MF_GRAYED);
}
if(is_hsv)
{
m_image->EnableMenuItem(ID_MODE_HSV, MF_DISABLED|MF_GRAYED);
m_image->EnableMenuItem(ID_MODE_GRAYSCALE, MF_DISABLED|MF_GRAYED);
m_image->EnableMenuItem(ID_MODE_YCrCb, MF_DISABLED|MF_GRAYED);
}
....
}
The view menu contains image information, histogram and history. If these menus are selected, they are then set to checked and updated after each step.
void CMyPhotoEditorDlg::OnUpdateAdjustmentsBrightness(CCmdUI *pCmdUI)
{
ImgPreview dlg;
...
dlg.DoModal();
if(dlg.is_ok)
{
....
if(is_histogram) UpdateHistogram(0);
if(is_history) history_list.AddString("Adjust Image Brightness");
}
}
Note that the explanation above is simple and just give some main ideas, many codes accompanied with comments are easy to understand and use.
History
Version 1.0.
This is the simplest program. I will comeback with an updated version.