UP | HOME

Leica S4E & S8APO

Author: Mitch Richling
Updated: 2021-12-01

Copyright 2021 Mitch Richling. All rights reserved.

Table of Contents

Check out my home page for more stuff: https://www.mitchr.me/

1 Introduction

This page contains my personal notes for the Leica S8APO & S4E stereo microscopes. Being my personal notes, the content here isn't terribly polished; however, I've gone ahead and published it anyhow in hope someone might find it useful.

2 Microscope Symbols & Formulas

2.1 Variables

Table 1: Symbols & Formulas
Variable Description Units Notes
\(S_{MAG}\) Magnification At Sensor Ratio  
\(S_{FOVh}\) Sensor Horizontal Field Of View mm  
\(S_{FOVv}\) Sensor Vertical Field Of View mm  
\(I_{SCLh}\) Image Horizontal Scale Pixel/mm  
\(I_{SCLv}\) Image Vertical Scale Pixel/mm  
\(S_w\) Sensor Physical Width mm  
\(S_h\) Sensor Physical Height mm  
\(S_d\) Sensor Physical Diagonal mm  
\(S_A\) Sensor Physical Area square mm  
\(I_w\) Image/Sensor Pixel Width pixel  
\(I_h\) Image/Sensor Pixel Height pixel  
\(I_{MP}\) Total number of Image/Sensor Pixels Megapixel  
\(L_s\) Length At Sensor mm  
\(L_o\) Object Length mm  
\(A\) Auxiliary Ratio  
\(Z\) Zoom Ratio  
\(O_c\) Camera Objective Ratio camera relay lens, camera port lens
\(O_d\) Camera Objective Image Circle Diameter mm  
\(P_w\) Sensor Pixel Width mm  
\(P_h\) Sensor Pixel Height mm  
\(P_{AR}\) Pixel Aspect Ratio Ratio  
\(V_{FOV}\) Visual Field Of View mm  
\(E_{FN}\) Eyepiece Field Number mm  
\(E_{MAG}\) Eyepiece magnification Ratio  
\(V_{MAG}\) Visual Magnification Ratio  
\(R_c\) Reticle Unit Conversion Factor mm/Ru \(1\,\mathrm{Ru}=1\,\mathrm{mmu}\) @ \(Z=1\) & \(A=1\)
\(R_s\) Reticle Scale mm/Ru  
\(L_R\) Length in Reticle Units Ru  
\(I_{AR}\) Image Aspect Ratio Ratio  
\(P_{IJSPAR}\) ImageJ Scale Pixel Aspect Ratio Ratio  
\(W\) Working Distance mm  

2.2 Formulas

Image/Sensor magnification
\[S_{MAG} = \frac{L_s}{L_o} = A \cdot Z \cdot O_c\]
Image/Sensor Horizontal Field Of View
\[S_{FOVh} = \frac{\min(O_d, S_w)}{S_{MAG}}\]
Image/Sensor Vertical Field Of View
\[S_{FOVv} = \frac{\min(O_d, S_h)}{S_{MAG}}\]
Image/Sensor Horizontal Scale
\[I_{SCLh} = \frac{S_{MAG} \cdot I_w}{S_w} = \frac{S_{MAG}}{P_w}\]
Image/Sensor Vertical Scale
\[I_{SCLv} = \frac{S_{MAG} \cdot I_h}{S_h} = \frac{S_{MAG}}{P_h}\]
Visual magnification
\[V_{MAG} = A \cdot Z \cdot E_{MAG}\]
Visual Field Of View
\[V_{FOV} = \frac{E_{FN}}{A \cdot Z}\]
Reticle Scale
\[\frac{R_c}{Z \cdot A}\]
Reticle Length Measurements
\[\mathrm{Length}=R_s \cdot L_R\]
Sensor Diagonal length
\[S_d = \sqrt{S_w^2 + S_h^2}\]
Sensor Pixel Width
\[P_w = \frac{S_w}{I_w}\]
Sensor Pixel Height
\[P_h = \frac{S_h}{I_h}\]
Pixel Aspect Ratio
\[P_{AR} = \frac{P_w}{P_h} = \frac{S_w \cdot I_h}{I_w \cdot S_h}\]
Image Aspect Ratio
\[I_{AR} = \frac{I_w}{I_h}\]
ImageJ Scale Pixel Aspect Ratio
\[P_{IJSPAR} = \frac{1}{P_{AR}}\]

3 S4E

3.1 Magnification and FOV at Various Zoom Levels With 10x/23 Eyepiece

Table 2: S4E – Magnification and FOV at Various Zoom Levels With 10x/23 Eyepiece
  \(A=1.0\times\) \(W=110\,\mathrm{mm}\) \(A=0.5\times\) \(W=200\,\mathrm{mm}\) \(A=1.6\times\) \(W=55\,\mathrm{mm}\)
\(Z\) \(E_{MAG}\) \(V_{FOV}\) \(E_{MAG}\) \(V_{FOV}\) \(E_{MAG}\) \(V_{FOV}\)
0.63 6.30 36.51 3.15 73.02 10.08 22.82
0.80 8.00 28.75 4.00 57.50 12.80 17.97
1.00 10.00 23.00 5.00 46.00 16.00 14.38
1.25 12.50 18.40 6.25 36.80 20.00 11.50
1.60 16.00 14.38 8.00 28.75 25.60 8.98
2.00 20.00 11.50 10.00 23.00 32.00 7.19
2.50 25.00 9.20 12.50 18.40 40.00 5.75
3.00 30.00 7.67 15.00 15.33 48.00 4.79

3.2 Reticle Scale, Magnification, and FOV at Zoom Stops With 10x/23 Eyepiece

Table 3: S4E – Reticle Scale, Magnification, and FOV at Zoom Stops With 10x/23 Eyepiece
\(A\) \(Z\) \(E_{MAG}\) \(E_{FN}\) \(R_s\) \(V_{MAG}\) \(V_{FOV}\)
0.5 0.63 10 23 3.17460 3.15 73.02
0.5 3.00 10 23 0.66667 15.00 15.33
1.0 0.63 10 23 1.58730 6.30 36.51
1.0 3.00 10 23 0.33333 30.00 7.67
1.6 0.63 10 23 0.99206 10.08 22.82
1.6 3.00 10 23 0.20833 48.00 4.79

4 S8APO

4.1 Magnification and FOV at Various Zoom Levels With 10x/23 Eyepiece

Table 4: S8APO – Magnification and FOV at Various Zoom Levels With 10x/23 Eyepiece
  \(A=1.0\times\) \(W=75\,\mathrm{mm}\) \(A=0.63\times\) \(W=101\,\mathrm{mm}\) \(A=2.0\times\) \(W=25\,\mathrm{mm}\)
\(Z\) \(V_{MAG}\) \(V_{FOV}\) \(V_{MAG}\) \(V_{FOV}\) \(V_{MAG}\) \(V_{FOV}\)
1.0 10.0 23.00 6.30 36.51 20 11.50
1.25 12.5 18.40 7.88 29.21 25 9.20
1.6 16.0 14.38 10.08 22.82 32 7.19
2.0 20.0 11.50 12.60 18.25 40 5.75
2.5 25.0 9.20 15.75 14.60 50 4.60
3.2 32.0 7.19 20.16 11.41 64 3.59
4.0 40.0 5.75 25.20 9.13 80 2.88
5.0 50.0 4.60 31.50 7.30 100 2.30
6.3 63.0 3.65 39.69 5.79 126 1.83
8.0 80.0 2.88 50.40 4.56 160 1.44

4.2 Reticle Scale, Magnification, and FOV at Zoom Stops With 10x/23 Eyepiece

Table 5: S8APO – Reticle Scale, Magnification, and FOV at Zoom Stops With 10x/23 Eyepiece
\(A\) \(Z\) \(E_{MAG}\) \(E_{FN}\) \(R_s\) \(V_{MAG}\) \(V_{FOV}\)
0.63 1 10 23 1.58730 6.30 36.51
0.63 8 10 23 0.19841 50.40 4.56
1.00 1 10 23 1.00000 10.00 23.00
1.00 8 10 23 0.12500 80.00 2.88
2.00 1 10 23 0.50000 20.00 11.50
2.00 8 10 23 0.06250 160.00 1.44

4.3 Numerical Aperture With No Auxiliary Lens

Table 6: S8APO Numerical Aperture
Zoom NA
1.0 0.026
1.25 0.031
1.6 0.038
2.0 0.046
2.5 0.056
3.2 0.069
4.0 0.081
5.0 0.093
6.3 0.100
8.0 0.100

4.4 Leica C-Mount Video Objective Data

Table 7: Leica C-Mount Video Objective Data
Leica Part # Leica Part Name Mag \(O_d\) Notes Ref
10445928 Leica Video Objective \(0.32\times\) 0.32 6.7 Estimated \(O_d\) co32
10450528 Leica Video Objective \(0.5\times\) 0.50 10.5 Measured \(O_d\) co50
10447367 Leica Video Objective \(0.63\times\) 0.63 13.2 Estimated \(O_d\) co63
10446307 Leica Video Objective \(0.8\times\) 0.80 16.8 Estimated \(O_d\) co80

Note the image circles are all the same size at the sensor for a particular video objective, but FOV will change with the auxiliary & video objective.

With \(O_d=0.5\times\) we can capture the full image circle with a Micro Four Thirds (Olympus OM-D E-M1 Mark II) sensor, but only about 37% of the image circle with an IMX477 (RPI HQ) sensor. Note that about 65% of the Micro Four Thirds sensor is outside the image circle – i.e. wasted pixels.

Table 8: Image Circles with \(O_d=0.5\times\)
  \(Z=1\) \(Z=8\)
\(A=0.63\) 10450528-10446335-z1_200.png 10450528-10446335-z8_200.png
\(A=1.00\) 10450528-NONE-z1_200.png 10450528-NONE-z8_200.png

With \(O_d=0.32\times\) the image circle shrinks, and now we can capture about 76% of the image circle with an IMX477 (RPI HQ); however, about 3% percent of the sensor is outside the image circle – i.e. wasted sensor pixels. Note that with the smaller image circle the Micro Four Thirds sensor is even less efficiently used with about 84% of the sensor pixels wasted.

Table 9: Image Circles with \(O_d=0.32\times\)
  \(Z=1\) \(Z=8\)
\(A=0.63\) 10445928-10446335-z1_200.png 10445928-10446335-z8_200.png
\(A=1.00\) 10445928-NONE-z1_200.png 10445928-NONE-z8_200.png

4.5 Attaching Cameras

The Leica documentation suggests using a chain of adapters for attaching a generic digital camera. The first part of the chain is one of the following three parts: 10447436 1.6× DSLR tube, 10446175 2.5× DSLR tube, or 10445930 1.0× video/photo objective. Next will be one or more adapters for your camera. The result can be a tower of adapters taller than your microscope! For cameras with large sensors, this really is the only way to go. An excellent write-up for this approach may be found here. A good discussion on MicrobeHunter.com may found here.

For cameras with Micro Four Thirds and smaller sensors, a simpler approach is connect your camera via a c-mount adapter to one of Leica's "C-Mount Video Objectives": 10445928 \(0.32\times\), 10450528 \(0.5\times\), 10447367 \(0.63\times\), or 10446307 \(0.8\times\). These adapters are intended to be used with Leica's microscope cameras, but they will work with any c-mount camera – including your SLR with a c-mount adapter.

With the \(0.5\times\) objective one will obtain a nice 10.5mm image circle which is just about perfect for whole field imaging with the 13mm tall sensor in a Micro Four Thirds camera.

I use two cameras with my S8APO:

4.6 Camera FOV & Image Scales

Table 10: Camera FOV & Image Scales
          RPI         OLY        
\(A\) \(Z\) \(O_c\) \(O_d\) \(S_{MAG}\) \(P_{IJSPAR}\) \(I_{SCLh}\) \(I_{SCLv}\) \(S_{FOVh}\) \(S_{FOVv}\) \(P_{IJSPAR}\) \(I_{SCLh}\) \(I_{SCLv}\) \(S_{FOVh}\) \(S_{FOVv}\)
0.63 1 0.50 10.5 0.32 1.0000000 203.23 203.23 19.96 14.96 0.9961686 93.85 94.21 33.33 33.33
0.63 8 0.50 10.5 2.52 1.0000000 1625.81 1625.81 2.49 1.87 0.9961686 750.79 753.67 4.17 4.17
1.00 1 0.50 10.5 0.50 1.0000000 322.58 322.58 12.57 9.42 0.9961686 148.97 149.54 21.00 21.00
1.00 8 0.50 10.5 4.00 1.0000000 2580.65 2580.65 1.57 1.18 0.9961686 1191.72 1196.31 2.62 2.62
2.00 1 0.50 10.5 1.00 1.0000000 645.16 645.16 6.29 4.71 0.9961686 297.93 299.08 10.50 10.50
2.00 8 0.50 10.5 8.00 1.0000000 5161.29 5161.29 0.79 0.59 0.9961686 2383.45 2392.62 1.31 1.31

5 Reticle Unit Conversion Software For free42 and DM42

5.1 The menu

Table 11: Application Menu
Key Without [SHIFT] With [SHIFT] Notes
A Store A Display A Actually stored in variable "mrcvA"
Z Store Z Display Z Actually stored in variable "mrcvZ"
Rc Store Rc Display Rc Actually stored in variable "mrcvRc"
▒▒▒▒      
▒▒▒▒      
CONV Convert from Reticle Units Convert from Reticle Units  
Setting 1: "4;.5" Leica S4; A=0.50; Z=0.63 Leica S4; A=0.50; Z=3.00 Modify as required!
Setting 2: "4;1" Leica S4; A=1.00; Z=0.63 Leica S4; A=1.00; Z=3.00 Modify as required!
Setting 3: "4;1.6" Leica S4; A=1.60; Z=0.63 Leica S4; A=1.60; Z=3.00 Modify as required!
Setting 4: "8;.63" Leica S8APO; A=0.63; Z=1.00 Leica S8APO; A=0.63; Z=8.00 Modify as required!
Setting 5: "8;1" Leica S8APO; A=1.00; Z=1.00 Leica S8APO; A=1.00; Z=8.00 Modify as required!
Setting 6: "8;2" Leica S8APO; A=2.00; Z=1.00 Leica S8APO; A=2.00; Z=8.00 Modify as required!

The setting sections of the code should be modified to meet the end user's needs. In my case, I have two zoom microscopes (Leica S4E and Leica S8APO). They both have two zoom stops (upper and lower), and I have two auxiliary lenses for each. That's a total of 12 different configurations. I decided use 6 menu keys each with two sets of settings – the scope's lower zoom stop is used when pressed without [SHIFT], and the upper zoom stop when use with [SHIFT].

5.2 The code

While this code isn't very useful without being customized to the particular settings the end user needs, it can be test driven as-is by cut-n-pasting the code below into free42 or by installing the raw file from github.

@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@@ (MRCONV)
@@@@ DSC: Convert Reticle Units to Physical Units
LBL "MRCONV"
LBL 01           @@@@ Conversion & Variable Menu Page
CLMENU
"A"
KEY 1 XEQ 03
"Z"
KEY 2 XEQ 04
"Rc"
KEY 3 XEQ 05
"CONV"
KEY 6 XEQ 06
KEY 7 GTO 02
KEY 8 GTO 02
KEY 9 GTO 00
MENU
STOP
GTO 01
LBL 02           @@@@ Quick Settings Menu Page
CLMENU
"4;.5"             @@@@ Setting 1
KEY 1 GTO 07       
"4;1"              @@@@ Setting 2
KEY 2 GTO 08       
"4;1.6"            @@@@ Setting 3
KEY 3 GTO 09       
"8;.63"            @@@@ Setting 4
KEY 4 GTO 10       
"8;1"              @@@@ Setting 5
KEY 5 GTO 11       
"8;2"              @@@@ Setting 6
KEY 6 XEQ 12
KEY 7 GTO 01
KEY 8 GTO 01
KEY 9 GTO 00
MENU
STOP
GTO 02
LBL 00           @@@@ Application Exit
EXITALL          
RTN              
LBL 03           @@@@ Code for menu key A
FC? 64           
STO "mrcvA"      
VIEW "mrcvA"     
RTN              
LBL 04           @@@@ Code for menu key Z
FC? 64           
STO "mrcvZ"      
VIEW "mrcvZ"     
RTN              
LBL 05           @@@@ Code for menu key Rc
FC? 64           
STO "mrcvRc"     
VIEW "mrcvRc"    
RTN              
LBL 06           @@@@ Code for menu key CONV
RCL× "mrcvRc"    
RCL÷ "mrcvA"     
RCL÷ "mrcvZ"     
RTN              
LBL 07           @@@@ Code for Setting 1 -- "4;.5"
1                  @@@@ Rc Value for Setting 1 -- "4;.5"
STO "mrcvRc"     
R↓               
0.5                @@@@ A Value for Setting 1 -- "4;.5"
STO "mrcvA"      
R↓               
FC? 64           
0.63               @@@@ Z Value for no SHIFT Setting 1 -- "4;.5"
FS? 64           
3.0                @@@@ Z Value for SHIFT Setting 1 -- "4;.5"
STO "mrcvZ"      
GTO 01           
LBL 08           @@@@ Code for Setting 2 -- "4;1"
1                  @@@@ Rc Value for Setting 2 -- "4;1"
STO "mrcvRc"     
R↓               
1.0                @@@@ A Value for Setting 2 -- "4;1"
STO "mrcvA"      
R↓               
FC? 64           
0.63               @@@@ Z Value for no SHIFT Setting 2 -- "4;1"
FS? 64           
3.0                @@@@ Z Value for SHIFT Setting 2 -- "4;1"
STO "mrcvZ"      
GTO 01           
LBL 09           @@@@ Code for Setting 3 -- "4;1.6"
1                  @@@@ Rc Value for Setting 3 -- "4;1.6"
STO "mrcvRc"     
R↓               
1.6                @@@@ A Value for Setting 3 -- "4;1.6"
STO "mrcvA"      
R↓               
FC? 64           
0.63               @@@@ Z Value for no SHIFT Setting 3 -- "4;1.6"
FS? 64           
3.0                @@@@ Z Value for SHIFT Setting 3 -- "4;1.6"
STO "mrcvZ"      
GTO 01           
LBL 10           @@@@ Code for Setting 4 -- "8;.63"
1                  @@@@ Rc Value for Setting 4 -- "8;.63"
STO "mrcvRc"     
R↓               
0.63               @@@@ A Value for Setting 4 -- "8;.63"
STO "mrcvA"      
R↓               
FC? 64           
1.0                @@@@ Z Value for no SHIFT Setting 4 -- "8;.63"
FS? 64           
8.0                @@@@ Z Value for SHIFT Setting 4 -- "8;.63"
STO "mrcvZ"      
GTO 01           
LBL 11           @@@@ Code for Setting 5 -- "8;1"
1                  @@@@ Rc Value for Setting 5 -- "8;1"
STO "mrcvRc"     
R↓               
1.0                @@@@ A Value for Setting 5 -- "8;1"
STO "mrcvA"      
R↓               
FC? 64           
1.0                @@@@ Z Value for no SHIFT Setting 5 -- "8;1"
FS? 64           
8.0                @@@@ Z Value for SHIFT Setting 5 -- "8;1"
STO "mrcvZ"      
GTO 01           
LBL 12           @@@@ Code for Setting 6 -- "8;2"
1                  @@@@ Rc Value for Setting 6 -- "8;2"
STO "mrcvRc"     
R↓               
2.0                @@@@ A Value for Setting 6 -- "8;2"
STO "mrcvA"      
R↓               
FC? 64           
1.0                @@@@ Z Value for no SHIFT Setting 6 -- "8;2"
FS? 64           
8.0                @@@@ Z Value for SHIFT Setting 6 -- "8;2"
STO "mrcvZ"
GTO 01
END

6 S-Series Leica Parts

Table 12: S-Series Leica Parts
Leica Part # Leica Part Name
10446298 Leica S8 APO
10446293 Leica S4 E
10447136 10x/23B eyepiece for eyeglasses, fixed
10447137 10x/23B eyepiece for eyeglasses, adjustable
10447138 16x/15B eyepiece for eyeglasses, fixed
10447139 16x/15B eyepiece for eyeglasses, adjustable
10446447 Reticle 10 mm/0.1 mm
10446448 Reticle 5 mm/0.1 mm
10446449 Reticle 5 mm/0.05 mm
10447000 Reticle 100 scale intervals / 0.002"
10447001 Reticle 100 scale intervals / 0.001"
10447002 Reticle 150 scale intervals / 0.0005"
10446334 Achro Auxiliary \(0.32\times\) for A8, WD 200 mm
10446335 APO Auxiliary \(0.63\times\) for A8, WD 100 mm
10446336 APO Auxiliary \(1.6\times\) for A8, WD 37 mm
10446337 APO Auxiliary \(2.0\times\) for A8, WD 25 mm
10446316 Auxiliary \(0.32\times\) for S4/S6, WD 300 mm
10446318 Auxiliary \(0.5\times\) for S4/S6, WD 200 mm
10446319 Auxiliary \(0.63\times\) for S4/S6, WD 155 mm
10446320 Auxiliary \(0.75\times\) for S4/S6, WD 130 mm
10446321 Auxiliary \(1.6\times\) for S4/S6, WD 55 mm
10446322 Auxiliary \(2.0\times\) for S4/S6, WD 35 mm
10446325 Auxiliary \(0.3–0.4\times\) for S4/S6 (Adjustable), WD 200–350mm
10446323 Auxiliary \(0.6–0.75\times\) for S4/S6 (ErgoObjective) WD 77–137mm
10450817 Auxiliary \(0.5\times\) for S9, WD 200 mm
10450818 Auxiliary \(0.63\times\) for S9, WD 150 mm
10450819 Auxiliary \(0.75\times\) for S9, WD 130 mm
10450820 Auxiliary \(1.6\times\) for S9, WD 50 mm
10450821 Auxiliary \(2.0\times\) for S9, WD 35 mm
10446324 Lens shield
10450831 Updated lens shield
10445928 Leica Video Objective \(0.32\times\)
10450528 Leica Video Objective \(0.5\times\)
10447367 Leica Video Objective \(0.63\times\)
10446307 Leica Video Objective \(0.8\times\)

7 Image Sensor Data

Table 13: Image Sensor Data
Type Sensor \(S_w\) \(S_h\) \(I_w\) \(I_h\) \(S_d\) \(S_A\) \(I_{MP}\) \(P_w\) \(P_h\) \(P_{AR}\) \(P_{IJSPAR}\) Ref
1/4" IMX219 3.6800 2.760 3280 2464 4.60 10 8.1 1.122 1.120 1.00163 0.99838 IMX219
1/3.2" IMX179 3.2880 2.512 3280 2464 4.14 8 8.1 1.002 1.019 0.98328 1.01700 IMX179
1/2.5" MT9P031 5.7000 4.280 2592 1944 7.13 24 5.0 2.199 2.202 0.99883 1.00117 MT9P031
1/2.3" Leica mc170 6.1000 4.600 2592 1944 7.64 28 5.0 2.353 2.366 0.99457 1.00546 mc170
1/2.3" Leica mc190 6.1000 4.600 3648 2736 7.64 28 10.0 1.672 1.681 0.99457 1.00546 mc190
1/2.3" IMX477 RPI 6.2868 4.712 4056 3040 7.86 29 12.3 1.550 1.550 1.00000 1.00000 RPI
1/1.8" IMX334 7.9000 4.640 3952 2320 9.16 36 9.2 1.999 2.000 0.99949 1.00051 IMX334
2/3" IMX264 8.5000 7.100 2464 2056 11.08 60 5.1 3.450 3.453 0.99895 1.00105 IMX264
16mm film 10.3000 7.400 4120 2960 12.68 76 12.2 2.500 2.500 1.00000 1.00000 16mm
1" IMX183 13.1300 8.760 5472 3648 15.78 115 20.0 2.399 2.401 0.99924 1.00076 IMX183
1.1" IMX304 14.2000 10.400 4104 3006 17.60 147 12.3 3.460 3.460 1.00008 0.99992 IMX304
4/3" OMD EM1 M2 17.4000 13.000 5184 3888 21.72 226 20.2 3.356 3.344 1.00385 0.99617 OLY
APS-C Nikon D3200 23.2000 15.400 6016 4000 27.85 357 24.1 3.856 3.850 1.00166 0.99834 APSC
35mm film 36.0000 24.000 14400 9600 43.27 864 138.2 2.500 2.500 1.00000 1.00000 35mm

8 DIY RPI Camera

rpicam1_800.jpg

Figure 1: A live feed from the microscope with a glass scale on the stage.

rpicam2_800.jpg

Figure 2: A good view of the wire management for the camera.

rpicam4_800.jpg

Figure 3: A 30fps live feed illuminated only by my desk lamps. This is an exceptionally bright microscope.

8.1 The Idea

If you have arrived here via direct link, then you might be interested to know that your browser is pointed to the middle of a larger document – my personal stereo microscope notes. While these are my personal notes, I have attempted to expand this section a bit to make it easier to follow for someone wishing to replicate my camera setup.

Microscope cameras with built in image analysis software are pretty cool. Simply connect the camera to your monitor & mouse, and you can do simple image processing and measurement without a computer. These solutions are expensive for what you get, and the analysis software is pretty limited. So I thought, why not build my own? It's just a tiny computer and a camera in a compact case after all. If I used a Raspberry Pi, then I could actually run my favorite image analysis software (Fiji/ImageJ) right on the camera.

8.2 Bill Of Materials

8.3 System Setup

Note: These instructions are for the Bullseye based Raspberry Pi OS relased in November of 2021!

8.3.1 Minimal Configuration

The system setup steps I used for my camera will not work for others because I made use of several aspects of my personal home directory configuration others will not have. What follows are simplified configuration steps I believe will for for most people out of the box. The only tricky bit is Fiji because it is such a dynamic project, things change.

#### Refresh packages and update everything
sudo apt-get update
sudo apt-get upgrade
sudo reboot
#### Camera packages
sudo apt install -y libcamera-apps
sudo apt install -y libcamera-dev libepoxy-dev libjpeg-dev libtiff5-dev
#### Install my favorite  packages
sudo apt install -y emacs gnuplot maxima sbcl telnet zsh tmux gitk xterm imagemagick nomacs gimp ruby exiv2 git openssl
#### Install & Setup java
sudo apt install -y openjdk-8-jdk
#### Download Fiji software & launcher
cd ~
test -e fiji-nojre.zip || wget 'https://downloads.imagej.net/fiji/latest/fiji-nojre.zip' 
test -e ImageJ.sh || wget 'https://github.com/imagej/imagej2/raw/master/bin/ImageJ.sh'
#### Unpack Fiji
unzip fiji-nojre.zip
#### Put in place ImageJ.sh script (hardwire the install directory)
sed 's/^DIRECTORY=.*/DIRECTORY=~\/Fiji.app/' ImageJ.sh > ~/Fiji.app/ImageJ.sh
chmod a+rx ~/Fiji.app/ImageJ.sh
#### Workaround for a bug in current Fiji
mv ~/Fiji.app/jars/FilamentDetector-1.0.0.jar ~/Fiji.app/jars/FilamentDetector-1.0.0.bad
#### Link ImageJ.sh into /usr/bin/ as ImageJ
sudo ln -s ~/Fiji.app/ImageJ.sh /usr/bin/ImageJ
#### Run Fiji and update *EVERYTHING*
/usr/bin/ImageJ
#### Clone the git repo
cd ~
git clone 'https://github.com/richmit/microscope.git'
#### Link piSnap to /bin/
sudo ln -s ~/microscope/piSnap.sh /usr/bin/piSnap
chmod a+rx /usr/bin/piSnap
### Link the RPI_tools into Fiji
ln -s ~/microscope/RPI_tools.ijm ~/Fiji.app/macros/toolsets/RPI_tools.ijm

8.3.2 My Configuration

Most Unix/Linux users have pretty strong opinions on how to setup a personal working environment. Emacs vs VI anyone? This section documents my personal configuration and is pretty specific to my operating environment. Some differences from the "Minimal Configuration" section above:

  • Makes use of scripts and data found in my home directory
  • Uses my environment setup scripts to put piSnap.sh in the right place and link it.
  • ~/bin is where the binaries go
  • I use my preferred login name for the main account, and remove the 'pi' user from the system
  • Quite a bit more apt installed software (Emacs, sbcl, maxima, gnuplot, paraview, etc…)
  • I install the free42 calculator along with some software on the calculator itself.
  • My primary system for photography and image analysis is a Windows system
    • This system has a nice, color corrected 4K monitor, and a couple scanners attached
    • Some of the software I use is proprietary and only runs on Windows
    • With regard to free software, I run mostly the same stack on Windows (Fiji, GIMP, Imagemagick, Emacs, etc…) in MSYS2
8.3.2.1 System Setup
#### Refresh packages and update everything
sudo apt-get update
sudo apt-get upgrade
#### DO: Turn off auto login for pi user
#### DO: Turn on SSH & VNC
sudo reboot
#### Camera packages
sudo apt install -y libcamera-apps
sudo apt install -y libcamera-dev libepoxy-dev libjpeg-dev libtiff5-dev
sudo apt install -y qtbase5-dev libqt5core5a libqt5gui5 libqt5widgets5 qtbase5-dev libqt5core5a libqt5gui5 libqt5widgets5
sudo apt install -y python3-pip libboost-dev libgnutls28-dev openssl meson libglib2.0-dev libgstreamer-plugins-base1.0-dev
#### Install my favorite  packages
sudo apt install -y emacs gnuplot maxima sbcl telnet zsh tmux gitk xterm imagemagick nomacs gimp ruby exiv2
#### Install & Setup java
sudo apt install -y openjdk-8-jdk
#### Make local links
for f in ruby perl sbcl; do
  if [ -e /usr/bin/$f ]; then
    sudo ln -s /usr/bin/$f /usr/local/bin/$f
  else
    echo Missing: /usr/bin/$f
  fi
done
#### Create my account
sudo addgroup richmit
sudo adduser --gid 1001 richmit ## use GID from previous command
#### Add me to all the groups the pi user is in -- or at least users that start with pi ;)
sudo usermod -aG `grep '[:,]pi' /etc/group | cut -d: -f3 | tr '[:space:]' , | sed 's/,$//'` richmit
#### Change my shell
sudo usermod -s /usr/bin/zsh richmit
#### Change root's password
sudo passwd root
#### DO: Add my account to sudo
#### Login with my new account
sudo su - richmit
#### Setup directories & links in $HOME
cd ~
mkdir -p ~/bin
mkdir -p ~/tmp/tmux/sockets
mkdir -p ~/Pictures/pi-cam
mkdir -p ~/synced/world/pi-data/
mkdir -p ~/synced/world/dotfiles/
mkdir -p ~/synced/world/dotfilesSecure/
mkdir -p ~/synced/world/stuff/homeNetwork/
mkdir -p ~/synced/world/stuff/my_ref/
mkdir -p ~/synced/world/stuff/notes/
mkdir -p ~/synced/world/my_prog/learn/ex-ruby/
mkdir -p ~/synced/world/my_prog/lispStuff/lispy/
mkdir -p ~/synced/world/my_prog/MJRdebianPakageTools/
mkdir -p ~/synced/world/my_prog/tmuxStuff/
mkdir -p ~/synced/world/my_prog/UNIXutils/
mkdir -p ~/synced/world/my_prog/utils/
mkdir -p ~/synced/world/my_prog/microscope
mkdir -p ~/synced/world/my_prog/ImageJ
mkdir -p ~/synced/world/my_prog/mpms
mkdir -p ~/synced/world/my_prog/dir-inventory/
mkdir -p ~/synced/core/
mkdir -p ~/synced/Doc2/gadgets/leica_microscopes/
ln -s ~/synced/world ~/world
ln -s ~/synced/core ~/core
#### DO: Sync data from laptop via rsync
#### Setup dotfiles & ~/bin
./world/my_prog/UNIXutils/SelectSetup.rb --loc=HOME; ./world/my_prog/UNIXutils/SetupBin.rb
#### DO: Reboot
#### DO: Login as myself
#### Delete pi user
sudo userdel -r pi
#### DO: Enable automatic login
#### DO: Change hostname to pi-cam
#### DO: Reboot
#### Download Fiji software & launcher
cd ~
test -e fiji-nojre.zip || wget 'https://downloads.imagej.net/fiji/latest/fiji-nojre.zip' 
test -e ImageJ.sh || wget 'https://github.com/imagej/imagej2/raw/master/bin/ImageJ.sh'
#### Unpack Fiji
unzip fiji-nojre.zip
#### Put in place ImageJ.sh script (hardwire the install directory)
sed 's/^DIRECTORY=.*/DIRECTORY=~\/Fiji.app/' ImageJ.sh > ~/Fiji.app/ImageJ.sh
chmod a+rx ~/Fiji.app/ImageJ.sh
#### Workaround for a bug in current Fiji
mv ~/Fiji.app/jars/FilamentDetector-1.0.0.jar ~/Fiji.app/jars/FilamentDetector-1.0.0.bad
#### Run Fiji and update
~/Fiji.app/ImageJ.sh
### Link the RPI_tools into Fiji
ln -s ~/world/my_prog/microscope/RPI_tools.ijm ~/Fiji.app/macros/toolsets/RPI_tools.ijm
### Link the PhilaJ into Fiji
ln -s ~/world/my_prog/ImageJ/PhilaJ/PhilaJ.ijm ~/Fiji.app/macros/toolsets/PhilaJ.ijm
#### Setup dotfiles & ~/bin -- this time to put ImageJ.sh & ImageJ into ~/bin
./world/my_prog/UNIXutils/SelectSetup.rb --loc=HOME; ./world/my_prog/UNIXutils/SetupBin.rb
#### DO: Add ImageJ.sh to launch menu -- icon in ~/core/icons
#### Install free42 calculator
cd ~
rm -rf ~/free42
git clone 'https://github.com/thomasokken/free42.git'
cd free42/gtk
make BCD_MATH=1 AUDIO_ALSA=1
sudo cp free42dec /usr/local/bin/free42-3.0.6
sudo rm -f /usr/local/bin/free42
sudo ln -s /usr/local/bin/free42-3.0.6 /usr/local/bin/free42
#### DO: Add free42 to launch menu -- icon in ~/core/icons
#### DO: Add piSleep to launch menu -- icon in ~/core/icons
#### DO: Logout
#### DO: Login
8.3.2.2 System Sync

I use rsync to keep data in sync between my camera and primary workstation.

date; echo back-dat-sync;        rsync -rlt --log-format=%f --delete --delete-excluded --modify-window=2                  --rsh='ssh' pi-cam.home.mitchr.me:synced/pi-data/                            /c/Users/richmit/Documents/world/pi-cam/;
date; echo back-pic-sync;        rsync -rlt --log-format=%f                            --modify-window=2                  --rsh='ssh' pi-cam.home.mitchr.me:Pictures/pi-cam/                           /c/Users/richmit/Pictures/pi-cam/;
date; echo microscope-code;      rsync -rlt --log-format=%f --delete --delete-excluded --modify-window=2 --exclude '.git' --rsh='ssh' /c/Users/richmit/Documents/world/my_prog/microscope/             pi-cam.home.mitchr.me:synced/world/my_prog/microscope/;
date; echo imagej-code;          rsync -rlt --log-format=%f --delete --delete-excluded --modify-window=2 --exclude '.git' --rsh='ssh' /c/Users/richmit/Documents/world/my_prog/ImageJ/                 pi-cam.home.mitchr.me:synced/world/my_prog/ImageJ/;
date; echo dotfiles;             rsync -rlt --log-format=%f --delete --delete-excluded --modify-window=2 --exclude '.git' --rsh='ssh' /c/Users/richmit/Documents/world/dotfiles/                       pi-cam.home.mitchr.me:synced/world/dotfiles/;
date; echo dotfilesSecure;       rsync -rlt --log-format=%f --delete --delete-excluded --modify-window=2 --exclude '.git' --rsh='ssh' /c/Users/richmit/Documents/world/dotfilesSecure/                 pi-cam.home.mitchr.me:synced/world/dotfilesSecure/;
date; echo homeNetwork;          rsync -rlt --log-format=%f --delete --delete-excluded --modify-window=2 --exclude '.git' --rsh='ssh' /c/Users/richmit/Documents/world/stuff/homeNetwork/              pi-cam.home.mitchr.me:synced/world/stuff/homeNetwork/;
date; echo my_ref;               rsync -rlt --log-format=%f --delete --delete-excluded --modify-window=2 --exclude '.git' --rsh='ssh' /c/Users/richmit/Documents/world/stuff/my_ref/                   pi-cam.home.mitchr.me:synced/world/stuff/my_ref/;
date; echo notes;                rsync -rlt --log-format=%f --delete --delete-excluded --modify-window=2 --exclude '.git' --rsh='ssh' /c/Users/richmit/Documents/world/stuff/notes/                    pi-cam.home.mitchr.me:synced/world/stuff/notes/;
date; echo mpms;                 rsync -rlt --log-format=%f --delete --delete-excluded --modify-window=2 --exclude '.git' --rsh='ssh' /c/Users/richmit/Documents/world/my_prog/mpms/                   pi-cam.home.mitchr.me:synced/world/my_prog/mpms/;
date; echo dir-inventory;        rsync -rlt --log-format=%f --delete --delete-excluded --modify-window=2 --exclude '.git' --rsh='ssh' /c/Users/richmit/Documents/world/my_prog/dir-inventory/          pi-cam.home.mitchr.me:synced/world/my_prog/dir-inventory/;
date; echo ruby-examples;        rsync -rlt --log-format=%f --delete --delete-excluded --modify-window=2 --exclude '.git' --rsh='ssh' /c/Users/richmit/Documents/world/my_prog/learn/ex-ruby/          pi-cam.home.mitchr.me:synced/world/my_prog/learn/ex-ruby/;
date; echo lispy;                rsync -rlt --log-format=%f --delete --delete-excluded --modify-window=2 --exclude '.git' --rsh='ssh' /c/Users/richmit/Documents/world/my_prog/lispStuff/lispy/        pi-cam.home.mitchr.me:synced/world/my_prog/lispStuff/lispy/;
date; echo MJRdebianPakageTools; rsync -rlt --log-format=%f --delete --delete-excluded --modify-window=2 --exclude '.git' --rsh='ssh' /c/Users/richmit/Documents/world/my_prog/MJRdebianPakageTools/   pi-cam.home.mitchr.me:synced/world/my_prog/MJRdebianPakageTools/;
date; echo tmuxStuff;            rsync -rlt --log-format=%f --delete --delete-excluded --modify-window=2 --exclude '.git' --rsh='ssh' /c/Users/richmit/Documents/world/my_prog/tmuxStuff/              pi-cam.home.mitchr.me:synced/world/my_prog/tmuxStuff/;
date; echo UNIXutils;            rsync -rlt --log-format=%f --delete --delete-excluded --modify-window=2 --exclude '.git' --rsh='ssh' /c/Users/richmit/Documents/world/my_prog/UNIXutils/              pi-cam.home.mitchr.me:synced/world/my_prog/UNIXutils/;
date; echo utils;                rsync -rlt --log-format=%f --delete --delete-excluded --modify-window=2 --exclude '.git' --rsh='ssh' /c/Users/richmit/Documents/world/my_prog/utils/                  pi-cam.home.mitchr.me:synced/world/my_prog/utils/;
date; echo core;                 rsync -rlt --log-format=%f --delete --delete-excluded --modify-window=2 --exclude '.git' --rsh='ssh' /c/Users/richmit/Documents/core/                                 pi-cam.home.mitchr.me:synced/core/;
date; echo leicaS8APO-docs;      rsync -rlt --log-format=%f --delete --delete-excluded --modify-window=2 --exclude '.git' --rsh='ssh' /c/Users/richmit/Documents/Doc2/gadgets/leica_microscopes/       pi-cam.home.mitchr.me:synced/Doc2/leica_microscopes/;
date

8.4 RPI Software

My goals:

  • Capture images directly from ImageJ/Fiji so I could immediately preform an image analysis
  • My #1 most used image analysis activity is measuring stuff (lengths, etc…) so setting image scale needs to be easy
  • Capture images from the command line as well, but such that it is easy to load them in ImageJ/Fiji later
  • See a live view from the camera
  • Simple to sync data between camera and primary workstation
  • Identical ImageJ/Fiji UI between Windows and RPI

In the end I wrote a command line script and a set of ImageJ/Fiji toolbar macros that work together. I say they work together in that they both save newly captured images to the same directory using the same file name conventions. For example, this makes it easy to load the last thing captured by the command line script into ImageJ/Fiji.

8.4.1 RPI Image Capture Script

This script is pretty simple. It provides a way to capture one or more images outside of ImageJ/Fiji, but in a way compatible with the ImageJ/Fiji macros provided below. I use this script quite a lot to provide a live view of the camera feed.

#!/bin/bash
# -*- Mode:Shell-script; Coding:us-ascii-unix; fill-column:158 -*-
################################################################################################################################################################
##
# @file      piSnap.sh
# @author    Mitch Richling https://www.mitchr.me
# @brief     @EOL
# @keywords  raspberry pi hq camera image capture
# @std       bash
# @copyright 
#  @parblock
#  Copyright (c) 2021, Mitchell Jay Richling <https://www.mitchr.me> All rights reserved.
#
#  Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met:
#
#  1. Redistributions of source code must retain the above copyright notice, this list of conditions, and the following disclaimer.
#
#  2. Redistributions in binary form must reproduce the above copyright notice, this list of conditions, and the following disclaimer in the documentation
#     and/or other materials provided with the distribution.
#
#  3. Neither the name of the copyright holder nor the names of its contributors may be used to endorse or promote products derived from this software without
#     specific prior written permission.
#
#  THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
#  IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
#  FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
#  SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR
#  TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
#  @endparblock
################################################################################################################################################################

#---------------------------------------------------------------------------------------------------------------------------------------------------------------
read -r -d '' HELPT <<EOF

Take a snapshot using the Raspberry Pi HQ Camera and save it in a standard way

Use: piSnap.sh [options]
  Options:
    -p        Preview!  No images are captured. Other arguments are ignored
    -k        Show a preview, and capture when [enter] is pressed.  
              Without -k an image is immediatly captured with no preview
    -s        Show image after capture with nomacs
    -a ANNO   Annotation
    -g GROUP  Group name -- used for grouping similar captures
              Adds a "_GROUP-NNN" component to the captured filename where
              GROUP is the group name provided on the command line and NNN
              is a zero padded integer index.  If the previous capture has
              a diffrent group name, then the current capture will have a
              fresh time stamp in the name and an index of 001.  If the
              previous capture has the same group name, then the current
              capture will reuse the time stamp in the name and an index
              incremented by 1.
    -v        Verbose mode
    -f        Fake Capture Mode that uses convert instead of libcamera-still
              Used for debugging.  Most useful when combined with -v.
    -b BIN    Full path to the libcamera-still binary
              Default: /usr/bin/libcamera-still
    -d DIR    Directory to store captured images.  
              Default: $HOME/Pictures/pi-cam
              Note: The related ImageJ/Fiji macro expects the default value!
    -e ENC    File format: jpg, bmp, gif, png, rgb
              Default: jpg
  File Names
    Image names are like: YYYYMMDDHHMMSS_GROUP-ANNOTATION.ENC
                          |              |     |
                          |              |     File Annoation
                          |              Group Name
                          date/time stamp
      - The "_GROUP" component of the name will not be pressent 
        if the -g option was not provided.
      - The "-ANNOTATION" component of the name will not be pressent 
        if the -a option was not provided.
      - The -a and -g arguments are translated automatically into
        something suitable:  
          - Non-alphanumeric characters are converted to underscores.  
          - Leading underscores and dashes are removed
          - The -g argument has all dashes removed
EOF

#---------------------------------------------------------------------------------------------------------------------------------------------------------------
FAKE_CAP='N'
SHOW='N'
WKEY='N'
VERB='N'
PREVIEW='N'
GROUP=''
OGROUP=''
ANNOT=''
OANNOT=''
ODIR="$HOME/Pictures/pi-cam"
IENC='jpg'
RASPISP='/usr/bin/libcamera-still'
while [[ "$1" = -* ]]; do
   case "$1" in
    -k ) WKEY='Y';                                              ;; # Capture multiple images
    -d ) ODIR="$2"; shift;                                      ;; # Output directory
    -g ) OGROUP="$2"; shift;                                    ;; # Group name
    -a ) OANNOT="$2"; shift;                                    ;; # Annotation
    -v ) VERB='Y';                                              ;; # Verbose mode
    -f ) FAKE_CAP='Y';                                          ;; # Fake Capture mode
    -e ) IENC="$2"; shift;                                      ;; # Output image format
    -s ) SHOW='Y';                                              ;; # Open captured images
    -p ) PREVIEW='Y';                                           ;; # Preview only
    -b ) RASPISP="$2"; shift;                                   ;; # Location of libcamera-still binary
    *  ) echo "ERROR: Unknown option: $1"; echo "$HELPT"; exit; ;;
   esac
   shift;
done
if [ -n "$OANNOT" ]; then
  OANNOT="$OANNOT"
  ANNOT=`echo -n "$OANNOT" | tr -sc '[[:graph:]]' '_' |                 sed 's/[-_]*$//' | sed 's/^[-_]*//' `
fi
if [ -n "$OGROUP" ]; then
  GROUP=`echo -n "$OGROUP" | tr -sc '[[:graph:]]' '_' | tr -s '-' '_' | sed 's/[-_]*$//' | sed 's/^[-_]*//' `
fi

#---------------------------------------------------------------------------------------------------------------------------------------------------------------
if [ "$VERB" = 'Y' ]; then                     
  echo "DEBUG: WKEY     $WKEY     "
  echo "DEBUG: VERB     $VERB     " 
  echo "DEBUG: PREVIEW  $PREVIEW  "
  echo "DEBUG: ANNOT    $ANNOT    "
  echo "DEBUG: OANNOT   $OANNOT   "
  echo "DEBUG: GROUP    $GROUP    "
  echo "DEBUG: OGROUP   $OGROUP   "
  echo "DEBUG: WIDTH    $WIDTH    "
  echo "DEBUG: ODIR     $ODIR     " 
  echo "DEBUG: IENC     $IENC     "
fi    

#---------------------------------------------------------------------------------------------------------------------------------------------------------------
if [ -n "$2" ]; then
  echo "ERROR: Arguments ignored: $@"
  echo "$HELPT"
  exit
fi

if [[ ! "$IENC" =~ ^(jpg|bmp|gif|png|rgb)$ ]]; then
  echo "ERROR: Encodeing of '$IENC' is not supported!"
  echo "$HELPT"
  exit
fi

if [ ! -x "$RASPISP" ]; then
  if [ "$FAKE_CAP" = 'Y' ]; then
    if [ "$VERB" = 'Y' ]; then                     
      echo "DEBUG: In FAKE_CAP mode. Didn't find $RASPISP"
    fi
  else
    echo "ERROR: $RASPISP not found!"
    echo "$HELPT"
    exit
  fi
fi

if [ ! -d "$ODIR" ]; then
  mkdir "$ODIR"
  if [ -d "$ODIR" ]; then
    echo "WARNING: Output directory was created: $ODIR"
  else
    echo "ERROR: Output directory not found/created: $ODIR"
    echo "$HELPT"
    exit
  fi
fi

#---------------------------------------------------------------------------------------------------------------------------------------------------------------
DACMD="$RASPISP"
OFILE=''
if [ "$IENC" = "raw" ]; then
  DACMD="$DACMD -r"
  IENC='jpg'
fi

if [ "$PREVIEW" = 'Y' ]; then
  DACMD="$DACMD -t 0"
else
  if [ "$WKEY" = "Y" ]; then
    DACMD="$DACMD -t 0 -k"
  else
    DACMD="$DACMD -t 1 -n"
  fi
  if [ "$IENC" = "jpg" ]; then
    DACMD="$DACMD -q 100"
  fi
  OFILE=$ODIR'/'`date '+%Y%m%d%H%M%S'`
  if [ -n "$GROUP" ]; then
    OFILE=${OFILE}'_'${GROUP}
  fi
  if [ -n "$ANNOT" ]; then
    OFILE=${OFILE}'-'${ANNOT}
  fi
  OFILE=${OFILE}'.'$IENC
  DACMD="$DACMD -e $IENC -o $OFILE"
fi
if [ "$VERB" = 'Y' ]; then                     
  echo "DEBUG: Command to run: $DACMD"
fi
if [ "$FAKE_CAP" = 'Y' ]; then
  if [ "$PREVIEW" = 'Y' ]; then
    DACMD='true'
  else
    DACMD="convert -size 1024x1024 xc:white $OFILE"
  fi
  if [ "$VERB" = 'Y' ]; then                     
    echo "DEBUG: In FAKE_CAP mode. New command to run: $DACMD"
  fi
fi
$DACMD

#---------------------------------------------------------------------------------------------------------------------------------------------------------------
if [ -n "$OFILE" -a -e "$OFILE" ]; then
  echo "INFO: Captured Image File:"
  ls -l "$OFILE" | sed 's/^/    /'
  if [ "$SHOW" = 'Y' ]; then
    if [ -x '/usr/bin/nomacs' ]; then
      /usr/bin/nomacs "$OFILE"
    else
      echo 'ERROR: Unable to open image (/usr/bin/nomacs) not found.'
    fi
  fi
else
  echo "ERROR: No image captured!"
fi

8.4.2 ImageJ/Fiji Toolset/Macros

The code below provides an ImageJ/Fiji toolset. You activate the tool set via the toolset menu:

toolsetmenu.gif

Once activated, it will add four buttons to the toolbar:

toolsetbuttons.gif

Button Functions:

toolsetbutton-capture.gif
Capture, ssave, and open an image from the camera. Images are stored in the same location, and with the same file name conventions, used by piSnap.sh.
toolsetbutton-settings.gif
Change some configuration options

dialog-settings.gif

  • Specify a group for the captured image filenames. See piSnap for filename details.
  • Specify an annotation for the captured image filenames. See piSnap for filename details.
  • Set captured image format (jpg or png)
  • Set captured image scale (100% or 50%)
  • Set the scale factor for capture preview
  • Set the scale factor for the live video feed
  • Turn on/off asking for settings for each capture – essentially pops up the settings dialog box everything the capture button is hit
  • Turn on/off repeated capture mode – repeatedly captures images. Most useful if a group is set.
  • Turn on/off live video preview before a capture.
  • Turn on/off loading images after capture.
  • Turn on/off running the set image scale tool after each capture if the image scale is not already set
  • Turn on/off debugging
toolsetbutton-video.gif
Live video feed from camera. Note the scale setting for the live video feed is diffrent from the capture preview scale.

If "Change Settings Before Capture" is set, then a simplified settings dialog box will appear before the video feed window.

dialog-settings-v.gif

toolsetbutton-load.gif
Load previous capture(s) from camera using the ImageJ/Fiji macro above or the piSnap.sh script.

dialog-loadOld.gif

toolsetbutton-scale.gif
Set image scale based upon microscope settings. This script contains hardwired settings for my specific microscope and lenses, and may require modification for your microscope.

dialog-setscale.gif

  • The choice of microscope enables a calibration correction factor in the final computation
  • The "Adjust for Resolution" option adjusts the scale if the number of horizontal pixels in the image differs from the horizontal resolution of the sensor. Most commonly this use used when the sensor is used in 2x2 mode yielding smaller image. This option also works if the image is resized after the fact.
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

var gbl_ALL_debug     = false;                    // RPI-CODE
var gbl_ALL_doScl     = true;                     // RPI-CODE
var gbl_lil_group     = "";                       // RPI-CODE
var gbl_lil_which     = "Last";                   // RPI-CODE
var gbl_pic_anno      = "";                       // RPI-CODE
var gbl_pic_doSet     = true;                     // RPI-CODE
var gbl_pic_group     = "";                       // RPI-CODE
var gbl_pic_ifmt      = "jpg";                    // RPI-CODE
var gbl_pic_ipad      = 3;                        // RPI-CODE
var gbl_pic_loadem    = true;                     // RPI-CODE
var gbl_pic_pviewDo   = true;                     // RPI-CODE
var gbl_pic_pviewScl  = 4;                        // RPI-CODE
var gbl_pic_repeat    = false;                    // RPI-CODE
var gbl_pic_res       = "100%";                   // RPI-CODE
var gbl_pic_useCam    = true;                     // RPI-CODE
var gbl_ssm_aux       = "0.63";                   // RPI-CODE
var gbl_ssm_cam       = "RPI";                    // RPI-CODE
var gbl_ssm_gbl       = false;                    // RPI-CODE
var gbl_ssm_res       = false;                    // RPI-CODE
var gbl_ssm_scope     = "Leica S8API";            // RPI-CODE
var gbl_ssm_vobj      = "0.32";                   // RPI-CODE
var gbl_ssm_zoom      = "1.00";                   // RPI-CODE
var gbl_vid_pviewScl  = "1";                      // RPI-CODE

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

macro "Capture From RPI Camera Action Tool - Cc11 F06fa F16fa F4472 F6333 Ld2e3 Le0e3 Lf2e3 Cfff V5866" {
  captureImageFromRPI();
}

macro "Setup RPI Camera Action Tool - Cc11 F06fa F16fa F4472 F6333 C000 T5f14s" {
  configureRPI();
}

macro "Live RPI Video Preview Action Tool - Cc11 L7730 L77f0 F26d9 Cfff F4993 F5875" {
  videoPreviewFromRPI();
}

macro "Set Scale Action Tool - Cc11 L1cfc L1a1e Lfafe L8b8d L5b5d Lbbbd T4707R T9707P Te707I" {
  setScaleForMicrograph(false);
}

macro "Open Previous RPI Capture(s) Action Tool - Cc11 L000f L0fff Lfff3 Lf363 L6340 L4000 T3c07R T8c07P Tdc07I" {
  getCaptureRPI();
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Capture an image.  See piSnap.sh filename conventions.
// RPI-CODE
function configureRPI() {
  do {
    needMoreData = false;
    Dialog.create("Configure RPI Capture Settings");
    Dialog.addString("File group name:",                                           gbl_pic_group, 5);
    Dialog.addString("File annotation:",                                           gbl_pic_anno,  15);
    Dialog.addChoice("Image Format:", newArray("jpg", "png"),                      gbl_pic_ifmt);
    Dialog.addChoice("Image Size:", newArray("100%", "50%"),                       gbl_pic_res);
    Dialog.addChoice("Capture Preview Scale (1/n):", newArray("1", "2", "4", "8"), gbl_pic_pviewScl);
    Dialog.addChoice("Live Video Scale (1/n):", newArray("1", "2", "4", "8"),      gbl_vid_pviewScl);
    Dialog.addCheckbox("Change settings before capture",                           gbl_pic_doSet);
    Dialog.addCheckbox("Repeated capture mode",                                    gbl_pic_repeat);
    Dialog.addCheckbox("Video preview before capture",                             gbl_pic_pviewDo);
    Dialog.addCheckbox("Load image after capture",                                 gbl_pic_loadem);
    Dialog.addCheckbox("Set scale after capture/load",                             gbl_ALL_doScl);
    Dialog.addCheckbox("Debugging",                                                gbl_ALL_debug);

    Dialog.show();
    gbl_pic_group    = Dialog.getString();
    gbl_pic_anno     = Dialog.getString();
    gbl_pic_ifmt     = Dialog.getChoice();
    gbl_pic_res      = Dialog.getChoice();
    gbl_pic_pviewScl = Dialog.getChoice();
    gbl_vid_pviewScl = Dialog.getChoice();
    gbl_pic_doSet    = Dialog.getCheckbox();
    gbl_pic_repeat   = Dialog.getCheckbox();
    gbl_pic_pviewDo  = Dialog.getCheckbox();
    gbl_pic_loadem   = Dialog.getCheckbox();
    gbl_ALL_doScl    = Dialog.getCheckbox();
    gbl_ALL_debug    = Dialog.getCheckbox();
    if ( (lengthOf(gbl_pic_group)>0) && !(matches(gbl_pic_group, "(^\\p{Alnum}+$)"))) {
      showMessage("ERROR(configureRPI)", "Group name must contain only alphanumeric characters!");
      needMoreData = true;
    }
    if ( (lengthOf(gbl_pic_anno)>0) && !(matches(gbl_pic_anno, "(^\\p{Alnum}+$)"))) {
      showMessage("ERROR(configureRPI)", "Annotation name must contain only alphanumeric characters!");
      needMoreData = true;
    }
  } while (needMoreData);
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Live video preview
// RPI-CODE
function videoPreviewFromRPI() {
  // Make sure we have libcamera-still installed -- if we don't, then we are probably
  // not running on a RPI..
  if (gbl_pic_useCam)
    if (!(File.exists("/usr/bin/libcamera-still")))
      exit("ERROR(captureImageFromRPI): Could not find /usr/bin/libcamera-still!");

  if (gbl_pic_useCam) {
    pList = exec("/bin/bash", "-c", "ps -eo pid,comm | grep libcamera- | grep -v grep");
    if (lengthOf(pList) > 10)
      exit("ERROR(captureImageFromRPI): libcamera processes are already running!  Can't start video preview.\n\nProcess List:\n" + pList);
  }

  // Ask for camera settings
  if (gbl_pic_doSet) {
    Dialog.create("Configure RPI Live Video Settings");
    Dialog.addChoice("Live Video Scale (1/n):", newArray("1", "2", "4", "8"), gbl_vid_pviewScl);
    Dialog.addCheckbox("Change settings before capture",                      gbl_pic_doSet);
    Dialog.show();
    gbl_vid_pviewScl = Dialog.getChoice();
    gbl_pic_doSet    = Dialog.getCheckbox();
  }

  psv = parseInt(gbl_vid_pviewScl);
  pww = round(4056/psv);
  pwh = round(3040/psv);

  if (gbl_pic_useCam) {
    setOption("WaitForCompletion", false);
    exec("/usr/bin/libcamera-still", "-t", "0", "-p", "0,0," + pww + "," + pwh, "--info-text", "fps:%fps/exp:%exp/a_gain:%ag/d_gain:%dg/focus:%focus");
  }
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Capture an image.  See piSnap.sh filename conventions.
// RPI-CODE
function captureImageFromRPI() {
  needOne = true;
  while (needOne || gbl_pic_repeat) {
    needOne = false;
    // Make sure we have libcamera-still installed -- if we don't, then we are probably
    // not running on a RPI..
    if (gbl_pic_useCam)
      if (!(File.exists("/usr/bin/libcamera-still")))
        exit("ERROR(captureImageFromRPI): Could not find /usr/bin/libcamera-still!");

    // Make sure we can find the user home directory
    piImagePath = getDirectory("home");
    if (!(File.exists(piImagePath)))
      exit("ERROR(captureImageFromRPI): Could not find home directory!");

    // Look for ~/Pictures.  Try to create it if it is missing.
    piImagePath = String.join(newArray(piImagePath, "Pictures"), File.separator);
    if (!(File.exists(piImagePath))) {
      if (gbl_ALL_debug)
        print("DEBUG(captureImageFromRPI): Attempting to create directory: " + piImagePath);
      File.makeDirectory(piImagePath);
      if (!(File.exists(piImagePath))) {
        exit("ERROR(captureImageFromRPI): Directory creation failed: " + piImagePath);
      }
    }

    // Look for ~/Pictures/pi-cam.  Try to create it if it is missing.
    piImagePath = String.join(newArray(piImagePath, "pi-cam"), File.separator);
    if (!(File.exists(piImagePath))) {
      if (gbl_ALL_debug)
        print("DEBUG(captureImageFromRPI): Attempting to create directory: " + piImagePath);
      File.makeDirectory(piImagePath);
      if (!(File.exists(piImagePath))) {
        exit("ERROR(captureImageFromRPI): Directory creation failed: " + piImagePath);
      }
    }

    // Check again that piImagePath really exists...
    if (!(File.exists(piImagePath))) {
      exit("ERROR(captureImageFromRPI): Could not find/create image directory: " + piImagePath);
    }

    // Ask for camera settings
    if (gbl_pic_doSet)
      configureRPI();

    // Construct filename: timestamp
    piImageFileName = makeDateString();
    // Construct filename: group
    if (gbl_pic_group != "")
      piImageFileName = piImageFileName + "_" + gbl_pic_group;   
    // Construct filename: anno
    if (lengthOf(gbl_pic_anno)>0)
      piImageFileName = piImageFileName + "-" + gbl_pic_anno;
    // Construct filename: ext
    piImageFileName = piImageFileName + "." + gbl_pic_ifmt;

    // Construct full file name path
    piImageFullFileName = String.join(newArray(piImagePath, piImageFileName), File.separator);
    if (gbl_ALL_debug)
      print("DEBUG(captureImageFromRPI): Image file: " + piImageFullFileName);

    resOpt = "";
    if (gbl_pic_res == "50%")
      resOpt = "--width 2028 --height 1520";

    // Run libcamera-still
    if (gbl_pic_useCam) {

      pList = exec("/bin/bash", "-c", "ps -eo pid,comm | grep libcamera- | grep -v grep");
      while (lengthOf(pList) > 10) {
        waitForUserWithCancel("ERROR(captureImageFromRPI)", "libcamera processes are running.  Please close them.\n\nProcess List:\n" + pList);
        pList = exec("/bin/bash", "-c", "ps -eo pid,comm | grep libcamera- | grep -v grep");
      }

      if (gbl_pic_pviewDo) {
        psv = parseInt(gbl_pic_pviewScl);
        pww = round(4056/psv);
        pwh = round(3040/psv);

        pid = exec("/bin/bash", "-c", "libcamera-still -t 0 --info-text 'fps:%fps/exp:%exp/a_gain:%ag/d_gain:%dg/focus:%focus'" + " -p 0,0," + pww + "," + pwh + " -s " + resOpt + " -e " + gbl_pic_ifmt + " -o '" + piImageFullFileName + "' >/dev/null 2>&1 & echo $!", "&");

        pid = String.trim(pid);

        if ( !(matches(pid, "(^[0-9][0-9]*$)")))
          exit("ERROR(captureImageFromRPI): Can't get PID of libcamera-still process -- it may not have started!");

        showMessage("RPI Capture", "Click OK to Capture Image");

        procList = exec("/bin/bash", "-c", "ps -eo pid,comm | grep '^ *" + pid + "  *libcamera-still'");
        if (lengthOf(procList) < 10)
          exit("ERROR(captureImageFromRPI): Unable to trigger capture (Can't find libcamera-still process)!");
        showStatus("Waiting for capture process");
        exec("/bin/bash", "-c", "kill -SIGUSR1 " + pid);
        c=1;
        do {
          showProgress(c, 30);
          wait(40);
          procList = exec("/bin/bash", "-c", "ps -eo pid,cmd | grep '^ *" + pid + "  *libcamera-still'");
          c++;
        } while ( (c<30) && (lengthOf(procList) > 10));
        wait(100);
      } else {
        exec("libcamera-still -t 1 -n -q 100 " + resOpt + " -e " + gbl_pic_ifmt + " -o " + piImageFullFileName);
      }
    } else {
      if (gbl_pic_pviewDo)
        showMessage("RPI Capture", "Click OK to Capture Image");
      File.append("FAKE RPI CAPTURE", piImageFullFileName);
    }

    // If we got an image, then we load it
    if (File.exists(piImageFullFileName)) {
      if (gbl_pic_useCam && gbl_pic_loadem) {
        open(piImageFullFileName);
        if (gbl_ALL_doScl && !(isImageScaled()))
          setScaleForMicrograph(true);
      }
    } else {
      exit("ERROR(captureImageFromRPI): Image file not found!: " + piImageFullFileName);
    }

    if (gbl_pic_repeat && !(gbl_pic_doSet))
      showMessageWithCancel("RPI Capture", "Capture another image?");
  }
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Get a list of group names for captured files
// RPI-CODE
function getCaptureGroupsRPI() {
  piFilesDir = String.join(newArray(getDirectory("home"), "Pictures", "pi-cam"), File.separator);

  // Make sure the pi-cam directory exists
  files = newArray(0);
  if ( !(File.exists(piFilesDir)))
    return files;

  // List of files in pi-cam directory
  files = getFileList(piFilesDir);
  if (files.length == 0)
    return files;

  // Filter out non-image files
  files = Array.filter(files, "(\\.(png|jpg)$)");
  if (files.length == 0)
    return files;

  // Transform to group names
  for(i=0; i<files.length; i++) {
    if (matches(files[i], "(^.............._.*)")) {
      tmp = indexOf(files[i], "-");
      files[i] = substring(files[i], 15, tmp);
    } else {
      files[i] = "_NONE_";
    }
  }

  // Sort group list
  files = Array.sort(files);

  // Replace duplicate group names with _
  lastGrp = "";
  for(i=0; i<files.length; i++) {
    if (files[i] == lastGrp)
      files[i] = "_";
    else 
      lastGrp = files[i];
  }

  // Filter out _ strings
  files = Array.deleteValue(files, "_"); 

  // Find last file
  return files;  
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Return the list of captured files in the given groupName
// "_ANY_" & "_NONE_" are special groups...
// RPI-CODE
function getCaptureFileNamesRPI(groupName) {
  piFilesDir = String.join(newArray(getDirectory("home"), "Pictures", "pi-cam"), File.separator);

  // Make sure the pi-cam directory exists
  files = newArray(0);
  if ( !(File.exists(piFilesDir)))
    return files;

  // List of files in pi-cam directory
  files = getFileList(piFilesDir);
  if (files.length == 0)
    return files;

  // Filter out non-image files
  files = Array.filter(files, "(\\.(png|jpg)$)");
  if (files.length == 0)
    return files;

  // Filter out files not in requested group
  if (groupName == "_ANY_") {
    // Do nothing. ;)
  } else if (groupName == "_NONE_") {
    files = Array.filter(files, "(^..............[.-].*)");
    if (files.length == 0)
      return files;
  } else if (lengthOf(groupName)>0) {
    files = Array.filter(files, "(^.............._" + groupName + ".*)");
    if (files.length == 0)
      return files;
  }

  // Sort file list
  files = Array.sort(files);

  // Find last file
  return files;
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Return the filename for the most recent pi-cam capture.  See piSnap.sh filename conventions.
// RPI-CODE
function getCaptureRPI() {
  allGroups = getCaptureGroupsRPI();
  tmp = newArray(1);
  tmp[0] = "_ANY_";
  allGroups = Array.concat(tmp, allGroups);
  if (gbl_lil_group == "") {
    if (gbl_pic_group != "") {
      gbl_lil_group = gbl_pic_group;
    } else {
      gbl_lil_group = "_ALL_";
    }
  }
  Dialog.create("Load Previous RPI Capture(s)");
  Dialog.addChoice("Capture Group:", allGroups, gbl_lil_group);
  Dialog.addChoice("Which images:", newArray("First", "First 10", "All", "Last 10", "Last"), gbl_lil_which);
  Dialog.show();

  gbl_lil_group = Dialog.getChoice();
  gbl_lil_which = Dialog.getChoice();

  files = getCaptureFileNamesRPI(gbl_lil_group);
  len  = files.length;
  if (indexOf(gbl_lil_which, " ") > 0)
    num = parseInt(substring(gbl_lil_which, indexOf(gbl_lil_which, " ")+1));
  else
    num = 1;
  num = minOf(num, len);
  if (num > 0) {
    piPath = String.join(newArray(getDirectory("home"), "Pictures", "pi-cam"), File.separator);
    if      (startsWith(gbl_lil_which, "First"))
      files = Array.slice(files, 0, num);
    else if (startsWith(gbl_lil_which, "Last"))
      files = Array.slice(files, len-num, len);
    for(i=0; i<files.length; i++) {
      open(String.join(newArray(piPath, files[i]), File.separator));
      if (gbl_ALL_doScl)
        if ( !(isImageScaled()))
          setScaleForMicrograph(false);
    }
  } else {
    exit("ERROR(getCaptureRPI): No images found!");
  }
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Set image scale for RPI Microscope Camera
// RPI-CODE
function setScaleForMicrograph(freshFromCamera) {

  if (nImages == 0)
    exit("ERROR(setScaleForMicrograph): No open images found!");

  Dialog.create("Set Scale for Stereo Microscope Photograph");
  Dialog.addChoice("Microscope:", newArray("Leica S8API"),   "Leica S8API");
  Dialog.addChoice("Zoom Stop:",  newArray("1.00", "8.00"),  "1.00");
  Dialog.addChoice("Auxiliary:",  newArray("0.63", "1.00"),  "0.63");
  Dialog.addChoice("Video Obj:",  newArray("0.32", "0.50"),  "0.32");
  Dialog.addChoice("Camera:",     newArray("RPI", "OLY"),    "RPI");
  if (freshFromCamera)
    Dialog.addMessage("Adjust for Resolution: YES");
  else
    Dialog.addCheckbox("Adjust for Resolution", gbl_ssm_res);
  Dialog.addCheckbox("Global Scale", gbl_ssm_gbl);
  Dialog.show();

  gbl_ssm_scope = Dialog.getChoice();
  gbl_ssm_zoom  = Dialog.getChoice();
  gbl_ssm_aux   = Dialog.getChoice();
  gbl_ssm_vobj  = Dialog.getChoice();
  gbl_ssm_cam   = Dialog.getChoice();
  if ( !(freshFromCamera))
    gbl_ssm_res   = Dialog.getCheckbox();
  gbl_ssm_gbl   = Dialog.getCheckbox();

  List.clear();
  List.set("Leica S8API", d2s(0.994507340589, 15));
  scopeCalFactor = parseFloat(List.get(gbl_ssm_scope));

  List.clear();
  List.set("OLY", d2s(5184.0 / 17.4,   10));
  List.set("RPI", d2s(4056.0 / 6.2868, 10));
  ijPixHorzScale = parseFloat(List.get(gbl_ssm_cam)) * parseFloat(gbl_ssm_aux) * parseFloat(gbl_ssm_zoom) * parseFloat(gbl_ssm_vobj) * scopeCalFactor;

  if (freshFromCamera || gbl_ssm_res) {
    List.clear();
    List.set("OLY", 5184);
    List.set("RPI", 4056);
    sensorRes = parseInt(List.get(gbl_ssm_cam));
    imgWidth  = getWidth();
    if (sensorRes != imgWidth)
      ijPixHorzScale = ijPixHorzScale * imgWidth / sensorRes;
  }

  ijPixHorzScale = d2s(ijPixHorzScale, 10);

  List.clear();
  List.set("OLY", d2s(5184.0 * 13.0 / 17.4 / 3888.0, 10));
  List.set("RPI", d2s(1.0,                           10));
  ijPixAspectRatio = List.get(gbl_ssm_cam);

  setScaleOptions = " known=1 unit=mm distance=" + ijPixHorzScale + " pixel=" + ijPixAspectRatio;
  if (gbl_ssm_gbl) {
    setScaleOptions = setScaleOptions + " global";
  }

  run("Set Scale...", setScaleOptions);
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Takes an integer and returns a zero padded string
// RPI-CODE
function intToZeroPadString(anInt, width) {
  result = d2s(anInt, 0);
  while (lengthOf(result) < width) {
    result = "0" + result;
  }
  return result;
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Returns a string for the current date/time YYYYMMDDhhmmss
// RPI-CODE
function makeDateString() {
  getDateAndTime(year, month, dayOfWeek, dayOfMonth, hour, minute, second, msec);
  dateBitVal = newArray(year, month+1, dayOfMonth, hour, minute, second);
  dateBitWid = newArray(4, 2, 2, 2, 2, 2);
  dateString = "";
  for(i=0; i<6; i++) {
    dateString = dateString + intToZeroPadString(dateBitVal[i], dateBitWid[i]);
  }
  return dateString;
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
// Check if image has scale.  If not, try to set it or query if RPI iamge.
// RPI-CODE
function isImageScaled() {
  getPixelSize(pixelLengthUnit, pixelWidth, pixelHeight);
  return (is("global scale") || ( !(startsWith(pixelLengthUnit, "pixel"))) || (pixelHeight != 1));
}

9 EOF
































































































Created by Mitch Richling <http://www.mitchr.me/>. Rendered on 2021-12-01 Wed 18:19 via Emacs 27.2 (Org mode 9.4.4)