Если под яркостью понимать «осветление» или «затемнение», то в Direct2D этого можно добиться, как минимум, тремя эффектами: Brightness, Exposure, HighlightsShadows. Есть еще универсальный эффект цветовой матрицы. Но эффект Brightness своим существованием напрямую отвечает за яркость, поэтому на нем и сосредоточимся.
Что такое яркость?
Вообще, понятия «яркости» и «насыщенности» носят очень интуитивный характер. Поэтому никто толком сформулировать не может. И если неожиданно поинтересоваться: «что значит плоский?», человек начнет что-то изображать ладонью, все время повторяя — «ну, плоский». Вот с яркостью точно также.
Три определения яркости из разных источников.
Определение из Wiki. О субъективности понятия говорит наличие и содержание вкладки «обсуждение» статьи.
Светлота́ (англ. lightness) — одна из основных характеристик цвета наряду с насыщенностью и тоном. Это субъективная яркость участка изображения, отнесённая к субъективной яркости аналогично освещённой поверхности, воспринимаемой человеком как белая.
Wiki
Определение из блога Юлии Келиди:
По сути, яркость цвета — это то, с какой скоростью мы выделили один цвет из всех других. Это относительная величина, но чаще всего самыми яркими являются чистые цвета спектра, либо самые светлые, т.е. приближенные к белому.
блог Юлии Келиди
Специализированное определение со SkillBox’а:
Если сравнивать эффект Brightness с результатами эффекта Saturation, то это разные понятия. В отличие от определения SkillBox’а, яркость одного цвета лежит в диапазоне от абсолютно белого, до абсолютно черного. В то время как насыщенность лежит в интервале от своего самого «сочного» до серого.
Понять степень яркости можно только через сравнительную степень — чуть светлее, чуть темнее, совсем темный и т.д. Если внимательно прочитать определения, то все они говорят об относительности сравниваемого цвета, либо с его окружением, либо с черным цветом, либо с белым.
Яркость, это как точка в евклидовой геометрии — аксиома. То есть то, что мы понимаем даже не мозжечком, а подвздошной чакрой.
Эффект Brightness
Эффект Brightness появился в версии Direct2D 1.1. Минимальные требования Windows 8 или Platform Update for Windows 7. У меня в виртуалке две 7-ки. Так вот, в той, где IE10, этот эффект присутствует.
Для версии 1.1 в стандартной поставке Delphi ничего нет, мы это обсуждали ранее. Поэтому подключаем модуль Winapi.D2DMissing из библиотеки SVGIconImageList.
В этом модуле содержится описание для интерфейсов: ID2D1DeviceContext, ID2D1Image и ID2D1Effect. Остальное ищем и дописываем руками.
CLSID для эффекта Brightness:
1 2 |
const CLSID_D2D1Brightness: TGUID = '{8cea8d1e-77b0-4986-b3b9-2f0c0eae7887}'; |
Для того, чтобы задать кривую яркости нужны две точки, белая и черная. Чтобы установить их, описываем тип свойств яркости:
1 2 3 4 5 6 |
type TD2D1_BRIGHTNESS_PROP = ( D2D1_BRIGHTNESS_PROP_WHITE_POINT = 0, D2D1_BRIGHTNESS_PROP_BLACK_POINT = 1, D2D1_BRIGHTNESS_PROP_FORCE_DWORD = $ffffffff ); |
Нам понадобится еще один тип и небольшая функция:
1 2 3 4 5 6 7 8 9 10 11 12 |
type TD2D_VECTOR_2F = record x: single; y: single; end; TD2D1_VECTOR_2F = TD2D_VECTOR_2F; function Vector2F(x: single; y: single): TD2D1_VECTOR_2F; begin Result.x := x; Result.y := y; end; |
Таким образом, итоговая функция для создания эффекта выглядит так:
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 |
function GetBrightnessEffect(AContext: ID2D1DeviceContext; AImage: ID2D1Image; const AWhitePoint, ABlackPoint: TPointF; out AEffect: ID2D1Effect): Boolean; var v: TD2D1_VECTOR_2F; hr: HRESULT; begin // Проверить наличие контекста и создать эффект Result := Assigned(AContext) and Succeeded(AContext.CreateEffect( CLSID_D2D1Brightness, AEffect)); // Если не сложилось, выти if not Result then Exit; // Установить входной параметр - изображение // На вход может быть подан эффект (as ID2D1Image) AEffect.SetInput(0, AImage); // сформировать и назначить значения свойствам v := Vector2F(AWhitePoint.X, AWhitePoint.Y); hr := AEffect.SetValue(Cardinal(D2D1_BRIGHTNESS_PROP_WHITE_POINT), D2D1_PROPERTY_TYPE_VECTOR2, @v, SizeOf(TD2D1_VECTOR_2F)); Result := Result and Succeeded(hr); v := Vector2F(ABlackPoint.X, ABlackPoint.Y); hr := AEffect.SetValue(Cardinal(D2D1_BRIGHTNESS_PROP_BLACK_POINT), D2D1_PROPERTY_TYPE_VECTOR2, @v, SizeOf(TD2D1_VECTOR_2F)); Result := Result and Succeeded(hr); end; |
Кривая яркости
Чтобы посчитать значения пикселей итогового растрового изображения используется передаточная функция. Результатом работы функции является кривая яркости. Форму кривой задают две точки — белая и черная. Рассмотрим рисунок:
Слева находится исходник. Справа — изображение, обработанное эффектом яркости. В центре видим диаграмму. Это кривая изменения яркости. По оси X — яркость исходного изображения, по оси Y — посчитанная яркость итогового изображения.
Перетаскивая точки, можно видеть, как меняется яркость правого изображения.
В MSDN описано уравнение, по которому можно построить кривую. На рис.1. кривая строится по указанной в статье передаточной функции. Но есть большое подозрение, что представленное уравнение — неполное.
Передаточная функция
Далее следует перевод фрагмента статьи MSDN.
Эффект Brightness использует белую и черную точки для создания передаточной функции обработки растрового изображения. Следующее уравнение описывает эту функцию. Интенсивность входного сигнала находится в диапазоне от 0 до 1.
1. Для преобразования данных изображения из линейного пространства в нелинейное, используется это уравнение:
2. Обработка изображения происходит в соответствии со следующими значениями:
- input — значения интенсивности пикселей входного изображения от 0 до 1.
- WhitePt (x, y) — местоположение кривой преобразования для более ярких пикселей.
- BlackPt (x, y) — это положение кривой преобразования для более темных пикселей.
3. Обратное преобразование данных изображения в линейное пространство происходит согласно этому уравнению:
Окончательное выходное уравнение и его составные части показаны здесь.
Претензии к функции
Складывается ощущение, что яркость считается по несколько иным законам. Это уравнение — какой-то неполный фрагмент из них. Но это мое личное мнение.
Доводы:
- Указан расчет коэффициента C, но он нигде не используется.
- Указан расчет переменной ElbowCalc, но она нигде не используется.
- Откуда-то свалилась константа 0.37.
Рассмотрим формулу в пункте 1. DtoLogI — на самом деле это расчет X по Y. Если из этой формулы вывести обратное уравнение, то получим следующее:
Последнее слагаемое в знаменателе, ни что иное, как коэффициент С. И формула становится:
Получилось классическое уравнение параболы. Коэффициент С нашел свое применение. Выходит, что в пункте 3 представлена урезанная формула, которая никак не может быть обратной для пункта 1.
Далее, фраза Input ≥ Elbow Point, явно указывает на ElbowPt. Однако, в результате экспериментов оказалось, что ElbowCalc куда более похож на правду.
Если построить кривую по реальным данным, то в каких-то случаях кривые почти совпадают, в каких-то не совпадают совсем. Но главное отличие — получившаяся по реальному изображению кривая, на самом деле оказалась ломаной.
Если ввести параметры, как на рис.3., то получится кривая 2-го порядка. То есть именно такая, какая должна быть для уравнения параболы. Однако, кривая, построенная по реальным данным, имеет вид прямой.
Правое изображение совершенно черное. По диаграмме видно, что на всем диапазоне входящей яркости, на выходе имеем слабое колебание вокруг точки с почти нулевой яркостью.
При этом параболическая кривая явно проигрывает по смыслу просто прямой. Потому что непонятно, зачем вдруг такое замысловатое распределение яркости. Зато с прямой все выглядит логично, понятно, ожидаемо и легко объяснимо.
Решение без функции
От кривой яркости отказываться не хочется. Это очень удобно — манипулировать не числами, а мышкой. Поэтому было сделано следующим образом.
При старте приложения формируется битмап, с вертикально расположенными линиями, цветом от 0 до 255.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
function GetGrayTemplate: TBitmap; var i,j: Integer; s: PRGBQuad; begin Result := TBitmap.Create; Result.PixelFormat := pf32Bit; Result.Width := 256; Result.Height := 5; s := PRGBQuad(Result.ScanLine[Result.Height-1]); for i := 0 to Result.Height-1 do for j := 0 to Result.Width-1 do begin s^.rgbRed := j; s^.rgbBlue := j; s^.rgbGreen := j; s^.rgbReserved := 255; inc(s); end; end; |
Когда нужно получить кривую, применяем эффект Brightness к шаблону, и считываем получившуюся яркость. По оси X — значение цвета из шаблона, по оси Y — из получившегося изображения.
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 |
// Получить кривую яркости, заданную белой и черной // точками по шаблону, сделанному GetGrayTemplate // Размер AData задается вызывающей стороной. // Имеет смысл в диапазоне (6..256) // Результат: размер массива var GCanvas: TDirect2DCanvas = nil; function GetRealBrightnessCurve(const AWPoint, ABPoint: TPointF; const ABitmap: TBitmap; var AData: array of TPointF): Integer; var rct: TRect; bmp: TBitmap; effect: ID2D1Effect; context: ID2D1DeviceContext; bitmap: ID2D1Bitmap; WicImage: TWicImage; i,cnt: Integer; sp: PRGBQuad; dp: PRGBQuad; pnt: TPointF; begin Result := Length(Adata); ZeroMemory(@AData, Result*SizeOf(TPointF)); rct := ABitmap.Canvas.ClipRect; bmp := CreateBmpRect(rct, pf32bit); if not (Assigned(GCanvas)) then GCanvas := TDirect2DCanvas.Create( bmp.Canvas.Handle, bmp.Canvas.ClipRect); WicImage := TWicImage.Create; try WicImage.Assign(ABitmap); GCanvas.RenderTarget.CreateBitmapFromWicBitmap( WICImage.Handle, nil, Bitmap); (GCanvas.RenderTarget as ID2D1DCRenderTarget).BindDC( bmp.Canvas.Handle, bmp.Canvas.ClipRect); GCanvas.RenderTarget.QueryInterface(ID2D1DeviceContext, context); if Assigned(context) and (assigned(bitmap)) then GetBrightnessEffect(context, bitmap as ID2D1Image, AWPoint, ABPoint, effect); GCanvas.BeginDraw; try if Assigned(effect) then context.DrawImage(effect as ID2D1Image); finally GCanvas.EndDraw; end; sp := (ABitmap.ScanLine[ABitmap.Height-1]); dp := (bmp.ScanLine[bmp.Height-1]); cnt := bmp.Width; for i := 0 to cnt - 1 do begin pnt.X := sp^.rgbBlue/255; pnt.Y := dp^.rgbBlue/255; AData[Trunc(pnt.X * Result)] := pnt; inc(sp); inc(dp); end; pnt := AData[0]; for i := 1 to Result - 1 do begin if (AData[i].X=0) and (AData[i].Y=0) then AData[i] := pnt else pnt := AData[i]; end; finally FreeAndNil(WicImage); FreeAndNil(bmp); end; end; initialization finalization FreeAndNil(GCanvas); |
На рис.4. видно, что при таких заданных точках, помимо эффекта яркости, получаем еще и инвертирование. Это логично, если посмотреть на диаграмму. Чем темнее пиксель в исходнике, тем светлее результат.
Как применять эффект
В предыдущей статье подробно изложено, как рисовать с помощью TDirect2DCanvas. Также, рассказано об интерфейсах ID2D1DeviceContext и ID2D1Effect. Поэтому некоторые детали реализации пропущу.
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 |
procedure TFmMain.pbPaint(Sender: TObject); var rct: TRect; ImageRect: TRectF; context: ID2D1DeviceContext; matrix: TD2DMatrix3X2F; effect: ID2D1Effect; begin // Получить контекст FCanvas.RenderTarget.QueryInterface(ID2D1DeviceContext, context); // Инициализировать, если еще не создана, FBitmap: ID2D1Bitmap InitBitmap; // Посчитать прямоугольник с учетом масштаба rct := pb.ClientRect; if FZoom < 1 then ImageRect := GetImageZoomRect(FWicImage,VRectF(rct),FZoom) else ImageRect := GetImageZoomRect(FWicImage,VRectF(rct)); // Назначить рендеру необходимый холст (FCanvas.RenderTarget as ID2D1DCRenderTarget).BindDC( pb.Canvas.Handle, rct); // Если есть контекст, значит интерфейс ID2D1Image точно есть if Assigned(context) then // Получить эффект Brightness GetBrightnessEffect(context, FBitmap as ID2D1Image, FWhitePoint, FBlackPoint, effect); // Старт процесс рисования FCanvas.BeginDraw; try // Очистить все белым FCanvas.RenderTarget.Clear(Vcl.Direct2D.D2D1ColorF(clWhite)); // Сформировать матрицу для аффинного масштаба matrix := TD2DMatrix3X2F.Scale(ImageRect.Width/FWICImage.Width, ImageRect.Height/FWICImage.Height,D2D1PointF(0,0)); if ImageRect.Left > 0 then matrix._31 := ImageRect.Left; if ImageRect.Top > 0 then matrix._32 := ImageRect.Top; // Применить аффинный масштаб FCanvas.RenderTarget.SetTransform(matrix); // Если версия ОС позволяет, рисовать эффект if Assigned(Effect) then context.DrawImage(effect as ID2D1Image) else // Если ОС древняя, рисовать исходный битмап if Assigned(FBitmap) then FCanvas.RenderTarget.DrawBitmap(FBitmap); // Вернуть масштаб matrix := TD2DMatrix3X2F.Identity; FCanvas.RenderTarget.SetTransform(matrix); finally // Закончить рисовать, отправить рендерить в видеокарту FCanvas.EndDraw; end; end; |
Примеры
У меня таких много. Приведу еще один.
Скачать
Исходники (Delphi XE 7-10) 2.84 Mб
Исполняемый файл D2DEffects 0.1 (zip) 2.78 Мб
Однако, забавно, что уравнение кривой яркости на MSDN не совсем некорректное. А если посмотреть иные источники? Наверняка ведь принцип рассчёта тот же. Но не это главное, главное, что эффект работает и показано, как его использовать.
Надеюсь, в следующих статьях будет затронут и упомянутый Saturation, и контраст, и, что особо интересно, есть ли в D2D эффект перецвечивания, оно же пигментация, оно же hue ( http://en.wikipedia.org/wiki/Hue )?
Вообще, имхо, все эти эффекты из теории цвета напрашиваются в отдельный подцикл — яркость, контраст, насыщенность, пигментация, изменения цветовой температуры….
Вообще, как по мне, то тема крайне интересная, так как на практике применений может быть множество, начиная от банальной настройки отображения UI и обработки фотографий, заканчивая, например, подбором гаммы из N сочетаемых между собой цветов (полезно, например, для разработки все того же UI, что бы он не был вырвиглазным, если у программиста не очень развито чувство цветовой гармонии))).
А в целом — благодарствую за статью, как обычно, материал на высоте. Спасибо!
Спасибо!
Иные источники смотрел. Нужна была именно эта передаточная функция. Они же могут быть разные, суть — нелинейное преобразование. И тут могут быть самые разнообразные вариации.
Контраст и резкость — следующая статья, там тоже косяк с математикой (вопиющий причем).
Изменение цветности и не только — через одну (минуя статьи, которые не по этой теме могут появиться).
И, конечно, Saturation — эффект отлично и очень наглядно работает.
Вообще, все о чем говоришь — про цветовые эффекты, уже сделано. Осталось только описать, но, блин, статьи писать раз в 10 дольше, чем кодить.