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.
- {sem2surface-0.2.1/src/sem2surface.egg-info → sem2surface-0.2.2}/PKG-INFO +19 -4
- {sem2surface-0.2.1 → sem2surface-0.2.2}/README.md +18 -3
- {sem2surface-0.2.1 → sem2surface-0.2.2}/pyproject.toml +1 -1
- {sem2surface-0.2.1 → sem2surface-0.2.2/src/sem2surface.egg-info}/PKG-INFO +19 -4
- {sem2surface-0.2.1 → sem2surface-0.2.2}/src/sem2surface.py +54 -11
- {sem2surface-0.2.1 → sem2surface-0.2.2}/src/sem2surface_cli.py +1 -6
- {sem2surface-0.2.1 → sem2surface-0.2.2}/tests/test_cli.py +1 -1
- {sem2surface-0.2.1 → sem2surface-0.2.2}/tests/test_core.py +38 -0
- {sem2surface-0.2.1 → sem2surface-0.2.2}/LICENSE +0 -0
- {sem2surface-0.2.1 → sem2surface-0.2.2}/setup.cfg +0 -0
- {sem2surface-0.2.1 → sem2surface-0.2.2}/src/sem2surface.egg-info/SOURCES.txt +0 -0
- {sem2surface-0.2.1 → sem2surface-0.2.2}/src/sem2surface.egg-info/dependency_links.txt +0 -0
- {sem2surface-0.2.1 → sem2surface-0.2.2}/src/sem2surface.egg-info/entry_points.txt +0 -0
- {sem2surface-0.2.1 → sem2surface-0.2.2}/src/sem2surface.egg-info/requires.txt +0 -0
- {sem2surface-0.2.1 → sem2surface-0.2.2}/src/sem2surface.egg-info/top_level.txt +0 -0
- {sem2surface-0.2.1 → sem2surface-0.2.2}/src/sem2surface_gui.py +0 -0
- {sem2surface-0.2.1 → sem2surface-0.2.2}/tests/test_fft.py +0 -0
- {sem2surface-0.2.1 → sem2surface-0.2.2}/tests/test_gui_preview.py +0 -0
- {sem2surface-0.2.1 → sem2surface-0.2.2}/tests/test_reference_example.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sem2surface
|
|
3
|
-
Version: 0.2.
|
|
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
|

|
|
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.
|
|
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.
|
|
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.
|
|
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
|

|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sem2surface
|
|
3
|
-
Version: 0.2.
|
|
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
|

|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
-
#
|
|
541
|
-
#
|
|
542
|
-
crop_rows
|
|
543
|
-
for image in images:
|
|
544
|
-
crop
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
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.
|
|
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
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|