For my project, I first load an image from file, and put every pixel into a 2D pixels[,]
array. Then, I want to examine each pixel and split them up into "bins" based on how they are colored, and then sort each bin. So I have a Bin
object, which encapsulates a List<Pixel>
, and I have a List<Bin>
containing a (relatively small) number of bins.
My problem is that when I try to fill these bins from very large images (1920x1200 = 2.3 million pixels, eg), the algorithm I'm using is slower than I'd like, and I've traced the problem down to some C# language-specific features which seem to be taking way longer than I was expecting. I'd like some advice on how to better use C# to remove these bottlenecks.
After loading an image, I call a function called "fillBinsFromSource", which takes an enumerable list of pixels, finds which bin they belong in, and puts them there:
public void fillBinsFromSource(IEnumerable<Pixel> source)
{
Stopwatch s = new Stopwatch();
foreach (Pixel p in source)
{
s.Start();
// algorithm removed for brevity
// involves a binary search of all Bins and a List.Add call
s.Stop();
}
}
For large images, it's expected that my algorithm will take a while, but when I put a Stopwatch outside the function call, it turns out that it takes about twice as long as the time accrued on s
, which means doing the enumeration using foreach
is taking up half the time of this function (about 800 ms of the 1.6 seconds for a 1920x1200 image).
The reason I need to pass an enumerable list is because sometimes users will add only a small region of a picture, not an entire picture. The time-consuming call passes down several iterators, first from an list of images, then from each image in the list, like so (simplified):
class ImageList : IEnumerable<Pixel>
{
private List<Image> imageList;
public IEnumerator<Pixel> GetEnumerator()
{
foreach (Image i in imageList)
foreach (Pixel p in i)
yield return p;
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
class Image : IEnumerable<Pixel>
{
private Pixel[,] pixels; // all pixels in the image
private List<Pixel> selectedPixels;// all pixels in the user's selection
public IEnumerator<Pixel> GetEnumerator()
{
if (selectedPixels == null)
for (int i = 0; i < image.Width; i++)
for (int j = 0; j < image.Height; j++)
yield return pixels[i, j];
else
for (int i = 0; i < selectedPixels.Count; i++)
yield return selectedPixels[i];
}
IEnumerator IEnumerable.GetEnumerator()
{
return GetEnumerator();
}
Then finally I call this
ImageList list; // pretend it contains only 1 image, and it's large
fillBinsFromSource(list);
Question 1) Because of the need to enumerate over both the 2D array of pixels AND the selected region, depending on what the user has chosen, the enumeration is just really slow. How can I speed this up?
Then, after filling all these bins with Pixel
objects, I sort them. I call List<Pixel>.Sort()
and rely on IComparable
, like so:
ImageList list; // pretend it contains only 1 image, and it's large
fillBinsFromSource(list);
foreach(Bin b in allBins)
b.Sort(); // calls List<Pixel>.Sort()
class Pixel : IComparable
{
// store both HSV and RGB
float h, s, v;
byte r, g, b;
// we sort by HSV's value property
public int CompareTo(object obj)
{
// this is much faster than calling CompareTo on a float
Pixel rhs = obj as Pixel;
if (v < rhs.v)
return -1;
else if (v > rhs.v)
return 1;
return 0;
}
Question 2) Suppose allBins
has 7 elements; sorting 7 separate lists which have a total of 2.3 million Pixel
s in them takes about 2 seconds. Sorting one list of 2.3 million random int
s takes under 200 milliseconds. I can appreciate that there's some level of speed-up using primitive types, but is it really over 10x slower to use IComparable
? Are there any speedups to be had here?
I apologize for the long question, if anyone has any advice for me, I'd appreciate it!
fillBinsFromSource
, I would simply do the basic loop inImage
class and use the callback. This seems particularly (and unnecessarily) messy from a design point of view though, but I would avoid using any iterators at all. I would like to prevent muddying up my design though, if possible. – FarnsworthBitmap
class before this project, and had falsely assumedGetPixel
was decently implemented. Before recently implementing the fix involvingLockBits
, simply loading the image into the initial pixel array took like 5 seconds. Fortunately that's been fixed and the loading of the image is separate from the code above. – Farnsworth