How to create 8 bpp BMP in Magick.NET? - c#

Using Magick.NET-Q8-AnyCPU. I want to convert existing TIFF image to grayscale 8 bpp BMP image. I tried this:
byte[] input = <existing TIFF image>;
using (var image = new MagickImage(input))
{
image.Grayscale();
image.ColorType = ColorType.Palette;
image.Depth = 8;
image.Quantize(new QuantizeSettings() { Colors = 256, DitherMethod = DitherMethod.No });
byte[] result = image.ToByteArray(MagickFormat.Bmp);
return result;
}
In FastStone Viewer the image is reported as 8-bit, HOWEVER in file Properties > Details the image is reported as Bit depth: 32. I need it to be 8 here. I can convert this image in Paint.NET, and when I choose there Bit Depth: 8-bit then new image will properly show 8-bit depth in file properties.
So, Paint.NET creates proper 8-bit bitmap. How to do it with Magick.NET?

Windows Explorer displays all compressed BMP files as 32-bit contrary to their actual bit depths.
I don't know it's a bug or not but I'm a little bit closer to call it a bug.
Because; after creating a 8bpp BMP file with your code, when I open the file with a binary editor, in the bitmap header struct I saw that the bits per pixel field value (blocks 28-29) was 8 as it must be. Also, the next byte 01 (offset 30) means that the data compressed with Run-length encoding which is a straightforward lossless data compression algorithm.
Therefore, I can say that there's no problem with the image you produced with Magick.NET, it's certainly an 8bpp BMP image file, but compressed.
Unlike Magick.NET's defaults, it seems that Paint.NET produce uncompressed BMP files, that's why you see different bit depths due to weirdness of Windows Explorer.
To fix this, you can disable compression so the bit depth value displayed in the properties dialog will be the value you expect.
image.Settings.Compression = CompressionMethod.NoCompression;
byte[] result = image.ToByteArray(MagickFormat.Bmp);

It seems that's impossible. Neither image.Depth = 8 nor image.BitDepth(8) works.
May be the root is in:
// ImageMagick.MagickImage.NativeMethods.X{ver.}
[DllImport("Magick.Native-Q8-x{ver.}.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern void MagickImage_SetBitDepth(IntPtr Instance, UIntPtr channels, UIntPtr value);
or
// ImageMagick.MagickImage.NativeMethods.X{ver.}
[DllImport("Magick.Native-Q8-x{ver.}.dll", CallingConvention = CallingConvention.Cdecl)]
public static extern void MagickImage_WriteStream(IntPtr Instance, IntPtr settings, ReadWriteStreamDelegate writer, SeekStreamDelegate seeker, TellStreamDelegate teller, ReadWriteStreamDelegate reader, out IntPtr exception);
Looks like it can not create 8 bits .bmp, though there is no problem with .png.
var original = #"D:\tmp\0.tif";
var copy = #"D:\tmp\0.bmp";
using (var image = new MagickImage(original))
{
image.Grayscale();
image.ColorType = ColorType.Palette;
image.Quantize(new QuantizeSettings() { Colors = 256, DitherMethod = DitherMethod.No });
byte[] result = image.ToByteArray(MagickFormat.Png8);
File.WriteAllBytes(copy, result);
}
Console.WriteLine("Press 'Enter'..."); // one have 8 bits .png here
Console.ReadLine();
using (var image = new MagickImage(copy))
{
byte[] result = image.ToByteArray(MagickFormat.Bmp3);
File.WriteAllBytes(copy, result);
} // but ends up with 32 bits .bmp again here
I also noticed that
image.Quantize(new QuantizeSettings() { Colors = 16, DitherMethod = DitherMethod.No });
produces 4 bits result. Gradual increasing gives 32 bits, but never 8 bits.

Related

How to set bit depth in Magick.NET Read

How can I specify the bit depth for the MagickImage.Read() function when reading binary files?
I have a 1024x1024 image represented by 8-bit grayscale values (total file length = 1024x1024 = 1048576 bytes). Using ImageMagick v.7.0.8-7 Q16 x64, I can convert the file using
magick.exe -depth 8 -size 1024x1024 -format Gray Gray:filepath.bin convertedfile.png
When I try to convert the file using Magick.NET Q16-AnyCPU v7.5.0.1,
public MagickImage ReadNewMagickImageFromBinary(string fileName){
MagickReadSettings settings = new MagickReadSettings();
settings.Width = 1024;
settings.Height = 1024; //if I use settings.Height = 512; , I'm OK.
settings.Format = MagickFormat.Gray;
//settings.Depth = 8; //didn't work
//settings.SetDefine(MagickFormat.Gray, "depth", "8"); //also didn't work
MagickImage newImage = new MagickImage();
newImage.Depth = 8; //this appears to be ignored once the Read function is called
newImage.Read(fileName, settings);
return newImage;
}
I get the error
Message: ImageMagick.MagickCorruptImageErrorException : unexpected
end-of-file '': No such file or directory #
error/gray.c/ReadGRAYImage/241
Indicating that the program has read past the end of the file. I've confirmed that Magick.NET is reverting to a 16-bit depth instead of the 8-bit depth I want. I can read the file using settings.Height = 512 instead of 1024, which gives me a squashed version of my grayscale image.
I learned from Memory consumption in Magick.NET that Magick.NET Q16 stores pixels in memory with 16-bit precision; I'm fine with that but it doesn't seem that should preclude 8-bit reading capabilities.
How do I force Magick.NET Q16 to read pixels in with an 8-bit depth?
I just published Magick.NET 7.6.0.0 that now has better API for reading raw pixels. You should change your code to this:
public MagickImage ReadNewMagickImageFromBinary(string fileName)
{
var width = 1024;
var height = 1024;
var storageType = StorageType.Char;
var mapping = "R";
var pixelStorageSettings = new PixelStorageSettings(width, height, storageType, mapping);
return new MagickImage(fileName, pixelStorageSettings);
}
One workaround is to use the Magick.NET Q8 version instead of Magick.NET Q16. I successfully read the file using the program Magick.NET Q8-AnyCPU 7.5.0.1.
I'm still hoping there's a solution that allows me to still use Magick.NET Q16, but this works for now.

Using SharpAvi to save screenshots to an AVI produces 100 frames of blank video

My game takes a screenshot each game loop and stores it memory. The user can then press "print screen" to trigger "SaveScreenshot" (see code below) to store each screenshot as a PNG and also compile them into an AVI using SharpAvi. The saving of images works fine, and a ~2sec AVI is produced, but it doesn't show any video when played. It's just the placeholder VLC Player icon. I think this is very close to working, but I can't determine what's wrong. Please see my code below. If anyone has any ideas, I'd be very appreciative!
private Bitmap GrabScreenshot()
{
try
{
Bitmap bmp = new Bitmap(this.ClientSize.Width, this.ClientSize.Height);
System.Drawing.Imaging.BitmapData data =
bmp.LockBits(this.ClientRectangle, System.Drawing.Imaging.ImageLockMode.WriteOnly,
System.Drawing.Imaging.PixelFormat.Format24bppRgb);
GL.ReadPixels(0, 0, this.ClientSize.Width, this.ClientSize.Height, PixelFormat.Bgr, PixelType.UnsignedByte,
data.Scan0);
bmp.UnlockBits(data);
bmp.RotateFlip(RotateFlipType.RotateNoneFlipY);
return bmp;
} catch(Exception ex)
{
// occasionally getting GDI generic exception when rotating the image... skip that one.
return null;
}
}
private void SaveScreenshots()
{
var directory = "c:\\helioscreenshots\\";
var rootFileName = string.Format("{0}_", DateTime.UtcNow.Ticks);
var writer = new AviWriter(directory + rootFileName + ".avi")
{
FramesPerSecond = 30,
// Emitting AVI v1 index in addition to OpenDML index (AVI v2)
// improves compatibility with some software, including
// standard Windows programs like Media Player and File Explorer
EmitIndex1 = true
};
// returns IAviVideoStream
var aviStream = writer.AddVideoStream();
// set standard VGA resolution
aviStream.Width = this.ClientSize.Width;
aviStream.Height = this.ClientSize.Height;
// class SharpAvi.KnownFourCCs.Codecs contains FOURCCs for several well-known codecs
// Uncompressed is the default value, just set it for clarity
aviStream.Codec = KnownFourCCs.Codecs.Uncompressed;
// Uncompressed format requires to also specify bits per pixel
aviStream.BitsPerPixel = BitsPerPixel.Bpp32;
var index = 0;
while (this.Screenshots.Count > 0)
{
Bitmap screenshot = this.Screenshots.Dequeue();
var screenshotBytes = ImageToBytes(screenshot);
// write data to a frame
aviStream.WriteFrame(true, // is key frame? (many codecs use concept of key frames, for others - all frames are keys)
screenshotBytes, // array with frame data
0, // starting index in the array
screenshotBytes.Length); // length of the data
// save it!
// NOTE: compared jpeg, gif, and png. PNG had smallest file size.
index++;
screenshot.Save(directory + rootFileName + index + ".png", System.Drawing.Imaging.ImageFormat.Png);
}
// save the AVI!
writer.Close();
}
public static byte[] ImageToBytes(Image img)
{
using (var stream = new MemoryStream())
{
img.Save(stream, System.Drawing.Imaging.ImageFormat.Png);
return stream.ToArray();
}
}
From what I see, you're providing the byte-array in png-encoding, yet the stream is configured as KnownFourCCs.Codecs.Uncompressed.
Furthermore, from the manual:
AVI expects uncompressed data in format of standard Windows DIB, that is bottom-up bitmap of the specified bit-depth. For each frame, put its data in byte array and call IAviVideoStream.WriteFrame()
Next, all encoders expect input image data in specific format. It's BGR32 top-down - 32 bits per pixel, blue byte first, alpha byte not used, top line goes first. This is the format you can often get from existing images. [...] So, you simply pass an uncompressed top-down BGR32
I would retrieve the byte-array directly from the Bitmap using LockBits and Marshal.Copy as described in the manual.

Reading a ROS CompressedImage data byte array in C#

I would like to access the value of each individual pixel value of a 16UC1-formatted png image, which I receive as a byte[].
I am realtively new to image processing in C# and I got stuck at this problem for days now.
I can work with a "typical" bgr8-formatted jpg/png byte array simply by:
private static Bitmap getBitmap(byte[] array)
{
return new Bitmap(new MemoryStream(array));
}
I tried many things for the 16UC1-format. The furthest i got is:
private Bitmap getBitmap(byte[] array)
{
var bitmap = new Bitmap(640,480,PixelFormat.Format16bppRgb555);
var bitmapData = bitmap.LockBits(new Rectangle(0, 0, 640, 480), ImageLockMode.WriteOnly, PixelFormat.Format16bppRgb555);
System.Runtime.InteropServices.Marshal.Copy(bitmapData.Scan0, array, 0, array.Length);
bitmap.UnlockBits(bitmapData);
return bitmap;
}
this at least returns a bitmap, though it is completely black.
Trying PixelFormat.Format16bppGrayScale instead of PixelFormat.Format16bppRgb555 gives me a "General error in GDI+".
When writing the byte array to a file by e.g. by
File.WriteAllBytes(filename, array);
I can see the image with image viewers like IrfanView, though Windows photo viewer fails.
Reading the file as a Bitmap is not required. I want to avoid file operations for performance reasons. I simply want to access each individual xy-pixel of that image.
Update:
I started using Emgu.CV and applying imdecode as Dan suggested below.
private Bitmap getCompressedDepthBitmap(byte[] data)
{
Mat result = new Mat(new Size(640, 480), DepthType.Cv16U, 1);
CvInvoke.Imdecode(data,LoadImageType.AnyDepth, result);
return result.Bitmap;
}
This again gives me a black image. (By saving the byte array via WriteAllBytes I see useful contents.) I also tried
Image<Gray, float> image = result.ToImage<Gray, float>();
image.Save(Path.Combine(localPath, "image.png"));
which as well gave me a black image.
I am planning to normalize the Mat now somehow, maybe this helps...
Thank you for your interest and your support!
After hours and hours of wasted working time and despair I finally found the solution...
One important thing I was missing in my description above is that the image data byte[] is coming from of a ROS sensor_msgs/CompressedImage.msg.
The data byte array, which is supposed to contain the png data, sometimes starts with a 12byte header; seemingly only if the data is (1 channel) compressedDepth image. I acciendently found this info here.
Removing these awesomely obnoxous 12 bytes and continung as usual does the job:
var bitmap = new Bitmap(new MemoryStream(dataSkip(12).ToArray()));

Fast way to compare 2 byte arrays

I am uploading jpeg images as fast as i can to a web service (it is the requirement I have been given).
I am using async call to the web service and I calling it within a timer.
I am trying to optimise as much as possible and tend to use an old laptop for testing. On a normal/reasonable build PC all is OK. On the laptop I get high RAM usage.
I know I will get a higher RAM usage using that old laptop but I want to know the lowest spec PC the app will work on.
As you can see in the code below I am converting the jpeg image into a byte array and then I upload the byte array.
If I can reduce/compress/zip the bye array then I am hoping this will be 1 of the ways of improving memory usage.
I know jpegs are already compressed but if I compare the current byte array with the previous byre array then uploading the difference between this byte arrays I could perhaps compress it even more on the basis that some of the byte values will be zero.
If I used a video encoder (which would do the trick) I would not be real time as much I would like.
Is there an optimum way of comparing 2 byte arrays and outputting to a 3rd byte array? I have looked around but could not find an answer that I liked.
This is my code on the client:
bool _uploaded = true;
private void tmrLiveFeed_Tick(object sender, EventArgs e)
{
try
{
if (_uploaded)
{
_uploaded = false;
_live.StreamerAsync(Shared.Alias, imageToByteArray((Bitmap)_frame.Clone()), Guid.NewGuid().ToString()); //web service being called here
}
}
catch (Exception _ex)
{
//do some thing but probably time out error here
}
}
//web service has finished the client invoke
void _live_StreamerCompleted(object sender, AsyncCompletedEventArgs e)
{
_uploaded = true; //we are now saying we start to upload the next byte array
}
private wsLive.Live _live = new wsLive.Live(); //web service
private byte[] imageToByteArray(Image imageIn)
{
MemoryStream ms = new MemoryStream();
imageIn.Save(ms,System.Drawing.Imaging.ImageFormat.Jpeg); //convert image to best image compression
imageIn.Dispose();
return ms.ToArray();
}
thanks...
As C.Evenhuis said - JPEG files are compressed, and changing even few pixels results in complettly differrent file. So - comparing resulting JPEG files is useless.
BUT you can compare your Image objects - quick search results in finding this:
unsafe Bitmap PixelDiff(Bitmap a, Bitmap b)
{
Bitmap output = new Bitmap(a.Width, a.Height, PixelFormat.Format32bppArgb);
Rectangle rect = new Rectangle(Point.Empty, a.Size);
using (var aData = a.LockBitsDisposable(rect, ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb))
using (var bData = b.LockBitsDisposable(rect, ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb))
using (var outputData = output.LockBitsDisposable(rect, ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb))
{
byte* aPtr = (byte*)aData.Scan0;
byte* bPtr = (byte*)bData.Scan0;
byte* outputPtr = (byte*)outputData.Scan0;
int len = aData.Stride * aData.Height;
for (int i = 0; i < len; i++)
{
// For alpha use the average of both images (otherwise pixels with the same alpha won't be visible)
if ((i + 1) % 4 == 0)
*outputPtr = (byte)((*aPtr + *bPtr) / 2);
else
*outputPtr = (byte)~(*aPtr ^ *bPtr);
outputPtr++;
aPtr++;
bPtr++;
}
}
return output;
}
If your goal is to find out whether two byte arrays contain exactly the same data, you can create an MD5 hash and compare these as others have suggested. However in your question you mention you want to upload the difference which means the result of the comparison must be more than a simple yes/no.
As JPEGs are already compressed, the smallest change to the image could lead to a large difference in the binary data. I don't think any two JPEGs contain binary data similar enough to easily compare.
For BMP files you may find that changing a single pixel affects only one or a few bytes, and more importantly, the data for the pixel at a certain offset in the image is located at the same position in both binary files (given that both images are of equal size and color depth). So for BMPs the difference in binary data directly relates to the difference in the images.
In short, I don't think obtaining the binary difference between JPEG files will improve the size of the data to be sent.

.NET - Bitmap.Save ignores Bitmap.SetResolution on Windows 7

I'm writing a .NET 4 application that imports and saves images for printing. It's important that the saved images resolution (DPI not pixel dimensions) be set to the value we specify so they print correctly.
Some of the images we import come without the resolution value (bad EXIF when they were generated), so we have to correct that before writing them. We use Bitmap.SetResolution for that. It works fine on XP and Windows 8, but when we write (Bitmap.Save) the images on Windows 7, they are always written with the original resolution meta info, ignoring SetResolution.
Here's a test we made, works on XP and 8, not on 7.
string originalFile = #"D:\temp\img\original_img.jpg";
string newFile = #"D:\temp\img\new_img.jpg";
Bitmap bitmap = (Bitmap)Image.FromFile(originalFile);
bitmap.SetResolution(200, 200);
bitmap.Save(newFile, ImageFormat.Jpeg);
Image image = Image.FromFile(newFile);
int dpiX = (int)Math.Round(image.HorizontalResolution, MidpointRounding.ToEven);
int dpiY = (int)Math.Round(image.VerticalResolution, MidpointRounding.ToEven);
Console.WriteLine("DPI is {0} x {1}", dpiX, dpiY);
Before saving, debug always shows the correct resolution assigned by SetResolution, the saved image is where the problem is.
This is probably what was reported here:
http://social.msdn.microsoft.com/Forums/vstudio/en-US/62368caa-05f4-4798-9c59-5d82f881a97c/systemdrawingbitmapsetresolution-is-completely-broken-on-windows-7?forum=netfxbcl
But the issue there seems to remain unsolved. Is there really no way to just make it work? Do I have to use extra libraries for this?
I've found a workaround that will do the job. It's not elegant but...
Instead of applying the resolution to the original image, make a copy of it and work on the copy:
Bitmap bitmap = (Bitmap)Image.FromFile(originalFile);
Bitmap newBitmap = new Bitmap(bitmap)
newBitmap.SetResolution(200, 200);
newBitmap.Save(newFile, ImageFormat.Jpeg);
Now it works on Windows 7. Go figure.
I like Hans Passant's idea, though, it's cleaner. I don't know if what I did messes up with the image, if there is recompression or not.
Hmya, this is a bug in a Windows component. The Windows group is always very reluctant to get bugs like this fixed, breaking changes are postponed to a next Windows version. It did get fixed in Windows 8. Do consider how unusual it is what you are doing, the DPI of an image should always be set by the device that recorded the image. Like the camera or scanner, they never get this wrong. There just isn't any device around that has a 200 dots-per-inch resolution.
If you are desperate enough to find a workaround then you could consider patching the file itself. Not hard to do for a JPEG file, the fields in the file header are pretty easy to get to:
using System.IO;
...
public static void SetJpegResolution(string path, int dpi) {
using (var jpg = new FileStream(path, FileMode.Open, FileAccess.ReadWrite, FileShare.None))
using (var br = new BinaryReader(jpg)) {
bool ok = br.ReadUInt16() == 0xd8ff; // Check header
ok = ok && br.ReadUInt16() == 0xe0ff;
br.ReadInt16(); // Skip length
ok = ok && br.ReadUInt32() == 0x4649464a; // Should be JFIF
ok = ok && br.ReadByte() == 0;
ok = ok && br.ReadByte() == 0x01; // Major version should be 1
br.ReadByte(); // Skip minor version
byte density = br.ReadByte();
ok = ok && (density == 1 || density == 2);
if (!ok) throw new Exception("Not a valid JPEG file");
if (density == 2) dpi = (int)Math.Round(dpi / 2.56);
var bigendian = BitConverter.GetBytes((short)dpi);
Array.Reverse(bigendian);
jpg.Write(bigendian, 0, 2);
jpg.Write(bigendian, 0, 2);
}
}

Categories

Resources