I seem to have a sepia tone that is almost working properly. For some reason a portion of the image turns out to be lime green! Does anyone know what i might be doing wrong? Met
You have 2 problems in your algo (at least, if you follow algo description from here).
First, as others pointed out, you have byte type overflow. Second, all your output color values must be based on the input color values, not calculated sequentially.
Here's the fixed main loop code:
for (int i = 0; i < rgbValues.Length; i += 4)
{
int inputRed = rgbValues[i + 2];
int inputGreen = rgbValues[i + 1];
int inputBlue = rgbValues[i + 0];
rgbValues[i + 2] = (byte) Math.Min(255, (int)((.393 * inputRed) + (.769 * inputGreen) + (.189 * inputBlue))); //red
rgbValues[i + 1] = (byte) Math.Min(255, (int)((.349 * inputRed) + (.686 * inputGreen) + (.168 * inputBlue))); //green
rgbValues[i + 0] = (byte) Math.Min(255, (int)((.272 * inputRed) + (.534 * inputGreen) + (.131 * inputBlue))); //blue
}
Note that inside the Min function I cast the color value from double
to int
otherwise Min(double, double)
overload is called and 255 gets first converted to double and then possibly back to byte, involving an extra rounding.
In case if someone needs a sample console app sepia converter, here's the final code I have:
namespace ConsoleApplication8_Sepia
{
using System;
using System.Drawing;
using System.Drawing.Imaging;
class Program
{
static void Main(string[] args)
{
Bitmap b = (Bitmap)Bitmap.FromFile("c:\\temp\\source.jpg");
SepiaBitmap(b);
b.Save("c:\\temp\\destination.jpg", ImageFormat.Jpeg);
}
private static void SepiaBitmap(Bitmap bmp)
{
Rectangle rect = new Rectangle(0, 0, bmp.Width, bmp.Height);
BitmapData bmpData = bmp.LockBits(rect, ImageLockMode.ReadWrite, PixelFormat.Format32bppRgb);
IntPtr ptr = bmpData.Scan0;
int numPixels = bmpData.Width * bmp.Height;
int numBytes = numPixels * 4;
byte[] rgbValues = new byte[numBytes];
System.Runtime.InteropServices.Marshal.Copy(ptr, rgbValues, 0, numBytes);
for (int i = 0; i < rgbValues.Length; i += 4)
{
int inputRed = rgbValues[i + 2];
int inputGreen = rgbValues[i + 1];
int inputBlue = rgbValues[i + 0];
rgbValues[i + 2] = (byte)Math.Min(255, (int)((.393 * inputRed) + (.769 * inputGreen) + (.189 * inputBlue))); //red
rgbValues[i + 1] = (byte)Math.Min(255, (int)((.349 * inputRed) + (.686 * inputGreen) + (.168 * inputBlue))); //green
rgbValues[i + 0] = (byte)Math.Min(255, (int)((.272 * inputRed) + (.534 * inputGreen) + (.131 * inputBlue))); //blue
}
System.Runtime.InteropServices.Marshal.Copy(rgbValues, 0, ptr, numBytes);
bmp.UnlockBits(bmpData);
}
}
}
To fix the issue, change the loop like this:
for (int i = 0; i < rgbValues.Length; i += 4)
{
int red = rgbValues[i + 2];
int green = rgbValues[i + 1];
int blue = rgbValues[i + 0];
rgbValues[i + 2] = (byte)Math.Min((.393 * red) + (.769 * green) + (.189 * blue), 255.0); // red
rgbValues[i + 1] = (byte)Math.Min((.349 * red) + (.686 * green) + (.168 * blue), 255.0); // green
rgbValues[i + 0] = (byte)Math.Min((.272 * red) + (.534 * green) + (.131 * blue), 255.0); // blue
}
There are arithmetic overflows occurring in your calculations, that's the reason for the wrong colors. An expression of type double
is being explicitly cast to byte
before it is compared to 255, therefore it will never be greater than 255.
Your values are overflowing and wrapping around.
Your attempt to protect against this with (rgbValues[i + 0]) > 255
has no effect because a byte[]
cannot store values over 255 anyway, so the values overflowed and wrapped as soon as you put them in rgbValues
. You need to clamp them before storing them in the array. C# has a function Math.Min()
that would be excellent for this purpose.
On the other hand, given that you are getting overflow, you probably want to fix that in the first place — clamping will create an "overexposure" effect (because overexposure is clamping), which is probably undesirable. Adjust your coefficients so that you are changing the color but not changing the (perceived) brightness (I don't have a reference for this; sorry).
As an entirely separate problem, as noted by @Yacoder, your first line modifies the inputs the second one uses, and so on, so your calculation will be off. You need to either the three inputs or the three outputs in temporary variables.
You might also want to look into whether System.Drawing.Imaging
has a color matrix image transformation operation, because that's what you're doing by hand here, and the system-provided version will probably be faster. (I don't know C# so I can't comment on that.)