{
Fluorescence Macros

These macros were written for a system comprising a Scion LG-3 framegrabber in a
Mac IIfx, a Uniblitz shutter with the T132 controller from Vincent Associates,
and a Dage SIT camera mounted on a Nikon Optiphot-2 upright microscope.

They provide acquisition of full or partial frames; easy live montaging; automatic
stamping with scale bar, date, and time; full shutter control from the keyboard,
including automatic shutter control from acquisition macros; background subtraction;
and rudimentary timelapse capabilities.

==== Customization =====
To match other configurations, you must:
1) Change the Set Background macro so that it can find the Gauss(15x15) kernel
   (part of the NIH-Image distribution set).
2) Adjust the Set Mag macro to get correct scale bars
3) Change the shutter control macros if you are using a different shutter
   and/or controller
4) Remove the video sync code from acquisition macros if you are not using the
   Scion LG-3;
5) Replace the Video Rate Capture commands if you are not using the Scion
   framegrabber, with its built-in memory.

====== Written by ======
Chi-Bin Chien, Dept. of Biology, UC San Diego
Comments and bug reports to: chien@jeeves.ucsd.edu
(I can't promise any support for these macros, but will do my best to respond.)

Last modified 1/4/94
Requires Image version 1.53
}

{
==== List of Macros ====
Add Slice [+]	adds slice
Delete Slice [-]	deletes slice
Goto [G]		go to desired slice

Set Mag [M]		set mag before acquiring
Store Bkgd [F12]	acquire a background frame

Bkgd on-off		turn background subtract on/off
Timestamp [F11]	toggle timestamp on/off
Autoshutter		toggle Autoshutter mode
Open Shutter [O]	open shutter
Close Shutter[C]	close shutter

Live [L]		go to live video
Average [ ]		take averaged picture

Live and Average are fancy context-sensitive macros:
 in the Camera window, Live starts capturing and Average adds to a stack
 in a non-Camera window with a ROI selected, Live starts a Live paste, and Average
   pastes in an averaged picture for the ROI
 in a non-Camera window with no ROI, Live switches to the Camera window, and starts
   capturing

Flash to Stack [F14]	takes flash picture, adds to stack
Flash to Disk [F13]		takes flash picture, saves to disk
Slow Flash [F9]		opens shutter 100 ms before averaging
Timelapse			take a timed series of flashes, to disk

AddBlankSlice [B]	adds a black frame to the current stack, with date and scale bar
Invert [I]			inverts the window, or all slices in the stack
Print All [P]		print all slices of all windows
}

{ Global variables }
var
mag, barlength: real;	{ objective magnification, size of scale bar to use (m) }
camerapid: integer;	{ PID of camera window }
bgpid: integer;		{ background window }
bgsub: boolean;		{ whether to subtract background }
tsoff: boolean;		{ whether to omit timestamp from captured images }
asoff: boolean;		{ whether the AutoShutter feature is off }
shopen: boolean;		{ whether we think the shutter is open }
tlperiod: real;		{ timelapse period }

{ Macros start here }

macro 'Fluorescence Macros'; begin end;
macro '(-'; begin end;

macro 'Add Slice [+]'; begin AddSlice end;
macro 'Delete Slice [-]'; begin DeleteSlice end;

{ Error(s) issues the error message S and terminates the macro. }
procedure Error(s:string);
begin
  PutMessage(s); exit;
end;

{ CheckForStack and CheckForSelection are utility routines. }

procedure CheckForStack;
begin
  if nSlices=0 then Error('This window is not a stack');
end;

procedure CheckForSelection;
var x1,y1,x2,y2,lw,l,t,w,h:integer;
begin
  GetRoi(l,t,w,h);
  GetLine(x1,y1,x2,y2,lw);
  if (w=0) and (x1<0) then Error('Please make a rectangular selection.');
end;

{
GetSliceRange is a procedure that gets the implied parameters first and last,
then does range checking to make sure this is a valid range of slices.

GetSliceSequence gets the implied parameters first, last, and step, doing range checking to make sure it's a valid sequence of slices.
}

procedure GetSliceRange(smart:boolean);
var default:integer;
begin
  first:=GetNumber('First slice:',1);
  if (first<1) or (first>nSlices) then Error('Not a valid slice number.');
  default:=nSlices;
  if smart and (first=1) and (SliceNumber > 1) then default:=SliceNumber;
  last:=GetNumber('Last slice:',default);
  if (last<first) or (last>nSlices) then Error('Not a valid range of slices.');
end;

procedure GetSliceSequence(smart:boolean);
begin
  GetSliceRange(smart);
  step:=GetNumber('Step:', 1);
  if (step<1) then Error('Step must be 1 or greater.');
end;

macro 'Goto [G]';
var s: integer;
begin
  CheckForStack;
  s:=GetNumber('Slice to go to:', 1);
  SelectSlice(s);
end;

procedure SetMag;
var m :real;
begin
  if (mag>0) then m:=mag else m:=10.;
  m:=GetNumber('Magnification of objective:', m);
  if (m<4.) or (m>100.) then Error('Magnifications must be between 4x and 100x.');

  mag:=m;
  if (nPics>0) then setscale(mag*.066, 'm');		{ e.g., 6.60 pixels/m at 100x }
  { Set barlength.  Default is 1000 m/mag, rounded to the nearest 5 m;
  special cases for 16x, 60x. }
  barlength:=5*round(200./mag);
  if (mag=16.) then barlength:=50.;
  if (mag=60.) then barlength:=20.;
  if (mag>=6) and (mag<6.31) then barlength:=200.;

  ShowMessage('scale bar=', barlength, 'm');
end;

macro 'Set Mag [M]';		{ set mag, barlength }
begin
  SetMag;
end;

macro 'Store Bkgd [F12]';
{ make a new window called 'Background', to be used for background subtractions }
begin
  if (camerapid<>0) then SelectPic(camerapid) else begin
    StartCapturing;
    camerapid := PidNumber;
  end;
  AverageFrames; SelectAll; Copy;
  if (bgpid<>0) then SelectPic(bgpid) else begin	{ no background window yet }
    SetNewSize(640,480);
    MakeNewWindow('Background');
    bgpid:=PidNumber;
  end;
  Paste; Invert; Convolve('video fx:NIH Image:Kernels:Gauss(15x15)');}

  bgsub:=true;
end;

macro 'Bkgd on-off';
begin
  if bgsub then begin
    ShowMessage('Bkgd subtract off.');
    bgsub:=false; exit;
  end;
  SelectWindow('Background'); bgpid:=PidNumber;
  ShowMessage('Bkgd subtract on.');
  bgsub:=true;
end;

macro 'Timestamp [F11]';		{ toggle timestamp }
begin
  tsoff:=not tsoff;
  if tsoff then ShowMessage('Timestamp off.')
	else ShowMessage('Timestamp on.');
  beep;
end;

macro 'Autoshutter';
begin
  asoff := not asoff;
  if asoff then ShowMessage('Autoshutter off.')
	else ShowMessage('Autoshutter on.');
end;

procedure OpenShutter;
begin
  OpenSerial('300 baud');
  PutSerial('@');
  shopen:=true;
end;

procedure CloseShutter;
begin
  OpenSerial('300 baud');
  PutSerial('A');
  shopen:=false;
end;

macro 'Open Shutter [O]'; begin OpenShutter; end;
macro 'Close Shutter [C]'; begin CloseShutter; end;

{
LabelFrame puts up a scale bar, date, and timestamp if TS is true.
year, month, day, hour, minute, second are passed implicitly.
(l,t) is upper-left corner of ROI.
}
procedure LabelFrame(l,t,:integer; ts:boolean);
var bar:integer;
begin
  if (mag=0.) then SetMag;
  SetForegroundColor(0); SetFont('Helvetica'); SetFontSize(18);
  MoveTo(l+13, t+12); Write(month:2,'/',day:2,'/',year-1900);
  MoveTo(l+21, t+30); Write(barlength:2,' m');
  bar := mag*.066*barlength;
  SetLineWidth(3); MoveTo(l+9,t+38); LineTo(l+9+bar, t+38);

  if (ts) then begin
    MoveTo(l+9, t+53); Write(hour:2,':');
    if (minute <10) then Write('0',minute:1,':') else Write(minute:2,':');
    if (second<10) then Write('0',second:1) else Write(second:2);
  end;
end;

{
If background subtraction is turned on, BkgdSub will subtract the background
from the current ROI in the current picture. It uses (l,t,w,h) to know where
to get the background from.
}
procedure BkgdSub;
var pid:integer;
begin
  pid:=PidNumber; KillROI;
  if bgsub then begin
    ChoosePic(bgpid); if (w=0) then SelectAll else MakeROI(l,t,w,h); Copy;
	   SelectPic(pid); RestoreROI; Paste; Add;
  end;
end;

{ Live and Average are fancy context-sensitive macros:
 in the Camera window, Live starts capturing and Average adds to a stack
 in another window with a ROI selected, Live starts a Live paste, and Average
   pastes in an averaged picture for the ROI
 in another window with no ROI, Live switches to the Camera window, and starts
   capturing
}
macro 'Live [L]';
var l,t,w,h:integer;
begin
  GetROI(l,t,w,h);
  RequiresVersion(1.53);

  if not asoff then OpenShutter;
  if (camerapid=0) or (WindowTitle='Camera') or (w=0) then begin
    StartCapturing;
    camerapid := PidNumber; exit;
  end;

{ If we get to here, we're in a non-Camera window with a ROI; start Live Paste}
  PasteLive;
end;

macro 'Average [ ]';		{ assumes that Live has been called already }
var
  l,t,w,h,destpid,p:integer;
  year,month,day,hour,minute,second,DoW:integer;
  camera:boolean;
begin
  GetROI(l,t,w,h);
  RequiresVersion(1.53);
  
  if not asoff and not shopen then begin
    OpenShutter; Wait(.1);		{ Compensate for SIT's lag time }
  end;
  destpid:=PidNumber;
  camera:=(WindowTitle='Camera');
  if camera then begin	{ try to find the first stack }
    p:=0;
    repeat
      p:=p+1;
      Choosepic(p);
    until (nSlices>0) or (p>=nPics);
    if (nSlices>0) then destpid:=p;
	{ else leave destpid pointing at Camera, later code will start a new stack }
    Choosepic(camerapid);
  end;
  if not camera then begin
    ChoosePic(camerapid);
   	if (w>0) then MakeROI(l,t,w,h);
  end;
  
  AverageFrames; if (w=0) then SelectAll; Copy;
  GetTime(year,month,day,hour,minute,second,DoW);
  if not asoff then CloseShutter;

  SelectPic(destpid);
  if (w>0) and not camera then begin		{ we were in PasteLive mode }
    MakeRoi(l,t,w,h); Paste; BkgdSub; exit;
  end;

  if nSlices>0 then begin
    SelectSlice(nSlices);
    AddSlice;
  end;
  if nSlices=0 then begin
    if (camera and (w>0)) then SetNewSize(w,h) else SetNewSize(640,480);
    MakeNewStack(month:2,'/',day:2,'/',year-1900:2,' ',hour:2,'.',minute:2,'.',second:2);
  end;
  Paste; BkgdSub; LabelFrame(0,0,not tsoff);
  if (camera and (w>0)) then SelectPic(camerapid);
end;

{ Flash just flashes and puts the averaged picture in the Camera window. }
macro 'Flash [F1]';
begin
  if (WindowTitle<>'Camera') then Error('Flash only works in the Camera window.');
  OpenSerial('300 baud');
  
  { wait for even frame of the video signal, then flash and grab }
  repeat until ((BitAnd(Scion[3],48)=0));
  PutSerial('B'); AverageFrames('Video Rate Capture',2);
end;

{
Flash to Stack flashes once and puts the averaged picture in the first stack
that it finds. If there's no stack, it creates one. If you frame a ROI in the
camera window, only the ROI contents will be saved in the stack...
} 
macro 'Flash to Stack [F14]';
var
  l,t,w,h,destpid,p:integer;
  year,month,day,hour,minute,second,DoW:integer;
  camera:boolean;
begin
  GetROI(l,t,w,h);
  RequiresVersion(1.53);
  
  destpid:=PidNumber;
  camera:=(WindowTitle='Camera');
  if (camerapid=0) then begin
    Capture; camerapid := PidNumber;
	   if (destpid=0) then destpid:=camerapid else ChoosePic(destpid);
  end;
 
  if camera then begin	{ try to find the first stack }
    p:=0;
    repeat
      p:=p+1;
      ChoosePic(p);
    until (nSlices>0) or (p>=nPics);
    if (nSlices>0) then destpid:=p;
	{ else leave destpid pointing at Camera, later code will start a new stack }
    Choosepic(camerapid);
  end;
  if not camera then begin
    ChoosePic(camerapid);
   	if (w>0) then MakeROI(l,t,w,h) else SelectAll;
  end;

  OpenSerial('300 baud');
	{ wait for even frame of the video signal, then flash and grab }
  repeat until ((BitAnd(Scion[3],48)=0));
  PutSerial('B'); AverageFrames('Video Rate Capture',2);
  GetTime(year,month,day,hour,minute,second,DoW);
  if (w=0) then SelectAll; Copy;

  SelectPic(destpid);
  if nSlices>0 then begin
    SelectSlice(nSlices);
    AddSlice;
  end;
  if nSlices=0 then begin
    if (camera and (w>0)) then SetNewSize(w,h) else SetNewSize(640,480);
    MakeNewStack(month:2,'/',day:2,'/',year-1900:2,' ',hour:2,'.',minute:2,'.',second:2);
  end;
  Paste; BkgdSub; LabelFrame(0,0,not tsoff);
  if (camera and (w>0)) then SelectPic(camerapid);
end;

{
Flash to Disk flashes once and saves the averaged picture to disk. You must
be in the camera window; if a ROI is selected, only the ROI will be saved.
} 
macro 'Flash to Disk [F13]';
var
  l,t,w,h:integer;
  year,month,day,hour,minute,second,DoW:integer;
begin
  if (WindowTitle<>'Camera') then Error('Must be in the Camera window!');
  GetROI(l,t,w,h);
  RequiresVersion(1.53);

  OpenSerial('300 baud');
	{ wait for even frame of the video signal, then flash and grab }
  repeat until ((BitAnd(Scion[3],48)=0));
  PutSerial('B'); AverageFrames('Video Rate Capture',2);
  GetTime(year,month,day,hour,minute,second,DoW);
  if (w=0) then SelectAll else MakeROI(l,t,w,h);
  BkgdSub; LabelFrame(l,t,not tsoff); MakeROI(l,t,w,h);
  SaveAs(month:2,'/',day:2,'/',year-1900:2,' ',hour:2,'.', minute:2,'.', second:2);
  if (w>0) then begin
    SetLineWidth(1); DrawBoundary;
  end;
end;

{
Slow Flash opens the shutter, waits 100 ms, then takes an averaged
picture which is added to the first stack that it finds. If there's no stack,
it creates one. If you frame a ROI in the camera window, only the ROI contents
will be saved in the stack...
} 
macro 'Slow Flash [F9]';
var
  l,t,w,h,destpid,p:integer;
  year,month,day,hour,minute,second,DoW:integer;
  camera:boolean;
begin
  GetROI(l,t,w,h);
  RequiresVersion(1.53);
  
  destpid:=PidNumber;
  camera:=(WindowTitle='Camera');
  if (camerapid=0) then begin
    Capture; camerapid := PidNumber;
	   if (destpid=0) then destpid:=camerapid else ChoosePic(destpid);
  end;
 
  if camera then begin	{ try to find the first stack }
    p:=0;
    repeat
      p:=p+1;
      ChoosePic(p);
    until (nSlices>0) or (p>=nPics);
    if (nSlices>0) then destpid:=p;
	{ else leave destpid pointing at Camera, later code will start a new stack }
    Choosepic(camerapid);
  end;
  if not camera then begin
    ChoosePic(camerapid);
   	if (w>0) then MakeROI(l,t,w,h) else SelectAll;
  end;

  OpenSerial('300 baud');
	{ wait for even frame of the video signal, then flash and grab }
  repeat until ((BitAnd(Scion[3],48)=0));
  PutSerial('@'); Wait(0.1); AverageFrames('Video Rate Capture',2);
  GetTime(year,month,day,hour,minute,second,DoW); CloseShutter;
  if (w=0) then SelectAll; Copy;

  SelectPic(destpid);
  if nSlices>0 then begin
    SelectSlice(nSlices);
    AddSlice;
  end;
  if nSlices=0 then begin
    if (camera and (w>0)) then SetNewSize(w,h) else SetNewSize(640,480);
    MakeNewStack(month:2,'/',day:2,'/',year-1900:2,' ',hour:2,'.',minute:2,'.',second:2);
  end;
  Paste; BkgdSub; LabelFrame(0,0,not tsoff);
  if (camera and (w>0)) then SelectPic(camerapid);
end;

macro 'Set Timelapse';
begin
  if (tlperiod=0.) then tlperiod:=60.;
  tlperiod:=GetNumber('Seconds between pictures:',tlperiod);
end;

{
Timelapse starts taking a timelapse series of pictures, every tlperiod seconds (use
Set Timelapse to set this period). Use command-period to abort the series.
}
macro 'Timelapse [F15]';
var
  l,t,w,h:integer;
  year,month,day,hour,minute,second,DoW:integer;
  next_tc, dt:real;
begin
  if (WindowTitle<>'Camera') then Error('Must be in the Camera window!');
  GetROI(l,t,w,h);
  RequiresVersion(1.53);

  OpenSerial('300 baud');
  if (tlperiod=0.) then tlperiod:=60.;
  dt:=tlperiod * 60.;		{ a tick is 1/60th of a second }
  
  GetTime(year,month,day,hour,minute,second,DoW);
  tc:=TickCount+60*(tlperiod-(second mod tlperiod));
  
  while true do begin
	while (TickCount<=tc) do begin
	  ShowMessage((tc-TickCount)/60);
	end;
	
	{ wait for even frame of the video signal, then flash and grab }
    repeat until ((BitAnd(Scion[3],48)=0));
    PutSerial('B'); AverageFrames('Video Rate Capture',2);
    GetTime(year,month,day,hour,minute,second,DoW);
    if (w=0) then SelectAll else MakeROI(l,t,w,h);
    BkgdSub; LabelFrame(l,t,not tsoff);
    if (w=0) then SelectAll else MakeROI(l,t,w,h);
    SaveAs(month:2,'/',day:2,'/',year-1900:2,' ',hour:2,'.', minute:2,'.', second:2);
    if (w>0) then begin
      SetLineWidth(1); DrawBoundary;
    end;
    if (w=0) then SelectAll else MakeROI(l,t,w,h);	
	tc:=tc+dt;
  end;
end;

macro 'AddBlankSlice [B]';
var
  year,month,day,h,mm,s,DoW,bar:integer;
begin
  SetBackgroundColor(255);
  AddSlice;
  GetTime(year,month,day,h,mm,s,DoW);
  LabelFrame(0,0,false);
end;

macro 'Invert [I]';
var i,s:integer;
begin
  if (nSlices = 0) then Invert else begin
    s:=SliceNumber;
    for i:=1 to nSlices do begin
      ChooseSlice(i);
      Invert;
    end;
    SelectSlice(s);
  end;
end;

{
PrintROI does the work of printing the ROI (or the whole frame, if no ROI is set).
If the ROI is too big to print on a LaserWriter at 86% (i.e., wider than
~640 pixels or higher than ~840), it is printed as overlapping panels.
}
procedure PrintROI;
var 
  ROIset: boolean;
  left,top,width,height,right,bottom,l,t,w,h:integer;
begin
  GetRoi(left,top,width,height);
  if (width>0) then ROIset:=true else begin
    ROIset:=false;
    left:=0; top:=0;
    GetPicSize(width,height);
  end;
  if (width<=640) and (height<=840) then Print else begin
    right:=left+width-1; bottom:=top+height-1;
    l:=left;
    repeat
      t:=top;
      repeat
        if l+640>right then w:=right-l+1 else w:= 640;
        if t+800>bottom then h:= bottom-t+1 else h:=840;
        MakeRoi(l,t,w,h); Print;
        t:=t+800;
      until (t>bottom-40);
      l:=l+600;
    until (l>right-40);
  end;
  if not ROIset then KillRoi else MakeRoi(left,top,width,height);
end;

macro 'Print All [P]';
var i,j: integer;
begin
  PutMessage('Remember to set 86% magnification in Page Setup.');
  for i:=1 to nPics do begin
    SelectPic(i);
    if nSlices=0 then PrintROI;
    if nSlices>0 then begin
      for j:=1 to nSlices do begin
        SelectSlice(j);
        PrintROI;
      end;
    end;
  end;
end;