views:

1182

answers:

8

For a hobby project I'm going to build a program that when given an image bitmap will create a cross-stitch pattern as a PDF. I'll be using Cocoa/Objective C on a Mac.

The source bitmap will typically be a 24bpp image, but of the millions of colours available, only a few exist as cross-stitch threads. Threads come in various types. DMC is the most widely available, and almost their entire range is available as RGB values from various web sites. Here's one, for instance.

DMC#  Name               R   G   B
----- ------------------ --- --- ---
blanc White              255 255 255
208   Lavender - vy dk   148  91 128
209   Lavender - dk      206 148 186
210   Lavender - md      236 207 225
211   Lavender - lt      243 218 228
      ...etc...

My first problem, as I see it, is from a starting point of the RGB from a pixel in the image choosing the nearest colour available from the DMC set. What's the best way of finding the nearest DMC colour mathematically, and ensuring that it's a close fit as a colour too?

Although I'll be using Cocoa, feel free to use pseudo-code (or even Java!) in any code you post.

+3  A: 

This is called color quantization, and there are many algorithms available.

One very basic is to just treat RGB colors as points in space, and use plain old Euclidian distance between colors to figure out how "close" they are. This has drawbacks, since human eyes have different sensitivity at different places in this space, so such a distance would not correspond well to how humans perceive the colors. You can use various weighting schemes to improve that situation.

unwind
Instead of home-brewed weighting schemes, I'd suggest an alternate color space.
bzlm
+9  A: 

Use the LAB color space and find the color with the nearest euclidean distance. Doing this in the RGB color space will yield counter-intuitive results. (Or use the HSL color space.)

So just iterate over each pixel and find the color with the closest distance within the color space you choose. Note that the distance must be computed circularly for some color spaces (e.g. those employing hue).

(Most color quanization revolves around actually choosing a palette, but that has already been taken care of in your case, so you can't use the more popular quantization techniques.)

Also, check out this question.

To find the HSB hue in Cocoa, it looks like you can use the getHue method declared in NSColor.h.

However, if you just convert an image to a cross-stitch design using this technique, it will be very hard to actually stitch it. It will be full of single-pixel color fields, which sort of defeats the purpose of cross-stitching.

bzlm
You're assuming the source image is a photograph, but yes. There's a whole other bunch of cross-stitch stuff I've considered, but didn't want to overload the question.
banjollity
No, I actually wasn't assuming that. I was just talking about how the "per pixel" approach doesn't consider the image as a whole. But if your source image is already heavily quantized, then it's ofcourse no problem. (I had my share of thumb-soring bitching stitching.)
bzlm
I don't think details about your intent would overload the question.
bzlm
Note that if you use a space like HSL, you'd need to do a circular Euclidean distance on the hue channel.
Mr Fooz
I've edited the answer to make this more clear. Euclid would be proud.
bzlm
This question was for one part of the whole problem. Heavily quantised images, white backgrounds, as few colours as possible all make for a better cross-stitch. Plus the other tricks like half-stitches for rudimentary anti-aliasing, stitching in lines for fine detail, etc. Too much for 1 question.
banjollity
+1  A: 

get the source for the ppmquant application from the netpbm set of utilities

Alnitak
A: 

Depending on the relevance of the correctness of your color operations, remember to take color spaces into account. While I have studied this somewhat, due to my photography hobby, I'm still a bit confused about everything.

But, as previously mentioned, use LAB as much as possible, because (afaik) it's color space agnostic, while all other methods (RGB/HSL/CMYK) mean nothing (in theory) without a defined color space.

RGB, for example, is just three percentage values (0-255 => 0-100%, with 8-bit color depth). So, if you have an RGB-triplet of (0,255,0), it translates to "only green, and as much of it as possible". So, the question is "how red is red?". This is the question that a color space answers - sRGB 100%-green is not as green as AdobeRGB 100%-green. It's not even the same hue!

Sorry if this went to the offtopic side of things

Henrik Paul
I agree, thinking in terms of hue (using LAB or HSx) is important here.
bzlm
Wikipedia: "Additionally, many of the 'colors' within Lab space fall outside the gamut of human vision, and are therefore purely imaginary; these 'colors' cannot be reproduced in the physical world." - LAB just broke my head!
banjollity
Well, why limit ourselves to the physical world? You're all just in my head anyway.
bzlm
+1  A: 

Others have pointed out various techniques for color quantization. It's possible to use techniques like Markov Random Fields to try to penalize the system for switching thread colors at neighboring pixel locations. There are some generic multi-label MRF libraries out there including Boykov's.

To use one of these, the data elements would be the input colors, the labels would be the set of thread colors, the data terms could be something like the Euclidean distance in LAB space suggested by bzlm, and the neighborhood terms would penalize for switching thread colors.

Mr Fooz
Who'da thought stiching required a degree in computer science?
bzlm
Maybe a computer science degree should require some stitching!?
banjollity
+2  A: 

Interresting... :)

You would not only identify the nearest colors, you would also want to reduce the number of colors used. You don't want to end up with a stitching pattern that uses hundreds of different colors...

I put together some code that does this on a basic level. (Sorry that it's in C#, I hope that it can be somewhat useful anyway.)

There is some further tweaking that needs to be done before the method works well, of course. The GetDistance method weights the importance of hue, saturation and brightness against each other, finding the best balance between those is of course important in order to find the color that looks closest.

There is also a lot that can be done with the method of reducing the palette. In the example I just picked the most used colors, but you probably want to weight in how alike the colors are in the palette. This can be done by picking the most used color, reduce the count for the remaining colors in the list depending on the distance to the picked color, and then resort the list.

The Hsl class that holds a DMC color, can calculate the distance to another color, and find the nearest color in a list of colors:

public class Hsl {

 public string DmcNumber { get; private set; }
 public Color Color { get; private set; }
 public float Hue { get; private set; }
 public float Saturation { get; private set; }
 public float Brightness { get; private set; }
 public int Count { get; set; }

 public Hsl(Color c) {
  DmcNumber = "unknown";
  Color = c;
  Hue = c.GetHue();
  Saturation = c.GetSaturation();
  Brightness = c.GetBrightness();
  Count = 0;
 }

 public Hsl(string dmc, int r, int g, int b)
  : this(Color.FromArgb(r, g, b))
 {
  DmcNumber = dmc;
 }

 private static float AngleDifference(float a1, float a2) {
  float a = Math.Abs(a1 - a2);
  if (a > 180f) {
   a = 360f - a;
  }
  return a / 180f;
 }

 public float GetDistance(Hsl other) {
  return
   AngleDifference(Hue, other.Hue) * 3.0f +
   Math.Abs(Saturation - other.Saturation) +
   Math.Abs(Brightness - other.Brightness) * 4.0f;
 }

 public Hsl GetNearest(IEnumerable<Hsl> dmcColors) {
  Hsl nearest = null;
  float nearestDistance = float.MaxValue;
  foreach (Hsl dmc in dmcColors) {
   float distance = GetDistance(dmc);
   if (distance < nearestDistance) {
    nearestDistance = distance;
    nearest = dmc;
   }
  }
  return nearest;
 }

}

This code sets up a (heavily reduced) list of DMC colors, loads an image, counts the colors, reduces the palette and converts the image. You would of course also want to save the information from the reduced palette somewhere.

Hsl[] dmcColors = {
 new Hsl("blanc", 255, 255, 255),
 new Hsl("310", 0, 0, 0),
 new Hsl("317", 167, 139, 136),
 new Hsl("318", 197, 198, 190),
 new Hsl("322", 81, 109, 135),
 new Hsl("336", 36, 73, 103),
 new Hsl("413", 109, 95, 95),
 new Hsl("414", 167, 139, 136),
 new Hsl("415", 221, 221, 218),
 new Hsl("451", 179, 151, 143),
 new Hsl("452", 210, 185, 175),
 new Hsl("453", 235, 207, 185),
 new Hsl("503", 195, 206, 183),
 new Hsl("504", 206, 221, 193),
 new Hsl("535", 85, 85, 89)
};

Bitmap image = (Bitmap)Image.FromFile(@"d:\temp\pattern.jpg");

// count colors used
List<Hsl> usage = new List<Hsl>();
for (int y = 0; y < image.Height; y++) {
 for (int x = 0; x < image.Width; x++) {
  Hsl color = new Hsl(image.GetPixel(x, y));
  Hsl nearest = color.GetNearest(dmcColors);
  int index = usage.FindIndex(h => h.Color.Equals(nearest.Color));
  if (index != -1) {
   usage[index].Count++;
  } else {
   nearest.Count = 1;
   usage.Add(nearest);
  }
 }
}

// reduce number of colors by picking the most used
Hsl[] reduced = usage.OrderBy(c => -c.Count).Take(5).ToArray();

// convert image
for (int y = 0; y < image.Height; y++) {
 for (int x = 0; x < image.Width; x++) {
  Hsl color = new Hsl(image.GetPixel(x, y));
  Hsl nearest = color.GetNearest(reduced);
  image.SetPixel(x, y, nearest.Color);
 }
}

image.Save(@"d:\temp\pattern.png", System.Drawing.Imaging.ImageFormat.Png);
Guffa
A: 

I've just come across this site and found this subject which I am researching. There has been no action here since March, so I am wondering if Banjollity has found a magical solution to the problem. At present I use djpeg to reduce the number of colours to a reasonable number, then I convert to DMC using my own program which uses euclidian space with RBG, and finally use Paint to remove the spotty pixels. Longwinded but it works, except for the rather poor converion to DMC colours which is why am looking for an alternative.

A: 

Hi,

To the original poster, did you every get anywhere with this? I have just stumbled upon this question trying to find and answer to write exactly the same tool.

If you did finish would you be willing to share it? it seems a shame to re-invent the wheel.

Best Regards

Motley
Sounds like it's time for a StitchOverflow.com!
bzlm