What I'd like to do is to load an image from disk and create a BitmapSource
from it.
The image is 14043px x 9933px and is b/w (1bpp). But I run into a OutOfMemoryException
because the following code consumes about 800 MB RAM.
The following code generates an ImageSource
in the dimensions of my specific file.
I did this to see if I could make it work without using the actual file on my disk.
public System.Windows.Media.ImageSource getImageSource(){
int width = 14043;
int height = 9933;
List<System.Windows.Media.Color> colors = new List<System.Windows.Media.Color>();
colors.Add(System.Windows.Media.Colors.Black);
colors.Add(System.Windows.Media.Colors.White);
BitmapPalette palette = new BitmapPalette(colors);
System.Windows.Media.PixelFormat pf = System.Windows.Media.PixelFormats.Indexed1;
int stride = width / pf.BitsPerPixel;
byte[] pixels = new byte[height * stride];
for (int i = 0; i < height * stride; ++i)
{
if (i < height * stride / 2)
{
pixels[i] = 0x00;
}
else
{
pixels[i] = 0xff;
}
}
BitmapSource image = BitmapSource.Create(
width,
height,
96,
96,
pf,
palette,
pixels,
stride);
return image;
}
In my calculation the image should consume about 16.7 MB.
Also, I cannot specify a cache option when using BitmapSource.create. But the image must be cached on load.
The return value of this method is set as the source of an image control.
ISSUE REOPENED
After @Clemens posted the answer, wich worked very well in the first place. I noticed a very bad behaviour while inspecting my TaskManager. This is the code I am using, it is excatly the same as @Clemens answer.
public ImageSource getImageSource(){
var width = 14043;
var height = 9933;
var stride = (width + 7) / 8;
var pixels = new byte[height * stride];
for (int i = 0; i < height * stride; i++){
pixels[i] = 0xAA;
}
WriteableBitmap bitmap = new WriteableBitmap(width, height, 96, 96, PixelFormats.BlackWhite, null);
bitmap.WritePixels(new Int32Rect(0, 0, width, height), pixels, stride, 0);
bitmap.Freeze();
return bitmap;
}
Before running any code, my taskmanager shows the following: (1057 MB free)
After starting the app it turnes out that this method has a very high peak of memory usage: (497 MB free after initial peak)
I tried a couple things and found out that @Clemens routine probably isn't the problem. I changed the code to this:
private WriteableBitmap _writeableBitmap; //Added for storing the bitmap (keep it in scope)
public ImageSource getImageSource(){
var width = 14043;
var height = 9933;
var stride = (width + 7) / 8;
var pixels = new byte[height * stride];
for (int i = 0; i < height * stride; i++){
pixels[i] = 0xAA;
}
_writeableBitmap = new WriteableBitmap(width, height, 96, 96, PixelFormats.BlackWhite, null);
_writeableBitmap.WritePixels(new Int32Rect(0, 0, width, height), pixels, stride, 0);
_writeableBitmap.Freeze();
return null; //Return null (Image control source will be set to null now but bitmap still stored in private field)
}
I wanted to keep the bitmap in memory but not affect the image control, this is the result: (997 MB free) (As you can see, the blue line increases just a tiny bit at the right side)
With this knowledge I believe there is something wrong with my image control as well. The peak starts when the writeableBitmap is assigned to the image control. This is all my xaml:
<Window x:Class="TifFileViewer.MainWindow"
xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:local="clr-namespace:TifFileViewer"
xmlns:xctk="http://schemas.xceed.com/wpf/xaml/toolkit"
Title="MainWindow" Height="563" Width="1046">
<Grid Margin="10">
<Image x:Name="imageControl"/>
</Grid>
</Window>
EDIT: I'd conclude that this is a DIY approach since as @Clemens cleverly pointed out in his answer, freezing the bitmap does the same but with a one-liner.
You really need to get your hands dirty to achieve what you're looking for ;)
(with correction from @Clemens)
The .NET framework doesn't treat less than 8 bit-per-pixel images very well. It systematically converts them to 32BPP which in my case brought my process to nearly 200Mb. Read here it's a bug or it's by design.
Whether using WriteableBitmap
(with/without pointers) or BitmapSource.Create
it will consume that much memory, BUT; there's only one place (BitmapImage
) where it's behaving appropriately and fortunately it's a critical place for achieving what you're looking for!
Note: the framework will accept less than or equal to 8 bits per pixel images only if 1 byte equals 1 pixel. As you and I are seeing this, a 1 bit per pixel image means 1 byte = 8 pixels; I've followed this norm. While some could say this as a bug, it's probably a convenience for the dev for not dealing with bits directly.
(specifically for a 1BPP image)
As I said, you will have to get your hands dirty but I'll explain everything so you should get up and running pretty quickly ;)
What I did:
BitmapImage
out of that PNG fileThe application memory usage does not go up, actually it gets to 60Mb but goes down to 35Mb shortly later probably because the garbage collector collects the byte[]
used initially. Anyway it never reaches 200 nor 800 Mb as you've experienced!
What you need (.NET 4.5)
Pngcs45.dll
to Pngcs.dll
otherwise a FileNotFoundException
will occurWhy did I use PNGCS?
Because the same issues as laid out above apply to PngBitmapEncoder
from WPF since it relies on BitmapFrame
for adding content to it.
Code:
using System;
using System.Windows;
using System.Windows.Media.Imaging;
using Hjg.Pngcs;
namespace WpfApplication3
{
public partial class MainWindow : Window
{
public MainWindow()
{
InitializeComponent();
Loaded += MainWindow_Loaded;
}
private void MainWindow_Loaded(object sender, RoutedEventArgs e)
{
int width = 14043;
int height = 9933;
int stride;
byte[] bytes = GetBitmap(width, height, out stride);
var imageInfo = new ImageInfo(width, height, 1, false, true, false);
PngWriter pngWriter = FileHelper.CreatePngWriter("test.png", imageInfo, true);
var row = new byte[stride];
for (int y = 0; y < height; y++)
{
int offset = y*stride;
int count = stride;
Array.Copy(bytes, offset, row, 0, count);
pngWriter.WriteRowByte(row, y);
}
pngWriter.End();
var bitmapImage = new BitmapImage();
bitmapImage.BeginInit();
bitmapImage.UriSource = new Uri("test.png", UriKind.Relative);
bitmapImage.CacheOption = BitmapCacheOption.OnLoad;
bitmapImage.CreateOptions = BitmapCreateOptions.PreservePixelFormat;
bitmapImage.EndInit();
Image1.Source = bitmapImage;
}
private byte[] GetBitmap(int width, int height, out int stride)
{
stride = (int) Math.Ceiling((double) width/8);
var pixels = new byte[stride*height];
for (int y = 0; y < height; y++)
{
for (int x = 0; x < width; x++)
{
var color = (byte) (y < height/2 ? 0 : 1);
int byteOffset = y*stride + x/8;
int bitOffset = x%8;
byte b = pixels[byteOffset];
b |= (byte) (color << (7 - bitOffset));
pixels[byteOffset] = b;
}
}
return pixels;
}
}
}
Now you can enjoy your 1BPP image.
It is important that you freeze the bitmap. Also, since you are using a 1 bit per pixel format, you should calculate the pixel buffer's stride as width / 8
.
The following method creates a bitmap with pixels set to alternating black and white.
public ImageSource CreateBitmap()
{
var width = 14043;
var height = 9933;
var stride = (width + 7) / 8;
var pixels = new byte[height * stride];
for (int i = 0; i < height * stride; i++)
{
pixels[i] = 0xAA;
}
var format = PixelFormats.Indexed1;
var colors = new Color[] { Colors.Black, Colors.White };
var palette = new BitmapPalette(colors);
var bitmap = BitmapSource.Create(
width, height, 96, 96, format, palette, pixels, stride);
bitmap.Freeze(); // reduce memory consumption
return bitmap;
}
Alternatively you could use the BlackWhite
format without a BitmapPalette:
var format = PixelFormats.BlackWhite;
var bitmap = BitmapSource.Create(
width, height, 96, 96, format, null, pixels, stride);
EDIT:
If you create a WriteableBitmap
instead of using BitmapSource.Create
the large bitmap also works with an Image control in a Zoombox:
public ImageSource CreateBitmap()
{
...
var bitmap = new WriteableBitmap(width, height, 96, 96, format, palette);
bitmap.WritePixels(new Int32Rect(0, 0, width, height), pixels, stride, 0);
bitmap.Freeze();
return bitmap;
}
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With