sem2surface 0.2.1__tar.gz → 0.2.2__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sem2surface
3
- Version: 0.2.1
3
+ Version: 0.2.2
4
4
  Summary: 3D surface reconstruction from multi-detector SEM/BSE images
5
5
  Author: Vladislav A. Yastrebov
6
6
  License-Expression: BSD-3-Clause
@@ -186,6 +186,12 @@ decomposition, Radon search, and oriented gradients.
186
186
  When timestamps are disabled, an existing output with the same name is replaced.
187
187
  Choose a dedicated output folder or enable timestamps when results must be kept.
188
188
 
189
+ For TIFF acquisitions, `sem2surface` automatically removes a microscope
190
+ annotation footer when it detects a nearly uniform bright separator followed by
191
+ a statistically distinct lower band. The detector works with both 8-bit Zeiss
192
+ and 16-bit FEI exports, leaves footer-free images unchanged, and records every
193
+ detected crop in the processing log.
194
+
189
195
  ## Reference examples and scaling
190
196
 
191
197
  The [examples directory](https://github.com/vyastreb/sem2surface/tree/master/examples)
@@ -206,6 +212,15 @@ python examples/Surface_1/test_without_gui.py
206
212
  <!-- PyPI cannot resolve repository-relative images. Keep this absolute URL. -->
207
213
  ![Reconstruction of the indented surface](https://raw.githubusercontent.com/vyastreb/sem2surface/master/img/indent_superposition.jpg)
208
214
 
215
+ ## Changes in 0.2.2
216
+
217
+ - Replaced the historical hard-coded footer threshold with bit-depth-independent
218
+ SEM annotation-band detection.
219
+ - Added safeguards against cropping isolated bright lines in specimen data.
220
+ - Added per-image logging of detected footers and removed row counts.
221
+ - Added regression coverage for 8-bit Zeiss, 16-bit FEI, footer-free, and
222
+ isolated-line images.
223
+
209
224
  ## Development
210
225
 
211
226
  Create an isolated environment and install the editable project with its test
@@ -240,7 +255,7 @@ For a first trial, create a separate account and API token on
240
255
  version:
241
256
 
242
257
  ```bash
243
- python -m twine upload --repository testpypi dist/sem2surface-0.2.1*
258
+ python -m twine upload --repository testpypi dist/sem2surface-0.2.2*
244
259
  ```
245
260
 
246
261
  When prompted, use `__token__` as the username and the complete TestPyPI token,
@@ -248,7 +263,7 @@ including its `pypi-` prefix, as the password. Test the uploaded wheel in a new
248
263
  environment without resolving dependencies from TestPyPI:
249
264
 
250
265
  ```bash
251
- python -m pip install --index-url https://test.pypi.org/simple/ --no-deps sem2surface==0.2.1
266
+ python -m pip install --index-url https://test.pypi.org/simple/ --no-deps sem2surface==0.2.2
252
267
  sem2surface --version
253
268
  ```
254
269
 
@@ -256,7 +271,7 @@ For the real release, create a PyPI account and API token at
256
271
  [pypi.org](https://pypi.org/), then run:
257
272
 
258
273
  ```bash
259
- python -m twine upload dist/sem2surface-0.2.1*
274
+ python -m twine upload dist/sem2surface-0.2.2*
260
275
  ```
261
276
 
262
277
  PyPI and TestPyPI use separate accounts and tokens. Never commit a token or put
@@ -147,6 +147,12 @@ decomposition, Radon search, and oriented gradients.
147
147
  When timestamps are disabled, an existing output with the same name is replaced.
148
148
  Choose a dedicated output folder or enable timestamps when results must be kept.
149
149
 
150
+ For TIFF acquisitions, `sem2surface` automatically removes a microscope
151
+ annotation footer when it detects a nearly uniform bright separator followed by
152
+ a statistically distinct lower band. The detector works with both 8-bit Zeiss
153
+ and 16-bit FEI exports, leaves footer-free images unchanged, and records every
154
+ detected crop in the processing log.
155
+
150
156
  ## Reference examples and scaling
151
157
 
152
158
  The [examples directory](https://github.com/vyastreb/sem2surface/tree/master/examples)
@@ -167,6 +173,15 @@ python examples/Surface_1/test_without_gui.py
167
173
  <!-- PyPI cannot resolve repository-relative images. Keep this absolute URL. -->
168
174
  ![Reconstruction of the indented surface](https://raw.githubusercontent.com/vyastreb/sem2surface/master/img/indent_superposition.jpg)
169
175
 
176
+ ## Changes in 0.2.2
177
+
178
+ - Replaced the historical hard-coded footer threshold with bit-depth-independent
179
+ SEM annotation-band detection.
180
+ - Added safeguards against cropping isolated bright lines in specimen data.
181
+ - Added per-image logging of detected footers and removed row counts.
182
+ - Added regression coverage for 8-bit Zeiss, 16-bit FEI, footer-free, and
183
+ isolated-line images.
184
+
170
185
  ## Development
171
186
 
172
187
  Create an isolated environment and install the editable project with its test
@@ -201,7 +216,7 @@ For a first trial, create a separate account and API token on
201
216
  version:
202
217
 
203
218
  ```bash
204
- python -m twine upload --repository testpypi dist/sem2surface-0.2.1*
219
+ python -m twine upload --repository testpypi dist/sem2surface-0.2.2*
205
220
  ```
206
221
 
207
222
  When prompted, use `__token__` as the username and the complete TestPyPI token,
@@ -209,7 +224,7 @@ including its `pypi-` prefix, as the password. Test the uploaded wheel in a new
209
224
  environment without resolving dependencies from TestPyPI:
210
225
 
211
226
  ```bash
212
- python -m pip install --index-url https://test.pypi.org/simple/ --no-deps sem2surface==0.2.1
227
+ python -m pip install --index-url https://test.pypi.org/simple/ --no-deps sem2surface==0.2.2
213
228
  sem2surface --version
214
229
  ```
215
230
 
@@ -217,7 +232,7 @@ For the real release, create a PyPI account and API token at
217
232
  [pypi.org](https://pypi.org/), then run:
218
233
 
219
234
  ```bash
220
- python -m twine upload dist/sem2surface-0.2.1*
235
+ python -m twine upload dist/sem2surface-0.2.2*
221
236
  ```
222
237
 
223
238
  PyPI and TestPyPI use separate accounts and tokens. Never commit a token or put
@@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta"
4
4
 
5
5
  [project]
6
6
  name = "sem2surface"
7
- version = "0.2.1"
7
+ version = "0.2.2"
8
8
  description = "3D surface reconstruction from multi-detector SEM/BSE images"
9
9
  readme = "README.md"
10
10
  requires-python = ">=3.10"
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: sem2surface
3
- Version: 0.2.1
3
+ Version: 0.2.2
4
4
  Summary: 3D surface reconstruction from multi-detector SEM/BSE images
5
5
  Author: Vladislav A. Yastrebov
6
6
  License-Expression: BSD-3-Clause
@@ -186,6 +186,12 @@ decomposition, Radon search, and oriented gradients.
186
186
  When timestamps are disabled, an existing output with the same name is replaced.
187
187
  Choose a dedicated output folder or enable timestamps when results must be kept.
188
188
 
189
+ For TIFF acquisitions, `sem2surface` automatically removes a microscope
190
+ annotation footer when it detects a nearly uniform bright separator followed by
191
+ a statistically distinct lower band. The detector works with both 8-bit Zeiss
192
+ and 16-bit FEI exports, leaves footer-free images unchanged, and records every
193
+ detected crop in the processing log.
194
+
189
195
  ## Reference examples and scaling
190
196
 
191
197
  The [examples directory](https://github.com/vyastreb/sem2surface/tree/master/examples)
@@ -206,6 +212,15 @@ python examples/Surface_1/test_without_gui.py
206
212
  <!-- PyPI cannot resolve repository-relative images. Keep this absolute URL. -->
207
213
  ![Reconstruction of the indented surface](https://raw.githubusercontent.com/vyastreb/sem2surface/master/img/indent_superposition.jpg)
208
214
 
215
+ ## Changes in 0.2.2
216
+
217
+ - Replaced the historical hard-coded footer threshold with bit-depth-independent
218
+ SEM annotation-band detection.
219
+ - Added safeguards against cropping isolated bright lines in specimen data.
220
+ - Added per-image logging of detected footers and removed row counts.
221
+ - Added regression coverage for 8-bit Zeiss, 16-bit FEI, footer-free, and
222
+ isolated-line images.
223
+
209
224
  ## Development
210
225
 
211
226
  Create an isolated environment and install the editable project with its test
@@ -240,7 +255,7 @@ For a first trial, create a separate account and API token on
240
255
  version:
241
256
 
242
257
  ```bash
243
- python -m twine upload --repository testpypi dist/sem2surface-0.2.1*
258
+ python -m twine upload --repository testpypi dist/sem2surface-0.2.2*
244
259
  ```
245
260
 
246
261
  When prompted, use `__token__` as the username and the complete TestPyPI token,
@@ -248,7 +263,7 @@ including its `pypi-` prefix, as the password. Test the uploaded wheel in a new
248
263
  environment without resolving dependencies from TestPyPI:
249
264
 
250
265
  ```bash
251
- python -m pip install --index-url https://test.pypi.org/simple/ --no-deps sem2surface==0.2.1
266
+ python -m pip install --index-url https://test.pypi.org/simple/ --no-deps sem2surface==0.2.2
252
267
  sem2surface --version
253
268
  ```
254
269
 
@@ -256,7 +271,7 @@ For the real release, create a PyPI account and API token at
256
271
  [pypi.org](https://pypi.org/), then run:
257
272
 
258
273
  ```bash
259
- python -m twine upload dist/sem2surface-0.2.1*
274
+ python -m twine upload dist/sem2surface-0.2.2*
260
275
  ```
261
276
 
262
277
  PyPI and TestPyPI use separate accounts and tokens. Never commit a token or put
@@ -23,7 +23,7 @@ from scipy.ndimage import gaussian_filter
23
23
  from skimage.transform import radon
24
24
 
25
25
 
26
- __version__ = "0.2.1"
26
+ __version__ = "0.2.2"
27
27
  DEFAULT_PIXEL_SIZE = 1e-6
28
28
  _PIXEL_WIDTH_TAGS = ("PixelWidth=", "Image Pixel Size =")
29
29
 
@@ -261,6 +261,49 @@ def _read_image(path: Path) -> np.ndarray:
261
261
  return np.asarray(convert_to_grayscale(array), dtype=np.float64)
262
262
 
263
263
 
264
+ def _find_sem_data_rows(image: np.ndarray) -> int:
265
+ """Return the number of rows above a SEM annotation footer.
266
+
267
+ FEI and Zeiss exports use different bit depths and footer layouts, but both
268
+ place a nearly uniform bright separator near the bottom of the image. A
269
+ separator is accepted only when the pixels below it have a substantially
270
+ different median intensity, which avoids treating an isolated bright line
271
+ in the specimen as a footer.
272
+ """
273
+ array = np.asarray(image, dtype=np.float64)
274
+ if array.ndim != 2:
275
+ raise ValueError("Footer detection requires a two-dimensional image")
276
+ rows, columns = array.shape
277
+ if rows < 16 or columns < 16:
278
+ return rows
279
+
280
+ finite = array[np.isfinite(array)]
281
+ if finite.size == 0:
282
+ return rows
283
+ low, high = np.percentile(finite, (0.1, 99.9))
284
+ intensity_span = float(high - low)
285
+ if not np.isfinite(intensity_span) or intensity_span <= 0:
286
+ return rows
287
+
288
+ # The 3% tolerance includes FEI separator values (64512) when a few
289
+ # saturated specimen pixels raise the 99.9th percentile to 65535.
290
+ bright_threshold = high - 0.03 * intensity_span
291
+ bright_fraction = np.mean(array >= bright_threshold, axis=1)
292
+ first_candidate = max(1, int(np.ceil(0.5 * rows)))
293
+ minimum_footer_rows = max(4, int(np.ceil(0.01 * rows)))
294
+ candidate_stop = rows - minimum_footer_rows + 1
295
+
296
+ for row in range(first_candidate, candidate_stop):
297
+ if bright_fraction[row] < 0.98 or bright_fraction[row - 1] >= 0.5:
298
+ continue
299
+ preceding_rows = min(64, row)
300
+ specimen_median = float(np.nanmedian(array[row - preceding_rows : row]))
301
+ footer_median = float(np.nanmedian(array[row:]))
302
+ if abs(footer_median - specimen_median) >= 0.10 * intensity_span:
303
+ return row
304
+ return rows
305
+
306
+
264
307
  def _plot_image_decomposition(
265
308
  imgs: np.ndarray,
266
309
  intensity: np.ndarray,
@@ -537,16 +580,16 @@ def _construct_surface(
537
580
  if len(widths) != 1:
538
581
  raise ValueError("All detector images must have the same width")
539
582
 
540
- # Preserve the footer marker used by the reference data while applying the
541
- # same crop safely to every detector image.
542
- crop_rows: list[int] = []
543
- for image in images:
544
- crop = image.shape[0]
545
- row_means = np.mean(image, axis=1)
546
- marker = np.flatnonzero(np.abs(row_means[1:] - 1437.0) < 2)
547
- if marker.size:
548
- crop = max(1, int(marker[0]))
549
- crop_rows.append(crop)
583
+ # Detect each footer independently, then apply the smallest common crop so
584
+ # all detector images retain identical dimensions.
585
+ crop_rows = [_find_sem_data_rows(image) for image in images]
586
+ for path, image, crop in zip(paths, images, crop_rows):
587
+ if crop < image.shape[0]:
588
+ log(
589
+ log_file,
590
+ f"SEM annotation footer detected in {path.name}: "
591
+ f"removed {image.shape[0] - crop} rows",
592
+ )
550
593
  cut_y = min(crop_rows)
551
594
  if cut_y < 2:
552
595
  raise ValueError("Automatic footer detection left too little image data")
@@ -3,15 +3,10 @@
3
3
  from __future__ import annotations
4
4
 
5
5
  import argparse
6
- from importlib.metadata import PackageNotFoundError, version
7
6
  from pathlib import Path
8
7
  from typing import Sequence
9
8
 
10
-
11
- try:
12
- VERSION = version("sem2surface")
13
- except PackageNotFoundError: # Running directly from an unpacked source tree.
14
- VERSION = "0.2.1"
9
+ from sem2surface import __version__ as VERSION
15
10
 
16
11
 
17
12
  def build_parser() -> argparse.ArgumentParser:
@@ -8,7 +8,7 @@ def test_cli_exposes_version(capsys):
8
8
  with pytest.raises(SystemExit) as raised:
9
9
  parser.parse_args(["--version"])
10
10
  assert raised.value.code == 0
11
- assert "0.2.1" in capsys.readouterr().out
11
+ assert "0.2.2" in capsys.readouterr().out
12
12
 
13
13
 
14
14
  def test_cli_requires_three_images(capsys):
@@ -4,6 +4,7 @@ import numpy as np
4
4
  import pytest
5
5
 
6
6
  from sem2surface import (
7
+ _find_sem_data_rows,
7
8
  compute_image_gradients,
8
9
  construct_surface,
9
10
  convert_to_grayscale,
@@ -58,3 +59,40 @@ def test_caller_owned_log_is_not_closed():
58
59
  with pytest.raises(ValueError):
59
60
  construct_surface([], log_file=stream)
60
61
  assert not stream.closed
62
+
63
+
64
+ def test_footer_detection_handles_fei_16_bit_separator():
65
+ rng = np.random.default_rng(3)
66
+ image = rng.integers(28000, 48000, size=(80, 64), dtype=np.uint16)
67
+ image.flat[:12] = 65535 # Saturated specimen pixels above the separator.
68
+ image[64] = 64512
69
+ image[65:] = 1024
70
+ image[68:76:3, 5:59:7] = 64512
71
+ image[-1] = 64512
72
+
73
+ assert _find_sem_data_rows(image) == 64
74
+
75
+
76
+ def test_footer_detection_handles_zeiss_8_bit_band():
77
+ rng = np.random.default_rng(5)
78
+ image = rng.integers(90, 190, size=(120, 64), dtype=np.uint8)
79
+ image[101] = 0
80
+ image[102:] = 255
81
+ image[105:117:3, 4:60:6] = 0
82
+
83
+ assert _find_sem_data_rows(image) == 102
84
+
85
+
86
+ def test_footer_detection_leaves_footer_free_image_unchanged():
87
+ rng = np.random.default_rng(7)
88
+ image = rng.integers(40, 220, size=(120, 64), dtype=np.uint8)
89
+
90
+ assert _find_sem_data_rows(image) == image.shape[0]
91
+
92
+
93
+ def test_footer_detection_ignores_isolated_bright_specimen_line():
94
+ rng = np.random.default_rng(11)
95
+ image = rng.integers(80, 180, size=(120, 64), dtype=np.uint8)
96
+ image[75] = 255
97
+
98
+ assert _find_sem_data_rows(image) == image.shape[0]
File without changes
File without changes