Wednesday, July 29, 2015

Read and write animated gifs with MATLAB

UPDATE: These scripts have been vastly improved. Find the current versions here.

In the process of developing my own image mangling toolbox for Matlab, I had routine need for generating animated gif files from image sequences.  It then follows that said .gif files might need to be read back again.  Matlab's inbuilt functions imread() and imwrite() don't make this trivial.  There are some suggestions floating around the internet, and there are some unanswered reports of unexpected behavior.  Not everything worked simply, but I came up with my own ways.

First, writing the image was fairly simple.  I had originally been using imagemagick to do the heavy lifting, but decided to go for a more direct route.  As mentioned, the imagemagick method does seem to have better output, but it's quite a bit slower in my experience.
function gifwrite(inarray,filepath,delay,method)
%   GIFWRITE(INARRAY, FILEPATH, {DELAY}, {METHOD})
%       write image stack to an animated gif
%       
%   INARRAY: 4-D image array (rgb, uint8)
%   FILEPATH: full name and path of output animation
%   DELAY: frame delay in seconds (default = 0.05)
%   METHOD: animation method, 'native' or 'imagemagick' (default = 'native')
%       'imagemagick' may have better quality, but is much slower

if nargin<4;
    method='native';
end

if nargin<3;
    delay=0.05;
end

numframes=size(inarray,4);

if strcmpi(method,'native');
    disp('creating animation')
    for n=1:1:numframes;
        [imind,cm]=rgb2ind(inarray(:,:,:,n),256);
        if n==1;
            imwrite(imind,cm,filepath,'gif','DelayTime',delay,'Loopcount',inf);
        else
            imwrite(imind,cm,filepath,'gif','DelayTime',delay,'WriteMode','append');
        end
    end
else
    disp('creating frames')    
    for n=1:1:numframes;
        imwrite(inarray(:,:,:,n),sprintf('/dev/shm/%03dgifwritetemp.png',n),'png');
    end
    
    disp('creating animation')
    system(sprintf('convert -delay %d -loop 0 /dev/shm/*gifwritetemp.png %s',delay*100,filepath));
    
    disp('cleaning up')    
    system('rm /dev/shm/*gifwritetemp.png');
end
return
Reading the image back wasn't straightforward at all.  The naive approach suggested by the documentation and posts online does not result in correct output.  Imread(...'frames','all') returns only a single colormap corresponding to the global color table in the file. Any file containing multiple images with local color tables will turn into a pile of garbage.
function outpict=gifreadcrap(filepath)
%   GIFREADCRAP(FILEPATH)
%       reads all frames of an animated gif into a 4-D RGB image array
%       seems imread() cannot correctly read animated gifs

[images map]=imread(filepath, 'gif','Frames','all');

s=size(images);
numframes=s(4);

outpict=zeros([s(1:2) 3 numframes],'uint8');
for n=1:1:numframes;
    outpict(:,:,:,n)=ind2rgb8(images(:,:,:,n),map);
end

return
Original image written with gifwrite() and results as returned by gifreadcrap()
Using imread() to read single frames in the hope of perhaps getting correct color information produced the exact same results. Imfinfo() can be used to fetch file information, and certainly, it returns color tables which should correspond to the LCTs in the file. This is where things get ugly.

I spent some time with a hex editor and the file standards documentation to verify what I suspected was a bug. While imfinfo() returned the LCT data, they were all shifted by exactly one byte. The immediately adjacent bytes were being read correctly, though. Sounds like an OBOE to me!

At this point, I discover that yes, indeed it is a bug in versions R14-2012a. A patch exists, but for the sake of anyone who doesn't care, I decided to integrate both the native solution and my own imagemagick workaround.

Keep in mind that it may still be necessary to coalesce the animation before opening it, depending on what you want to do in Matlab. If you're trying to import an optimized gif, you can use the included option to coalesce the image. This optional mode is similar to the other optional modes in these two functions in that it requires external tools and assumes a linux environment. All temporary file operations utilize /dev/shm for marginal speed improvement. Altering this temporary path for your own environment should be trivial. Both functions should work in default modes on other systems, but I have no intention of testing that.
function outpict=gifread(filepath,method,coalesce)
%   GIFREAD(FILEPATH, {METHOD}, {COALESCE})
%       reads all frames of an animated gif into a 4-D RGB image array
%       
%   FILEPATH: full path and filename
%   METHOD: file read method, 'native' or 'imagemagick' (default = 'native')
%       'imagemagick' method is a workaround for bug 813126 present in
%       R14SP3-2012a versions.  Bug consists of an OBOE in reading LCT data.
%       A patch does exist for these versions:
%       https://www.mathworks.com/support/bugreports/813126
%   COALESCE: 0 or 1, Specifies whether to coalesce the image sequence prior to
%       importing.  Used when loading optimized gifs. Requires imagemagick.
%       (optional, default 0)
 
if nargin<3;
    coalesce=0;
end
if nargin<2;
    method='native';
end
 
 
if coalesce==1
    system(sprintf('convert %s -layers coalesce /dev/shm/gifreadcoalescetemp.gif',filepath));
    filepath='/dev/shm/gifreadcoalescetemp.gif';
end
 
if strcmpi(method,'native')
    % use imread() directly (requires patched imgifinfo.m)
    [images map]=imread(filepath, 'gif','Frames','all');
    infostruct=imfinfo(filepath);
 
    s=size(images);
    numframes=s(4);
 
    outpict=zeros([s(1:2) 3 numframes],'uint8');
    for n=1:1:numframes;
        LCT=infostruct(1,n).ColorTable;
        outpict(:,:,:,n)=ind2rgb8(images(:,:,:,n),LCT);
    end
else
    % split the gif using imagemagick instead
    system(sprintf('convert %s /dev/shm/%%03d_gifreadtemp.gif',filepath));
    [~,numframes]=system('ls -1 /dev/shm/*gifreadtemp.gif | wc -l');
 
    numframes=str2num(numframes);
    [image map]=imread('/dev/shm/000_gifreadtemp.gif', 'gif');
    s=size(image);
 
    outpict=zeros([s(1:2) 3 numframes],'uint8');
    for n=1:1:numframes;
        [image map]=imread(sprintf('/dev/shm/%03d_gifreadtemp.gif',n-1), 'gif');
        outpict(:,:,:,n)=ind2rgb8(image,map);
    end
 
    system('rm /dev/shm/*gifreadtemp.gif');
end
 
if coalesce==1
    system(sprintf('rm %s',filepath));
end
 
return
While all of this works, the native reading method does require either a patched copy of imgifinfo.m or Matlab version 2012b or later. Of course, once the existence of the bug was known, fixing things was simple. All the hours over the last day and a half that I spent digging for answers online and with a hex editor were to only rediscover something that was already known but hidden behind MathWorks' member login. It kind of pisses me off enough that finding the solution does not resolve my focus. What possible purpose does restricting access to bug reports serve?

If there's one thing I should have learned from my experiences with asking things of forums, it's to never ask forums. The internet is littered with the evidence of my inability to learn this simple lesson. This very blog was an angry reaction to the previous spectacularly infuriating experience. If I'm bound to ask questions of a silent screen -- if I'm bound to carve my own conclusions and place them on them complete on someone else's mantle in the meager hope that I can help the next person avoid my fate, then I might as well do it in a squalor of my own crafting, without the unrealistic expectations of interaction tugging at my attention.

No comments:

Post a Comment