Juce Image memory

I have a PNG that is 1024 x 160256 in size, but it’s only 2 colors, so it’s only 2,2 MB on file and expands to 19,56 MB when loaded in an image viewer (in my case I’m using IrfanView for Windows).

If I load this image in a Juce::Image object, it takes 482 MB or memory on my Windows 10 computer. What’s the reason for such a large amount of memory? Looks like it’s 3 byte for each single pixel (R, G, B)… Is there a way to reduce it to a single byte, since it’s only 2 colors?

You can create a single channel image using Image::PixelFormat::SingleChannel in your constructor.

I assume IrfanView has the possibility to only read fragments of the image, since there is no way to display all 160256 pixels wide at the same time. The juce ImageFileFormat lacks this functionality afaik.

Either way, I would recommend to do a tiled approach. Maybe you can supply the image in smaller chunks too?

My intention is to display a pre-rendered animation, so I just crreated a frame stack in a png image, similarly to what many of us do with pre-rendered knobs or other widgets for plugins. In my case this animation contains 313 frames, 1024x512 pixels each, only two colors, but I can’t find a way to have Juce load it as a single channel image.

The reader probably reads it in the format that covers the original, since you cannot specify the format to read into.

Even if you manage to read it into one huge image, I would recommend to stack them rather than to place them horizontally. The reason is that each frame will be fragmented by the other frames.

An alternative would be a Gif Animation. Florian @basteln modified the juce GifReader once to read multi frame images.

Or a proper video format, but you need to write the reader yourself, juce only supports to place an OS video component, which doesn’t play that nicely with other juce drawings.

1 Like

Yes, frames are stacked vertically. In my first post I said that the resolution is 1024 x 160256.

I think I’ll just go with less frames and a lower frame resolution, unless I find a different way.

Oh sorry, my bad. I swapped the dimensions in my head… Time for a break :wink:

1 Like

@ZioGuido You could also consider using an animation format.
Someone made a Lottie wrapper for JUCE. Lottie is an vector based animation format.

Since they are frames of an animation and not a “continuous” image meant to be scrolled, I would consider putting all the single frame images in a ZIP file (which JUCE supports via ZipFile).

You can load the whole ZIP in memory and read the needed images from there.

1 Like

This was one of my initial ideas, but I don’t see how this would make a difference since I would need all frames in memory to play the animation, so once the content of the zip file is decompressed, I would still have 313 single Image objects in memory, each using 3 bytes per pixel. It would still be a lot of memory.

Unless I can extract one frame, display it, unload it from memory and load the next frame… Since all frames are the same size, I could pre-allocate one single Image object and replace its content at each new frame. This would probably save memory but use more cpu power.

Yeah… maybe worth trying this way.

You could perhaps load each frame and then transfer it into a SingleChannel image to keep in memory, so you’d have a CPU overhead at load time, but memory usage should then be reduced to single-bit-per-pixel. You might also just forgo all this and load your giant image as before and then transfer it into a SingleChannel image, so you’d have a large memory overhead at load, but it should then be freed up once done with.

You can also roll your own compression format and decode each image on-the-fly just before it gets displayed. Since you need only 2 colors, one bit per pixel should be enough, so you can fit 8 pixels in a single byte. That’s already a massive reduction in size. Of course JUCE can’t draw that as an image, you need a decoding function that turns these 8-pixels-per-byte into a regular RGB image. To compress this even more you may be able to use some simple RLE (run length encoding), perhaps in the time dimension.

Well, apparently the zip method is not useable, because even if the ZipFile object is kept in memory, extracting single frames, even if they’re only 9 KB files, takes longer than the frame rate which is set at 25 FPS, i.e. 40 milliseconds delay between frames.

Yes, this is the technique we used during the Amiga days to compress 8 frames of 2 color images into the space of a single frame.

I am trying to recreate the effect you can see in this video, starting at 0:36:
https://youtu.be/5aXsrYI3S6g?t=38

The background is a video reduced to two colors to create a 1 bit mask, where the white color is mutiplied with the background effect, which is rendered in real time.
This is where I got so far, taking less than 30 MB of RAM in total.

ezgif.com-video-to-gif-converter

2 Likes

I’ve watched that demo so many times back in the day. It’s still amazing!

Well, so this is my implementation. First I made a CLI command to read all .bmp files in a directory and create one binary file containing all frames chained, where each byte contains 8 pixels.

Since JUCE doesn’t read BMP files, I used this one-header library.

#include <JuceHeader.h>
using namespace juce;

#define LOADBMP_IMPLEMENTATION
#include "./loadbmp.h"

int main (int argc, char* argv[])
{
    String Dir = "./"; 	// default current directory
    if (argc > 1)		// get directory from first optinoal argument
        Dir = String(argv[1]);

    File fDir(Dir);
    if (!fDir.isDirectory())
    {
		printf("Please specify a directory\n");
		return 0;
	}

	int fileCount = 0;
	auto fileList = fDir.findChildFiles(File::TypesOfFileToFind::findFiles | File::TypesOfFileToFind::ignoreHiddenFiles, false);

	if (fileList.size() == 0)
	{
		printf("The directory is empty\n");
		return 0;
	}

	Array<uint8> animData;

	for (auto f : fileList)
	{
		// Skip non BMP files
		if (!f.getFileExtension().endsWithIgnoreCase("bmp"))
			continue;

		auto cFile = (Dir + f.getFileName()).toStdString();

		// Read bitmap file
		unsigned char* BMP = nullptr;
		unsigned int width, height;
		unsigned int err = loadbmp_decode_file(cFile.c_str(), &BMP, &width, &height, LOADBMP_RGB);
		if (err) { printf("Skipping File: %s - LoadBMP Load Error: %u\n", cFile.c_str(), err); continue; }

		printf("Loading file # %d: %s\n", fileCount, f.getFileName().toStdString().c_str());

		// Get ready to encode it
		uint8 byte = 0;
		int bit = 0;
		for (unsigned int i = 0; i < (width * height); ++i)
		{
			auto R = *BMP++;
			auto G = *BMP++;
			auto B = *BMP++;
			auto c = Colour::fromRGB(R, G, B);

			byte ^= ((c.getBrightness() > 0.5f ? 1 : 0) << bit++);

			// Byte complete, store it into the array and prepare next byte
			if (bit > 7)
			{
				animData.add(byte);
				bit = 0;
				byte = 0;
			}
		}

		fileCount++;
	}

	if (fileCount == 0)
		printf("No BMP files have been found\n");
	else
	{
		File animFile(Dir + "/Animation.bin");
		animFile.deleteFile();
		if (animFile.create().ok())
		animFile.replaceWithData(animData.data(), animData.size());
	}

    return 0;
}

The resulting Animation.bin file can be zipped and added to the resources of the destination project.

And here’s the reader/player:


// Class members:
int FilmFrame = 0;
int FilmFrames = 0;
MemoryBlock animData;
std::unique_ptr<Image> FilmBMP;

// On load

// Get zipped animation file from the resources
int size(0);
auto data = BinaryData::getNamedResource(String("Animation.zip").replace(".", "_").replace("-", "").toRawUTF8(), size);
if (size != 0)
{
	// Unzip into a MemoryBlock
	ZipFile zip(MemoryInputStream(data, size, false));
	std::unique_ptr<InputStream> is(zip.createStreamForEntry(0));
	is->readIntoMemoryBlock(animData, is->getTotalLength());

	// Prepare the frame Image
	FilmBMP.reset(new Image(Image::ARGB, 1024, 512, true));
	
	// Get the number of frames
	FilmFrames = is->getTotalLength() / (1024 * 512 / 8);
}

// repaint() is called by a Timer every 40 milliseconds (25 FPS)
void paint(Graphics& g) override
{
	g.fillAll(Colours::black);
	
	// Create some fancy background...
	// ...
	// ...

	// Extract the next frame from the animation file
	static constexpr int dataLen = 1024 * 512 / 8;
	int dataStart = FilmFrame * dataLen;
	int dataEnd = dataStart + dataLen;
	int x = 0, y = 0;

	// Frames are 2-colors only, where each bit in a byte represents black or white
	for (int b = dataStart; b < dataEnd; b++)
	{
		// Copy the pixels to the Image object
		auto byte = ((int8*)animData.getData())[b];
		
		for (int bit = 0; bit < 8; bit++)
		{
			// Make white transparant and paint only the black color
			auto col = ((byte >> bit) & 1) ? Colours::transparentWhite : Colours::black;
			FilmBMP->setPixelAt(x, y, col);

			if (++x >= 1024) { x = 0; ++y; }
		}
	}

	// Print the image
	g.drawImageAt(*FilmBMP.get(), 0, 0);

	// Prepare next frame and loop at end
	if (++FilmFrame >= FilmFrames)
		FilmFrame = 0;
}

This way I can have some 500 frames of 1024x512 in size in a 3 MB zipped file and take about 32 MB of RAM during playback.

1 Like