text: function out = seqIo( fName, action, varargin )
  % Utilities for reading and writing seq files.
  %
  % A seq file is a series of concatentated image frames with a fixed size
  % header. It is essentially the same as merging a directory of images into
  % a single file. seq files are convenient for storing videos because: (1)
  % no video codec is required, (2) seek is instant and exact, (3) seq files
  % can be read on any operating system. The main drawback is that each frame
  % is encoded independently, resulting in increased file size. The advantage
  % over storing as a directory of images is that a single large file is
  % created. Currently, either uncompressed, jpg or png compressed frames
  % are supported. The seq file format is modeled after the Norpix seq format
  % (in fact this reader can be used to read some Norpix seq files). The
  % actual work of reading/writing seq files is done by seqReaderPlugin and
  % seqWriterPlugin (there is no need to call those functions directly).
  %
  % seqIo contains a number of utility functions for working with seq files.
  % The format for accessing the various utility functions is:
  %  out = seqIo( fName, 'action', inputs );
  % The list of functions and help for each is given below. Also, help on
  % individual subfunctions can be accessed by: "help seqIo>action".
  %
  % Create interface sr for reading seq files.
  %   sr = seqIo( fName, 'reader', [cache] )
  % Create interface sw for writing seq files.
  %   sw = seqIo( fName, 'writer', info )
  % Get info about seq file.
  %   info = seqIo( fName, 'getInfo' )
  % Crop sub-sequence from seq file.
  %   seqIo( fName, 'crop', tName, frames )
  % Extract images from seq file to target directory or array.
  %   Is = seqIo( fName, 'toImgs', [tDir], [skip], [f0], [f1], [ext] )
  % Create seq file from an array or directory of images or from an AVI file.
  %   seqIo( fName, 'frImgs', info, varargin )
  % Convert seq file by applying imgFun(I) to each frame I.
  %   seqIo( fName, 'convert', tName, imgFun, varargin )
  % Replace header of seq file with provided info.
  %   seqIo( fName, 'newHeader', info )
  % Create interface sr for reading dual seq files.
  %   sr = seqIo( fNames, 'readerDual', [cache] )
  %
  % USAGE
  %  out = seqIo( fName, action, varargin )
  %
  % INPUTS
  %  fName      - seq file to open
  %  action     - controls action (see above)
  %  varargin   - additional inputs (see above)
  %
  % OUTPUTS
  %  out       - depends on action (see above)
  %
  % EXAMPLE
  %
  % See also seqIo>reader, seqIo>writer, seqIo>getInfo, seqIo>crop,
  % seqIo>toImgs, seqIo>frImgs, seqIo>convert, seqIo>newHeader,
  % seqIo>readerDual, seqPlayer, seqReaderPlugin, seqWriterPlugin
  %
  % Piotr's Computer Vision Matlab Toolbox      Version 2.61
  % Copyright 2014 Piotr Dollar.  [pdollar-at-gmail.com]
  % Licensed under the Simplified BSD License [see external/bsd.txt]
  
  switch lower(action)
    case {'reader','r'}, out = reader( fName, varargin{:} );
    case {'writer','w'}, out = writer( fName, varargin{:} );
    case 'getinfo', out = getInfo( fName );
    case 'crop', crop( fName, varargin{:} ); out=1;
    case 'toimgs', out = toImgs( fName, varargin{:} );
    case 'frimgs', frImgs( fName, varargin{:} ); out=1;
    case 'convert', convert( fName, varargin{:} ); out=1;
    case 'newheader', newHeader( fName, varargin{:} ); out=1;
    case {'readerdual','rdual'}, out=readerDual(fName,varargin{:});
    otherwise, error('seqIo unknown action: ''%s''',action);
  end
  end
  
  function sr = reader( fName, cache )
  % Create interface sr for reading seq files.
  %
  % Create interface sr to seq file with the following commands:
  %  sr.close();            % Close seq file (sr is useless after).
  %  [I,ts]=sr.getframe();  % Get current frame (returns [] if invalid).
  %  [I,ts]=sr.getframeb(); % Get current frame with no decoding.
  %  ts = sr.getts();       % Return timestamps for all frames.
  %  info = sr.getinfo();   % Return struct with info about video.
  %  [I,ts]=sr.getnext();   % Shortcut for next() followed by getframe().
  %  out = sr.next();       % Go to next frame (out=0 on fail).
  %  out = sr.seek(frame);  % Go to specified frame (out=0 on fail).
  %  out = sr.step(delta);  % Go to current frame+delta (out=0 on fail).
  %
  % If cache>0, reader() will cache frames in memory, so that calls to
  % getframe() can avoid disk IO for cached frames (note that only frames
  % returned by getframe() are cached). This is useful if the same frames are
  % accessed repeatedly. When the cache is full, the frame in the cache
  % accessed least recently is discarded. Memory requirements are
  % proportional to cache size.
  %
  % USAGE
  %  sr = seqIo( fName, 'reader', [cache] )
  %
  % INPUTS
  %  fName  - seq file name
  %  cache  - [0] size of cache
  %
  % OUTPUTS
  %  sr     - interface for reading seq file
  %
  % EXAMPLE
  %
  % See also seqIo, seqReaderPlugin
  if(nargin<2 || isempty(cache)), cache=0; end
  if( cache>0 ), [as, fs, Is, ts, inds]=deal([]); end
  r=@seqReaderPlugin; s=r('open',int32(-1),fName);
  sr = struct( 'close',@() r('close',s), 'getframe',@getframe, ...
    'getframeb',@() r('getframeb',s), 'getts',@() r('getts',s), ...
    'getinfo',@() r('getinfo',s), 'getnext',@() r('getnext',s), ...
    'next',@() r('next',s), 'seek',@(f) r('seek',s,f), ...
    'step',@(d) r('step',s,d));
  
    function [I,t] = getframe()
      % if not using cache simply call 'getframe' and done
      if(cache<=0), [I,t]=r('getframe',s); return; end
      % if cache initialized and frame in cache perform lookup
      f=r('getinfo',s); f=f.curFrame; i=find(f==fs,1);
      if(i), as=as+1; as(i)=0; t=ts(i); I=Is(inds{:},i); return; end
      % if image not in cache add (and possibly initialize)
      [I,t]=r('getframe',s); if(0), fprintf('reading frame %i\n',f); end
      if(isempty(Is)), Is=zeros([size(I) cache],class(I));
        as=ones(1,cache); fs=-as; ts=as; inds=repmat({':'},1,ndims(I)); end
      [~,i]=max(as); as(i)=0; fs(i)=f; ts(i)=t; Is(inds{:},i)=I;
    end
  end
  
  function sw = writer( fName, info )
  % Create interface sw for writing seq files.
  %
  % Create interface sw to seq file with the following commands:
  %  sw.close();              % Close seq file (sw is useless after).
  %  sw.addframe(I,[ts]);     % Writes video frame (and timestamp)
  %  sw.addframeb(bytes);     % Writes video frame with no encoding.
  %  info = sw.getinfo();     % Return struct with info about video.
  %
  % The following params must be specified in struct 'info' upon opening:
  %  width          - frame width
  %  height         - frame height
  %  fps            - frames per second
  %  quality        - [80] compression quality (0 to 100)
  %  codec          - string representing codec, options include:
  %   'monoraw'/'imageFormat100'      - black/white uncompressed
  %   'raw'/'imageFormat200'          - color (BGR) uncompressed
  %   'monojpg'/'imageFormat102'      - black/white jpg compressed
  %   'jpg'/'imageFormat201'          - color jpg compressed
  %   'monopng'/'imageFormat001'      - black/white png compressed
  %   'png'/'imageFormat002'          - color png compressed
  %
  % USAGE
  %  sw = seqIo( fName, 'writer', info )
  %
  % INPUTS
  %  fName  - seq file name
  %  info   - see above
  %
  % OUTPUTS
  %  sw     - interface for writing seq file
  %
  % EXAMPLE
  %
  % See also seqIo, seqWriterPlugin
  w=@seqWriterPlugin; s=w('open',int32(-1),fName,info);
  sw = struct( 'close',@() w('close',s), 'getinfo',@() w('getinfo',s), ...
    'addframe',@(varargin) w('addframe',s,varargin{:}), ...
    'addframeb',@(varargin) w('addframeb',s,varargin{:}) );
  end
  
  function info = getInfo( fName )
  % Get info about seq file.
  %
  % USAGE
  %  info = seqIo( fName, 'getInfo' )
  %
  % INPUTS
  %  fName  - seq file name
  %
  % OUTPUTS
  %  info   - information struct
  %
  % EXAMPLE
  %
  % See also seqIo
  sr=reader(fName); info=sr.getinfo(); sr.close();
  end
  
  function crop( fName, tName, frames )
  % Crop sub-sequence from seq file.
  %
  % Frame indices are 0 indexed. frames need not be consecutive and can
  % contain duplicates. An index of -1 indicates a blank (all 0) frame. If
  % contiguous subset of frames is cropped timestamps are preserved.
  %
  % USAGE
  %  seqIo( fName, 'crop', tName, frames )
  %
  % INPUTS
  %  fName      - seq file name
  %  tName      - cropped seq file name
  %  frames     - frame indices (0 indexed)
  %
  % OUTPUTS
  %
  % EXAMPLE
  %
  % See also seqIo
  sr=reader(fName); info=sr.getinfo(); sw=writer(tName,info);
  frames=frames(:)'; pad=sr.getnext(); pad(:)=0;
  kp=frames>=0 & frames<info.numFrames; if(~all(kp)), frames=frames(kp);
    warning('piotr:seqIo:crop','%i out of bounds frames',sum(~kp)); end
  ordered=all(frames(2:end)==frames(1:end-1)+1);
  n=length(frames); k=0; tid=ticStatus;
  for f=frames
    if(f<0), sw.addframe(pad); continue; end
    sr.seek(f); [I,ts]=sr.getframeb(); k=k+1; tocStatus(tid,k/n);
    if(ordered), sw.addframeb(I,ts); else sw.addframeb(I); end
  end; sw.close(); sr.close();
  end
  
  function Is = toImgs( fName, tDir, skip, f0, f1, ext )
  % Extract images from seq file to target directory or array.
  %
  % USAGE
  %  Is = seqIo( fName, 'toImgs', [tDir], [skip], [f0], [f1], [ext] )
  %
  % INPUTS
  %  fName      - seq file name
  %  tDir       - [] target directory (if empty extract images to array)
  %  skip       - [1] skip between written frames
  %  f0         - [0] first frame to write
  %  f1         - [numFrames-1] last frame to write
  %  ext        - [] optionally save as given type (slow, reconverts)
  %
  % OUTPUTS
  %  Is         - if isempty(tDir) outputs image array (else Is=[])
  %
  % EXAMPLE
  %
  % See also seqIo
  if(nargin<2 || isempty(tDir)), tDir=[]; end
  if(nargin<3 || isempty(skip)), skip=1; end
  if(nargin<4 || isempty(f0)), f0=0; end
  if(nargin<5 || isempty(f1)), f1=inf; end
  if(nargin<6 || isempty(ext)), ext=''; end
  sr=reader(fName); info=sr.getinfo(); f1=min(f1,info.numFrames-1);
  frames=f0:skip:f1; n=length(frames); tid=ticStatus; k=0;
  % output images to array
  if(isempty(tDir))
    I=sr.getnext(); d=ndims(I); assert(d==2 || d==3);
    try Is=zeros([size(I) n],class(I)); catch e; sr.close(); throw(e); end
    for k=1:n, sr.seek(frames(k)); I=sr.getframe(); tocStatus(tid,k/n);
      if(d==2), Is(:,:,k)=I; else Is(:,:,:,k)=I; end; end
    sr.close(); return;
  end
  % output images to directory
  if(~exist(tDir,'dir')), mkdir(tDir); end; Is=[];
  for frame=frames
    f=[tDir '/I' int2str2(frame,5) '.']; sr.seek(frame);
    if(~isempty(ext)), I=sr.getframe(); imwrite(I,[f ext]); else
      I=sr.getframeb(); f=fopen([f  info.ext],'w');
      if(f<=0), sr.close(); assert(false); end
      fwrite(f,I); fclose(f);
    end; k=k+1; tocStatus(tid,k/n);
  end; sr.close();
  end
  
  function frImgs( fName, info, varargin )
  % Create seq file from an array or directory of images or from an AVI file.
  %
  % For info, if converting from array, only codec (e.g., 'jpg') and fps must
  % be specified while width and height and determined automatically. If
  % converting from AVI, fps is also determined automatically.
  %
  % USAGE
  %  seqIo( fName, 'frImgs', info, varargin )
  %
  % INPUTS
  %  fName      - seq file name
  %  info       - defines codec, etc, see seqIo>writer
  %  varargin   - additional params (struct or name/value pairs)
  %   .aviName    - [] if specified create seq from avi file
  %   .Is         - [] if specified create seq from image array
  %   .sDir       - [] source directory
  %   .skip       - [1] skip between frames
  %   .name       - ['I'] base name of images
  %   .nDigits    - [5] number of digits for filename index
  %   .f0         - [0] first frame to read
  %   .f1         - [10^6] last frame to read
  %
  % OUTPUTS
  %
  % EXAMPLE
  %
  % See also seqIo, seqIo>writer
  dfs={'aviName','','Is',[],'sDir',[],'skip',1,'name','I',...
    'nDigits',5,'f0',0,'f1',10^6};
  [aviName,Is,sDir,skip,name,nDigits,f0,f1] ...
    = getPrmDflt(varargin,dfs,1);
  if(~isempty(aviName))
    if(exist('mmread.m','file')==2) % use external mmread function
      %  mmread requires full pathname, which is obtained via 'which'. But,
      % 'which' can fail (maltab bug), so best to just pass in full pathname
      t=which(aviName); if(~isempty(t)), aviName=t; end
      V=mmread(aviName); n=V.nrFramesTotal;
      info.height=V.height; info.width=V.width; info.fps=V.rate;
      sw=writer(fName,info); tid=ticStatus('creating seq from avi');
      for f=1:n, sw.addframe(V.frames(f).cdata); tocStatus(tid,f/n); end
      sw.close();
    else % use matlab mmreader function
      emsg=['mmreader.m failed to load video. In general mmreader.m is ' ...
        'known to have many issues, especially on Linux. I suggest ' ...
        'installing the similarly named mmread toolbox from Micah ' ...
        'Richert, available at Matlab Central. If mmread is installed, ' ...
        'seqIo will automatically use mmread instead of mmreader.'];
      try V=mmreader(aviName); catch %#ok<DMMR,CTCH>
        error('piotr:seqIo:frImgs',emsg); end; n=V.NumberOfFrames;
      info.height=V.Height; info.width=V.Width; info.fps=V.FrameRate;
      sw=writer(fName,info); tid=ticStatus('creating seq from avi');
      for f=1:n, sw.addframe(read(V,f)); tocStatus(tid,f/n); end
      sw.close();
    end
  elseif( isempty(Is) )
    assert(exist(sDir,'dir')==7); sw=writer(fName,info); info=sw.getinfo();
    frmStr=sprintf('%s/%s%%0%ii.%s',sDir,name,nDigits,info.ext);
    for frame = f0:skip:f1
      f=sprintf(frmStr,frame); if(~exist(f,'file')), break; end
      f=fopen(f,'r');  if(f<=0), sw.close(); assert(false); end
      I=fread(f); fclose(f); sw.addframeb(I);
    end; sw.close();
    if(frame==f0), warning('No images found.'); end %#ok<WNTAG>
  else
    nd=ndims(Is); if(nd==2), nd=3; end; assert(nd<=4); nFrm=size(Is,nd);
    info.height=size(Is,1); info.width=size(Is,2); sw=writer(fName,info);
    if(nd==3), for f=1:nFrm, sw.addframe(Is(:,:,f)); end; end
    if(nd==4), for f=1:nFrm, sw.addframe(Is(:,:,:,f)); end; end
    sw.close();
  end
  end
  
  function convert( fName, tName, imgFun, varargin )
  % Convert seq file by applying imgFun(I) to each frame I.
  %
  % USAGE
  %  seqIo( fName, 'convert', tName, imgFun, varargin )
  %
  % INPUTS
  %  fName      - seq file name
  %  tName      - converted seq file name
  %  imgFun     - function to apply to each image
  %  varargin   - additional params (struct or name/value pairs)
  %   .info       - [] info for target seq file
  %   .skip       - [1] skip between frames
  %   .f0         - [0] first frame to read
  %   .f1         - [inf] last frame to read
  %
  % OUTPUTS
  %
  % EXAMPLE
  %
  % See also seqIo
  dfs={'info',[],'skip',1,'f0',0,'f1',inf};
  [info,skip,f0,f1]=getPrmDflt(varargin,dfs,1);
  assert(~strcmp(tName,fName)); sr=reader(fName); infor=sr.getinfo();
  if(isempty(info)), info=infor; end; n=infor.numFrames; f1=min(f1,n-1);
  I=sr.getnext(); I=imgFun(I); info.width=size(I,2); info.height=size(I,1);
  sw=writer(tName,info); tid=ticStatus('converting seq');
  frames=f0:skip:f1; n=length(frames); k=0;
  for f=frames, sr.seek(f); [I,ts]=sr.getframe(); I=imgFun(I);
    if(skip==1), sw.addframe(I,ts); else sw.addframe(I); end
    k=k+1; tocStatus(tid,k/n);
  end; sw.close(); sr.close();
  end
  
  function newHeader( fName, info )
  % Replace header of seq file with provided info.
  %
  % Can be used if the file fName has a corrupt header. Automatically tries
  % to compute number of frames in fName. No guarantees that it will work.
  %
  % USAGE
  %  seqIo( fName, 'newHeader', info )
  %
  % INPUTS
  %  fName      - seq file name
  %  info       - info for target seq file
  %
  % OUTPUTS
  %
  % EXAMPLE
  %
  % See also seqIo
  [d,n]=fileparts(fName); if(isempty(d)), d='.'; end
  fName=[d '/' n]; tName=[fName '-new' datestr(now,30)];
  if(exist([fName '-seek.mat'],'file')); delete([fName '-seek.mat']); end
  srp=@seqReaderPlugin; hr=srp('open',int32(-1),fName,info); tid=ticStatus;
  info=srp('getinfo',hr); sw=writer(tName,info); n=info.numFrames;
  for f=1:n, srp('next',hr); [I,ts]=srp('getframeb',hr);
    sw.addframeb(I,ts); tocStatus(tid,f/n); end
  srp('close',hr); sw.close();
  end
  
  function sr = readerDual( fNames, cache )
  % Create interface sr for reading dual seq files.
  %
  % Wrapper for two seq files of the same image dims and roughly the same
  % frame counts that are treated as a single reader object. getframe()
  % returns the concatentation of the two frames. For videos of different
  % frame counts, the first video serves as the "dominant" video and the
  % frame count of the second video is adjusted accordingly. Same general
  % usage as in reader, but the only supported operations are: close(),
  % getframe(), getinfo(), and seek().
  %
  % USAGE
  %  sr = seqIo( fNames, 'readerDual', [cache] )
  %
  % INPUTS
  %  fNames - two seq file names
  %  cache  - [0] size of cache (see seqIo>reader)
  %
  % OUTPUTS
  %  sr     - interface for reading seq file
  %
  % EXAMPLE
  %
  % See also seqIo, seqIo>reader
  if(nargin<2 || isempty(cache)), cache=0; end
  s1=reader(fNames{1}, cache); i1=s1.getinfo();
  s2=reader(fNames{2}, cache); i2=s2.getinfo();
  info=i1; info.width=i1.width+i2.width;
  if( i1.width~=i2.width || i1.height~=i2.height )
    s1.close(); s2.close(); error('Mismatched videos'); end
  if( i1.numFrames~=i2.numFrames )
    warning('seq files of different lengths'); end %#ok<WNTAG>
  frame2=@(f) round(f/(i1.numFrames-1)*(i2.numFrames-1));
  
  sr=struct('close',@() min(s1.close(),s2.close()), ...
    'getframe',@getframe, 'getinfo',@() info, ...
    'seek',@(f) s1.seek(f) & s2.seek(frame2(f)) );
  
    function [I,t] = getframe()
      [I1,t]=s1.getframe(); I2=s2.getframe(); I=[I1 I2]; end
  end
  