2

As a follow up to this question: (How can I draw legible text on a bitmap (Winforms)?), I'm drawing legible but small text on top of a bitmap by calculating the "average" color beneath the text, and choosing an appropriately contrasting color for the text.

I've stolen Till's code from https://stackoverflow.com/a/6185448/3784949 for calculating "average" bmp color. Now I'm looking at the "color difference" algorithm suggested by http://www.w3.org/TR/AERT#color-contrast.

This suggests that I need to make my color brightness at least 125 "units" greater, and my color difference at least 500 units greater, where brightness and difference are calculated like this:

Color brightness is determined by the following formula:

((Red value X 299) + (Green value X 587) + (Blue value X 114)) / 1000

Color difference is determined by the following formula:

(maximum (Red value 1, Red value 2) - minimum (Red value 1, Red value 2)) + (maximum (Green value 1, Green value 2) - minimum (Green value 1, Green value 2)) + (maximum (Blue value 1, Blue value 2) - minimum (Blue value 1, Blue value 2))

How do I implement this? I can set my color by ARGB (I believe, it's a label foreground color); but how do I calculate how much to change each individual value to achieve the difference being required here? I'm not familiar with the math required to break the "difference" units down into their component parts.

As an example, my "average" for one bitmap is: Color [A=255, R=152, G=138, B=129]. How do I "add" enough to each part to achieve the two differences?

EDIT: To be specific, my confusion lies here:

  1. it looks like I need to add to three separate values (R,G,B) to achieve two different goals (new RGB adds up to original plus 125, new RGB adds up to original plus 500

  2. it looks like I may need to "weight" my added brighness values to add more to G than R than B.

I have no idea how to address #1. And I'm not positive I'm correct about #2.

EDIT: Proposed solution

I'm currently experimenting with this:

private Color GetContrastingFontColor(Color AverageColorOfBitmap,
                                      List<Color> FavoriteColors)
{
    IEnumerable<Color> AcceptableColors =
        (IEnumerable<Color>)FavoriteColors.Where(clr =>
        (GetColorDifferenceAboveTarget(AverageColorOfBitmap, clr, (float)200) > 0)
        && (GetBrightnessAboveTarget(AverageColorOfBitmap, clr, (float).125) > 0))
        .OrderBy(clr => GetColorDifferenceAboveTarget(
                            AverageColorOfBitmap, clr, (float)200));
    return AcceptableColors.DefaultIfEmpty(Color.Aqua).First();
}

It's a good framework, but I need to work on selecting the "best" candidate from the list. Right now it's just returning "the qualifying color with the greatest color difference that meets the brightness criteria". However, this allows me to modify the float values (W3's "500 color difference required" is complete crap, zero KnownColors qualify) and experiment.

Support code:

private float GetBrightnessAboveTarget(Color AverageColorOfBitmap, 
                                       Color proposed, float desiredDifference)
{
    float result = proposed.GetBrightness() - AverageColorOfBitmap.GetBrightness();
    return result - desiredDifference;
}

private float GetColorDifferenceAboveTarget(Color avg, Color proposed,
                                            float desiredDifference)
{
    float r1 = Convert.ToSingle(MaxByte(Color.Red, avg, proposed));
    float r2 = Convert.ToSingle(MinByte(Color.Red, avg, proposed));
    float r3 = Convert.ToSingle(MaxByte(Color.Green, avg, proposed));
    float r4 = Convert.ToSingle(MinByte(Color.Green, avg, proposed));
    float r5 = Convert.ToSingle(MaxByte(Color.Blue, avg, proposed));
    float r6 = Convert.ToSingle(MinByte(Color.Blue, avg, proposed));

    float result = (r1 - r2) + (r3 - r4) + (r5 - r6);
    return result - desiredDifference;
}

private byte MaxByte(Color rgb, Color x, Color y)
{
    if (rgb == Color.Red) return (x.R >= y.R) ? x.R : y.R;
    if (rgb == Color.Green) return (x.G >= y.G) ? x.G : y.G;
    if (rgb == Color.Blue) return (x.B >= y.B) ? x.B : y.B;
    return byte.MinValue;
}

private byte MinByte(Color rgb, Color x, Color y)
{
    if (rgb == Color.Red) return (x.R <= y.R) ? x.R : y.R;
    if (rgb == Color.Green) return (x.G <= y.G) ? x.G : y.G;
    if (rgb == Color.Blue) return (x.B <= y.B) ? x.B : y.B;
    return byte.MinValue;
}
Community
  • 1
  • 1
Cardinal Fang
  • 283
  • 2
  • 12
  • The range for color brightness is 125, while the range for the difference is 500 in the linked document. – Jared Windover-Kroes Jul 31 '14 at 20:33
  • 1
    OT, but the whole idea can work well only if your background is rather homogeneous. I would rather print twice, black and white with an offeset off 1,1. A contrasting color will look awful imo, what you need is a contrast in brightness. The brightness is best calculated by the system call of `color.GetBrightness()` (0f-1f) And the best result (which will be readable at the smallest sizes) is to provide the background. The most unobtrusive way is to print larger, semitransparent letters in the same color but either brighter or darker than the background.. – TaW Jul 31 '14 at 20:45
  • My point was just that you had the values reversed, so you can't get a brightness delta of 500 units. It confused me while trying to work out a way to modify the colors. – Jared Windover-Kroes Jul 31 '14 at 20:55
  • @Jared OH! Good catch. I'll fix that. – Cardinal Fang Jul 31 '14 at 21:02
  • @TaW - Keep in mind that I'm only calculating the color of the portion of the bitmap directly behind the auto-sized label. My testing so far indicates a fair amount of homogeneity in such small areas of a large photo (modern camera photos). I sampled some of the various "draw outline text" from SO, but with small fonts, it's not very legible on photos - the issue is partly (as I mention to Moby below) that black simply doesn't show up on a large range of photos. – Cardinal Fang Jul 31 '14 at 21:36
  • Outline? no that's right, not with small font sizes. But printing white with a black offset shadow should be readable most of the time. – TaW Jul 31 '14 at 21:43

2 Answers2

0

This is more an answer to the original question. I call it a homemeade outline.

Using transparency plus the maximum and minimum brightness you can get (white&black) it creates good contrast, at least it looks pretty good on my screen.

It is a mixture of shadowing and transparency. I have subtracted a little from the red component to get the aqua you thought about..

It is creating first a darker version of the background by printing the text 1 pixel up left and the 1 pixel down right. Finally it prints a bright version on top of that. Note that it is not really using black and white because with its semi-transparent pixels the hue really it that of each background pixel.

For an actual printout you will have to experiment, especially with the font but also with the two transparencies!

Also you should maybe switch between white on a black shadow and black on a white highlight, depending on the brightness of the spot you print on. But with this homemeade outline it really will work on both dark and bright backgrounds, it'll just look a little less elegant on a bright background.

using (Graphics G = Graphics.FromImage(pictureBox1.Image) )
{
  Font F = new Font("Arial", 8);
  SolidBrush brush0 = new SolidBrush(Color.FromArgb(150, 0, 0, 0))
  SolidBrush brush1 = new SolidBrush(Color.FromArgb(200, 255, 255, 222))

  G.DrawString(textBox1.Text, F, brush0 , new Point(x-1, y-1));
  G.DrawString(textBox1.Text, F, brush0 , new Point(x+1, y+1));
  G.DrawString(textBox1.Text, F, brush1, new Point(x, y));
}

Edit: This is called from a button click but really should be in the paint event. There the Graphics object and its using block G would be replaced by simply the e.Graphics event parameter..

I noticed that you are using a 'transparent' label to display the data to avoid the details of Graphics.DrawString and the Paint event.

Well that can be done and the result looks rather similar:

string theText ="123 - The quick brown fox..";
Label L1, L2, L3;

pictureBox1.Controls.Add(new trLabel());
L1 = (trLabel)pictureBox1.Controls[pictureBox1.Controls.Count - 1];
L1.Text = theText;
L1.ForeColor = Color.FromArgb(150, 0, 0, 0);
L1.Location = new Point(231, 31);  // <- position in the image, change!

L1.Controls.Add(new trLabel());
L2 = (trLabel)L1.Controls[pictureBox1.Controls.Count - 1];
L2.Text = theText;
L2.ForeColor = Color.FromArgb(150, 0, 0, 0);
L2.Location = new Point(2, 2);  // do not change relative postion in the 1st label!

L2.Controls.Add(new trLabel());
L3 = (trLabel)L2.Controls[pictureBox1.Controls.Count - 1];
L3.Text = theText;
L3.ForeColor = Color.FromArgb(200, 255, 255, 234);
L3.Location = new Point(-1,-1);  // do not change relative postion in the 2nd label!

However you will note that due to the impossiblity of having really transparent controls in Winforms we need a little extra effort. You probably use a label subclass like this:

public partial class trLabel : Label
{
    public trLabel()
    {
        SetStyle(ControlStyles.SupportsTransparentBackColor | ControlStyles.UserPaint, true);
        BackColor = Color.Transparent;
        Visible = true;
        AutoSize = true;
    }
}

This seems to work. But in reality it only seems that way, because upon creation each label gets a copy of its current background from its parent. Which never gets updated. Which is why I have to add the 2nd & 3rd label not to the picturebox I display the image in, but to the 1st and 2nd 'transparent' label respectively.

There simply is not real transparency between Winforms controls unless you draw things yourself.

So the DrawString solution is not really complicated. And it gives you the bonus of allowing you to twist several properties of the Graphics object like Smoothingmode, TextContrast or InterpolationMode

TaW
  • 53,122
  • 8
  • 69
  • 111
  • I'm interested, and I'd like to test with this, but up to this point I've been putting a transparent label on my picture box, and setting the forecolor. To avoid having to create a graphics object and handle paint messages, etc: is there a way to override the text drawing of the label to use this code to draw text? – Cardinal Fang Jul 31 '14 at 23:16
  • And what is G in this context? And brush01? Looks like there might be some code missing? – Cardinal Fang Jul 31 '14 at 23:18
  • I need to know what you were doing for brush01. I'm assuming G was your graphics context, but without brush01, I can't make this legible. – Cardinal Fang Aug 01 '14 at 04:21
  • sorry, was in bed ;-) brush01: that was just a typo, there are just two brushes. please see my edited answer(s). I have added a version with labels and a few notes. – TaW Aug 01 '14 at 06:52
  • Well, it's pretty, I give you that. I think this would be excellent for large graphical banners and the like. I put your code into the picturbox paint message and experimented. But at regular size text, it's not legible enough. I'd have to play with this quite a bit. For some reason the text is scaling with my photo - any idea why? At largest photo size, I still have to make the font over 50 points for it to draw at what I would estimate as 18 point. Also, for some reason, the PictureBox doesn't get a paint message after it first loads, I have to resize the window to get it to paint. – Cardinal Fang Aug 01 '14 at 09:03
  • I went and looked at the TextContrast link you provided (MSDN in German, no less!), and tried their sample RenderHint code. Works great, doesn't scale with my picture, much more legible. Wonder if I could combine it with your homemade outline. I think I will go with DrawString, after all. But I will still try to use my Contrasting Font Color code from above, as I definitely found that your black/white outline wasn't legible enough across a lot of my lighter photos. I tried reversing the black/white, but it didn't change that much. – Cardinal Fang Aug 01 '14 at 09:07
  • Sorry about those German MSDN pages - they keep sneaking in; and since the 'original version' radio button on the top right sticks, I now hardly notice.. To trigger the `Paint` event yourself you call `pictureBox1.Invalidate();`when you are done drawing. How large are the pictures and what SizeMode do you use for the PB? Does it scroll? What size in pixels do you aim at for the text? (We'll probably soon be nudged towards chat..) – TaW Aug 01 '14 at 09:58
-1

Short suggestion: Just use black or white.

The algorithms are giving you a passing criteria, but not an algorithm for determining what colors meet that criteria. So, you will have to create such an algorithm. A naive algorithm would be to loop through every possible color, and calculate the color difference, then see if the difference is greater than 125, and if so you have a good color to use. Better, you could search for the color with the maximum difference.

But that's foolish - if I gave you the color R=152, G=138, B=129 - what do YOU think is a very good color to contrast that with? Just by gut, I'm gonna guess 0,0,0. I picked a color with the farthest possible R value, G value, and B value. If you gave me the color 50,200,75 I'd pick R=255, G=0, B=255. Same logic. So my algorithm is if R<128 choose R = 255, else choose R = 0. Same thing for G, and B.

Now that algorithm only picks RGB values that are 0 or 255. But if you don't like that, now you need a mathematical definition for what is "pretty" and I'll leave you to figure that out on your own. :-)

Moby Disk
  • 3,761
  • 1
  • 19
  • 38
  • I actually did implement a black/white algorithm when first testing my bitmap color calculation. Two problems with it: white text is just not as pretty as Aqua or Yellow text on a dark background, and most of the b/w algorithms I tested returned black most of the time, which was fairly illegible on many photos. Ultimately, customer wants color on transparent background, so I'm willing to put some time into making it happen. Keep in mind that I'm not likely to just use the "calculated difference" color; I'll do the calclulation, then choose from a list of "known colors" that are close. – Cardinal Fang Jul 31 '14 at 21:31
  • Did the black color meet the color difference criteria? If so, why was it illegible? I wonder if the assumption about using the "average" color is not a good one. What if the image had areas of black and areas of white? Someone else's suggestion of using a shadow is a better general-purpose solution since it works no matter what the image color is. – Moby Disk Jul 31 '14 at 21:34
  • I used two of the bw algorithms suggested here: http://24ways.org/2010/calculating-color-contrast/. It chooses black a surprising amount of the time (possibly because many of customer's photos seem to be outdoors scenes, less red than greed). But two problems: 9 point text (largest allowed) simply doesn't outline/shadow that well. Second issue in comment below (too long for this). – Cardinal Fang Jul 31 '14 at 21:49
  • Second issue: frequently, photos don't fill the entire screen in both dimensions, meaning there is a black area on the sides or above the photo. Metadata text often at least partially resides in this black area. I realize this is a design choice, but for now, I'm working with it. So black text is rarely a good thing, which is why I still lean towards known, bright colors like Aqua and Yellow. – Cardinal Fang Jul 31 '14 at 21:51
  • If the text is in the black area on the sides or above the photo, then won't the "average" color beneath the text should be black? So then doesn't the algorithm should pick white? – Moby Disk Aug 01 '14 at 13:20
  • There isn't always letterboxing, just sometimes. I'm just saying that black text is rarely the correct choice (for average cases). BTW, I didn't vote your answer down. It has been useful to me as well. – Cardinal Fang Aug 01 '14 at 18:26