Saturday, July 18, 2015

Animate a plot in MATLAB

As part of an ongoing exercise in pointlessly observing unavoidable disasters, I use a script to log the status of my internet connection via the information provided by the DSL modem.  Normally, I just process this data into a pdf of daily plots of SNR and connection speed with average figures for each day.  It's not too pretty, but it gets the point across easily enough.


For other aspects of the data, or maybe just to satisfy an idle whim, I thought it might be more interesting to visualize the data in motion. I had been playing with more complex figure manipulation recently, so I thought it would be a good challenge.  I created a figure containing two axes which shows connection speed as well as signal and noise power.  For simplicity, I used subplot_tight(), but there are other simplified methods of getting decent custom geometry out of figures with multiple axes.  The background marking the days is generated simply:

% all the other data is prepared first
days=single(mod(round((t-12)/24),2)); % used to mark days

figure(1); clf; 
subplot_tight(4,1,2:4,0.05)
area(t,40*days-20,-20,'facecolor',0.9*[1 1 1],'edgecolor',[1 1 1]); hold on;
plot(t,PWRfill,'r:',t,NOISEfill,'b:',t,PWR,'r',t,NOISE,'b'); grid on; 
ax1=gca; 
set(gca,'layer','top'); % otherwise area() hides grid
xlabel('time (hours)'); ylabel('power (dBm)');

subplot_tight(4,1,1,0.05); 
area(t,1500*days,0,'facecolor',0.9*[1 1 1],'edgecolor',[1 1 1]); hold on;
plot(t,SPD); ax2=gca; set(gca,'layer','top'); grid on;
ylabel('Connection Speed kbps');

set(ax2,'XTickLabelMode', 'Manual','XTickLabel',[])

% keep subplots correlated on x when zooming
linkaxes([ax1 ax2],'x');

In the end, I decided against using the grid lines.  They clutter the animation too much in my opinion.  I left them in this code example simply to demonstrate the use of the 'Layer' property when using area() plots.

This also shows the overlay of duplicate datasets.  The original data as logged contains thousands of gaps where the modem stopped reporting valid data, either because the connection was down or because the modem had locked up.  While metrics like connection speed are only sensible to consider while the connection status is valid, things like noise power would really be most interesting at the extremes -- when the modem can't connect.  I used inpaint_nans() to fill only the small gaps in the data, leaving longer gaps unfilled on the assumption that they represent modem lockups.

3-10-15 to 7-18-15 overall plot before animation

 Once the figure is established with linked axes and everything is as desired, the animation routine incrementally steps through the data by setting the x-axis limits and capturing a frame as a .png file on disk.  Matlab has an inbuilt function for generating movies (as an array of structs) and writing them as an .avi.  In practice, this is a horrible burden on both ram and disk.  For a video containing roughly 1300 frames, this method required a 4.9GB memory footprint and created an even larger uncompressed .avi file.  The avifile() function can't utilize any compression in a linux environment, so it's pretty much useless to me.  Caching images as png keeps memory use from scaling with frame count, and it allows external tools to do the video work.  For some reason, Matlab does a much better job of creating png files than jpg files; they're much smaller too.

%% animate a plot
% assume axes are time-coordinated and linked
% acts generically only on one axis

width=36; % window width in hours
framestep=0.5; % time step between frames
outputdir='/home/assbutt/cad_and_projects/modemplots/';

% keep axes from autoscaling
a0=gca;
a=findall(gcf,'type','axes');
for n=1:1:length(a);
    axes(a(n));
    axis manual
end
axes(a0); % return to original axis

nframes=ceil((max(t)-width)/framestep)
perh=length(t)/max(t); % actual number of samples per hour
xlim=[0 width];
for f=1:1:nframes;
    set(gca,'xlim',xlim);

    % place a date label in the middle of the figure
    fdate=lineArray(round(perh*mean(xlim))); % date of center sample
    h=text(-100,0,sprintf('%s',fdate{:}),'horizontalalignment','center');
    set(h,'units','normalized','position',[0.5 -0.1 0],'fontsize',12);

    % image height is trimmed to an even pixel for video encoding
    pause(0.05); frame=getframe(gcf);
    frame.cdata=255-frame.cdata; sf=size(frame.cdata);
    imwrite(frame.cdata(1:end-(mod(sf(1),2)),:,:), ...
        [outputdir 'frame_' sprintf('%04d',f) '.png'],'png');

    xlim=xlim+framestep; delete(h);
end

I invert the image data during write because I normally use Matlab with the screen inverted. It's too easy for me to create plots that are difficult to read unless they stay that way.

The animation is a bit obnoxious because it grabs window focus constantly and makes it difficult to do anything else with the computer.  I simply ran it on a second machine to avoid the hassle.  Once everything was on disk, I compiled a video with avconv, though I suppose I could have gone directly to OpenShot with the image set.

avconv -r 24 -i frame_%04d.png -c:v libx264 -pix_fmt yuv420p out.mp4

I threw some extra titles and such on the ends using OpenShot and kicked it to YouTube so that it has somewhere to sit and collect dust.


Overall, I think the method works and helps get a point across.  It's easy to see the transition in the noise characteristics over time.  It's easy to identify the mid-day instability of the connection.  I should have used a thicker linewidth when plotting things; scaled down, it gets difficult to see details and read text.  Apologies for the blurriness.

Of course, maybe you don't want to lock axis scaling; maybe you want to change different aspects of the plot over the image sequence.  The point is just to show that getframe() exists and avifile() is a fat ugly pig.  Everything else is just hammering at graphics object properties in a loop.  

No comments:

Post a Comment