Сообщений 0    Оценка 140 [+2/-2]         Оценить  
Система Orphus

Методы и алгоритмы компьютерной графики в примерах на Visual C++

Глава 11. Просмотр и редактирование растровых изображений

Авторы: Алексей Поляков
Виталий Брусенцев

Источник: RSDN Magazine #4-2003
Опубликовано: 04.03.2004
Исправлено: 29.12.2005
Версия текста: 1.0
11.1. Создание многодокументного приложения
11.2. Класс CRaster для работы с растровыми изображениями
11.3. Модификация класса документа для обеспечения работы с изображениями
11.4. Использование виртуального экрана
11.5. Модификация класса облика
11.6. Редактирование изображений
11.6.1. Гистограмма яркости изображения
11.6.2. Программная схема выполнения преобразований. Графические фильтры
11.6.3. Таблица преобразования
11.6.4. Класс "Фильтр"
11.6.5. Использование гистограммы яркости для повышения контрастности изображения. Фильтр "Гистограмма"
11.6.6. Фильтр "Яркость/Контраст"
Заключение

Мы предлагаем вашему вниманию фрагмент из книги «Методы и алгоритмы компьютерной графики в примерах на Visual C++», которая готовится к выходу в издательстве БХВ в серии «Мастер программ».

Данная глава публикуется в сокращении.

11.1. Создание многодокументного приложения

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

Создадим каркас приложения для просмотра и редактирования рисунков:

  1. С помощью команды File | New | Projects | MFC AppWizard (exe) начнем создание приложения. Назовем проект BMViewer.
  2. На первом шаге выберем тип приложения Multiple documents.
  3. До шестого шага можно принять все установки по умолчанию.
  4. На шестом шаге генератора приложений изменим базовый класс облика с предложенного по умолчанию CView на CScrollView. Этот поступок не пройдет бесследно, а обеспечит нас впоследствии возможностью прокручивать изображения в окне облика, если они целиком в нем не поместятся. Здесь же можно изменить имена файлов, в которых будут размещены классы нашего приложения. Например, предложенное имя BMViewerView.h можно заменить на более лаконичное BMView.h.

Вот и все, каркас программы готов. Осталось теперь наделить его полезными качествами.

11.2. Класс CRaster для работы с растровыми изображениями

Структура файла с растровым изображением в формате Microsoft Windows Bitmap (BMP) была рассмотрена в прошлой главе, поэтому сразу перейдем к делу.

Создадим в программе специальный класс, который будет отвечать за загрузку и осуществлять поддержку операций по обработке растрового изображения. Назовем класс CRaster. Интерфейс класса CRaster приведен в листинге 11.1, а реализация методов — в листинге 11.2.

Листинг 11.1 Интерфейс класса CRaster. Файл Raster.h
// Raster.h : interface of CRaster class
// (C) Alexey Polyakov 2002-2003
/////////////////////////////////////////////////////////////////////////
#ifndef _RASTER_INCLUDED
#define _RASTER_INCLUDED
// макрос для определения количества байт в выровненной по DWORD строке пикселов в DIB 
// Width - длина строки в пикселах; BPP - бит на пиксел
#define BYTESPERLINE(Width, BPP) ((WORD)((((DWORD)(Width) * \
(DWORD)(BPP) + 31) >> 5)) << 2) 

class CRaster
{
   LPBITMAPINFO   m_pBMI;   //указатель на описание изображения
   PBYTE          m_pData;  //указатель на начало растровых данных
public:
   CRaster();
   ~CRaster();
   void Clear(); //очистка памяти
   // Возвращает:
   //   указатель на заголовок растра
   LPBITMAPINFO GetBMInfoPtr(){return m_pBMI;}
   //   указатель на таблицу цветов
   RGBQUAD* GetBMColorTabPtr();
   //   ширину в пикселах;
   LONG GetBMWidth();
   //   высоту в пикселах
   LONG GetBMHeight();
   //   указатель на растровые данные
   BYTE* GetBMDataPtr(){return m_pData;};
   //   указатель на пиксел
   BYTE* GetPixPtr(LONG x, LONG y);

   // Загружает из файла
   BOOL LoadBMP(CString FileName);
   // Выводит DIB на контекст pDC
   // x, y - позиция левого верхнего угла области назначения
   // cx, cy - размер области назначения
   // x0, y0 - позиция левого верхнего угла выводимой части изображения
   // cx0, cy0 - размер выводимой части изображения
   // str_mode – режим масштабирования
   // rop - растровая операция, определяет способ наложения изображения
   void DrawBitmap(CDC *pDC, LONG x=0, LONG y=0, LONG cx=0, LONG cy=0,
                   LONG x0=0, LONG y0=0, LONG cx0=0, LONG cy0=0,
                   int str_mode=COLORONCOLOR, DWORD rop=SRCCOPY);
    // Выводит DIB на контекст pDC с позиции (x,y) в масштабе scale
   void DrawBitmap(CDC *pDC, LONG x, LONG y, double scale,
                   int str_mode=COLORONCOLOR, DWORD rop=SRCCOPY);

   // Записывает BMP в файл
   BOOL SaveBMP(CString FileName);
   // Создает копию
   BOOL CreateCopy(CRaster *pOrg);
   // Создает растр заданного размера,
   // совместимый с параметрами BITMAPINFO 
   BOOL CreateCompatible(LPBITMAPINFO pBMI, LONG width=0, LONG height=0);
   // Возвращает гисторамму изображения
   BOOL GetHistogram(DWORD *pHist, int Range);
};
#endif
Листинг 11.2 Реализация методов класса CRaster. Файл Raster.cpp
// Raster.cpp : implementation of the CRaster class
// (C) Alexey Polyakov 2002-2003
////////////////////////////////////////////////////////////

#include "stdafx.h"
#include "Raster.h"

CRaster::CRaster()
{
   m_pData=NULL;
   m_pBMI=NULL;
}

CRaster::~CRaster()
{
   Clear();
};

void CRaster::Clear()
{

   if(m_pData!=NULL) delete[] m_pData;
   m_pData=NULL;

   if(m_pBMI!=NULL) delete[] m_pBMI;
   m_pBMI=NULL;

};

RGBQUAD* CRaster::GetBMColorTabPtr()
{
   return(LPRGBQUAD)(((BYTE*)(m_pBMI))+sizeof(BITMAPINFOHEADER));
};

LONG CRaster::GetBMWidth()
{
   if(m_pBMI==NULL) return 0;
   return m_pBMI->bmiHeader.biWidth;
};

LONG CRaster::GetBMHeight()
{
   if(m_pBMI==NULL) return 0;
   return m_pBMI->bmiHeader.biHeight;
};

BYTE* CRaster::GetPixPtr(LONG x, LONG y)
{
   if( x<0 || x>= m_pBMI->bmiHeader.biWidth ||
       y<0 || y>= m_pBMI->bmiHeader.biHeight ||
       m_pData == NULL)
      return NULL;
   return (m_pData+(BYTESPERLINE(m_pBMI->bmiHeader.biWidth,
   m_pBMI->bmiHeader.biBitCount)*y + x*m_pBMI->bmiHeader.biBitCount/8));
};

BOOL CRaster::LoadBMP(CString FileName)
{
   //Очистим
   Clear();
   //Открываем файл
   CFile File;
   if(!File.Open(FileName, CFile::modeRead)) return FALSE;

   ////////////////////////////////////////////////////////
   //Загружаем изображение
   //Читаем заголовок файла. Это дает его размер и положение
   //начала данных
   BITMAPFILEHEADER   FI;
   File.Read(&FI, sizeof(BITMAPFILEHEADER));

   //Проверяем, Windows Bitmap изображение ?
   if(FI.bfType!=0x4D42)
   { File.Close(); return FALSE;}

   //Смещаем позицию
   File.Seek(sizeof(BITMAPFILEHEADER), CFile::begin);

   //Считаем, что все от заголовка файла до начала растровых данных
   //есть BITMAPINFO
   //Выделяем память под заголовок
   m_pBMI=(LPBITMAPINFO)new BYTE[FI.bfOffBits-sizeof(BITMAPFILEHEADER)];
   if(m_pBMI==NULL) { File.Close(); return FALSE;}
   //Читаем BITMAPINFO
   File.Read(m_pBMI, FI.bfOffBits-sizeof(BITMAPFILEHEADER));

   //Умеем работать только с несжатыми данными
   if(m_pBMI->bmiHeader.biCompression!=0)
      { File.Close(); return FALSE;}

   //Переход к началу данных
   File.Seek(FI.bfOffBits, CFile::begin);
   //Выделяем память под данные
   //расчет размера
   if(m_pBMI->bmiHeader.biSizeImage==0)
      m_pBMI->bmiHeader.biSizeImage=
               BYTESPERLINE(m_pBMI->bmiHeader.biWidth,
               m_pBMI->bmiHeader.biBitCount)*m_pBMI->bmiHeader.biHeight;

   m_pData= new BYTE[m_pBMI->bmiHeader.biSizeImage];
   if(m_pData==NULL) { File.Close(); return FALSE;}
   //Читаем данные
   File.Read(m_pData, m_pBMI->bmiHeader.biSizeImage);

   File.Close();
   return TRUE;
};

void CRaster::DrawBitmap(CDC *pDC,
                         LONG x/*=0*/, LONG y/*=0*/,
                         LONG cx/*=0*/, LONG cy/*=0*/,
                         LONG x0/*=0*/, LONG y0/*=0*/,
                         LONG cx0/*=0*/, LONG cy0/*=0*/,
                         int str_mode/*=COLORONCOLOR*/,
                         DWORD rop /*=SRCCOPY*/)
{
   if(m_pBMI==NULL || m_pData==NULL) return;
   //размеры не заданы — габариты в пикселах
   if(cx==0) cx=GetBMWidth();
   if(cy==0) cy=GetBMHeight();

   if(cx0==0) cx0=GetBMWidth();
   if(cy0==0) cy0=GetBMHeight();
   HDC  hdc=pDC->GetSafeHdc();
   if(hdc==NULL) return;
   // Установка режима масштабирования
   int oldStretchMode=pDC->SetStretchBltMode(str_mode);
   ::StretchDIBits(hdc,      // дескриптор контекста устройства
                   x, y,     // позиция в области назначения
                   cx, cy,   // размеры обл. назначения
                   x0, y0,   //позиция в исходной области
                   cx0, cy0, //размеры исх. обл.
                   m_pData,  //данные
                   m_pBMI,   //заголовок растра
                   DIB_RGB_COLORS,   //опции
                   rop);     //код растровой операции
if(oldStretchMode!=0)
   pDC->SetStretchBltMode(oldStretchMode);
};

void CRaster::DrawBitmap(CDC *pDC, LONG x, LONG y, double scale,
                         int str_mode/*=COLORONCOLOR*/,
                         DWORD rop /*=SRCCOPY*/)
{
   if(m_pBMI==NULL || m_pData==NULL) return;

   LONG x0=0, y0=0;
   LONG cx0=GetBMWidth();
   LONG cy0=GetBMHeight();
   LONG cx=static_cast<LONG>(scale*cx0+0.5);
   LONG cy=static_cast<LONG>(scale*cy0+0.5);

   DrawBitmap(pDC, x, y, cx, cy, x0, y0, cx0, cy0, str_mode, rop);

};

BOOL CRaster::SaveBMP(CString FileName)
{
   //Открываем файл
   CFile File;
   if(!File.Open(FileName, CFile::modeCreate|CFile::modeWrite))
      return FALSE;

   ////////////////////////////////////////////////////////
   //Записываем изображение
   //Вычислим размер заголовка растра вместе с таблицей цветов
   DWORD SizeOfBMI= (DWORD)m_pBMI->bmiHeader.biSize +
   m_pBMI->bmiHeader.biClrUsed*sizeof(RGBQUAD);

   // Заголовок файла
   BITMAPFILEHEADER   FI;
   // Идентификатор типа файла BMP: 0x42 = "B" 0x4d = "M"
   FI.bfType = 0x4d42;      
   // Размер всего файла вместе с заголовками и данными
   FI.bfSize = (DWORD) sizeof(BITMAPFILEHEADER) + SizeOfBMI +
               m_pBMI->bmiHeader.biSizeImage;
   FI.bfReserved1 = 0;
   FI.bfReserved2 = 0;
   // Вычисляем смещение до начала растровых данных
   FI.bfOffBits = (DWORD) sizeof(BITMAPFILEHEADER) + SizeOfBMI;
   // Записываем заголовок файла
   File.Write(&FI, sizeof(BITMAPFILEHEADER));

   //Записываем BITMAPINFO вместе с таблицей цветов
   File.Write(m_pBMI, SizeOfBMI);

   //Данные
   File.Write(m_pData, m_pBMI->bmiHeader.biSizeImage);

   File.Close();
   return TRUE;
};

BOOL CRaster::CreateCopy(CRaster *pOrg)
{
   Clear();
   if(!pOrg) return FALSE;

   LPBITMAPINFO pOrgBMI=pOrg->GetBMInfoPtr();
   //Вычислим размер заголовка растра вместе с таблицей цветов
   DWORD SizeOfBMI= (DWORD)pOrgBMI->bmiHeader.biSize +
                    pOrgBMI->bmiHeader.biClrUsed*sizeof(RGBQUAD);

   // Выделим память под заголовок растра
   m_pBMI=(LPBITMAPINFO)new BYTE[SizeOfBMI];
   if(!m_pBMI) return FALSE;
   // Копируем заголовок растра
   memcpy(m_pBMI, pOrg->GetBMInfoPtr(), SizeOfBMI);

   // Расчет размера памяти под данные
   if(m_pBMI->bmiHeader.biSizeImage==0)
   m_pBMI->bmiHeader.biSizeImage=
   BYTESPERLINE(m_pBMI->bmiHeader.biWidth, m_pBMI->bmiHeader.biBitCount)*
   m_pBMI->bmiHeader.biHeight;
   // Выделяем память под данные
   m_pData= new BYTE[m_pBMI->bmiHeader.biSizeImage];
   if(!m_pData) return FALSE;

   // Копируем данные
   memcpy(m_pData, pOrg->GetBMDataPtr(), m_pBMI->bmiHeader.biSizeImage);

   return TRUE;
};

BOOL CRaster::CreateCompatible(LPBITMAPINFO pBMI, LONG width/*=0*/,
                                                  LONG height/*=0*/)
{
   if(!pBMI) return FALSE;

   if(width==0)    width=pBMI->bmiHeader.biWidth;
   if(height==0)   height=pBMI->bmiHeader.biHeight;

   // Проверяем, может существующий растр и так совместим
   if( m_pBMI!=NULL &&                       // существует
       m_pBMI->bmiHeader.biWidth==width &&   // такого же размера
       m_pBMI->bmiHeader.biHeight==height && // и глубина цвета совпадает
       m_pBMI->bmiHeader.biBitCount==pBMI->bmiHeader.biBitCount)
      return TRUE; // Растр и так совместим
   ////////////////////////////////////////////////////////
   // Создаем совместимый растр
   Clear();
   //Вычислим размер заголовка растра вместе с таблицей цветов
   DWORD SizeOfBMI= (DWORD) pBMI->bmiHeader.biSize +
                 pBMI->bmiHeader.biClrUsed*sizeof(RGBQUAD);

   // Выделим память под заголовок растра
   m_pBMI=(LPBITMAPINFO)new BYTE[SizeOfBMI];
   if(!m_pBMI) return FALSE;

   // Копируем заголовок растра
   memcpy(m_pBMI, pBMI, SizeOfBMI);

   // Устанавливаем размер
   m_pBMI->bmiHeader.biWidth=width;
   m_pBMI->bmiHeader.biHeight=height;

   // Расчет размера памяти под данные
   m_pBMI->bmiHeader.biSizeImage=BYTESPERLINE(m_pBMI->bmiHeader.biWidth,
              m_pBMI->bmiHeader.biBitCount)*m_pBMI->bmiHeader.biHeight;
   // Выделяем память под данные
   m_pData= new BYTE[m_pBMI->bmiHeader.biSizeImage];
   if(!m_pData) return FALSE;

   return TRUE;
};

BOOL CRaster::GetHistogram(DWORD *pHist, int Range)
{
   // Умеет работать только с данными RGB888
   if(m_pBMI->bmiHeader.biBitCount!=24) return FALSE;

   // Обнулим таблицу
   for(int i=0; i<Range; i++)
      pHist[i]=0;

   LONG DataStrLength=
   BYTESPERLINE(m_pBMI->bmiHeader.biWidth, m_pBMI->bmiHeader.biBitCount);
   BYTE *pCurPix=NULL;
   BYTE Brightness=0;
   for(int y=0, x=0; y<m_pBMI->bmiHeader.biHeight; y++)
      for(x=0; x<m_pBMI->bmiHeader.biWidth; x++)
      {
         // Адрес пиксела
         pCurPix=m_pData+y*DataStrLength+x*3;

         // Яркость рассчитывается как 0,3*Red+0,59*Green+0,11*Blue,
         // но пикселные данные хранятся в файле BMP, в порядке BGR
         Brightness=(BYTE)(( 0.11*(*pCurPix) +
                             0.59*(*(pCurPix+1))+
                             0.3*(*(pCurPix+2)))*Range/256);
         pHist[Brightness]+=1;
      }
   return TRUE;
};

Назначение большинства методов класса CRaster, мы надеемся, понятно из их названий и комментариев. В реализации также нет каких-то особенностей, которые достойны были бы специального рассмотрения. Этот класс никак не связан со структурой приложения, и вы можете использовать его в любой программе "под Windows". Причем если заменить использование классов CFile и CString в методах LoadBMP() и SaveBMP() на реализацию операций с помощью API-функций, то можно обойтись и без MFC. Однако с MFC все же удобнее.

В методе LoadBMP()не реализована распаковка сжатых изображений, поэтому класс CRaster умеет работать только с данными, так сказать, в их натуральном виде.

Кстати, в случае, когда данные хранятся в несжатом виде, мы могли бы и не выделять динамически память под хранение заголовков и данных изображения. Вместо этого можно использовать механизм, называемый "файлы, проецируемые в память". Об этом механизме кратко упоминается в начале главы 2, для освоения же работы с ним можно рекомендовать изучить [9]. В остальном работа с данными осуществлялась бы точно также. Механизм проецирования файлов особенно удобен при работе с файлами большого размера.

Класс CRaster содержит два метода DrawBitmap, выполняющих вывод изображения на контекст устройства. Аргументы одного из методов позволяют задать положение и размеры выводимой области исходного изображения и определить область назначения. По умолчанию изображение выводится полностью в масштабе 1:1, однако с помощью аргументов этой функции можно и изменить масштаб. Второй метод позволяет просто указать позицию начала вывода и масштаб, в котором должно быть нарисовано изображение. Оба метода внутри используют мощную API-функцию StretchDIBits(). Начиная с Windows 98, реализация этой функции умеет выводить растровые данные в форматах JPEG и PNG. Класс CDC, который мы обычно используем для рисования, имеет похожий метод CDC::StretchBlt(), но он в качестве исходного изображения просит указать контекст устройства, а не указатель на данные.

Режим масштабирования выбирается CDC-методом SetStretchBltMode():

int SetStretchBltMode( int nStretchMode );

Аргумент функции — iStretchMode — режим масштабирования.

Поддерживаются следующие режимы масштабирования:

При масштабировании фотографий и цветных рисунков в большинстве случаев наиболее подходящим является режимы COLORONCOLOR и HALFTONE. Далее мы расссмотрим на практике различия между этими режимами.

Метод GetHistogram() предназначен для получения гистограммы яркости изображения.О том, что это такое и зачем "оно" нужно, мы поговорим дальше.

В проект приложения добавим с помощью команды Project | Add to project | Files файлы Raster.h и Raster.cpp.

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

11.3. Модификация класса документа для обеспечения работы с изображениями

Прежде всего надо решить, что является данными, которыми будет управлять класс документа. Данными в нашем случае будет изображение, а для изображений мы завели класс CRaster, значит, в классе документа надо определить данные как объекты класса CRaster. В принципе, для целей показа картинки на экране хватит и одного объекта CRaster. Однако мы собираемся в дальнейшем наделить программу некоторыми возможностями по редактированию изображений, поэтому нам потребуется не один, а, как минимум, два объекта: один для хранения исходной картинки, второй — буфер для приема преобразованной картинки.

Порядок работы с двумя объектами CRaster в этом случае будет выглядеть следующим образом.

  1. Загружаем изображение в первый объект CRaster и показываем его на экране до тех пор, пока пользователь не даст команду выполнить какие-нибудь изменения изображения.
  2. Помещаем измененное изображение во второй объект CRaster и начинаем показывать второй объект-картинку.
  3. Может случиться так, что пользователю не понравится то, как мы изменили его картинку, тогда он отдает команду "Отменить преобразования". Легко — просто меняем объекты местами. Конечно, если мы хотим побаловать пользователя и предоставить ему возможность долго "капризничать", тогда нам придется завести бoльшее количество копий картинок, которые отражали бы последовательность произведенных преобразований. Организовать хранение копий можно в виде стека LIFO (last in — first out), где на самом верху будет храниться последняя из копий.

Полностью программный код классов приложения будет приведен в разд. 11.8, в тексте же рассмотрим ключевые моменты, необходимые для понимания основных идей.

Итак, заведем в классе нашего документа CBMDoc пару объектов CRaster, которые и будут хранить изображения:

   CRaster m_BM[2];     // два буфера для изображений
   CRaster *m_pCurBM;   // указатель на активный буфер

Указатель m_pCurBM будет хранить адрес текущего изображения, его-то мы и будем показывать.

Для загрузки изображения переопределим метод OnOpenDocument() класса CBMDoc (листинг 11.3). Сделать это можно с помощью ClassWizard. Мы надеемся, вы уже научились пользоваться генератором классов, поэтому не будем на этом останавливаться. Надо отметить, что каркас приложения при запуске программы автоматически создает пустой новый документ. Чтобы этого не происходило, переопределим в классе CBMApp метод-обработчик сообщения команды ID_FILE_NEW и оставим тело этого метода пустым. Если же вы захотите наделить программу функцией создания нового документа, то придется как-то иначе обрабатывать эту команду.

Листинг 11.3. Метод OnOpenDocument() класса CBMDoc. Файл BMDoc.cpp
BOOL CBMDoc::OnOpenDocument(LPCTSTR lpszPathName) 
{
   if (!CDocument::OnOpenDocument(lpszPathName))
      return FALSE;

   // Загружаем в первый буфер
   if(m_BM[0].LoadBMP(lpszPathName))
   {
      m_pCurBM=&m_BM[0];
      //Умеем редактировать только RGB888 (RGB24) данные
      if(m_pCurBM->GetBMInfoPtr()->bmiHeader.biBitCount!=24)
         m_bEditable=FALSE;
      else
         m_bEditable=TRUE;

      return TRUE;
   }
   return FALSE;
}

Как вы можете видеть из листинга 11.3, изображение загружается в первый из объектов CRaster. Этот объект становится текущим, его адрес запоминаем в переменной m_pCurBM. Далее проверяем формат цвета изображения. Если он не равен RGB888, то ставим флажок m_bEditable в значение FALSE. Это вовсе не означает, что картинки с отличающимся от RGB888 форматом цвета не будут показываться нашей программой, просто те функции по редактированию изображений, которые мы добавим далее, будут ориентированы на работу с RGB888, а флаг m_bEditable будет предостерегать от их неправильного использования. Если у вас появится желание редактировать "не RGB888"-картинки, то вам придется либо переделать функции преобразований, либо (что, кажется, проще) конвертировать картинки в формат RGB888 при загрузке из файла.

11.4. Использование виртуального экрана

В программировании, когда приходится работать с выводом на экран, широко используется концепция "виртуального экрана", на котором происходит подготовка изображения к показу на мониторе. Подготовка может занимать относительно длительное время и включать в себя ряд операций, которые вовсе не обязательно показывать пользователю. Копирование же из виртуального экрана на "реальный" происходит в одно действие, гораздо быстрее, чем построение картинки.

В нашем случае изображение может быть выведено на контекст устройства с помощью метода CRaster::DrawBitmap(), внутри этого метода используется API-функция StretchDIBits(), которая всем хороша, однако работает сравнительно медленно. Поэтому поэксплуатируем идею виртуального экрана. Создадим в программе такой экран, и будем выводить изображение не очень часто, а только тогда, когда его изменим. Более часто перерисовывать изображение придется объекту-облику, например при обработке сообщений прокрутки, при перерисовке окна после перекрытия другими окнами или при изменении размеров окна, в этом случае будем просто копировать изображение из виртуального экрана на контекст устройства дисплея с помощью метода CDC::BitBlt(), который работает относительно быстро.

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

Недостаток — требуется большое количество памяти под растр виртуального экрана. Допустим, у нас имеется изображение 100 на 100 пикселов, при глубине цвета 32 бита растр виртуального экрана будет занимать 40 Кбайт, а при масштабировании рисунка в 10 раз и создании виртуального экрана такого же размера его растр будет занимать уже 4 Мбайта. Такими темпами могут легко возникнуть проблемы с выводом больших рисунков.

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

Достоинство такого подхода в том, что мы экономим память и можем масштабировать изображения, не опасаясь нехватки памяти под виртуальный экран.

Недостаток — нам придется предпринять дополнительные действия по обработке сообщений о прокрутке изображения или изменении размеров окна, и каждый раз при поступлении таких сообщений выполнять вывод на виртуальный экран требуемой части изображения. Это может замедлить прокрутку изображений.

Поскольку за визуализацию изображений в нашей программе ответственен класс облика CBMView, возложим на него также и обязанности по обслуживанию виртуального экрана. Изменения, которые надо сделать, рассмотрены в следующем разделе.

11.5. Модификация класса облика

Для реализации виртуального окна заведем в объекте-облике пару переменных:

   CBitmap   m_VirtScreenBitmap;
   CDC       m_VirtScreenDC;

В объекте m_VirtScreenBitmap будем хранить растр виртуального экрана, а объект m_virtScreenDC будет контекстом виртуального экрана.

Контекст виртуального экрана должен быть совместим с контекстом окна, в который будет выполняться вывод. Добавим в класс CBMView обработчик сообщения WM_CREATE, в котором и создадим совместимый контекст (в листинг 11.4).

Листинг 11.4. Создание контекста для виртуального окна. Файл BMView.cpp
int CBMView::OnCreate(LPCREATESTRUCT lpCreateStruct)
{
   if (CScrollView::OnCreate(lpCreateStruct) == -1)
      return -1;
   
   // TODO: Add your specialized creation code here
   
   // Создадим совместимый контекст для виртуального экрана
   CPaintDC dc(this); 
   m_VirtScreenDC.CreateCompatibleDC(&dc);

   return 0;
}

Теперь нам остается только организовать вывод изображения на виртуальный экран. Для решения этой задачи нам потребуется сделать следующее:

  1. Добавить метод, в котором на виртуальный экран будет водиться изображение, содержащееся в объекте-документе. Назовем его UpdateVirtualScreen(). В этом же методе будем создавать растр для виртуального экрана, достаточный по размерам для вывода всего изображения.
  2. В метод CBMView::OnDraw() добавим копирование виртуального экрана в клиентскую часть окна вывода. Копирование будем выполнять с учетом позиции прокрутки.
  3. Переопределить (с помощью ClassWizard) виртуальный метод OnUpdate() так, чтобы в нем устанавливались размеры области прокрутки, соответствующие размеру изображения с учетом коэффициента масштабирования. Это требуется для того, чтобы пользователь мог, "прокручивая" изображение в окне, полюбоваться на любую его часть. Добавленный же AppWizard-ом (при создании каркаса приложения) в класс метод OnInitialUpdate() можно удалить (но тогда не забыть убрать и объявление этого метода в интерфейсе класса) или просто закомментировать в нем действия по установке размеров области прокрутки. Это можно сделать, так как реализация OnInitialUpdate() в базовом классе вызывает метод OnUpdate(), который мы уже модифицировали должным образом.
  4. Определить (с помощью ClassWizard) метод-обработчик сообщения WM_ERASEBKGND, для того чтобы самим контролировать перерисовку (очистку) фона окна облика. Это позволит избежать мерцания при перерисовке окна облика.

Текст этих методов приведен в листинге 11.5.

Листинг 11.5. Модифицированные методы облика. Файл BMview.cpp
// Цвет для заливки фона 
#define GRAY RGB(127, 127, 127)
BOOL CBMView::UpdateVirtualScreen()
{
   CBMDoc* pDoc = GetDocument();
   ASSERT_VALID(pDoc);

   // Получили указатель на активную картинку
   CRaster* pCurBM=pDoc->GetCurrentBMPtr(); 
   if(pCurBM==NULL) return FALSE;

   // Вычисляем размеры картинки с учетом масштаба
   LONG imgw=static_cast<LONG>(pCurBM->GetBMWidth()*m_dScale);
   LONG imgh=static_cast<LONG>(pCurBM->GetBMHeight()*m_dScale);

   // Если битмап уже существует, возьмем ее размер
   BITMAP BMStruct; BMStruct.bmWidth=BMStruct.bmHeight=0;
   if(m_VirtScreenBitmap.GetSafeHandle( ))
      m_VirtScreenBitmap.GetBitmap(&BMStruct);

   // Если размеры виртуального экрана меньше размеров картинки,
   // увеличим экран
   if(BMStruct.bmWidth<imgw || BMStruct.bmHeight<imgh)
   {
      CPaintDC dc(this); 
      // Размеры дисплея в пикселах 
      int scrw=dc.GetDeviceCaps(HORZRES);
      int scrh=dc.GetDeviceCaps(VERTRES);
      // Выберем временную битмап в контексте
      // это освободит m_VirtScreenBitmap
      // (если она была ранее выбрана в контексте)
      // и даст возможность удалить ее 
      CBitmap TempBM; TempBM.CreateCompatibleBitmap(&dc,1,1);
      m_VirtScreenDC.SelectObject(&TempBM);

      // Разрушим ранее существовавшую битмап
      m_VirtScreenBitmap.DeleteObject();

      // и на ее месте построим новую по размерам изображения,
      // не меньше размеров дисплея
      if(!m_VirtScreenBitmap.CreateCompatibleBitmap(&dc,
                      (imgw<scrw?scrw:imgw), (imgh<scrh?scrh:imgh)))
         return FALSE;
         // Новую битмап выберем в контексте виртуального экрана
         m_VirtScreenDC.SelectObject(&m_VirtScreenBitmap);
   }

   // Очистим виртуальный экран
   CBrush FonBrush(GRAY); // кисть для заливки фона
   m_VirtScreenBitmap.GetBitmap(&BMStruct); // узнаем размеры экрана
   m_VirtScreenDC.FillRect(&CRect(0,0,
                   BMStruct.bmWidth, BMStruct.bmHeight), &FonBrush);

   // Выведем на виртуальный экран картинку
   pCurBM->DrawBitmap(&m_VirtScreenDC, 0, 0, m_dScale, m_nStretchMode);
   // Обновим изображение на экране
   Invalidate();
   return TRUE;
};

void CBMView::OnDraw(CDC* pDC) 
{
   // TODO: Add your specialized code here and/or call the base class
   CBMDoc* pDoc = GetDocument();
   ASSERT_VALID(pDoc);

   // Получим размер клиентской части окна
   CRect ClientRect;
   GetClientRect(&ClientRect);

   // Копируем содержимое виртуального экрана
   // с учетом позиции прокрутки
   CPoint ScrollPos=GetScrollPosition();
   pDC->BitBlt(ScrollPos.x, ScrollPos.y,
               ClientRect.Width(), ClientRect.Height(),
               &m_VirtScreenDC, ScrollPos.x, ScrollPos.y, SRCCOPY);
}
void CBMView::OnUpdate(CView* pSender, LPARAM lHint, CObject* pHint)
{
   CBMDoc* pDoc = GetDocument();
   ASSERT_VALID(pDoc);

   // Обновим изображение на виртуальном экране
   if(UpdateVirtualScreen())
   {
      // Размер области прокрутки  
      CSize ScrollSize;

      // Область прокрутки - весь размер картинки с учетом масштаба
      ScrollSize=pDoc->GetCurrentBMSize();
      ScrollSize.cx=static_cast<LONG>(ScrollSize.cx*m_dScale);
      ScrollSize.cy=static_cast<LONG>(ScrollSize.cy*m_dScale);
      SetScrollSizes(MM_TEXT, ScrollSize);
   }
   else 
      AfxMessageBox("Ошибка при выводе на виртуальный экран");

   // Вызываем метод базового класса
   CScrollView::OnUpdate(pSender, lHint, pHint); 
}

Рассмотрим, как же все это работает. Когда метод OnOpenDocument() объекта-документа возвращает TRUE, каркас приложения создает объект-облик и посылает ему уведомление о необходимости отобразить содержимое документа. Это уведомление в нашем случае обрабатывается методом CBMView::OnUpdate(), который, в свою очередь, вызывает CBMView::UpdateVirtualScreen(). Метод UpdateVirtualScreen() создает растр нужного размера, присоединяет его дескриптору виртуального экрана, запрашивает у объекта-документа текущее изображение и выводит его на виртуальный экран. Вывод осуществляется с использованием переменных m_dScale, m_nStretchMode, которые задают масштаб и режим масштабирования. Эти переменные мы добавим в класс CBMView как раз для того, чтобы поэкспериментировать с маштабированием. В интерфейсе класса данные переменные описаны следующим образом:

   double   m_dScale;
   int      m_nStretchMode;

В конструкторе же эти переменные инициализируются начальными значениями:

CBMView::CBMView()
{
   m_dScale=1.0;
   m_nStretchMode=HALFTONE;
}

После того как картинка была выведена на виртуальный экран, вызывается метод Invalidate(), который сообщает облику, что ему следует обновить изображение на экране. Облик, получив это сообщение, вызывает свой метод OnDraw(), в котором изображение с виртуального экрана копируется на "реальный" экран.

Все. Уже, кажется, можно загрузить картинки и посмотреть, как они выглядят. На рис. 11.1 показана программа с загруженными четырьмя рисунками (ведь это у нас многодокументное приложение).


Рис. 11.1. Просмотр рисунков в программе BMViewer

Более того, в программах с MDI-интерфейсом у одного документа может быть несколько объектов-обликов, как на рис. 11.2. Создать еще один облик для документа можно командой Window | New window. Изменение данных объекта-документа будет отражаться во всех окнах (всеми объектами-обликами). Такую возможность можно использовать, например, для показа одного и того же изображения в разных окнах и в разных масштабах, далее мы посмотрим, как это выглядит на практике. Для изучения особенностей SDI- и MDI-приложений можно также порекомендовать книгу [4].

Чтобы можно было изменять масштаб вывода изображения, добавим в меню View программы команды Zoom In и Zoom Out, а для установки режима масштабирования — команды Stretch HALFTONE и Stretch COLORONCOLOR (рис. 11.3). Можно также назначить этим командам горячие клавиши (см. разд. 6.7).


Рис. 11.2. Четвертая картинка показана сразу в двух окнах


Рис. 11.3. Добавление команд масштабирования в меню программы

Обработчики этих команд добавим с помощью ClassWizard в класс облика (листинг 11.6). Эти функции изменяют состояние переменных m_dScale и m_nStretchMode. Для команд установки режимов масштабирования также добавлены методы OnUpdateViewStretchhalftone() и OnUpdateViewStretchcoloroncolor() для обработки сообщения UPDATE_COMMAND_UI. В этих функциях можно управлять состоянием соответствующих команд в интерфейсе программы (например, можно делать недоступными команды в зависимости от состояния программы). В данном случае мы просто маркируем соответствующий режим масштабирования.

Листинг 11.6. Обработка команд масштабирования. Файл BMview.cpp
void CBMView::OnViewZoomin() 
{
   // TODO: Add your command handler code here
   m_dScale*=2;
   OnUpdate(NULL, 0, NULL);
}

void CBMView::OnViewZoomout() 
{
   // TODO: Add your command handler code here
   m_dScale/=2;
   OnUpdate(NULL, 0, NULL);        
}

void CBMView::OnViewStretchhalftone() 
{
   // TODO: Add your command handler code here
   m_nStretchMode=HALFTONE;
   OnUpdate(NULL, 0, NULL); 
}

void CBMView::OnUpdateViewStretchhalftone(CCmdUI* pCmdUI) 
{
   // TODO: Add your command update UI handler code here
   pCmdUI->SetCheck(m_nStretchMode==HALFTONE);
}

void CBMView::OnViewStretchcoloroncolor() 
{
   // TODO: Add your command handler code here
   m_nStretchMode=COLORONCOLOR;
   OnUpdate(NULL, 0, NULL); 
}

void CBMView::OnUpdateViewStretchcoloroncolor(CCmdUI* pCmdUI) 
{
   // TODO: Add your command update UI handler code here
   pCmdUI->SetCheck(m_nStretchMode==COLORONCOLOR);
}

В приложениях с MDI-интерфейсом возможно также использовать механизм, называемый Splitted window, который позволяет показать несколько обликов в одном окне-рамке. Для этого потребуется лишь научить рамку дочернего окна нашего приложения работать с несколькими обликами. Сделать это довольно просто, надо всего-навсего на этапе 4 создания каркаса приложения генератора AppWizard зайти по кнопке Advanced в диалог Advanced Options (расширенные установки) и на закладке Window Style поставить галочку напротив Use split window. Но если мы это забыли сделать — тоже не беда, — зайдем в интерфейс класса CChildFrame и руками добавим переменную-объект класса CSplitterWnd:

// Attributes
protected:
   CSplitterWnd m_wndSplitter;

Затем с помощью ClassWizard переопределим в классе CChildFrame виртуальный метод OnCreateClient и немного подправим его код, как показано в листинге 11.7.

Листинг 11.7. Подправленный метод CChildFrame::OnCreateClient. Файл ChildFrm.cpp
BOOL CChildFrame::OnCreateClient(LPCREATESTRUCT lpcs, CCreateContext* pContext) 
{
   // TODO: Add your specialized code here and/or call the base class

//   return CMDIChildWnd::OnCreateClient(lpcs, pContext);
   return m_wndSplitter.Create( this,
      2, 2,                 // Максимальное количество строк и столбцов
      CSize( 10, 10 ),      // Минимальный размер окна
      pContext );
}

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

Хотя метод CSplitterWnd::Create() имеет параметры для задания максимального количества строк и столбцов, в документации сказано, что эти значения почему-то не должны превышать 2. Хотя если попробовать увеличить это значение "на свой страх и риск", то можно разбить рамку и на 9 различных обликов.

Активным считается тот облик, в котором последний раз щелкнули мышью. К нему и пременяются команды масштабирования. В верхнем ряду (рис. 11.4) показаны увеличенные фрагменты изображения: слева в режиме HALFTONE, справа — COLORONCOLOR. Видно, что режим COLORONCOLOR не пытается бороться с "лестничным эффектом".


Рис. 11.4. В одном окне-рамке отображаются четыре различных облика одного объекта-документа

11.6. Редактирование изображений

Ну вот, наконец-то мы добрались до самого интересного. Магия преобразований — вот, на мой взгляд, основная радость, которую дает цифровая обработка изображений. Используя возможности редактирования в таких программах как Adobe Photoshop или Ulead Photoimpact, человек даже с заурядными художественными способностями может превратить самую скучную фотографию в нечто более привлекательное. Конечно, за всем этим стоит прочная математическая база и кропотливый труд программистов. Далее мы рассмотрим два вида преобразований:

Точечные преобразования удобно выполнять с помощью таблиц преобразования, которые рассмотрены в разд. 11.6.3.

Обычно пространственное преобразование заключается в нахождении свертки значений группы пикселов. Свертка вычисляется как сумма пиксельных значений, попавших в зону преобразования, помноженных на весовые коэффициенты. В качестве весовых коэффициентов выступают элементы матрицы преобразования. Значения элементов матрицы преобразования и определяют тип преобразования. Размер матрицы преобразования соответствует области пикселов, которые будут участвовать в преобразовании. Центральный элемент матрицы — весовой коэффициент преобразуемого пиксела (x, y). Поэтому матрицы преобразования, обычно имеют нечетный размер (например, 3x3 или 5x5 элементов). Часто свертки заключают в себе сложный и глубокий математический смысл, который, к счастью, имеет простую и понятную практическую интерпретацию. Рассмотрим, например, свертку с помощью следующей матрицы M:

    1  1  1
M = 1  1  1
    1  1  1

Новое значение пиксела P (x, y) может быть рассчитано с использованием следующего псевдокода:

MX=3; // размер матрицы преобразования по x
MY=3; // размер матрицы преобразования по y
CountCoeffSumm=0; // счетчик суммы коэффициентов матрицы преобразования
NewP=0; // новое значение пиксела
for(j=-MY/2; j<= MY/2; j++)
   for(i=-MX/2; i<=MX/2; i++)
      {
         NewP = NewP + P(x+i, y+j)*M(i, j);
         CountCoeffSumm = CountCoeffSumm + M(i, j);
      }
P(x, y)=NewP/CountCoeffSumm;

Здесь предполагается целочисленное деление, т. е. результат MX/2 — значение 1.

В процессе преобразования выполняется подсчет коэффициентов матрицы преобразования, а после расчета нового значения оно делится на сумму коэффициентов. Это необходимо для того, чтобы привести результат к диапазону значений суммированных пикселов.

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

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

Само название процесса "геометрический" говорит о том, что суть преобразования заключается в изменении положения или других геометрических характеристик изображения. Примерами геометрических преобразований являются поворот, сдвиг, интерполяция (масштабирование) изображения.

Все типы преобразований рассмотрены в [5]. Достоинством книги является подробное рассмотрение "классических" преобразований растровых изображений, включая рассмотрение теоретических основ, и программной реализации. Недостатком, на мой взгляд, — не очень удачный перевод и использование устаревших средств и стиля программирования. Однако последнее — это не упрек автору, а лишь свидетельство того, что книга была написана на заре "революции" в области компьютерной графики.

О "внутреннем устройстве" и практическом применении цифровых фильтров можно прочитать в [15].

Теоретическим основам обработки изображений посвящена книга [1].

Далее мы рассмотрим суть некоторых ставших классическими процедур преобразования растровых изображений — наделим программу BMViewer возможностями редактирования картинок. В программе будет реализовано несколько преобразований, но что не менее важно, мы постараемся организовать в программе структуру, которая позволила бы легко включать в нее реализацию любых новых преобразований.

11.6.1. Гистограмма яркости изображения

Что такое гистограмма? Это такой график из столбиков. А что такое "гистограмма яркости изображения"? Гистограммой яркости изображения принято называть график, который показывает относительную частоту появления точек (пикселов) различных степеней яркости в изображении. Например, есть у нас изображение из 16 пикселов. Пусть 8 пикселов имеют яркость 1, 2 пиксела — яркость 4, оставшиеся 6 пикселов — яркость 7. На десятибалльной шкале яркости график такого изображения может выглядеть так, как показано на рис. 11.5.


Рис. 11.5. Гистограмма яркости мнимого изображения из шестнадцати пикселов

В реальных изображениях пикселов обычно гораздо больше, а шкала яркости включает значения от 0 до 255.

Яркость RGB-пиксела рассчитывается по следующей формуле:

Brightness = 0,3 * Red + 0,59 * Green + 0,11 * Blue.

Как видно из формулы, разные цвета имеют разные весовые коэффициенты. Это связано с различной восприимчивостью человеческого глаза к разным составляющим цвета.

Гистограмма яркости широко используется для анализа и редактирования изображений. Наш класс CRaster уже умеет рассчитывать гистограмму изображения. Эта возможность реализована в методе CRaster::GetHistogram(). Метод GetHistogram() получает два параметра (см. листинг 11.2):

Порядковый номер значения в массиве pHist соответствует яркости, а значение элемента — частоте появления этой яркости в картинке. Рассчитывается гистограмма довольно просто: в цикле для всех пикселов изображения сначала рассчитывается яркость пиксела, а затем увеличивается на единицу значение соответствующего элемента в массиве pHist.

Поскольку расчет гистограммы уже реализован, нам остается только нарисовать ее на экране. Для показа гистограммы на экране добавим в программу специальное диалоговое окно (рис. 11.6). Напомним, что новый шаблон диалогового окна вставляется командой Insert | Resource | Dialog | New. Присвоим шаблону идентификатор IDD_HIST и добавим в него рамочку (элемент picture тип frame) — этот элемент понадобится нам для рисования гистограммы, поэтому дадим ему идентификатор IDC_HIST_VIEW. В шаблон также добавлено два ползунка (элементы slider), и два элемента static, в которых будут показываться значения в виде текста.


Рис. 11.6. Шаблон окна диалога Image histogram

Изображение гистограммы, в принципе, можно нарисовать прямо по окну диалога, определив в классе диалога обработчик сообщения WM_PAINT и выполнив в нем соответствующие построения. Однако большую гибкость можно получить, если создать специализированный класс, который бы выводил гистограмму в заданное Windows-окно. Для создания такого класса воспользуемся все тем же ClassWizard-ом.

В окне ClassWizard-а нажмем AddClass | New и в появившемся окне New Class укажем имя нашего нового класса (рис. 11.7), назовем его CHistView, а в качестве базового класса укажем класс CStatic (из библиотеки MFC) — он обеспечивает функциональность статических (static) Windows-элементов управления: "рамка", "битмап", "пиктограмма"). Мы переопределим в классе CHistView обработчик сообщения WM_PAINT и научим его рисовать гистограмму. Затем мы сможем связать с элементом управления IDC_HIST_VIEW объект класса CHistView. Это позволит свободно размещать изображение гистограммы в нужном месте диалогового окна, не заботясь о том, чтобы у нас в коде были прописаны соответствующие координаты. Можно также разместить несколько элементов-гистограмм (например, для разных цветовых каналов) и при этом не придется каким-то образом модифицировать программный код вывода гистограммы.


Рис. 11.7. Создание класса CHistView

Ползунки-слайдеры, добавленные в диалог, потребуются нам в дальнейшем для коррекции гистограммы. Об этих элементах сказано здесь, чтобы дважды не описывать этот диалог. С помощью ClassWizard создадим класс CHistDlg, который будет обслуживать наш диалог (просто нажмите комбинацию клавиш <Ctrl>+<W>, находясь в редакторе шаблона диалогового окна). Свяжем созданные элементы управления с объектами в классе диалога (рис. 11.8). Причем с элементом IDC_HIST_VIEW мы связываем объект созданного класса CHistView.


Рис. 11.8. Переменные класса CHistDlg

Для рисования гистограммы добавим в класс CHistView (опять же с помощью ClassWizard-а) обработчик сообщения WM_PAINT (листинг 11.8).

Листинг 11.8. Метод CHistView::OnPaint(). Файл HistView.cpp
void CHistView::OnPaint()
{
   CPaintDC dc(this); // device context for painting

   // TODO: Add your message handler code here

   if(m_pHist==NULL || m_iRange==0 ) return;

   // Найдем среднее значение
   DWORD MaxBright=0, SumBright=0;
   for(int i=0; i<m_iRange; i++)
      SumBright+=m_pHist[i];

   // Пусть максимальное (показываемое на рисунке) значение
   // будет в три раза больше среднего
   MaxBright=3*SumBright/m_iRange;
   if(MaxBright==0) return;

   // Перо для рисования гистограммы
   CPen HistPen(PS_SOLID, 2, m_Color);
   CPen *pOldPen=dc.SelectObject(&HistPen);
   CGdiObject *pOldBrush=dc.SelectStockObject(NULL_BRUSH);

   // Найдем координаты окна вывода
   CRect FrameRect;
   GetWindowRect(&FrameRect);
   ScreenToClient(&FrameRect);

   // Нарисуем гистограмму в окне
   dc.Rectangle(&FrameRect);
   FrameRect.bottom-=1;
   double kx=((double)FrameRect.Width())/m_iRange;
   double ky=((double)FrameRect.Height())/MaxBright;

   int x=0, y=0;
   for(i=0; i<m_iRange; i++)
{
      x=FrameRect.left+(kx*i);
      y=FrameRect.bottom;
      dc.MoveTo(x, y);
      y=FrameRect.bottom -(ky*m_pHist[i]);
      if(y<FrameRect.top) y=FrameRect.top;
      dc.LineTo(x, y);
   }
   if(pOldPen) 
      dc.SelectObject(pOldPen);
   if(pOldBrush)
      dc.SelectObject(pOldBrush);

   // Do not call CStatic::OnPaint() for painting messages
}

Каждый раз, когда окно (в данном случае — элемент управления, связанный с объектом класса CHistView) должно быть показано на экране, Windows посылает ему сообщение WM_PAINT. Обрабатывая это сообщение, мы рисуем гистограмму.

Как вы наверняка заметили, гистограмма рисуется на основе значений, хранящихся в массиве, на который указывает переменная m_pHist, переменная же m_iRange задает размер массива. Эти переменные мы добавили в интерфейс класса CHistView (листинг 11.9). Для установки этих переменных добавлен метод SetData().

Листинг 11.9. Интерфейс класса CHistView. Файл HistView.h
class CHistView : public CStatic
{
// Construction
public:
   CHistView();

// Attributes
public:
   int           m_iRange;      //размер массива гистограммы
   const DWORD   *m_pHist;      //указатель на данные гистограммы
   COLORREF       m_Color;      //цвет, которым рисовать гистограмму

// Operations
public:
   // Устанавливает данные для отображения
   void SetData(const DWORD *pHist,  int Range)
               {m_pHist=pHist; m_iRange=Range;};
   // Устанавливает цвет рисования гистограммы
   void SetColor(const COLORREF &c) {m_Color=c;};

// Overrides
   // ClassWizard generated virtual function overrides
   //{{AFX_VIRTUAL(CHistView)
   //}}AFX_VIRTUAL

// Implementation
public:
   virtual ~CHistView();

   // Generated message map functions
protected:
   //{{AFX_MSG(CHistView)
   afx_msg void OnPaint();
   //}}AFX_MSG

   DECLARE_MESSAGE_MAP()
};

Для того чтобы диалог заработал, надо где-то его вызвать. Поэтому добавим в меню программы Edit команду Histogram, а в класс документа — метод-обработчик этой команды (листинг 11.10).

Листинг 11.10. Метод-обработчик команды вызова диалога с гистограммой. Файл BMDoc.cpp
void CBMDoc::OnEditHistogram() 
{
   const int Range=256;
   DWORD Hist[Range]; // гистограмма из Range градаций яркости

   // Запросим гистограмму у текущего изображения
   if(m_pCurBM==NULL || !m_pCurBM->GetHistogram(Hist, Range))
      return;
   // Создаем объект-диалог
   CHistDlg HDlg; 
   // Передадим гистограмму в диалог
   HDlg.SetData(Hist, Range);
   // Покажем гистограмму
   if(HDlg.DoModal()==IDCANCEL) return;
   // Требуется выполнить коррекцию контрастности  
   if(HDlg.m_iOffset_b !=0 || HDlg.m_iOffset_t!=NULL) 
   {
      // Настраиваем фильтр гистограммы
      m_HistogramFilter.Init(HDlg.m_iOffset_b, HDlg.m_iOffset_t);
      // Делаем фильтр активным 
      m_pCurFilter=&m_HistogramFilter;
      // Выполняем преобразование
      Transform();
   }
}

В листинге 11.10 все до строчки

// Требуется выполнить коррекцию контрастности

должно быть вам понятно. О том, что же это за "коррекция яркости", поговорим дальше.

Единственный момент — это передача данных гистограммы в диалог. Данные передаются в объект-диалог с помощью метода SetData(Hist, Range), который передает данные уже непосредственно элементу, отображающему гистограмму. Этот метод добавлен в интерфейс класса CHistDlg. Выглядит он следующим образом:

void SetData(const DWORD *pHist, int Range)
            {m_ctrlHist.SetData(pHist, Range);};
где m_ctrlHist объект класса CHistView.
Полностью интерфейс класса приведен в разд. 11.8, листинг 11.40, реализация — листинг 11.41.


Рис. 11.9. Гистограмма яркости тестового рисунка, экспортированного из программы Painter 4.2

Теперь можно посмотреть, какие же гистограммы яркости у наших изображений. Например, на рис. 11.9 показана гистограмма тестового рисунка, который мы экспортировали в формат BMP из программы Painter 4.2 в главе 10.

Гистограмма (рис. 11.9) показывает, что яркость в этом рисунке распределена неравномерно (смещена в область светлых тонов), а многие значения яркости и вовсе отсутствуют. Оно и понятно, ведь это искусственный рисунок, и мы использовали при его создании далеко не все цвета и оттенки. Фотографические изображения обычно имеют более плавные гистограммы с широким спектром яркости, как, например, на рис. 11.10.


Рис. 11.10. Гистограмма яркости фотографии

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

11.6.2. Программная схема выполнения преобразований. Графические фильтры

Поскольку мы собираемся реализовать целый ряд процедур преобразования изображений, следует хорошо обдумать, как они будут уживаться между собой и взаимодействовать с остальными модулями программы. Судя по интерфейсу многих графических редакторов и организации программ обработки видеоданных, в мультимедийном программировании широко распространена концепция фильтров. Что такое фильтр? Это некоторая программа, которая, пропуская через себя данные, преобразует их некоторым образом. В нашем случае данными являются значения цветов пикселов изображения. Такой подход выглядит очень удачным, так как он позволяет создавать четко структурированные модульные программы. Используем и мы эту идею. Представим, что у нас имеется набор фильтров, пропуская через которые данные изображения мы можем добиваться различных эффектов (рис. 11.11).


Рис. 11.11. Схема использования фильтров для преобразования изображений

Для того чтобы получить нужный эффект, достаточно просто указать программе, какой фильтр считать активным. В программе же где-то должна существовать "фильтровалка" — процедура, в которой будет выполняться само пропускание данных через фильтр.

Фильтры можно реализовать в виде классов, производных от какого-то одного базового класса. В базовом классе следует определить набор методов, общих для всех фильтров. В программе заведем переменную — указатель на активный фильтр. Используя этот указатель, "фильтровалка" будет обращаться к нужному фильтру.

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

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

Общая схема преобразования в этом случае будет выглядеть следующим образом:

  1. Пришла команда выполнить преобразование — создаем рабочий поток.
  2. Уведомляем объекты-облики о том, что начали преобразование. При этом облик запускает таймер и начинает переодически интересоваться, сколько процентов работы выполнено, показывая пользователю процент выполнения.
  3. В рабочем потоке выполняется преобразование и увеличивается процент выполнения.
  4. По окончании преобразования (или если пользователь прервал выполнение) в объекты-облики посылаются сообщения о завершении работы и показывается преобразованная картинка.

Поскольку данными в программе BMViewer заведует класс CBMDoc, именно

в него и поместим "фильтровалку". Для создания рабочего потока потребуется добавить в класс CBMDoc несколько методов:

Текст этих методов приведен в разд. 11.8, здесь же коротко рассмотрим только метод TransformLoop() (листинг 11.11).

Листинг 11.11. Метод CBMDoc::TransformLoop(). Файл BMDoc.cpp
void CBMDoc::TransformLoop()
{
   if(m_pCurFilter==NULL) return;
   if(!CreateCompatibleBuffer()) return;

   m_EventDoTransform.SetEvent();
   m_bEditable=FALSE;
   InformAllViews(UM_STARTTRANSFORM);

   CRaster   *pSBM=GetCurrentBMPtr(),      // источник
             *pDBM=GetBufferBMPtr();       // приемник 
   // Установили в фильтр источник и приемник преобразований
   m_pCurFilter->SetBuffers(pSBM, pDBM);

   for(LONG y=0; y<pSBM->GetBMHeight(); y++)
   {
      // Процент выполнения
      InterlockedExchange(&m_lExecutedPercent,
                          100*y/pSBM->GetBMHeight());
      // Проверим, не решили ли прервать преобразование
      if(!m_EventDoTransform.Lock(0))
      {
         InformAllViews(UM_ENDOFTRANSFORM, FALSE, 0);
         m_bEditable=TRUE;
         return;
      }
      LONG x=0;
      if( m_bEditHalf ) // Преобразовать только половину изображения
      {
         // Первую половину картинки копируем в буфер без преобразования
         x=pSBM->GetBMWidth()/2;
         BYTE *pSPix=NULL,  *pDPix=NULL;
         // Указатели на начало строк
         if((pSPix=pSBM->GetPixPtr(0, y))!=NULL && 
             (pDPix=pDBM->GetPixPtr(0, y))!=NULL)
            // ВНИМАНИЕ! Предполагается, что 1 пиксел = 24 бита = 3 байта
            memcpy(pDPix, pSPix, 3*x); 
      }
      // Преобразование с использованием текущего фильтра
      for(; x<pSBM->GetBMWidth(); x++)
         m_pCurFilter->TransformPix(x, y);

   }
   m_EventDoTransform.ResetEvent();
   m_bEditable=TRUE;

   SwapBM();          // сделать буфер текущим изображением
   SetModifiedFlag(); // флаг "данные изменились"

   InformAllViews(UM_ENDOFTRANSFORM, TRUE, 0);
   return;
};

В методе TransformLoop() мы сначала "зажигаем" событие "Выполняется преобразование" — объект m_EventDoTransform класса CEvent. Затем сообщаем текущему фильтру, какое изображение будет исходным, и какое — приемным (адреса объектов CRaster). Далее в цикле прогоняем через фильтр пикселы изображения. На текущий фильтр указывает переменная m_pCurFilter, которую мы завели в классе CBMDoc специально для этих целей. Тип этой переменной — "указатель на объект класса CFilter". Преобразование же данных выполняется с помощью метода CFilter::TransformPix(). Класс CFilter как раз и является базовым для всех фильтров. О нем рассказано в разд. 11.6.4.

В процессе преобразования перед обработкой очередной строки пикселов вычисляется процент выполнения как процент уже обработанных строк изображения. Вычисленное значение записывается в переменную m_lExecutedPercent с помощью API-функции InterlockedExchange() — эта функция позволяет предотвратить одновременное обращение к переменной из разных потоков. Далее проверяется, по-прежнему ли установлено событие m_EventDoTransform. И только затем обрабатываются пикселы строки. Причем в нашей программе в иллюстрационных целях мы позволяем пользователю посмотреть эффект преобразования на половине изображения. Если установлен флаг m_bEditHalf, первая половина строки копируется в неизменном виде.

После того как все пикселы изображения были обработаны, скидывается флаг m_EventDoTransform, буферное изображение становится активным и во все облики направляется сообщение UM_ENDOFTRANSFORM с параметром TRUE, который говорит о том, что преобразование завершилось и надо обновить изображение в окне облика.

Посылаемые в облики сообщения о начале и окончании кодирования определены нами в файле BMDoc.h следующим образом:

#define UM_STARTTRANSFORM    WM_USER+ 0x8000
#define UM_ENDOFTRANSFORM    UM_STARTTRANSFORM+1

WM_USER — это специальная константа, начиная с которой (вплоть до значения 0xBFFF) программист может определять сообщения для использования в своем приложении без опасений о том, что они будут конфликтовать с Windows-сообщениями. Однако в документации MSDN сказано, что в диапазоне до 0x7FFF некоторые предопределенные Windows-классы могут использовать значения в своих целях. Видимо, в связи с этим в [4] предлагается на всякий случай определять свои значения как WM_USER+7 и выше, что, в принципе, работает. Но почему +7 мне не известно, поэтому мы можем пойти дальше в своей осторожности и определить свои сообщения в диапазоне от 0x8000.

Для обработки наших сообщений в классе-облике потребуется сделать следующее:

1. В интерфейс класса CBMView (файл BMView.h) добавим объявление методов:

   afx_msg LONG OnStartTransform(UINT wParam, LONG lParam);
   afx_msg LONG OnEndTransform(UINT wParam, LONG lParam);

2. В карту сообщений класса CBMView (файл BMView.cpp) добавим макрокоманды:

   ON_MESSAGE(UM_STARTTRANSFORM, OnStartTransform)
   ON_MESSAGE(UM_ENDOFTRANSFORM, OnEndTransform)

3. Добавим в класс CBMView (файл BMView.cpp) реализацию этих методов:

LONG CBMView::OnStartTransform(UINT wParam, LONG lParam)
{
   OnStartTimer();
   return 0;
}

LONG CBMView::OnEndTransform(UINT wParam, LONG lParam)
{
   OnStopTimer();
   if(wParam)
      // обновим изображение на виртуальном экране
      UpdateVirtualScreen();
   return 0;
}

Как видно из приведенного текста методов, при получении сообщения UM_STARTTRANSFORM в объекте-облике вызывается метод OnStartTimer(). Этот метод создает таймер. Для обработки сообщений WM_TIMER, которые начнет посылать таймер, в класс CBMView с помощью ClassWizard добавили метод OnTimer(). В этом методе будет выполняться запрос процента выполнения операции и обновляться информация о выполнении. Процент выполнения операции будем показывать в заголовке окна облика. Можно было бы, конечно, вывести индикатор выполнения (progress bar) в строку состояния, но как мы дальше увидим, наша программа позволит одновременно выполнять преобразования в нескольких рисунках, а тогда не совсем ясно, как делить единственный индикатор.

Приход сообщения UM_ENDOFTRANSFORM обрабатывается методом OnEndTransform(), который зависит от значения аргумента wParam:

Далее им вызывается функция OnStopTimer(), которая разрушает таймер.

Полностью текст этих методов приведен в разд. 11.8.

Схема взаимодействия объектов и потоков показана на рис. 11.12.


Рис. 11.12. Схема работы программы при выполнении преобразования изображения

Выделение "долгоиграющих" операций обработки данных в отдельный поток позволяет пользователю сохранить контроль над выполнением программы, т. е. программа перестает "подвисать" на таких задачах. В нашем приложении пользователь, запустив фильтрацию на одном из открытых изображений, может переключиться на просмотр и редактирование другого изображения. При необходимости пользователь может остановить выполнение преобразования, для этого в программе предусмотрим команду, которая бы сбрасывала флаг m_EventDoTransform При сбросе этого флага цикл выполнения преобразования CBMDoc::TransformLoop() прерывается, потоковая функция завершается и рабочий поток прекращает свое существование.

11.6.3. Таблица преобразования

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

Например, нам надо преобразовать яркость пиксела. В этом случае старое значение яркости играет роль индекса элемента в таблице, а значение этого элемента является новым значением. Формула преобразования может выглядеть так:

V = TransformTable[V],

где V — значение яркости, а TransformTable — таблица преобразования. Конечно, таблица преобразования должна быть предварительно заполнена какими-то значениями.

Рассмотрим, например, как можно инвертировать цвет картинки.

Диапазон значений каждого 8-битного компонента цвета находится в пределах от 0 до 255.

Создадим таблицу преобразования из 256 элементов и заполним ее значениями от 255 до 0 (рис. 11.13).


Рис. 11.13. Таблица преобразования "инверсия"

После преобразования по вышеприведенной формуле с использованием таблицы (см. рис. 11.13) интенсивность 255 будет заменена на 0, 254 — на 1 и т. д. В случае такого простого преобразования, как инверсия цвета, использование таблицы может и не дать особого выигрыша по скорости, но если новое значение пиксела должно рассчитываться по более сложной формуле (чем V = 255 – V), то выигрыш будет весьма заметен. Кроме того, использование таблиц позволяет использовать единообразный подход к осуществлению различных преобразований.

11.6.4. Класс "Фильтр"

Структура (см. рис. 11.11) подразумевает существование в программе некоторого объекта-фильтра. Фильтры выполняют разные преобразования, но с точки зрения "фильтровалки" они все одинаковы и обращаться с ними она будет единообразно. Поэтому нам надо определить базовый класс CFilter для фильтра с минимальным, но основным набором методов, с помощью которых будет происходить общение. Интерфейс такого класса приведен в листинге 11.12.

Листинг 11.12. Базовый класс фильтров CFilter. Файл Filter.h
class CRaster;
// Базовый виртуальный класс
class CFilter
{
protected:
   CRaster *m_pSourceBM;
   CRaster *m_pDestBM;
public:
   // Устанавливает исходное и приемное изображения
   void SetBuffers( CRaster *pSource, CRaster *pDest=NULL)
      {   m_pSourceBM=pSource;   m_pDestBM=pDest;};
   // Виртуальный метод преобразования пиксела
   // будет переопределен в производных классах
   virtual BOOL TransformPix(LONG x, LONG y){ return FALSE;};
};

Данные класса — два указателя на объекты-картинки класса CRaster:

Методы класса:

Переменная-указатель на этот класс m_pCurFilter заведена в классе CBMDoc. Этой переменной присваивается адрес текущего фильтра. В классе CFilter уже объявлены необходимые для фильтрования методы, они и используются в методе CBMDoc::TransformLoop() (см. листинг 11.11). Так как метод CFilter::TransformPix() объявлен виртуальным, в методе TransformLoop() будет происходить вызов настоящего метода преобразования активного фильтра.

Для реализации точечных методов преобразования создадим класс CDotFilter (листинг 11.13).

Листинг 11.13. Базовый класс для точечных фильтров CDotFilter. Файл Filter.h
// Базовый класс для точечных фильтров
class CDotFilter: public CFilter
{
protected:
   // Таблицы преобразования для компонентов цвета
   BYTE BGRTransTable[3][256];
public:
   // Метод преобразования пиксела
   BOOL TransformPix(LONG x, LONG y);
};

Данными этого класса являются три таблицы преобразования компонентов RGB цвета. В принципе, для методов преобразования, рассмотренных далее, достаточно было бы определить одну таблицу, но более общим подходом будет все-таки формирование трех таблиц. Потому что возможны преобразования, изменяющие цветовую гамму (оттенок) изображения. Тут-то три таблицы и пригодятся.

Для точечного фильтра переопределен метод TransformPix(). Он будет общим для большинства рассмотренных далее точечных фильтров. Реализация метода приведена в листинге 11.14.

Листинг 11.14. Метод CDotFilter::TransformPix(). Файл Filter.cpp
BOOL CDotFilter::TransformPix(LONG x, LONG y)
{
   BYTE *pDPix=NULL, *pSPix=NULL;
   // Источник необходим
   if(m_pSourceBM==NULL )
      return FALSE;
   // Если приемник не задан, то преобразование помещаем в источник
   if(m_pDestBM==NULL)
      m_pDestBM=m_pSourceBM;
   // Получаем указатели на пикселы в источнике и приемнике
   if((pDPix=m_pDestBM->GetPixPtr(x, y))==NULL ||
      (pSPix=m_pSourceBM->GetPixPtr(x, y))==NULL)
      return FALSE;
   // Преобразование. Порядок BGR
   *pDPix=BGRTransTable[0][*pSPix];
   *(pDPix+1)=BGRTransTable[1][*(pSPix+1)];
   *(pDPix+2)=BGRTransTable[2][*(pSPix+2)];
   return TRUE;
};

В принципе, в случае точечных преобразований возможна такая реализация "фильтровалки", когда исходное изображение одновременно является и приемником преобразования (конечно, в этом случае сложнее сделать отмену преобразования). Эта ситуация отрабатывается внутри метода.

Хотя формат 24-битового цвета называют RGB, в файле формата BMP компоненты цвета хранятся в обратном порядке. Это не так важно, но надо знать и учитывать, что и как делается при формировании нового значения цвета пиксела.

Все, что останется сделать в производных от CDotFilter классах, описывающих разные эффекты, — это реализовать инициализацию таблиц преобразования.

Для реализации пространственных (матричных) методов преобразования создадим класс CMatrixFilter. Интерфейс класса приведен в листинге 11.15.

Листинг 11.15. Интерфейс базового для матричных фильтров класса CMatrixFilter. Файл Filter.h
// Пространственные (матричные) фильтры
// Базовый класс
class CMatrixFilter: public CFilter
{
protected:
   int m_rangX; // размер матрицы по X и Y
   int m_rangY;
   const int *m_pMatrix; // указатель на матрицу
public:
   // Метод преобразования пиксела
   BOOL TransformPix(LONG x, LONG y);
};

Данные класса: размер матрицы преобразования и указатель на матрицу. Как правило, используются квадратные матрицы преобразования, но кто знает, может для чего-то будет полезна и не квадратная матрица. Поэтому указывается размер матрицы по горизонтали и вертикали. Размер матрицы определяет зону пикселов, окружающую пиксел (x, y), которая будет вовлечена в расчет нового значения пиксела (x, y). Указателю на матрицу преобразования m_pMatrix будет присваиваться адрес матрицы, которая будет использована в преобразовании.

Реализация метода CMatrixFilter::TransformPix() приведена в листинге 11.16.

Листинг 11.16. Метод CMatrixFilter::TransformPix(). Файл Filter.cpp
BOOL CMatrixFilter::TransformPix(LONG x, LONG y)
{
   BYTE *pDPix=NULL, *pSPix=NULL;
   // Источник и приемник необходимы
   if( m_pSourceBM==NULL || m_pDestBM==NULL)
      return FALSE;

   // Определяем зону перекрытия изображения и 
   // матрицы преобразования. Это требуется для
   // обработки пикселов, находящихся на границах 
   // изображения
   int x_start=0;
   int dx=m_rangX/2, dy=m_rangY/2;

   if(x-dx<0) x_start=dx-x;

   int y_start=0;
   if(y-dy<0) y_start=dy-y;

   int x_finish=m_rangX;
   if(x+dx>m_pSourceBM->GetBMWidth()) 
      x_finish-=(x+dx-m_pSourceBM->GetBMWidth());

   int y_finish=m_rangY;
   if(y+dy>m_pSourceBM->GetBMHeight() ) 
      y_finish-=(y+dy-m_pSourceBM->GetBMHeight());

   // Расчет новых значений цвета пиксела
   // с учетом соседей, попавших в зону действия 
   // матрицы преобразования
   int NewBGR[3];
   int count=0;
   for(int c=0, mx=0, my=0; c<3; c++)
   {
      NewBGR[c]=0; count=0;
      for(my=y_start; my<y_finish; my++)
      for(mx=x_start; mx<x_finish; mx++)
      {
         if((pSPix=m_pSourceBM->GetPixPtr(x+(mx-dx),
                                          y+(my-dy)))!=NULL)
         {
            NewBGR[c]+=(m_pMatrix[my*m_rangX+mx]*(*(pSPix+c)));
            count+=m_pMatrix[my*m_rangX+mx];
         }
      }
   }
   // Адрес пиксела в изображении-приемнике
   pDPix=m_pDestBM->GetPixPtr(x, y);

   // Установка нового значения в приемное изображение
   for(c=0; c<3; c++)
   {
      // Приведение значения к допустимому диапазону
      if(count!=0)
         NewBGR[c]=NewBGR[c]/count;
      if(NewBGR[c]<0)
         NewBGR[c]=0;
      else if(NewBGR[c]>255)
         NewBGR[c]=255;

      *(pDPix+c)=NewBGR[c];
}

   return TRUE;
};

В методе CMatrixFilter::TransformPix() сначала определяется область перекрытия изображения и матрицы преобразования. Этот шаг необходим в связи с тем, что на границах изображения пиксел может не иметь соседей с одной или двух сторон. На рис. 11.14 показаны некоторые ситуации, когда в преобразовании задействованы не все коэффициенты матрицы. Пикселу, над которым выполняется преобразование, соответствует индекс матрицы с номером 5.


Рис. 11.14. Пересечение матрицы и изображения

Новое значение пиксела формируется с учетом значений всех пикселов и коэффициентов матрицы преобразования, попавших в область перекрытия изображения и матрицы преобразования.

11.6.5. Использование гистограммы яркости для повышения контрастности изображения. Фильтр "Гистограмма"

Гистограмма показывает, насколько полно используется в изображении диапазон яркостей. Изображения со слабым контрастом имеют гистограмму яркости, сгруппированную в небольшом диапазоне значений. Гистограмма таких изображений может быть смещена в область темных или светлых тонов, но может располагаться и в центре диапазона яркостей (рис. 11.15).


Рис. 11.15. Изображение со слабой контрастностью


Рис. 11.16. Изображение с хорошей контрастностью

Гистограмма изображения с хорошим контрастом, как правило, равномерно занимает весь диапазон яркостей (рис. 11.16). Такие изображения обычно воспринимаются как более качественные.

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


Рис. 11.17. Изображение с высокой контрастностью

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

Мы можем попытаться исправить ситуацию, "растянув" яркость пикселов картинки на весь диапазон от 0 до 255. При этом пикселы, которые были темными, станут еще темнее (вплоть до черного), а светлые пикселы станут еще светлее (вплоть до белого).

Шаблон окна диалога "Гистограмма" (IDD_HIST) имеет два ползунка (элементы slider). Используем их для того, чтобы определить нижнюю и верхнюю границы диапазона значений яркости.

Прежде всего, добавим в класс CHistDlg (с помощью ClassWizard) обработку сообщения WM_INITDIALOG. Сообщение WM_INITDIALOG посылается окну диалога перед тем, как оно будет показано на экране. В методе-обработчике этого сообщения установим начальные параметры ползунков и позиции "бегунков" (листинг 11.17).

Листинг 11.17. Обработка сообщения WM_INITDIALOG в классе CHistDlg. Файл HistDlg.cpp
BOOL CHistDlg::OnInitDialog()
{
   CDialog::OnInitDialog();
   // Ползунок нижней границы
   m_ctrlOffset_b.SetRange(0, 127);
   // Бегунок в крайнем левом положении
   m_ctrlOffset_b.SetPos(0);
   // Ползунок верхней границы
   m_ctrlOffset_t.SetRange(128, 255);
   // Бегунок в крайнем правом положении
   m_ctrlOffset_t.SetPos(255);
   // Текст
   m_strOffset_b="0";
   m_strOffset_t="0";
   UpdateData(FALSE);
   return TRUE;
}

Напомним, что переменные m_ctrlOffset_b и m_ctrlOffset_t — это объекты класса CSliderCtrl, связанные с ползунками, а m_strOffset_b и m_strOffset_t — объекты класса CString, связанные с элементами "static" в окне диалога (см. рис. 11.6 и 11.8).

Далее добавим в класс CHistDlg обработчики еще двух сообщений: WM_HSCROLL, которое будет поступать при перемещении бегунка (листинг 11.18), и сообщение о нажатии пользователем кнопки OK в диалоге (листинг 11.19). При обработке сообщения WM_HSCROLL в окне диалога будет выводиться позиция бегунков. При обработке сообщения "нажата кнопка OK" позиции бегунков запоминаются в переменных m_iOffset_b и m_iOffset_е. Эти переменные целого типа добавлены в класс CHistDlg специально для того, чтобы можно было извлечь из него информацию о позициях бегунков после закрытия окна диалога.

Листинг 11.18. Обработка сообщения WM_HSCROLL в классе CHistDlg. Файл HistDlg.cpp
void CHistDlg::OnHScroll(UINT nSBCode, UINT nPos, CScrollBar* pScrollBar)
{
   m_strOffset_b.Format("%d", m_ctrlOffset_b.GetPos());
   m_strOffset_t.Format("%d", 255-m_ctrlOffset_t.GetPos());
   UpdateData(FALSE);
   CDialog::OnHScroll(nSBCode, nPos, pScrollBar);
}
Листинг 11.19. Обработка сообщения "нажата кнопка OK" в классе CHistDlg.
Файл HistDlg.cpp
void CHistDlg::OnOK()
{
   m_iOffset_b=m_ctrlOffset_b.GetPos();
   m_iOffset_t=255-m_ctrlOffset_t.GetPos();
   CDialog::OnOK();
}

Теперь, когда все подготовительные операции выполнены, нам известен диапазон значений яркости, который надо растягивать. Остается только создать нужный фильтр и сообщить ему этот диапазон.

Коррекция яркости — это точечный процесс. Поэтому создадим класс фильтра гистограмма CHistogram как производный от класса CDotFilter (листинг 11.20).

Листинг 11.20. Интерфейс класса CHistogram. Файл Filter.h
// Гистограмма
class CHistogram: public  CDotFilter
{
public:
   BOOL Init(int offset_b, int offset_t);
};

У этого класса объявлен всего один новый метод Init(), в который передаются смещения от нижней и верхней границы диапазона яркостей (листинг 11.21). В методе Init() производится заполнение таблиц преобразования новыми значениями. При этом полный диапазон яркостей от 0 до 255 равномерно распределяется на заданный в диалоге "Гистограмма" диапазон.

Листинг 11.21. Метод CHistogram::Init(). Файл Filter.cpp
BOOL CHistogram::Init(int offset_b, int offset_t)
{
   int range=0;
   // Все элементы в таблицах с индексом от 0 до нижней границы
   // установим в 0
   for(int i=0, t=0; t<3; t++)
      for(i=0; i<offset_b; i++)
      {
         BGRTransTable[t][i]=0;
      }
   // Все значения в таблицах с индексом от 255 до верхней границы
   // установим в 255
   for(t=0; t<3; t++)
      for(i=255; i>=256-offset_t; i--)
      {
         BGRTransTable[t][i]=255;
      }
   // Все значения в таблицах с индексом от нижней до верхней границы
   // равномерно распределим на диапазон от 0 до 255
   double step=256./(256-(offset_b+offset_t));
   for(t=0; t<3; t++)
   {
      double value=0.;
      for(i=offset_b; i<256-offset_t; i++)
      {
         BGRTransTable[t][i]=(int)((value)+0.5);
         value+=step;
      }
   }
   return TRUE;
};

Вернемся теперь к листингу 11.10 и посмотрим, что же происходит после строки:

// Требуется выполнить коррекцию контрастности

После того как пользователь нажал кнопку OK в диалоге "Гистограмма", мы проверяем, "а не сдвинул ли он бегунки на ползунках". Если такое событие произошло, то мы инициализируем объект m_HistogramFilter (объявлен в интерфейсе класса CBMDoc), и делаем его активным, а затем вызываем метод CBMDoc::Transform(), запускающий рабочий поток, в котором и происходит преобразование (см. листинг 11.11).

Посмотрим, какой же эффект даст наша коррекция яркости. Установим диапазон яркостей, который должен быть растянут, передвинем бегунки так, как показано на рис. 11.18.


Рис. 11.18. Коррекция яркости

Результат коррекции яркости показан на рис. 11.19, а гистограмма изображения после коррекции — на рис. 11.20. Для того чтобы эффект преобразования был лучше заметен, на рис. 11.19 показано изображение, лишь половина которого была преобразована. Метод CBMDoc::Transform() имеет такую возможность (листинг 11.11). Гистограмма же на рис. 11.20 соответствует полностью откорректированному изображению.

Мы рассмотрели процесс коррекции яркости вручную. Эту операцию можно выполнить и автоматически. При автоматической коррекции яркости алгоритм должен сначала найти границы корректируемого диапазона. Для этого нужно задать некоторое пороговое значение яркости (например, определить его от уровня средней яркости), а затем выполнить сканирование гистограммы изображения и найти индексы элементов гистограммы, значения которых находятся на заданном пороге, — определить диапазон коррекции. Далее коррекция выполняется точно так же, как и при "ручном" варианте.


Рис. 11.19. Результат коррекции яркости


Рис. 11.20. Гистограмма яркости после коррекции

11.6.6. Фильтр "Яркость/Контраст"

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

Данное преобразование является точечным. Для его реализации добавим в программу фильтр "Яркость/Контраст" класс CBrightCont, производный от класса CDotFilter. Интерфейс класса приведен в листинге 11.22.

Листинг 11.22. Интерфейс класса CBrightCont. Файл Filter.h
// Яркость-контраст
class CBrightCont: public  CDotFilter
{
public:
   BOOL Init(int b_offset, int c_offset);
};

Интерфейс класса CBrightCont ничем не отличается от CHistogram (листинг 11.20). Однако значения b_offset и c_offset параметров метода CBrightCont::Init() могут быть как положительными, так и отрицательными, что будет соответствовать увеличению или уменьшению яркости/контрастности изображения.

Реализация метода CBrightCont::Init() приведена в листинге 11.23. Этот метод инициализирует таблицы преобразования. Сначала выполняется смещение яркости на заданную величину, а затем либо "сжатие", либо "растяжение" диапазона яркости. Причем при сжатии значения яркости изменяются не равномерно, а пропорционально их удаленности от "серой середины", определенной константой CONTRAST_MEDIAN. Значение CONTRAST_MEDIAN 159 задает более светлый оттенок серого (чем, например, 127 — арифметическая середина диапазона яркости) и для многих изображений дает хороший результат. Вы можете поэкспериментировать со значением этой константы и посмотреть, что из этого получится. Однако более правильный подход — не задавать середину диапазона яркости константным значением, а определять на основе гистограммы исходного изображения.

Растяжение диапазона яркости выполняется так же, как было рассмотрено в предыдущем разделе, за исключением того, что смещение по шкале яркости сверху и снизу одинаково и задается параметром c_offset. Еще одним отличием является то, что после преобразования яркости работа по коррекции контрастности происходит со значениями таблицы преобразования, полагая при этом, что они являются индексами в таблице, полученной после коррекции яркости.

Листинг 11.23. Метод CBrightCont::Init(). Файл Filter.cpp
// "Серая середина"
#define CONTRAST_MEDIAN 159
BOOL CBrightCont::Init(int b_offset, int c_offset)
{
   int i=0,   // Индекс цвета в таблице преобразования
      t=0,   // Индекс таблицы
      // Индекс цвета, соответствующего нижней границе яркости
      t_index=0,
      // Индекс цвета, соответствующего верхней границе яркости
      b_index=0,
      value_offset; // Смещение значения цвета
   double value=0.; // Новое значение цвета
   // Изменяем яркость
   for(i, t=0; t<3; t++)
      for(i=0; i<256; i++)
      {
         if( i+b_offset>255) BGRTransTable[t][i]=255;
         else if( i+b_offset<0) BGRTransTable[t][i]=0;
         else BGRTransTable[t][i]=i+b_offset;
      }
   // Изменяем контрастность
   if(c_offset<0)// Уменьшаем контрастность
   {
      for(i=0, t=0; t<3; t++)
      for(i=0; i<256; i++)
      if(BGRTransTable[t][i]<CONTRAST_MEDIAN)
      {
         // Расчитываем смещение в зависимости от удаленности цвета от
         // "серой середины"
         value_offset=(CONTRAST_MEDIAN-BGRTransTable[t][i])*c_offset/128;
         if(BGRTransTable[t][i]-value_offset>CONTRAST_MEDIAN)
            BGRTransTable[t][i]=CONTRAST_MEDIAN;
         else BGRTransTable[t][i]-=value_offset;
      }
      else
      {
         // Расчитываем смещение в зависимости от удаленности цвета от
         // "серой середины"
         value_offset=(BGRTransTable[t][i]-CONTRAST_MEDIAN)*c_offset/128;
         if(BGRTransTable[t][i]+value_offset<CONTRAST_MEDIAN)
            BGRTransTable[t][i]=CONTRAST_MEDIAN;
         else BGRTransTable[t][i]+=value_offset;
      }
   }
   else   if(c_offset>0)
   //Увеличиваем контрастность
   {
      // Расчет нижней границы цвета
      int offset_b=c_offset*CONTRAST_MEDIAN/128;
      // Все значения в таблице ниже нижней границы получат значения 0
      for(t=0; t<3; t++)
      for(b_index=0; b_index<256; b_index++)
      {
         if(BGRTransTable[t][b_index]<offset_b)
            BGRTransTable[t][b_index]=0;
         else break;
      }
      // Расчет верхней границы цвета
      int offset_t=c_offset*128/CONTRAST_MEDIAN;
      // Все значения выше верхней границы получат значения 255
      for(t=0; t<3; t++)
      for(t_index=255; t_index>=0; t_index--)
      {
         if(BGRTransTable[t][t_index]+offset_t>255)
            BGRTransTable[t][t_index]=255;
         else break;
      }
      // Расчет шага изменения интенсивности цвета
      double step=256./(256-(offset_b+offset_t));
      // "Растягиваем" интенсивность цветов между нижней и верхней
      // границами, чтобы они занимали весь диапазон от 0 до 255
      for(t=0; t<3; t++)
      {
         value=0.;
         for(i=b_index; i<=t_index; i++)
         {
            if(BGRTransTable[t][i]>=offset_b ||
               BGRTransTable[t][i]<256-offset_t)
            {
               value=(int)((BGRTransTable[t][i]-offset_b)*step+0.5);
               if(value>255) value=255;
               BGRTransTable[t][i]=(int)(value);
            }
         }
      }
   }
   return TRUE;
};

Для того чтобы пользователь мог указать значения коррекции яркости и контрастности, добавим в программу диалог "Яркость/Контраст" (рис. 11.21). Создание окна диалога очень мало отличается от создания окна для диалога "Гистограмма".


Рис. 11.21. Шаблон диалога коррекции яркости и контрастности


Рис. 11.22. Переменные класса CBrightContDlg.

Заключение

Итак, мы реализовали базовый механизм для внедрения обработки растров в MFC-приложение. Рассмотренные графические фильтры реализуют наиболее популярные растровые преобразования. Но это еще не все! Далее мы рассмотрим такие интересные эффекты, как “Рельеф”, “Контур”, “Чёткость”, а также рассмотрим вопросы печати растровых изображений.


Эта статья опубликована в журнале RSDN Magazine #4-2003. Информацию о журнале можно найти здесь
    Сообщений 0    Оценка 140 [+2/-2]         Оценить