Advanced color blending with GDI+ - c#

Using GDI+ with Windows Forms, I want to be able to draw with a pen and blend color based on the destination pixel color.
For example, if I draw a line and it passes over black pixels, I want it to be a lighter color (like white for example) so that it's visible. When that same line passes over white pixels, it should be a darker color (black for example) so that it's still clearly visible.
Is there any way to do this with GDI+?

As Hans Passant proposed, you could paint using what's currently in the canvas as the image for a texture brush (you might need double buffering for that to work correctly) and use a ColorMatrix to modify the colors being painted on the canvas.
There is a color matrix that inverts the colors similar to a XOR, the problem is it won't work with the middle gray. A color matrix that inverts RGB and leaves alpha intact would be:
-1, 0, 0, 0, 0
0,-1, 0, 0, 0
0, 0,-1, 0, 0
0, 0, 0, 1, 0
1, 1, 1, 0, 1
Something similar, albeit slower would be to copy the canvas to an image and process that image pixel per pixel with rules such as if the color is brighter than 0.5, make it slightly darker else, make it slightly brighter. Then, you paint with that processed image as a texture brush. This would give a better result but it would be significantly slower than using a ColorMatrix.

You could try XORing the pen color. Paint.NET does this with the selection border to make it visible on any color.

Oh, I don't think this is too difficult. You could create a pen that automatically changes colors based on wherever it is. Simply read the pixel at where the location of the pen is (See example), Get the Alpha component and set the pen color to black or white if it's greater than or less than 255/2 respectively :)

Related

Change InkDrawingAttributes Opacity for Pen brush

I using a color picker to draw with a pen, but using this code I can't change de opacity of the pen color:
InkDrawingAttributes inkDrawingAttributes = InkCanvas.InkPresenter.CopyDefaultDrawingAttributes();
inkDrawingAttributes.Color = ColorPenSelected;
InkCanvas.InkPresenter.UpdateDefaultDrawingAttributes(inkDrawingAttributes);
This way works well with a pen:
But, using the InkCanvas.InkPresenter.CopyDefaultDrawingAttributes() the inkDrawingAttributes.PencilProperties is null and I can't change the Opacity. It is not allowed to change the opacity.
I could change the opacity with this code:
InkDrawingAttributes inkDrawingAttributes = InkDrawingAttributes.CreateForPencil();
inkDrawingAttributes.Color = ColorPenSelected;
inkDrawingAttributes.PencilProperties.Opacity = (double)ColorPenSelected.A * 5 / 255;
InkCanvas.InkPresenter.UpdateDefaultDrawingAttributes(inkDrawingAttributes);
Using as a pencil in CreateForPencil().
Now, I could change the Opacity. However, the brush texture is different, even using Opacity 100%, compared to the first image. There are many dots in the line, instead an unique line:
So, How can I change the opacity for the Pen brush and keep the same texture as the first image? With a continuous line, without dots as in the second image.
I initially thought that you could set the inkDrawingAttributes.Color as an ARGB value, but this isn't possible for the below reason.
Taken from learn.microsoft.com
The value of Color is an ARGB value. However, the value of the
transparency component (A, or alpha channel) is ignored and the
InkStroke is rendered at full opacity.
I'm afraid it would seem that this isn't possible. You could perhaps render the stroke as semi transparent afterwards - But doesn't seem like you can set the opacity of the pen.

Drawing opacity on a transparent Form C#

I have a form that is transparent. (done simply by matching the background color, and transparency key.
When attempting to draw things that are transparent, it simply does not work. As the alpha variable gets closer to 0, the color drawn simply gets closer to the original background color of the form.
SolidBrush opaqueBrush = new SolidBrush(Color.FromArgb(255, 0, 0, 255));
SolidBrush semiTransBrush = new SolidBrush(Color.FromArgb(128, 0, 0, 255));
SolidBrush superTransBrush = new SolidBrush(Color.FromArgb(10, 0, 0, 255));
using these 3 brushes to draw on a regular form would result in the desired effect. But using it on my transparent form simply matches superTransBrush with my transparencyKey/ Background color, and the semiTransBrush is somewhere between my desired color and the background. Anyway around this?
There does not seem to be a way around this from .NET Forms itself.
The definition of alpha blending in .NET states:
The alpha value indicates the transparency of the color — the extent
to which the color is blended with the background color. Alpha values
range from 0 through 255, where 0 represents a fully transparent
color, and 255 represents a fully opaque color.
Alpha blending is a
pixel-by-pixel blending of source and background color data. Each of
the three components (red, green, blue) of a given source color is
blended with the corresponding component of the background color
according to the following formula:
displayColor = sourceColor × alpha / 255 + backgroundColor × (255 – alpha) / 255
Notice there's no consideration given to the transparency of the background itself when calculating alpha blending.
The closest workaround would be to set the CompositingMode to source copy:
e.Graphics.CompositingMode = CompositingMode.SourceCopy;
This prevents blending the transparent colour with the background colour, so your brush will be blue, instead of a blend of blue + background colour. Your brush will still be opaque, however.
Background
Windows Forms is build on top of GDI and GDI+.
GDI is a very old technology dating back a long way in Windows history, and wasn't designed to be alpha aware.
GDI+ Which was shipped with WindowsXP and successors was designed to complement GDI with alpga blending, anti-aliasing, and other such features you'd expect from a modern 2D graphics library.
Unfortunately, GDI+ does not make GDI alpha aware, it only fakes alpha in some cases.
Your Scenario
In your case, it's faking the alpha channel, and drawing the SolidBrush color onto your form, before performing the transparency operation to the form, thus, the renderer sees a different color to the one you specified for transparency and ignores it.
My Scenario
In my case, I was trying to draw a drop shadow to a borderless form. It worked. but it was buggy as hell.
I have achieved what you're trying to do, but you won't do it purely with Managed C#/.NET; you're going to have to call into GDI/GDI+ directly using P/Invoke.

Change background colour of an anti-aliased image using C# with anti-aliasing

I have an image where I need to change the background colour (E.g. changing the background of the example image below to blue).
However, the image is anti-aliased so I cannot simply do a replace of the background colour with a different colour.
One way I have tried is creating a second image that is just the background and changing the colour of that and merging the two images into one, however this does not work as the border between the two images is fuzzy.
Is there any way to do this, or some other way to achieve this that I have no considered?
Example image
Just using GDI+
Image image = Image.FromFile("cloud.png");
Bitmap bmp = new Bitmap(image.Width, image.Height);
using (Graphics g = Graphics.FromImage(bmp)) {
g.Clear(Color.SkyBlue);
g.InterpolationMode = InterpolationMode.NearestNeighbor;
g.PixelOffsetMode = PixelOffsetMode.None;
g.DrawImage(image, Point.Empty);
}
resulted in:
Abstractly
Each pixel in your image is a (R, G, B) vector, where each component is in the range [0, 1]. You want a transform, T, that will convert all of the pixels in your image to a new (R', G', B') under the following constraints:
black should stay black
T(0, 0, 0) = (0, 0, 0)
white should become your chosen color C*
T(1, 1, 1) = C*
A straightforward way to do this is to choose the following transform T:
T(c) = C* .* c (where .* denotes element-wise multiplication)
This is just standard image multiplication.
Concretely
If you're not worried about performance, you can use the (very slow) methods GetPixel and SetPixel on your Bitmap to apply this transform for each pixel in it. If it's not clear how to do this, just say so in a comment and I'll add a detailed explanation for that part.
Comparison
Compare this to the method presented by LarsTech. The method presented here is on the top; the method presented by LarsTech is on the bottom. Notice the undesirable edge effects on the bottom icon (white haze on the edges).
And here is the image difference of the two:
Afterthought
If your source image has a transparent (i.e. transparent-white) background and black foreground (as in your example), then you can simply make your transform T(a, r, g, b) = (a, 0, 0, 0) then draw your image on top of whatever background color you want, as LarsTech suggested.
If it is a uniform colour you want to replace you could convert this to an alpha. I wouldn't like to code it myself!
You could use GIMP's Color To Alpha source code (It's GPL), here's a version of it
P.S. Not sure how to get the latest.
Background removal /replacement, IMO is more art than science, you’ll not find one algorithm fit all solution for this BUT depending on how desperate or interested you are in solving this problem, you may want to consider the following explanation:
Let’s assume you have a color image.
Use your choice of decoding mechanism and generate a gray scale / luminosity image of your color image.
Plot a graph (metaphorically speaking) of numeric value of the pixel(x) vs number of pixels in the image for that value(y). Aka. a luminosity histogram.
Now if your background is large enough (or small), you’d see a part of the graph representing the distribution of a range of pixels which constitute your background. You may want to select a slightly wider range to handle the anti-aliasing (based on a fixed offset that you define if you are dealing with similar images) and call it the luminosity range for your background.
It would make your life easier if you know at least one pixel (sample/median pixel value) out of the range of pixels which defines your background, that way you can ‘look up’ the part of the graph which defines your background.
Once you have the range of luminosity pixels for the background, you may run through the original image pixels, compare their luminosity values with the range you have, if it falls within, replace the pixel in the original image with the desired color, preferably luminosity shifted based on the original pixel and the sample pixel, so that the replaced background looks anti-aliased too.
This is not a perfect solution and there are a lot of scenarios where it might fail / partially fail, but again it would work for the sample image that you had attached with your question.
Also there are a lot of performance improvement opportunities, including GPGPU etc.
Another possible solution would be to use some of the pre-built third party image processing libraries, there are a few open source such as Camellia but I am not sure of what features are provided and how sophisticated they are.

GDI+ text has a black edge

I'm clearing an image with a transparent color (120 alpha) and then drawing a string onto it with a gradient, and then drawing that image onto a larger image but the text has blackish edge to it instead of being nice and smooth like it should be. The text looks fine if the background is drawn with 255 alpha.
120 Alpha: Image
255 Alpha: Image
As you can see, the text is much easier to read with the background fully opaque
Note: the green dot is my cursor
Edit: gfx.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAlias; removes the black edges but it's blurry, I'll try some other combinations of graphics settings and see how this goes.
Edit: gfx.TextRenderingHint = System.Drawing.Text.TextRenderingHint.AntiAliasGridFit; Looks much better, although the A's in the Arial font look a little funky
This is normal behaviour. You must change your drawing order to get it right.
Since you draw the text onto a semi-transparent surface its anti-aliasing pixels will be semi-transparent, too, but somwehre in-between the text color and the background of the first image.
Now, if you draw the result onto another image you will have uniform transparent pixels where no text is, no transparency where text is and varying tranparencies and colors for the antialiasing pixels.
Note that those will have various colors as the antialiasing tries to spread color diferences as well as differences in brightness.
Either write on a non-transparent surface or delay the writing to the end. (Or turn off all anti-aliasing. But that's not nice.)

Drawing a contrasted string on an image

So, I have a snapshot of a video source, which I get into an Image, grab a Graphics object for it, and then draw a timestamp in the bottom right of the image. No problem thus far. However, I cannot guarantee what colour is going to be behind the text, so no matter what brush I use, it will almost certainly clash with some of the images that it is drawn on, making the text unreadable.
I am wondering if anyone knows of a way (either a method in .net, or a nice algorithm), for determining the best colour for a string based on the image behind it.
Cheers
just draw the string 5 times.
One time 1(or2) pixels to the left in black
One time 1(or2) pixels to the right in black
One time 1(or2) pixels above it in black
One time 1(or2) pixels below it in black
and the final time in white on the place where you want it
The only reliable way is to use a contrasting outline.
Back in the days of Commodore 64 sprite graphics, if you wanted something to stand out against any background, you used XOR blitting. Some people referred to this as 'reverse video'.
You can draw lines this way using ControlPaint.DrawReversibleLine, but that won't work for text.
This CodeProject article shows how you can create an XOR brush using interop to gdi32.dll.
Or, if it is allowed, you could use a background color (your choice) for the text (for example white text on black background).
Otherwise, you would need to capture the rectangle where the text is written (for every frame), create the negative image of it, and then get the median color in the rectangle and use it to write the text.
A more complex solution would get you to use two layers (initial picture - L1 and Text (transparent background, black text) - L2),
and before combining them, take all the pixels from L2 that contain text and change the color for the each pixel of the text to the "negative" underlying pixel color value of the L1, but you won't get something that's too usable from a "viewer's" point of view.
This could be a number of variations on the answer by reinier.
Draw underlying text (the four offset ones mentioned by reinier) not in black, but actually in a contrasting color to the foreground color of the actualy text.
Draw the text twice: once in a contrasting color but in bold and/or a slightly larger size, then in the text foreground color over that. Might have to fiddle a bit with coordinates and even need to do the drawing per word or even character to get both passes to nicely align and not give an ugly end result.
Do what reinier suggested, but perhaps not four times (all four directions), but maybe three or even two times to get a kind of "shaded" look.
Let go of the whole "draw text pixel by pixel using API calls" approach and use advanced multilayer compositing techniques like the ones available in WPF design.
For some examples of the last option, check out slides 18 and 21 in Advanced OSM Cartography on SlideShare.
The following snippet shows how to invert a color (background) and then applies Dinah's suggestion to create the background using Graphics.DrawString().
private static Color InvertColor(Color c)
{
return Color.FromArgb(255 - c.R, 255 - c.G, 255 - c.B);
}
// In the following, constants and inplace vars can be parameters in your code
const byte ALPHA = 192;
var textColor = Color.Orange;
var textBrush = new SolidBrush(Color.FromArgb(ALPHA, textColor));
var textBrushBkg = new SolidBrush(Color.FromArgb(ALPHA, InvertColor(textColor)));
var font = new Font("Tahoma", 7);
var info = "whatever you wanna write";
var r = new Rectangle(10, 10, 10, 10);
// write the text
using (var g = Graphics.FromImage(yourBitmap))
{
g.Clear(Color.Transparent);
// to avoid bleeding of transparent color, must use SingleBitPerPixelGridFit
g.TextRenderingHint = TextRenderingHint.SingleBitPerPixelGridFit;
// Draw background for text
g.DrawString(info, font, textBrushBkg, r.Left - 1, r.Top - 1);
g.DrawString(info, font, textBrushBkg, r.Left + 1, r.Top + 1);
g.DrawString(info, font, textBrushBkg, r.Left + 1, r.Top - 1);
g.DrawString(info, font, textBrushBkg, r.Left - 1, r.Top + 1);
// Draw text
g.DrawString(info, font, textBrush, r.Left, r.Top);
}

Categories

Resources