Black Level Calculation for Raw Bayer Image

I have black Raw Bayer Images RGGB color space. I want to go over each pixel in their channel and sum them up for each channel, then divide it by the number of pixels for each channel. I'm trying to build a fast optimized algorithm.

Here is how I have started. The code runs but I still have some issues with the results. Please comment about optimizing my code and if there a better way to calculate the black level of an image.

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;

namespace BlackLevelCorrectionParallel
{
class Program
{
static void Main(string[] args)
{
string directoryPath = @"C:\Examples\blc\";
uint width = 1800;
uint height = 1200;
uint bpp = 10;
uint colorSpace = 0; //RGGB
List<BlcResults> results = new List<BlcResults>();
Parallel.ForEach(Directory.GetFiles(directoryPath, "*.raw").Select(Path.GetFullPath), rawImagePath =>
{
Image newImage = new Image(width, height, bpp, colorSpace);
BlcResults res = newImage.CalculateBlackLevel();
});
}
}
}


Here is my image class - it basically has two functions - readImage and Algorithm.

using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text;
namespace BlackLevelCorrectionParallel
{
public class Image
{
public uint Width { get; set; }
public uint Height { get; set; }
public uint BPP { get; set; }
public uint ColorSpace { get; set; }
public byte[] Data { get; set; }
public Image(uint width, uint height, uint bPP, uint colorSpace)
{
BPP = bPP;
ColorSpace = colorSpace;
Height = height;
Width = width;
}

{
byte[] fileData = null;

if (!File.Exists(path))
{
throw new FileNotFoundException(path);
}

var bytesPerPixel = (BPP + 7) / 8;
var dataSize = Width * Height * bytesPerPixel;
Data = new byte[Width * Height * bytesPerPixel];
var sdata = new short[dataSize / 2];

for (int i = 0, shortIndex = 0; i < dataSize; i += 2, shortIndex++)
{
CopyBytesToShort(fileData[i], fileData[i + 1], out sdata[shortIndex], (int)BPP, false);
}
Buffer.BlockCopy(sdata, 0, Data, 0, Data.Length);
}

private void CopyBytesToShort(byte byte1, byte byte2, out short retShort, int bitsPerPixel, bool isPerformShift)
{
short lsb, msb;
lsb = byte1;
msb = byte2;
if (isPerformShift)
{
lsb <<= 16 - bitsPerPixel;
msb <<= (16 - (bitsPerPixel - 8));
}
else
{
msb <<= 8;
}
retShort = (short)(msb | lsb);
}

public BlcResults CalculateBlackLevel()
{
double channelGR = 0;
double channelR = 0;
double channelGB = 0;
double channelB = 0;

if (ColorSpace == 0) //RGGB
{
for (int i = 0; i < Width; i++)
{
for (int j = 0; j < Height; j++)
{
if (i % 2 == 0 && j % 2 == 0)
{
channelR += Data[i * Height + j];
}
else if (i % 2 == 0 && j % 2 == 1)
{
channelGR += Data[i * Height + j];
}
else if (i % 2 == 1 && j % 2 == 0)
{
channelGB += Data[i * Height + j];
}
else if (i % 2 == 1 && j % 2 == 1)
{
channelB += Data[i * Height + j];
}
}
}
}
else if (ColorSpace == 1)
{

}
else if (ColorSpace == 2)
{

}
else if (ColorSpace == 3)
{

}

double avgChannelB = channelB / ((Width / 2) * (Height / 2));
double avgChannelGB = channelGB / ((Width / 2) * (Height / 2));
double avgChannelGR = channelGR / ((Width / 2) * (Height / 2));
double avgChannelR = channelR / ((Width / 2) * (Height / 2));
BlcResults results = new BlcResults(channelB, channelGB, channelGR, channelR);
return results;
}
}
}


Helper class for results

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;

namespace BlackLevelCorrectionParallel
{
public class BlcResults
{
double AvgChannelB { get; set; }
double AvgChannelGb { get; set; }
double AvgChannelGr { get; set; }
double AvgChannelR { get; set; }
public BlcResults(double channelB, double channelGb, double channelGr, double channelR)
{
AvgChannelB = channelB;
AvgChannelGb = channelGb;
AvgChannelGr = channelGr;
AvgChannelR = channelR;
}
}
}


Your CalculateBlackLevel function can be simplified.

If I understand correctly, your image is a sequence of pixels stored in a packed array (no stride). Each pixel is using 4 bytes. The Height of the image is the number of lines. The Width of the image is the number of channels. (Width / 4 = width of the image)

A few tips:

• don't use floating when you don't need them. They are slow.
• don't use two loops for the coordinates when one can do the job as well (and faster)
• you don't need to compute where you are in the image since it's inferred by the loop. Lots of computations and tests can be avoided.
• at the end of the color calculation I assume you want to store the average you just computed and not the sum (if so there is a small bug in the code provided).

I'm a bit surprised that the image seems to be stored by column instead of by line. (This is unusual to me).

I wrote a pseudo C# code for the principle of what I'd do on a line by line image but this can be adapted to your format. What is missing is mostly casts. The loop is made for the following pattern:

R G
G B


You can notice there are no multiplication, modulo, test/jump so it should be substantially faster. It turns out two levels of loops are needed but for a different reason.

    public BlcResults CalculateBlackLevel()
{
long channelGR = 0; // don't use floats here. Keep integer types.
long channelR = 0;  // use long instead of ints if you plan having
long channelGB = 0; // pictures of more than 2^24 pixels
long channelB = 0;
long size = Width * Height; // number of bytes in the image

if (ColorSpace == 0) //RGGB
{
// for all channels (o is for offset)
for (long o = 0; o < size;)
{
// compute the first line pattern
// first find where the line ends
long end_line = o + Width;
while(o < end_line) // line B/GB
{
// do the next pair of channels
channelB += Data[++o];
channelGB += Data[++o];
}
// compute the second line pattern
// first find where the line ends
end_line = o + Width;
while(o < end_line) // line GR/R
{
// do the next pair of channels
channelGR += Data[++o];
channelR += Data[++o];
}
}
}
else if (ColorSpace == 1)
{

}
else if (ColorSpace == 2)
{

}
else if (ColorSpace == 3)
{

}

// now you need double

double pixel_count = (1.0 * size) / 4.0;

double avgChannelB = (1.0 * channelB) / pixel_count;
double avgChannelGB = (1.0 * channelGB) / pixel_count;
double avgChannelGR = (1.0 * channelGR) / pixel_count;
double avgChannelR = (1.0 * channelR) / pixel_count;

BlcResults results = new BlcResults(avgChannelB , avgChannelGB , avgChannelGR , avgChannelR );
return results;
}


I got a bit more time today.

In CopyBytesToShort, if you ever set isPerformShift to true:

            lsb <<= 16 - bitsPerPixel;
// (16 - (bitsPerPixel - 8)) = 24 - bitsPerPixel
msb <<= (24 - bitsPerPixel);


bitsPerPixel needs to be bigger than 8 for this to set msb to any other value than 0

It looks you plan to handle more bit depths. I'd suggest to have one class for each range of BPP so accessing values would be simple and fast. Let's focus on the 8BPP one.

public class Image
{
public uint Width { get; set; }
public uint Height { get; set; }
public uint BPP { get; set; }
public uint ColorSpace { get; set; }
public byte[] Data { get; set; }
public Image(uint width, uint height, uint bPP, uint colorSpace)
{
if(bPP != 8)
{
throw new Exception("Unsupported BPP");
}

if( ((width & 1) != 0) || ((height & 1) != 0)) )
{
throw new Exception("Width and Height expected to be even");
}

BPP = bPP;
ColorSpace = colorSpace;
Height = height;
Width = width;
}

{
// The original call was copying bytes into shorts then into bytes
// on a little endian architecture this is another way to do nothing
// on a big endian architecture swapping the bytes while copying them from fileData into Data would be more efficient.
// If you expect more that 8BPP then maybe you should have one class of image for each depth.
// So 8 bits for depths from 1 to 8, 16 bits for depths from 9 to 16, ...

if(!File.Exists(path))
{
throw new FileNotFoundException(path);
}

if(Data.Length != Width * Height)
{
throw new Exception("size of the file does not match the expected number of channels");
}
}

public BlcResults CalculateBlackLevel()
{
long channelGR = 0;
long channelR = 0;
long channelGB = 0;
long channelB = 0;
long size = Width * Height;

switch(ColorSpace)
{
case 0: //RGGB, maybe make an union for the colorspaces
{
for(long offset = 0; offset < size;)
{
long line_end = offset + Height;

while(offset < line_end)
{
channelR += Data[offset++];
channelGR += Data[offset++];
}

// given the format, there can only be an even number of columns/rows,
// so no need to test for overflow

line_end = offset + Height;

while(offset < line_end)
{
channelGB += Data[offset++];
channelB += Data[offset++];
}
}

break;
}
default:
{
throw new Exception("Colorspace is not supported");
}
}

double weight = (double)size / 4.0;

double avgChannelB = (double)channelB / size;
double avgChannelGB = (double)channelGB / size;
double avgChannelGR = (double)channelGR / size;
double avgChannelR = (double)channelR / size;

BlcResults results = new BlcResults(avgChannelB, avgChannelGB, avgChannelGR, avgChannelR);
return results;
}
}


I'm still surprised that, based on the original code, the image is store column by column instead of line by line. Could you tell the source/generator for the images? I've seen upside down, backward and forward, but never rotated. Or is it a specific RAW format ?