Fast lossless encoding of SKBitmap images - c#

I'm trying to store large 4096x3072 SKBitmap images with lossless compression as fast as I can. I've tried storing them as PNG using SKImage.FromBitmap(bitmap).Encode(SKEncodedImageFormat.Png, 100) but this was really slow. Then using information from this question and this example code I made a method to store them as a Tiff image, which was a lot faster but still not fast enough for my purposes. The code has to work on Linux as well. This is my current code:
public static class SKBitmapExtensions
{
public static void SaveToPng(this SKBitmap bitmap, string filename)
{
using (Stream s = File.OpenWrite(filename))
{
SKData d = SKImage.FromBitmap(bitmap).Encode(SKEncodedImageFormat.Png, 100);
d.SaveTo(s);
}
}
public static void SaveToTiff(this SKBitmap img, string filename)
{
using (var tifImg = Tiff.Open(filename, "w"))
{
// Set the tiff information
tifImg.SetField(TiffTag.IMAGEWIDTH, img.Width);
tifImg.SetField(TiffTag.IMAGELENGTH, img.Height);
tifImg.SetField(TiffTag.COMPRESSION, Compression.LZW);
tifImg.SetField(TiffTag.PHOTOMETRIC, Photometric.RGB);
tifImg.SetField(TiffTag.ROWSPERSTRIP, img.Height);
tifImg.SetField(TiffTag.BITSPERSAMPLE, 8);
tifImg.SetField(TiffTag.SAMPLESPERPIXEL, 4);
tifImg.SetField(TiffTag.XRESOLUTION, 1);
tifImg.SetField(TiffTag.YRESOLUTION, 1);
tifImg.SetField(TiffTag.PLANARCONFIG, PlanarConfig.CONTIG);
tifImg.SetField(TiffTag.EXTRASAMPLES, 1, new short[] { (short)ExtraSample.UNASSALPHA });
// Copy the data
byte[] bytes = img.Bytes;
// Swap red and blue
convertSamples(bytes, img.Width, img.Height);
// Write the image into the memory buffer
for (int i = 0; i < img.Height; i++)
tifImg.WriteScanline(bytes, i * img.RowBytes, i, 0);
}
}
private static void convertSamples(byte[] data, int width, int height)
{
int stride = data.Length / height;
const int samplesPerPixel = 4;
for (int y = 0; y < height; y++)
{
int offset = stride * y;
int strideEnd = offset + width * samplesPerPixel;
for (int i = offset; i < strideEnd; i += samplesPerPixel)
{
byte temp = data[i + 2];
data[i + 2] = data[i];
data[i] = temp;
}
}
}
}
And the test code:
SKBitmap bitmap = SKBitmap.Decode("test.jpg");
Stopwatch stopwatch = new();
stopwatch.Start();
int iterations = 20;
for (int i = 0; i < iterations; i++)
bitmap.SaveToTiff("encoded.tiff");
stopwatch.Stop();
Console.WriteLine($"Average Tiff encoding time for a {bitmap.Width}x{bitmap.Height} image = {stopwatch.ElapsedMilliseconds / iterations} ms");
stopwatch.Restart();
for (int i = 0; i < iterations; i++)
bitmap.SaveToPng("encoded.png");
stopwatch.Stop();
Console.WriteLine($"Average PNG encoding time for a {bitmap.Width}x{bitmap.Height} image = {stopwatch.ElapsedMilliseconds / iterations} ms");
As a result I get:
Average Tiff encoding time for a 4096x3072 image = 630 ms
Average PNG encoding time for a 4096x3072 image = 3092 ms
Is there any faster way to store these images? I can imagine that I can avoid copying the data at var bytes = img.Bytes but I'm not sure how. The encoded file size for the PNG is 10.3MB and for the Tiff it is 26MB now.

If you are not so interested in making the most optimal png (from a file size point of view) then you can get access to some faster encoding options through:
SKBitmap bitmap = SKBitmap.Decode("test.jpg");
using(var pixmap= bitmap.PeekPixels())
{
var filters = SKPngEncoderFilterFlags.NoFilters;
int compress = 0;
var options = new SKPngEncoderOptions(filters, compress);
using (var data = pixmap.Encode(options))
{
byte[] bytes = data.ToArray();
// use data - write bytes to file etc
}
}
In the above example:
compress = 0 will use no zlib compression so the pngs will effectively be similar size to an uncompressed TIFF. You could try a higher value for compress (I think 9 is maximum but slowest).
filters = SKPngEncoderFilterFlags.NoFilters will be fastest under all scenarios. It could make files produced with compress != 0 larger in file size. The filters option is used to try to improve compressibility at the file with SKPngEncoderFilterFlags.AllFilters producing potentially the most compressed file.

Related

C# - Padding image bytes with white bytes to fill 512 x 512

I'm using Digital Persona SDK to scan fingerprints in wsq format, for requeriment I need 512 x 512 image, the SDK only export 357 x 392 image.
The sdk provide a method to compress captured image from device in wsq format and return a byte array that I can write to disk.
-I've tried to allocate a buffer of 262144 for 512 x 512 image.
-Fill the new buffer with white pixel data each byte to value 255.
-Copy the original image buffer into the new image buffer. The original image doesn’t need to be centered but it's important to make sure to copy without corrupting the image data.
To summarize I've tried to copy the old image into the upper right corner of the new image.
DPUruNet.Compression.Start();
DPUruNet.Compression.SetWsqBitrate(95, 0);
Fid capturedImage = captureResult.Data;
//Fill the new buffer with white pixel data each byte to value 255.
byte[] bytesWSQ512 = new byte[262144];
for (int i = 0; i < bytesWSQ512.Length; i++)
{
bytesWSQ512[i] = 255;
}
//Compress capturedImage and get bytes (357 x 392)
byte[] bytesWSQ = DPUruNet.Compression.CompressRaw(capturedImage.Views[0].Width, capturedImage.Views[0].Height, 500, 8, capturedImage.Views[0].RawImage, CompressionAlgorithm.COMPRESSION_WSQ_NIST);
//Copy the original image buffer into the new image buffer
for (int i = 0; i < capturedImage.Views[0].Height; i++)
{
for (int j = 0; j < capturedImage.Views[0].Width; j++)
{
bytesWSQ512[i * bytesWSQ512.Length + j ] = bytesWSQ[i * capturedImage.Views[0].Width + j];
}
}
//Write bytes to disk
File.WriteAllBytes(#"C:\Users\Admin\Desktop\bytesWSQ512.wsq", bytesWSQ512);
DPUruNet.Compression.Finish();
When running that snippet I get IndexOutOfRangeException, I don't know if the loop or the calculation of indexes for new array are right.
Here is a representation of what I'm trying to do.
If someone is trying to achieve something like this or padding a raw image, I hope this will help.
DPUruNet.Compression.
DPUruNet.Compression.SetWsqBitrate(75, 0);
Fid ISOFid = captureResult.Data;
byte[] paddedImage = PadImage8BPP(captureResult.Data.Views[0].RawImage, captureResult.Data.Views[0].Width, captureResult.Data.Views[0].Height, 512, 512, 255);
byte[] bytesWSQ512 = Compression.CompressRaw(512, 512, 500, 8, paddedImage, CompressionAlgorithm.COMPRESSION_WSQ_NIST);
And the method to resize (pad) the image is:
public byte[] PadImage8BPP(byte[] original, int original_width, int original_height, int desired_width, int desired_height, byte pad_color)
{
byte[] canvas_8bpp = new byte[desired_width * desired_height];
for (int i = 0; i < canvas_8bpp.Length; i++)
canvas_8bpp[i] = pad_color; //Fill background. Note this type of fill will fail histogram checks.
int clamp_y_begin = 0;
int clamp_y_end = original_height;
int clamp_x_begin = 0;
int clamp_x_end = original_width;
int pad_y = 0;
int pad_x = 0;
if (original_height > desired_height)
{
int crop_distance = (int)Math.Ceiling((original_height - desired_height) / 2.0);
clamp_y_begin = crop_distance;
clamp_y_end = original_height - crop_distance;
}
else
{
pad_y = (desired_height - original_height) / 2;
}
if (original_width > desired_width)
{
int crop_distance = (int)Math.Ceiling((original_width - desired_width) / 2.0);
clamp_x_begin = crop_distance;
clamp_x_end = original_width - crop_distance;
}
else
{
pad_x = (desired_width - original_width) / 2;
}
//We traverse the captured image (either whole image or subset)
for (int y = clamp_y_begin; y < clamp_y_end; y++)
{
for (int x = clamp_x_begin; x < clamp_x_end; x++)
{
byte image_pixel = original[y * original_width + x];
canvas_8bpp[(pad_y + y - clamp_y_begin) * desired_width + pad_x + x - clamp_x_begin] = image_pixel;
}
}
return canvas_8bpp;
}

Creating a code to decompress byte-oriented RLE image

I'm trying to create a code to decompress an RLE Byte-Oriented image from a PostScript File I've already tried solutions found around the web and also tried to build my own ; but none of them produced the result i need.
After decompressing the rle image, i should have an RAW image i can open on photoshop (informing width, height and number of channels). However when i try to open the extracted image it doesn't work ; only a black output is show.
My inputs are an Binary ASCII Encoded file (encoded as a hexadecimal string) and a binary file ; both RLE Byte-Oriented compressed (in the hex file case, its just a question of converting it to bytes before trying the rle decompression).
https://drive.google.com/drive/u/0/folders/1Q476HB9SvOG_RDwK6J7PPycbw94zjPYU
I've posted samples here.
WorkingSample.raw -> Image Sample i got using another software, and its dimensions as well.
MySample.raw -> Image sample i built using my code, and its dimensions as well.
OriginalFile.ppf -> File containing the original image data and everything else.
ExtractedBinary.bin -> Only a binary portion from OriginalFile.ppf - makes it easier to read and work with the data.
This code was provided by the user nyerguds, he's part of the SO Community.
Original Source: http://www.shikadi.net/moddingwiki/RLE_Compression#Types_of_RLE
Its the one i tried to use but the results weren't correct. And to be honest i had difficulties understanding his code (he told me to change a few things in order to get it working for my case but i was unable to).
And here's what i tried to do following the PostScript Red Book:
Book: https://www.adobe.com/content/dam/acom/en/devnet/actionscript/articles/PLRM.pdf
The part:
"The RunLengthEncode filter encodes data in a simple-byte oriented format based on run length.
The compressed data format is a sequence of runs, where each run consists of a length byte followed by 1 to 128 bytes of data. If the length byte is in the range 0 to 127, the following length + 1 bytes (1 to 128 bytes) are to be copied literally upon decompression. If length is in the range of 129 to 255, the following single byte is to be replicated 257 - length times (2 to 128 times) upon decompression."
Page 142, RunLengthEncode Filter.
List<byte> final = new List<byte>();
var split01 = ArraySplit(bytefile, 2);
foreach (var binPart in split01)
{
try
{
if (binPart.ElementAt(0) <= 127)
{
int currLen = binPart[0] + 1;
for (int i = 0; i <= binPart[0]; i++)
{
final.Add(binPart[1]);
//Console.WriteLine(binPart[1]);
}
}
else if (binPart[0] >= 128)
{
int currLen = 257 - binPart[0];
for (int i = 0; i < currLen; i++)
{
final.Add(binPart[1]);
// Console.WriteLine(binPart[1]);
}
}
}
catch(Exception)
{
break;
}
}
File.WriteAllBytes(#"C:\test\again.raw", final.ToArray());
private static IEnumerable<byte[]> ArraySplit(byte[] bArray, int intBufforLengt)
{
int bArrayLenght = bArray.Length;
byte[] bReturn = null;
int i = 0;
for (; bArrayLenght > (i + 1) * intBufforLengt; i++)
{
bReturn = new byte[intBufforLengt];
Array.Copy(bArray, i * intBufforLengt, bReturn, 0, intBufforLengt);
yield return bReturn;
}
int intBufforLeft = bArrayLenght - i * intBufforLengt;
if (intBufforLeft > 0)
{
bReturn = new byte[intBufforLeft];
Array.Copy(bArray, i * intBufforLengt, bReturn, 0, intBufforLeft);
yield return bReturn;
}
}
private static byte[] StringToByteArray(String hex)
{
int iValue = 0;
int NumberChars = hex.Length;
if (NumberChars % 2 != 0)
{
string m = string.Empty;
}
byte[] bytes = new byte[NumberChars / 2];
try
{
for (int i = 0; i < NumberChars; i += 2)
{
bytes[i / 2] = Convert.ToByte(hex.Substring(i, 2), 16);
iValue = i;
}
}
catch (Exception e)
{
var value = iValue;
Console.WriteLine(e.Message);
}
return bytes;
}
The desired output would be an TIFF Grayscale. However, i can deal with PNG''s also.
I've managed to extract uncompressed data from this kind of file already ; with Emgu(OpenCV Wrapper) i was able to create a viewable image and do my logic on it.
My actual results from RLE Compressed are only invalid RAW files that can't be viewed even on photoshop or IrfanViewer.
Any input is appreciated. Thanks.
EDIT1: stuck on this part
for(int i=0; i < bytefile.Length; i+=2)
{
try
{
var lengthByte = bytefile[i];
if (lengthByte <= 127)
{
int currLen = lengthByte + 1;
for (int j = 0; j < currLen; j++)
{
final.Add(bytefile[i]);
i++;
}
}
if (bytefile[i] >= 128)
{
int currLen = 257 - bytefile[i];
for (int k = 0; k < currLen; k++)
{
final.Add(bytefile[i + 1]);
}
}
}
catch(Exception)
{
break;
}
}
This is the logic i'm following. Before it was raising an Exception but i figured it out (it was because i forgot to add the ending byte ; makes no difference in the final result).
Try this basic outline:
int i = 0;
while (i < bytefile.length)
{
var lengthByte = bytefile[i++];
if (lengthByte <= 127)
{
int currLen = lengthByte + 1;
for (int j = 0; j < currLen; j++)
final.Add(bytefile[i++]);
}
else
{
int currLen = 257 - lengthByte;
byte byteToCopy = bytefile[i++];
for (int j = 0; j < currLen; j++)
final.Add(byteToCopy);
}
}
This is how I understand what's specified above, anyway.
Although not explicitly stated, I believe you are attempting to extract a RunLength Encoded image from a Postscript file and save that out as a grayscale TIFF.
As a starting point for something like this, have you tried simply saving out an uncompressed image from a Postscript file as a grayscale TIFF to ensure your application logic responsible for building up the TIFF image data indeed works as you expect it to? I'd caution that would a be a good first step before moving onto now supporting decompressing RLE data to then turn into a TIFF.
The reason I think that's important is because your problem may have nothing to do with how you're decompressing the RLE data but rather how you're creating your output TIFF from presumably correctly decoded data.

How to crop image that is a byte array

I have an Azure Function and would want to crop an image that is inside a byte array, and then save it to a blob. I am not sure how to do the cropping part, because Bitmap is not available in Azure Functions. Here is my saving to blob:
private async static Task<string> CreateBlob(string name, byte[] data, TraceWriter log)
which works fine for what it does. Also, I have the URL of the image I want to crop, if that helps. Just don't know how to achieve this with an Azure Function. Any ideas?
EDIT
this is the original image, that I have inside a byte array. I want to crop another one from it with the size - 650x290
int cropStartX = 0;
int cropStartY = 0;
int cropWidth = 650;
int cropHeight = 290;
int multiply = cropWidth * cropHeight;
byte[] croppedImage = new byte[multiply];
for (int j = cropStartX; j < cropStartX + cropWidth; j++)
{
for (int k = cropStartY; k < cropStartY + cropHeight; k++)
{
if ((j + k * cropWidth) >= ((byte[])OriginalPic).Length)
break;
croppedImage[j + k * cropWidth] = ((byte[])OriginalPic)[j + k * cropWidth];
}
}
This is the size of the OriginalPic - byte[49121]
This is the output if I set:
croppedImage[j + k * cropWidth] = ((byte[])OriginalPic)[j + k * (cropWidth + 20)];
idk if this is what you are after, but assuming you know the dimensions of your picture, you just have to encode that into your index :
pseudocode
int cropWidth = 100;
int cropHeight = 50;
byte[cropWidth*cropHeight] croppedImage;
for(int i=cropStartX;i<cropStartX+cropWidth;i++)
{
for(int ii = cropStartY;ii<cropStartY+cropHeight;ii++)
{
croppedImage[i + ii*cropWidth] = image[i + ii*mainImageWidth];
}
}
I know this isnt as fast as if you can actually just do a memory copy function... but will do what you need.
Update: after looking at more of your information I am afraid that this method will not work for you as you have compressed image data. Basically jpeg compresses data at a size of about 1:10 which makes more sense now that your image byte array is only 49k as that is almost exactly the file size, which contains jpeg compressed information for a 1024x434 picture of rgb. Long story short you can not simply query pixel locations from that array to get meaningful data unless you were to uncompressed the image prior to cropping. Apologies as I should have seen this as soon as I noticed a "byte" was supposedly representing an rgb pixel (which does not match).

Reading 32-bit grayscale Tiff using Libtiff.Net

I've tried to read a 32-bit grayscale tiff file which each pixel in the image contains a floating point number. But during the reading process, the buffer array contains 4 values for each pixel. For instance [ pixel value = 43.0 --> byte values for the pixel = {0 , 0 , 44 , 66}]. I can't understand the relation between float pixel value and the byte values. I also wrote the image using the buffer but pixel values for output image are int values like 1073872896. Any suggestion would be appreciated.
using (Tiff input = Tiff.Open(#"E:\Sample_04.tif", "r"))
{
// get properties to use in writing output image file
int width = input.GetField(TiffTag.IMAGEWIDTH)[0].ToInt();
int height = input.GetField(TiffTag.IMAGELENGTH)[0].ToInt();
int samplesPerPixel = input.GetField(TiffTag.SAMPLESPERPIXEL)[0].ToInt();
int bitsPerSample = input.GetField(TiffTag.BITSPERSAMPLE)[0].ToInt();
int photo = input.GetField(TiffTag.PHOTOMETRIC)[0].ToInt();
int scanlineSize = input.ScanlineSize();
byte[][] buffer = new byte[height][];
for (int i = 0; i < height; i++)
{
buffer[i] = new byte[scanlineSize];
input.ReadScanline(buffer[i], i);
}
using (Tiff output = Tiff.Open("output.tif", "w"))
{
output.SetField(TiffTag.SAMPLESPERPIXEL, samplesPerPixel);
output.SetField(TiffTag.IMAGEWIDTH, width);
output.SetField(TiffTag.IMAGELENGTH, height);
output.SetField(TiffTag.BITSPERSAMPLE, bitsPerSample);
output.SetField(TiffTag.ROWSPERSTRIP, output.DefaultStripSize(0));
output.SetField(TiffTag.PHOTOMETRIC, photo);
output.SetField(TiffTag.PLANARCONFIG, PlanarConfig.CONTIG);
output.SetField(TiffTag.COMPRESSION, compression);
int j = 0;
for (int i = 0; i < h; i++)
{
output.WriteScanline(buffer[i], j);
j++;
}
}
}
Update 1:
I found out the relation between four bytes and the pixel value using BitConverter class in c# that is like this:
byte[] a = { 0, 0, 44, 66 } --> 43 = BitConverter.ToSingle(a, 0) and 1110179840 = BitConverter.ToInt32(a, 0). It seem bytes are converted to int32 and now the question is how convert byte values to float?
Update 2:
The original tiff file and the tiff after writing the snippet code have been attached.Why is the output tiff file messed up?
I added this line of code to convert pixel values to floating number and it works fine.
output.SetField(TiffTag.SAMPLEFORMAT, SampleFormat.IEEEFP);

Using TiffBitmapEncoder with Gray32Float

I'm trying to create a 32 BPP gray scale tiff using this code which I found on MSDN
BitmapSource image = BitmapSource.Create(
width,
height,
96,
96,
PixelFormats.Gray32Float,
null,
pixels,
stride);
FileStream stream = new FileStream("test file.tif", FileMode.Create);
TiffBitmapEncoder encoder = new TiffBitmapEncoder();
encoder.Compression = TiffCompressOption.None;
var bitmapFrame = BitmapFrame.Create(image);
encoder.Frames.Add(bitmapFrame);
encoder.Save(stream);
The file gets created and the image looks correct when I open it, but the file properties says that it is a 16 BPP (0-65536) image not a 32 bit floating point as specified by the Gray32Float parameter.
I've confirmed the file format is 16 BPP by looking at the file properties in windows explorer and by opening the file in ImageJ
I can create 32 BPP tiffs in Paint.Net and ImageJ, to confirm that format is supported.
Anyone know why the .Net TiffBitmapEncoder is creating the wrong type?
Under the hood, .Net uses the Windows Imaging Component (WIC). WIC supports reading of TIFFs in Gray32Float (GUID_WICPixelFormat32bppGrayFloat in WIC) but not writing. Take a look at the WIC Native Pixel Formats Overview. I had the same experience discovering the image was written as Gray16.
This is very frustrating. I've been attempting to writes some scientific data using Gray32Float, but I have not been successful.
Old question, but I tried, and almost made it, but still - doesn't work correctly:
What I have here is a solution which saves as 32bit, using TiffLib, but the value range is somehow not correct.
I save an image in float range of -0.5 to 3, and imageJ reads it as 32 bit, BUT the range is ~-1000K to ~3000K...
I tried using TiffLib adding the following functions:
public static void Write32BitTiff_(string path, int W, int H, float[] data, ref byte[] FileData, int numPage = 0)
{
var numBytes = sizeof(float);
var size = H * W * numBytes;
byte[] arr = null;
arr = new byte[size];
var ctr = 0;
byte[] floatVal;
for (int i = 0; i < size; i += numBytes)
{
try
{
float val = data[ctr++];
floatVal = BitConverter.GetBytes(val);
for (int j = 0; j < numBytes; j++)
arr[i + j] = floatVal[j];
}
catch (IndexOutOfRangeException)
{
break;
}
catch (Exception eee) { }
}
Tiff t = openTiff(path, W, H, numPage, numBytes * 8);
t.WriteRawStrip(0, arr, size);
t.Close();
t.Dispose();
}
Where "OpenTiff" looks like this:
private static Tiff openTiff(string path, int W, int H, int pageNum, int numBits, bool overrideFile = false)
{
Tiff t;
int numberOfPages;
if (!File.Exists(path) || overrideFile )
{
t = Tiff.Open(path, "w");
numberOfPages = 1;
}
else
{
var start = DateTime.Now;
t = Tiff.Open(path, "a");
numberOfPages = t.NumberOfDirectories() + 1; ;
numberOfPages = (pageNum > numberOfPages) ? pageNum : numberOfPages;
}
t.SetField(TiffTag.IMAGEWIDTH, W);
t.SetField(TiffTag.IMAGELENGTH, H);
const int NUM_CHANNELS = 1;//for RGB set 3. for ARGB set 4, not sure supported.
t.SetField(TiffTag.SAMPLESPERPIXEL, NUM_CHANNELS);
t.SetField(TiffTag.BITSPERSAMPLE, numBits);
t.SetField(TiffTag.PHOTOMETRIC, Photometric.MINISBLACK);
t.SetField(TiffTag.SUBFILETYPE, FileType.PAGE);
t.SetField(TiffTag.PAGENUMBER, pageNum, numberOfPages);
t.SetDirectory((short)pageNum);
}
So if this might help someone, or if someone could find the "bug" with it - this would be great!

Categories