Turn a folder of SEM/FESEM TIFF exports into a single interactive HTML page for side-by-side sample comparison — no manual importing or arranging.
One command scans the session folder, converts every .tif to browser-friendly
JPG, reads the instrument metadata, and generates _viewer/index.html:
- Filters — view type (Surface / Cross), magnification chips, sample multi-select with text search
- Grid layout — e.g. all samples at x500 side by side
- Matrix layout — table of samples (rows) × magnifications (columns); missing shots show as gaps at a glance
- Lightbox — click any image for fullscreen; arrow keys walk the filtered set; click again for 100 % pixel zoom
- Pin & compare — pin 2–4 images, open them side by side
- Each image is captioned with magnification, accelerating voltage (kV),
working distance (WD), and detector signal from the instrument's
.txtfile
Why the conversion step: browsers cannot display TIFF. The originals are never
touched — JPGs are written to a separate _viewer/img/ folder.
- Python 3.9+
- Pillow
pip install -r requirements.txt
python fesem_viewer.py "C:/path/to/SEM_session_folder"
Then double-click SEM_session_folder/_viewer/index.html. It opens in any
browser directly from disk — no server, no internet needed.
Options:
| Option | Effect |
|---|---|
--force |
Re-convert every TIFF (default skips already-converted, so re-running after adding new images is fast) |
--out DIR |
Write the viewer somewhere other than <root>/_viewer |
Re-run the same command any time you add images to the folder — conversion is incremental.
The viewer expects the layout most JEOL FESEMs export naturally:
SEM_session_folder/ ← pass this path to the script
├── SampleA/ ← one folder per sample
│ ├── Surface_x250.tif
│ ├── Surface_x250.txt ← paired instrument metadata (optional)
│ ├── Surface_x500.tif
│ ├── Surface_x500.txt
│ ├── Cross_x500.tif
│ └── Cross_x500.txt
├── SampleB/
│ └── ...
└── Group1/ ← nesting is fine
└── SubsampleC/ ← shown in the viewer as "Group1/SubsampleC"
└── ...
Sample = folder. The folder name is the sample label shown in the viewer,
so name folders after your samples (05BA4, 20GP4W3, …). Nested subfolders
work; the label becomes the relative path (DEW/BP 1).
Image file = <View>_x<mag>.tif
| Part | Meaning | Examples |
|---|---|---|
<View> |
Observation method. Surface or Cross get their own filter chips; anything else is grouped under "Other" |
Surface_x500.tif, Cross_x250.tif |
x<mag> |
Magnification | x250, x500, x1000, x2500 |
_2 suffix |
Second shot of the same view/mag (shown with a #2 badge) |
Cross_x500_2.tif |
Bare-number filenames (1300.tif) also work if the paired .txt exists —
the magnification is taken from the metadata.
Metadata file = same name, .txt extension. JEOL writes this
automatically next to each image. The viewer reads:
| Key | Used for |
|---|---|
$CM_MAG |
Magnification — overrides the filename |
$CM_ACCEL_VOLT |
kV in the caption |
$$SM_WD |
Working distance in the caption |
$CM_SIGNAL_NAME |
Detector (SEI, …) in the caption |
Without a .txt, the magnification falls back to the filename and the caption
has no kV/WD.
Filenames are typed by the operator and can be wrong. In a real 154-image
session, two files disagreed with the instrument record (one file named x250
was actually captured at x500). The viewer always displays the instrument's
value. Nearby magnifications are grouped for filtering (x251 files land in
the x250 chip), so a typo doesn't create a stray filter button.
SEM_session_folder/
└── _viewer/
├── index.html ← open this (self-contained, ~portable with the img folder)
└── img/ ← converted JPGs, named <sample>__<original>.jpg
Delete _viewer/ at any time and re-run to rebuild from scratch — the
original .tif/.txt files are read-only inputs.
- To share a session with someone, zip the
_viewer/folder — it contains everything the page needs. - Big sessions: images are lazy-loaded, so the page opens fast even with hundreds of micrographs.
- The
xprefix match is case-insensitive;Surface/surface/SURFACEall work.