Для множества алгоритмов распознавания требуется истинное черно-белое изображение. То есть такое, которое содержит только черный, и только белый, цвета. Зачем это нужно и как его получить очень быстро, давайте и поговорим.
Зачем нужно черно-белое изображение?
В начале любого распознавания необходимо провести предварительную обработку. И конечным пунктом обработки зачастую является получение черно-белого изображения.
Черно-белое изображение, это по сути массив логических единиц — либо что-то есть, либо нет. Истина/Ложь. «Что-то есть» — это искомый объект поиска. Не обязательно лицо или буква. Это может быть любая область, границы которой необходимо получить или распознать. И это не обязательно битмап.
А фото, как черно-белое изображение, подойдет?
Нет. Строго говоря, черно-белая фотография — это изображение в оттенках серого. Что для ряда алгоритмов распознавания смерти подобно. Алгоритму нужно два состояния — да/нет, сказал, отрезал. А градации серого — это «ну я не знаю» в количестве 256 вариантов.
Более того, даже если рисунок выглядит, как истинно черно-белый, это может оказаться не так и нарушить работу алгоритма. Результат работы на любом изображении без предварительной подготовки непредсказуем.
Например, если к черно-белому портрету Монро (сегодня она главная) применить пороговую обработку с порогом 251 — получим такое «мохнатое» чудище. Казалось бы — идеальный черно-белый исходник, на любом пороге в интервале 1..254 должен быть идеальный результат. Просто это не черно-белый исходник и гистограмма справа наглядно это показывает — там дофига градаций серого.
А в чем проблема?
Проблема как обычно — в скорости. Допустим, надо обрабатывать видеопоток в реальном времени. Можно потратить уйму времени на поиск готового решения или библиотеки. Хотя, все как обычно, есть под рукой. Надо просто суметь все это вкусно приготовить. Вот в этом и проблема.
Общий подход к подготовке изображения
Вначале надо перевести изображение в оттенки серого. Нам не нужны значения по всем каналам, хочется работать с одним параметром — и это будет яркость. Как сделать изображение в оттенках серого, описано тут. Отбросим пугающее слово Direct2D, нам просто нужны коэффициенты.
Далее, возможно, надо сделать легкое размытие, чтобы мостики, связывающие разные кляксы, стали тоньше и более слились с фоном. Возможно надо что-то сделать с общей яркостью, применить фильтр резкости и т.д. Не суть. В каждом случае — это набор кадров, имеющих некое одно общее свойство. Освещенность, размытость, перекос. Набор мероприятий с изображением зависит от каждого конкретного случая применения.
В конце концов нужно пройтись по всему битмапу, и если яркость пикселя больше некоего порога, сделать его белым, если меньше — черным. Или наоборот. Большинство алгоритмов работает из предположения, что фон — черный. Это логично, потому что черный цвет — это ноль для компьютера. Микроснимки вируса существуют, как правило, на белом фоне. Вот как раз для таких случаев нужно инвертировать фон в черное, а темный вирус — в белое.
На этом шаге должны исчезнуть все незначительные мостики между интересующими объектами. Если этого не произошло — вернуться назад и еще поработать с изображением.
Bitmap
В Delphi есть такой класс — TBitmap. Который позволяет работать с битовой матрицей любого формата быстро и правильно. Просто надо ухватить начало массива данных и работать с ним как с указателем. Давайте для начала приведем любое изображение к оттенкам серого.
Получить изображение в оттенках серого
Оттенки серого получаются, когда значения всех каналов равны. Если цвет — это комбинация красного(R), зеленого(G) и синего(B), то при значении всех каналов R=128, G=128, B=128, получим вот такой серый цвет, при значений всех каналов 191 — такой
Рассмотрим два пути. В первом, значения всех каналов суммируются и результат делится на 3. Во-втором, значение каждого канала необходимо умножить на некий свой коэффициент и сложить..
Таких коэффициентов на самом деле масса разновидностей. Рассмотрим два варианта.
1 2 3 4 |
// коэффициенты с индексом: 0 для R, 1 - G, 2 - B Weights: array[Boolean, 0..2] of Single = ( (0.2126, 0.7152, 0.0722), // HDTV (0.299, 0.587, 0.114)); // PAL и NTSC |
Цикл по матрице разнесен в разные процедуры, чтобы избавиться от условных переходов внутри цикла. Для быстроты. Можно сделать через указатель на функцию расчета и обойтись одним циклом, но пока для ясности пусть будет так. Задействуем позже.
Листинг 1: Преобразовать изображение в оттенки серого
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 |
function F2Byte(const v: single): Byte; begin if v < 1 then Result := 0 else if v > 255 then Result := 255 else Result := Round(v); end; // AMode = 1 PAL, 2 - HDTV, 3 - average function MakeGrayScaleBitmap(const AGraphic: TGraphic; const AMode: Integer): TBitmap; // перевод в оттенки серого путем среднего арифметического procedure AverageGrayscale(s,d: PRGBQuad; const count: Integer); var i: Integer; gray: Byte; begin // цикл по всему массиву // выставлен формат в 4 байта, поэтому не волнуемся и // не тратим время на вычисление смещений for i := 0 to count-1 do begin gray := F2Byte((s^.rgbRed + s^.rgbGreen + s^.rgbBlue)/3); d^.rgbBlue := gray; d^.rgbGreen := gray; d^.rgbRed := gray; d^.rgbReserved := s^.rgbReserved; inc(s); inc(d); end end; // перевод в оттенки серого путем веса на каждый канал procedure StandartGrayscale(s,d: PRGBQuad; const count: Integer); const // коэффициенты с индексом: 0 для R, 1 - G, 2 - B Weights: array[Boolean, 0..2] of Extended = ( (0.2126, 0.7152, 0.0722), // HDTV (0.299, 0.587, 0.114)); // PAL и NTSC var i: Integer; r,g,b: Extended; // коэффициенты, вес gray: Byte; begin r := Weights[(AMode<2), 0]; g := Weights[(AMode<2), 1]; b := Weights[(AMode<2), 2]; for i := 0 to count-1 do begin gray := F2Byte(r * s^.rgbRed + g * s^.rgbGreen + b * s^.rgbBlue); d^.rgbBlue := gray; d^.rgbGreen := gray; d^.rgbRed := gray; // ни на что особо не влияет, т.к. вряд ли захотим прозрачности d^.rgbReserved := s^.rgbReserved; // смело инкрементируем для 32-битной матрицы оба указателя inc(s); inc(d); end; end; var bmp: TBitmap; // битмап, содержащий изображение из AGraphic s: PRGBQuad; // указатель на текущий пиксель в bmp d: PRGBQuad; // указатель на текущий пиксель в Result begin // результат пока неизвестен Result := nil; if not (Assigned(AGraphic) and (AGraphic.Width>0) and (AGraphic.Height>0)) then Exit; // создать битмап - источник картинки bmp := TBitmap.Create; try // загрузить содержимое картинка в битмап bmp.Assign(AGraphic); // нам нужен формат точно на 4 байта bmp.PixelFormat := pf32Bit; // создать битмап-результат Result := TBitmap.Create; Result.Width := bmp.Width; Result.Height := bmp.Height; // нам нужен формат точно на 4 байта Result.PixelFormat := pf32bit; s := bmp.ScanLine[bmp.Height-1]; d := Result.ScanLine[Result.Height-1]; if AMode > 2 then // преобразовать методом среднего арифметического AverageGrayscale(s,d,bmp.Height * bmp.width) else // преобразовать методом коэффициентов на каждый канал StandartGrayscale(s,d,bmp.Height * bmp.width); finally FreeAndNil(bmp) end; end; |
Как видим, картинка 1000 x 988 обрабатывается порядка 16-20 миллисекунд. Справа присутствует черно-белая картинка. Получена следующим образом. Об этом уже упоминалось в начале статьи:
Проходим по всем пикселам изображения в оттенках серого и сравниваем каждый пиксель с некоторым пороговым значением. Если значение яркости ниже порога, полагаем пиксель результата черным, выше — белым. По требованию — наоборот.
Листинг 2: Получить черно-белое изображение из оттенков серого
- AGraphic должен быть заранее подготовленным изображением в оттенках серого;
- AThreshold — пороговое значение в интервале 0..255;
- AInvert — инвертировать результат, т.е. то что ниже порога станет белым, а не черным. И, соответственно, то что выше, станет черным.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
function MakeBWBitmap(const AGraphic: TGraphic; const AThreshold: Integer; const AInvert: Boolean = False): TBitmap; var i,j: Integer; bmp: TBitmap;// битмап, содержащий AGraphic s: PRGBQuad; // указатель на текущий пиксел в источнике d: PByte; // указатель на байт в результате v1,v2: Byte; // белый и черный, при AInvert - наоборот RowWidth: Integer; // ширина строки в результате Start: Integer; // стартовый адрес массива байт результата begin Result := nil; if not (Assigned(AGraphic) and (AGraphic.Width>0) and (AGraphic.Height>0)) then Exit; bmp := TBitmap.Create; try bmp.Assign(AGraphic); bmp.PixelFormat := pf32Bit; Result := TBitmap.Create; Result.Width := bmp.Width; Result.Height := bmp.Height; // делаем пиксель размером в байт, больше не надо Result.PixelFormat := pf8bit; // цвета на замену v1 := 255; v2 := 0; if AInvert then begin v2 := 255; v1 := 0; end; // указатель на начало массива пикселей исходника s := bmp.ScanLine[bmp.Height-1]; // указатель на начало массива результата d := Result.ScanLine[Result.Height-1]; // запомнили целочисленное значение указателя Start := Integer(d); // считаем ширину строки в байтах RowWidth := BytesPerScanline(bmp.Width, 8, 32); // цикл по строками for i := 0 to bmp.Height-1 do begin // расчет указателя на начало новой строки d := PByte(Start + i*RowWidth); // по-пиксельный цикл внутри строки for j := 0 to bmp.Width - 1 do begin // берем для сравнения любой, они одинаковы if s^.rgbRed > AThreshold then d^ := v1 else d^ := v2; // сместился на 4 байта, для pf32bit - // это корректно для всей матрицы inc(s); // сместился на следующий байт, // это корректно внутри строки, для всей матрицы - нет, // поэтому перед заходом в цикл считаем начало новой строки inc(d); end; end; finally FreeAndNil(bmp) end; end; |
На рисунке ниже представлен метод среднего арифметического для оттенков серого с последующей обработкой с порогом 185.
В отличие от предыдущего листинга, в котором смело инкрементируем указатели, здесь перед каждым вхождением в цикл по горизонтали считаем указатель на начало строки. Связано с тем, что ранее для обеих матриц указывали формат pf32bit. При таком формате данные идут аккуратно друг за другом блоками по 4 байта. Сейчас же заказан формат pf8bit и ширина строки матрицы должна быть высчитана с помощью штатной функции BytesPerScanline, которая возвращает реальное количество байт в строке bitmap.
Если поступать также, как в предыдущем листинге, без расчета указателя на начало строки, рискуем получить на выходе такую картину.
Если закомментировать строку 48 в листинге 2, можно получить результат, как на рисунке выше.
1 2 |
// расчет указателя на начало новой строки // d := PByte(Start + i*RowWidth); |
Поэтому, когда работаем с указателем на начало массива пикселей битмапа, всегда надо помнить про реальную ширину строки и функцию BytesPerScanline.
Результаты и выводы
На рисунке ниже применен метод HDTV для получения оттенков серого. Из всех ранее применяемых, он, на мой субъективный взгляд, дал самый приятный для глаза результат.
И результат, и время вполне приемлемы. Но следует учитывать, что во-первых общее время получения черно-белого изображения составило 17.020 + 17.947 = 34.967 миллисекунд, что в принципе можно считать неплохим показателем. Во-вторых, проводить математику прямо на битмапе как-то неправильно. Хочется иметь какой-то буфер с посчитанными значениями. Который можно многократно использовать для различных вычислений.
Также, в процессе вычислений получаем вещественные значения и всегда округляем к байту. Возможно, порог с плавающей запятой и хранение яркости в дробях даст более качественный результат. Давайте попробуем хранить яркость изображения в типе Single. И сравнивать с вещественным порогом.
Floatmap и кубок огня
Что объединяет эти два странных понятия? И то, и другое — творческий вымысел. Кубок — дело рук Дж. К. Роулинг, floatmap — моих.
Давайте напишем небольшой класс. Настолько небольшой, что изначально хотел делать вообще записью, но среди читателей оказывается немало почитателей Delphi 7, поэтому класс. Ничего не хочу сказать плохого про Delphi 7, сам сидел в ней долгое время. Когда-то соскочил в XE из-за работы, и понравилось, блин.
Класс всего лишь хранит вещественные значения и заменяет функции MakeGrayScaleBitmap и MakeBWBitmap, представленные выше.
Листинг 3: Класс TFloatMap
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 |
uses Winapi.Windows, System.SysUtils, System.Classes, Vcl.Graphics, System.Types; const EXCEPTION_GENERATE: Boolean = True; type TCalcGrayMode = (cgmCustom, cgmPal, cgmHDTV, cgmAverage); TCalcGrayFunc = function(const s: PRGBQuad): Single; TFloatMap = class private FData: PSingle; FWidth: Integer; FHeight: Integer; FLength: Integer; public constructor Create(const AWidth, AHeight: Integer); overload; destructor Destroy; override; procedure Clear; // init data (including, called from the constructor) function Init(const AWidth, AHeight: Integer): Boolean; overload; // make data from color graphis function Init(const AGraphic: TGraphic; const AMode: TCalcGrayMode; const ACalcFunc: TCalcGrayFunc = nil): Boolean; overload; // make TFloatMap from color graphis class function Make(const AGraphic: TGraphic; const AMode: TCalcGrayMode; const ACalcFunc: TCalcGrayFunc = nil): TFloatMap; // make bitmap function ToBitmap(const AFormat: TPixelFormat): TBitmap; // make grayscale 24bit bitmap function ToGrayScaleBitmap: TBitmap; // make black-white 8bit bitmap function ToBWBitmap(const AThreshold: Single; const AInvert: Boolean): TBitmap; // pointer of beginning data-array property Data: PSingle read FData; // sizes property Width: Integer read FWidth; property Height: Integer read FHeight; // length data-array (not size, size=length*SizeOf(single)) property Length: Integer read FLength; end; implementation function F2Byte(const v: integer): Byte; overload; begin if v < 1 then Result := 0 else if v > 255 then Result := 255 else Result := v; end; function F2Byte(const v: single): Byte; overload; begin if v < 0.1 then Result := 0 else if v > 254.99 then Result := 255 else Result := Round(v); end; // { TFloatMap } не понимает плагин скобки :) constructor TFloatMap.Create(const AWidth, AHeight: Integer); begin Init(AWidth, AHeight); end; destructor TFloatMap.Destroy; begin Clear; inherited Destroy; end; function TFloatMap.Init(const AWidth, AHeight: Integer): Boolean; begin Clear; FWidth := AWidth; FHeight := AHeight; FLength := Width * Height; Result := FLength > 0; if Result then GetMem(FData, Length*SizeOf(Single)); end; procedure TFloatMap.Clear; begin if Assigned(FData) then FreeMem(FData, Length*SizeOf(Single)); FData := nil; end; function CalcAvg(const s: PRGBQuad): Single; begin Result := (s^.rgbRed + s^.rgbGreen + s^.rgbBlue)/3; end; function CalcStdPAL(const s: PRGBQuad): Single; begin Result := (0.299 * s^.rgbRed + 0.587 * s^.rgbGreen + 0.114 * s^.rgbBlue); end; function CalcStdHDTV(const s: PRGBQuad): Single; begin Result := (0.2126 * s^.rgbRed + 0.7152 * s^.rgbGreen + 0.0722 * s^.rgbBlue); end; var CalcFuncArray: Array[TCalcGrayMode] of TCalcGrayFunc = ( nil, CalcStdPAL, CalcStdHDTV, CalcAvg ); function TFloatMap.Init(const AGraphic: TGraphic; const AMode: TCalcGrayMode; const ACalcFunc: TCalcGrayFunc = nil): Boolean; var calc: TCalcGrayFunc; bmp: TBitmap; i: Integer; s: PRGBQuad; d: PSingle; begin Result := Assigned(AGraphic) and Init(AGraphic.Width, AGraphic.Height); if not Result then Exit; calc := CalcFuncArray[AMode]; if not Assigned(calc) then calc := ACalcFunc; if not Assigned(calc) then begin if EXCEPTION_GENERATE then raise Exception.Create('Unknown grayscale mode') else calc := CalcAvg; end; bmp := TBitmap.Create; try bmp.Assign(AGraphic); bmp.PixelFormat := pf32Bit; s := bmp.ScanLine[bmp.Height-1]; d := FData; for i := 0 to Length-1 do begin d^ := calc(s); //d^ := CalcStdPAL(s); //d^ := (0.299 * s^.rgbRed + // 0.587 * s^.rgbGreen + 0.114 * s^.rgbBlue); inc(s); inc(d); end; finally FreeAndNil(bmp) end; end; class function TFloatMap.Make(const AGraphic: TGraphic; const AMode: TCalcGrayMode; const ACalcFunc: TCalcGrayFunc = nil): TFloatMap; begin Result := nil; if not Assigned(AGraphic) then Exit; Result := TFloatMap.Create(0,0); Result.Init(AGraphic, AMode, ACalcFunc); end; function TFloatMap.ToBitmap(const AFormat: TPixelFormat): TBitmap; var p: PByte; v: PSingle; i,j,k: Integer; gray: Byte; Start: Integer; RowWidth: Integer; pf: TPixelFormat; bytesCount: Integer; begin case AFormat of pf15bit, pf16bit, pf24bit: begin pf := AFormat; bytesCount := 3; end; pf32bit: begin pf := AFormat; bytesCount := 4; end; else begin pf := pf8bit; bytesCount := 1; end; end; Result := TBitmap.Create; Result.PixelFormat := pf; Result.Width := Width; Result.Height := Height; v := FData; p := Result.ScanLine[Height-1]; Start := Integer(p); RowWidth := BytesPerScanline(Width, bytesCount*8, 32); for i := 0 to Height-1 do begin p := PByte(Start + i*RowWidth); for j := 0 to Width-1 do begin gray := F2Byte(v^); for k := 1 to bytesCount do begin p^ := gray; inc(p); end; inc(v); end; end; end; function TFloatMap.ToGrayScaleBitmap: TBitmap; var p: PRGBTriple; v: PSingle; i,j: Integer; gray: Byte; Start: Integer; RowWidth: Integer; begin Result := TBitmap.Create; Result.PixelFormat := pf24bit; Result.Width := Width; Result.Height := Height; v := FData; p := Result.ScanLine[Height-1]; Start := Integer(p); RowWidth := BytesPerScanline(Width, 24, 32); for i := 0 to Height-1 do begin p := PRGBTriple(Start + i*RowWidth); for j := 0 to Width-1 do begin gray := F2Byte(v^); p^.rgbtBlue := gray; p^.rgbtGreen := gray; p^.rgbtRed := gray; inc(p); inc(v); end; end; end; function TFloatMap.ToBWBitmap(const AThreshold: Single; const AInvert: Boolean): TBitmap; var p: PByte; v: PSingle; i,j: Integer; v1,v2: Byte; Start: Integer; RowWidth: Integer; begin Result := TBitmap.Create; Result.PixelFormat := pf8bit; Result.Width := Width; Result.Height := Height; v := FData; p := Result.ScanLine[Height-1]; Start := Integer(p); RowWidth := BytesPerScanline(Width, 8, 32); v1 := 255; v2 := 0; if AInvert then begin v2 := 255; v1 := 0; end; for i := 0 to Height-1 do begin p := PByte(Start + i*RowWidth); for j := 0 to Width-1 do begin if v^ > AThreshold then p^ := v1 else p^ := v2; inc(p); inc(v); end; end; end; |
Основная цель класса — хранить вещественное значение яркости и на основе этих данных получать разного рода изображения, в том числе и черно-белые.
Метод TFloatMap.Init
Получает на вход изображение AGraphic: TGraphic и тип преобразования в оттенки серого — AMode: TCalcGrayMode. Преобразованные данные хранятся внутри класса и доступны через указатель на начало массива в свойстве Data: PSingle.
1 2 3 4 5 6 |
TCalcGrayMode = ( cgmCustom, // пользовательская функция cgmPal, // используются коэффициенты PAL cgmHDTV, // используются коэффициенты HDTV cgmAverage // метод среднего арифметического ); |
Если используется режим пользовательской функции, необходимо ее передать в параметре ACalcFunc: TCalcGrayFunc.
1 2 |
// функция должна вернуть значение яркости/серости TCalcGrayFunc = function(const s: PRGBQuad): Single; |
При подсчете яркости метод использует указатели на функции, которые запрятаны внутри модуля.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
function CalcAvg(const s: PRGBQuad): Single; begin Result := (s^.rgbRed + s^.rgbGreen + s^.rgbBlue)/3; end; function CalcStdPAL(const s: PRGBQuad): Single; begin Result := (0.299 * s^.rgbRed + 0.587 * s^.rgbGreen + 0.114 * s^.rgbBlue); end; function CalcStdHDTV(const s: PRGBQuad): Single; begin Result := (0.2126 * s^.rgbRed + 0.7152 * s^.rgbGreen + 0.0722 * s^.rgbBlue); end; var CalcFuncArray: Array[TCalcGrayMode] of TCalcGrayFunc = ( nil, CalcStdPAL, CalcStdHDTV, CalcAvg ); |
Выбор нужной функции происходит очень просто — из массива. Если в константе EXCEPTION_GENERATE содержится TRUE при НЕуказанной функции будет сгенерировано исключение, иначе возьмется метод среднего арифметического.
1 2 3 4 5 6 7 8 9 10 11 |
calc := CalcFuncArray[AMode]; if not Assigned(calc) then calc := ACalcFunc; if not Assigned(calc) then begin if EXCEPTION_GENERATE then raise Exception.Create('Unknown grayscale mode') else calc := CalcAvg; end; |
Метод TFloatMap.ToBitmap
На вход получает желаемый формат AFormat: TPixelFormat и генерирует битмап в сохраненных оттенках серого. На самом деле результативная матрица имеет только 3 варианта формата: pf32bit, pf24bit и pf8bit. Потому что экзотика на 2 байта или 1-4 бита — это не нужно.
Также, для pf8bit преобразования палитры сейчас не происходит. Потому что оставил эту тему для будущей статьи. Когда напишу, изменю и тут. Подписывайтесь на телегу, чтобы не пропустить )))
Метод TFloatMap.ToGrayScaleBitmap
Возвращает 24-битную матрицу в оттенках серого. Внутри вызова ToBitmap не происходит, чтобы сэкономить время выполнения.
Метод TFloatMap.ToBWBitmap
Возвращает 8-битную матрицу черно-белого изображения. Внутри вызова ToBitmap не происходит, чтобы сэкономить время выполнения.
На вход два параметра:
- AThreshold: Single — пороговое значение
- AInvert: Boolean — надо ли инвертировать.
Результаты и выводы
В итоге у нас есть такое приложение, интерфейс которого представлен на рис.6. Сравнивая с результатами аналогичного метода для Bitmap, видим, что FloatMap делает изображение в оттенках серого на 3-5 миллисекунд медленнее.
«Ну и зачем было тратить мое время?» — спросите Вы.
Не надо горячиться. Посмотрите на время получения черно-белого изображения. И суммарно получается 20.825 + 2.638 = 23.463. Это более чем на 10 миллисекунд быстрее, чем если бы делали через битмап. Напомню, там у нас получилось 34.967 миллисекунд.
Но и это не предел. Давайте еще разгоним. Скажем, до 11-12 миллисекунд. Заинтересовал? Читаем дальше, ставим звезды, комментируем.
Форсаж
Зачем нужно изображение в оттенках серого? Чтобы по значению яркости разделить на черное-белое в зависимости от выбранного порога. Но нам не нужно сравнивать с соседними пикселами, анализировать некую область пикселов. Давайте попробуем сразу получать черно-белое изображение считая яркость внутри алгоритма.
Листинг 4: Сразу черно-белое изображение по цветному
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 |
// вычисление яркости // среднее function CalcAvg(const s: PRGBQuad): Single; begin Result := ((s^.rgbRed + s^.rgbGreen + s^.rgbBlue)/3); end; // PAL function CalcStd1(const s: PRGBQuad): Single; begin Result := (0.299 * s^.rgbRed + 0.587 * s^.rgbGreen + 0.114 * s^.rgbBlue); end; // HDTV function CalcStd2(const s: PRGBQuad): Single; begin Result := (0.2126 * s^.rgbRed + 0.7152 * s^.rgbGreen + 0.0722 * s^.rgbBlue); end; type TCalcGrayFunc = function(const s: PRGBQuad): Single; // сделать черно-белое изображение из любого изображения function MakeBWBitmap(const AGraphic: TGraphic; const AMode: Integer; const AThreshold: Integer; const AInvert: Boolean = False): TBitmap; var bmp: TBitmap; i,j: Integer; s: PRGBQuad; d: PByte; v1,v2: Byte; RowWidth: Integer; Start: Integer; calc: TCalcGrayFunc; begin Result := nil; case AMode of 1: calc := CalcStd1; 2: calc := CalcStd2; 3: calc := CalcAvg; else raise Exception.Create('Unknown grayscale mode'); end; bmp := TBitmap.Create; try bmp.Assign(AGraphic); bmp.PixelFormat := pf32Bit; if bmp.Height * bmp.Width > 1 then begin Result := TBitmap.Create; Result.Width := bmp.Width; Result.Height := bmp.Height; Result.PixelFormat := pf8bit; v1 := 255; v2 := 0; if AInvert then begin v2 := 255; v1 := 0; end; // указатель на начало массива пикселей исходника s := bmp.ScanLine[bmp.Height-1]; // указатель на начало массива результата d := Result.ScanLine[Result.Height-1]; // запомнили целочисленное значение указателя Start := Integer(d); // считаем ширину строки в байтах RowWidth := BytesPerScanline(bmp.Width, 8, 32); // цикл по строками for i := 0 to bmp.Height-1 do begin // расчет указателя на начало новой строки d := PByte(Start + i*RowWidth); // по-пиксельный цикл внутри строки for j := 0 to bmp.Width - 1 do begin if calc(s) > AThreshold then d^ := v1 else d^ := v2; inc(s); inc(d); end; end; end; finally FreeAndNil(bmp); end; end; |
В функцию грузим параметры и для оттенков серого, и для черно-белого изображения, и цветное изображение. Делаем выбор расчета яркости как в TFloatMap. Немного дублируем код. Просто не хочу добавлять ссылку на модуль с классом (IP76.FloatMap), а в модуле выносить описания в секцию interface. Пусть этот модуль (BWTools, в исходниках) для работы только с bitmap будет автономным, без зависимостей.
Получение черно-белого значения происходит очень просто:
1 2 3 4 |
if calc(s) > AThreshold then d^ := v1 else d^ := v2; |
Где calc — указатель на текущую функцию расчета. Получаем такое время.
Галка на Both at once отвечает за получение черно-белого изображения из цветного. То есть два-в-одном. Перевод в градации серого и определение по порогу происходит внутри одного цикла. Время сократилось на 10 миллисекунд. Крутняк!
Листинг 5: Получить черно-белое изображение FloatMap
Пишем аналогичный алгоритм для TFloatMap. Сразу спойлер — на самом деле пригодится другой, более быстрый метод, поэтому и помещаю код в спойлер. Сорян за каламбур.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 |
function TFloatMap.InitBW(const AGraphic: TGraphic; const AThreshold: Single; const AInvert: Boolean; const AMode: TCalcGrayMode; const ACalcFunc: TCalcGrayFunc = nil): Boolean; var calc: TCalcGrayFunc; v1,v2: Single; bmp: TBitmap; i: Integer; s: PRGBQuad; d: PSingle; begin Result := Assigned(AGraphic) and Init(AGraphic.Width, AGraphic.Height); if not Result then Exit; calc := CalcFuncArray[AMode]; if not Assigned(calc) then calc := ACalcFunc; if not Assigned(calc) then begin if EXCEPTION_GENERATE then raise Exception.Create('Unknown grayscale mode') else calc := CalcAvg; end; v1 := 255; v2 := 0; if AInvert then begin v2 := 255; v1 := 0; end; bmp := TBitmap.Create; try bmp.Assign(AGraphic); bmp.PixelFormat := pf32Bit; s := bmp.ScanLine[bmp.Height-1]; d := FData; for i := 0 to Length-1 do begin if calc(s) > AThreshold then d^ := v1 else d^ := v2; inc(s); inc(d); end; finally FreeAndNil(bmp) end; end; class function TFloatMap.MakeBW(const AGraphic: TGraphic; const AThreshold: Single; const AInvert: Boolean; const AMode: TCalcGrayMode; const ACalcFunc: TCalcGrayFunc = nil): TFloatMap; begin Result := nil; if not Assigned(AGraphic) then Exit; Result := TFloatMap.Create(0,0); Result.InitBW(AGraphic, AThreshold, AInvert, AMode, ACalcFunc); end; |
1 2 3 |
map := TFloatMap.MakeBW(Image1.Picture.Graphic, (SpinEdit1.Value/10), chbInvert.Checked, TCalcGrayMode(GetGrayScaleMode)); bmp := map.ToBitmap(pf8bit); |
Получилось 18.99 миллисекунд, что конечно лучше предыдущего показателя, но есть вопрос. Посмотрим на рисунок 4. Время на оттенки серого 8.692 и получение черно-белого 2.616. Ожидается 8.692 + 2.616 = 11.308.
Для начала именно так и поступим — вначале наполним данными FloatMap а потом получим из него черно-белое изображение. Потом проповедь.
Новых методов не пишем, используем то, что есть:
1 2 3 4 |
map := TFloatMap.Make(Image1.Picture.Graphic, TCalcGrayMode(GetGrayScaleMode)); bmp := FFloatMap.ToBWBitmap((SpinEdit1.Value/10), hbInvert.Checked); |
Image1 — цветное изображение Монро. Мы сделали ровно то, что описали выше и хотим получить в результате. Вначале инициализируем данными, а потом получаем на их основе результат.
Трудно поверить, но время даже меньше ожидаемого.
Итак, проповедь. Класс создавался для хранения данных с целью дальнейшего быстрого получения тех или иных результатов. Например ЧБ изображения. Когда класс применяется по назначению, все получается хорошо. Когда не по назначению, возникают косяки. Суть проповеди — топор использовать по назначению!
Что мы делали ранее: получали оттенок серого(долго), из него извлекали ЧБ, сохраняли в массив(долго), получали битмап, где округлялись вещественные значения из массива(очень долго).
Теперь у нас так: получили оттенок серого(долго), сохранили, получили битмап, внутри получения анализируем порог и либо 0, либо 255. Никаких округлений. Мега-шустро.
Давайте посмотрим как обстоят дела на большой картинке.
Разница почти в 100 миллисекунд. Это уже что-то да значит )
Что дальше
Хотел рассказать, как все то же самое сделать силами OpenCV. Какие методы существуют для автоматического поиска порога. Показать как на черно-белом изображении найти эллипс, прямоугольник и линию. Каким образом можно определить контур и получить координаты контура, чтобы можно было потом нарисовать силами векторной графики. Например, когда используем «волшебную кисть» надо же определить координаты контура выделяемой области для эффекта «бегущих муравьев».
Видимо, уже когда-нибудь потом. И так три дня писал. Если есть интерес к этой теме, пишите в комментарии. Что больше интересует из перечисленного выше. Возможно, скорректирую планы и ускорюсь с выдачей )
Если есть желающие взять на себя часть тем, милости прошу. Сделаю авторизацию авторам и передохну )))
Скачать
Cпасибо за внимание!
Надеюсь, материал был полезен. Возможно, будет продолжение. Не пропустите, подписывайтесь на телегу.
Если есть вопросы, с удовольствием отвечу.
Если есть критика, многозначительно промолчу… Шутка. Критика очень приветствуется!
Исходники (Delphi XE 7-10) 319 Кб
Исходники D7 (Delphi 7) 315 Кб
Исполняемый файл (zip) 1.23 Мб
мало что понял, но интересно