How to get the maximum performance when getting the color of 1 specific pixel, for 35 images?

So the deal: I have an ASP.Net API built on Azure that has two parts: a scheduled job that fetches 35 images (Width: 2000px, Height: 1450px) from an external website and stores it in mine server. The second part is the GET API, which takes X and Y as a parameter. Now what I want to do is this: When the user enters XY, I want the API to go through all 35 images and get the color of that particular point.

I currently have this:

string Data = "";


foreach (string FilePath in   Directory.GetFiles(HttpContext.Current.Server.MapPath("~/Content/Images/")))
{ 
     Bitmap ImageHolder = new Bitmap(FilePath);
     Color color = ImageHolder.GetPixel(PixelX, PixelY);

     string ColorString = color.R.ToString() + " " + color.G.ToString() + " " + color.B.ToString();
     string Time = Path.GetFileNameWithoutExtension(FilePath);

     Data = Data + Time + "|" + ColorString + " ";
}
return Data;

      

Answer:

1015 | 0 0 0 1020 | 0 0 0 1025 | 0 0 0 1030 | 0 0 0 1035 | 0 0 0 1040 | 0 0 0 1045 | 0 0 0 1050 | 0 0 0 1055 | 0 0 0 1100 | 0 0 0 1105 | 0 0 0 1110 | 0 0 0 1115 | 0 0 0 1120 | 0 0 0 1125 | 0 0 0 1130 | 0 0 0 1135 | 0 0 0 1140 | 0 0 0 1145 | 0 0 0 1150 | 0 0 0 1155 | 0 0 0 1200 | 0 0 0 1205 | 0 0 0 1210 | 0 0 0 1215 | 0 0 0 1220 | 0 0 0 1225 | 0 0 0 1230 | 0 0 0 1235 | 0 0 0 1240 | 0 0 0 1245 | 0 0 0 1250 | 0 0 0

It all works now. But, when I post this and use it as an API, the response times sometimes exceed 6000 milliseconds.

Using StopWatch, I can see that all the code is working fine , but sometimes it takes up to 500 milliseconds when the code runs:

Bitmap ImageHolder = new Bitmap(FilePath);

      

Any ideas on how to decrease response times and speed up the process? Pre-calculating everything in the planned canopy job seems to be an effort, since the images are quite large and we'll talk about many points when storing them.

+3


source to share


2 answers


It seems that the files you are dealing with are simple 24-bit bitmaps, right?

In this case, you can avoid using GDI + alltogether (it's a bad idea to use it in ASP.NET anyway), and just parse the bitmap data directly. This will mean that you don't even need to read the entire file - just the header and whatever pixel you need.

If you are really working with simple 24-bit bitmaps, the following should work very well:

Color GetPixel(string fileName, int x, int y)
{
    var buffer = new byte[32];

    using (var file = File.OpenRead(fileName))
    {
        if (file.Read(buffer, 0, 32) < 32) return Color.Empty;

        // Bitmap type. Pretty much everything you find is BM.
        var type = Encoding.ASCII.GetString(buffer, 0, 2);
        if (type != "BM") return Color.Empty;

        // Data offset
        var offset = BitConverter.ToInt32(buffer, 10);

        // Windows bitmaps have width and height in a fixed place:
        var width = BitConverter.ToInt32(buffer, 18);
        var height = BitConverter.ToInt32(buffer, 22);

        if (width < x || height < y) return Color.Empty;

        // Three bytes per pixel, padded to multiples of four
        var rowSize = width * 3 + ((4 - ((width * 3) % 4)) % 4);

        // And get our pixel - since we're non-compressed, non-indexed, 
        // 32-bit pixels, this is easy. Note that bitmaps are usually stored
        // top to bottom:
        file.Seek(offset + ((rowSize * (height - y - 1)) + x * 3), SeekOrigin.Begin);

        if (file.Read(buffer, 0, 3) < 3) return Color.Empty;
        // Alpha
        buffer[3] = 0xFF;

        var color = BitConverter.ToInt32(buffer, 0);
        return Color.FromArgb(color);
    }
}

      

Even if your files are not simple 24-bit maps, this cron job should have no problem converting them - if you want to easily index into bitmap data, you don't have much choice anyway :)



Only a few changes are required to support 8-bit indexed bitmaps:

Color GetPixel(string fileName, int x, int y)
{
    var buffer = new byte[32];

    using (var file = File.OpenRead(fileName))
    {
        if (file.Read(buffer, 0, 32) < 32) return Color.Empty;

        // Bitmap type. Pretty much everything you find is BM.
        var type = Encoding.ASCII.GetString(buffer, 0, 2);
        if (type != "BM") return Color.Empty;

        // Data offset
        var offset = BitConverter.ToInt32(buffer, 10);

        // Windows bitmaps have width and height in a fixed place:
        var width = BitConverter.ToInt32(buffer, 18);
        var height = BitConverter.ToInt32(buffer, 22);

        if (width < x || height < y) return Color.Empty;

        // One byte per pixel, padded to multiples of four
        var rowSize = width + ((4 - ((width) % 4)) % 4);

        // Now we're going to read an index into our palette
        file.Seek(offset + ((rowSize * (height - y - 1)) + x), SeekOrigin.Begin);
        if (file.Read(buffer, 0, 1) < 1) return Color.Empty;

        // Jump to the palette record and get the actual color
        file.Seek(54 + buffer[0] * 4, SeekOrigin.Begin);
        if (file.Read(buffer, 0, 4) < 4) return Color.Empty;

        var color = BitConverter.ToInt32(buffer, 0);
        return Color.FromArgb(color);
    }
}

      

If you want to avoid writing bitmap parsing code, you just need to make sure the bitmaps are always loaded and parsed in memory - the bottleneck is not GetPixel

, it loads Bitmap

from disk.

It might be worth caching the headers and palette of each of the images to avoid some searches - I'm not sure if this helps at all, but the basic idea is this:

private static readonly ConcurrentDictionary<string, byte[]> _cache;

Color GetPixel(string fileName, int x, int y)
{
  var buffer = new byte[3];

  using (var file = File.OpenRead(fileName))
  {
    byte[] headers;
    if (_cache.ContainsKey(fileName))
    {
      headers = _cache[fileName];
    }
    else
    {
      headers = new byte[1078];
      if (file.Read(headers, 0, headers.Length) < headers.Length) return Color.Empty;

      _cache.TryAdd(fileName, headers);
    }

    // Now read the headers as before, using the headers local instead of buffer
    // ...

    file.Seek(offset + ((rowSize * (height - y - 1)) + x), SeekOrigin.Begin);

    if (file.Read(buffer, 0, 1) < 1) return Color.Empty;

    var color = BitConverter.ToInt32(headers, 54 + buffer[0] * 4);

    return Color.FromArgb(color);
  }
}

      

+4


source


These are quite large images and will take a significant amount of time to read from disk and a reasonable amount of RAM (which may never be recovered during compaction - "LOB heap fragmentation"). The internals of the Bitmap.ctor (file) are simply passed into the native GDI method - so your latency is entirely IO based.



The fastest way to achieve your goal is to calculate the file-based pixel offset based on your knowledge of the file format and open the file stream yourself, search for the pixel and read only the data you want. It's a bit tricky, but these file formats are well documented and an existing file library may exist for them. The main thing here is not to parse the entire file into RAM.

+2


source







All Articles