Dedup format layer; public accessors replace private reach-throughs
- SrasFile: _parse_v6 now retains per-angle x_delta and the verbatim preamble/background byte spans, and gains public data_offset, y_pos_per_angle, and iter_angle_blocks() (which now owns the ragged block-offset walk used three separate places before). - sras_edit_scans: the 70-line re-parse of the v6 header sections (_read_v6_sections/_reread_span/_v6_angle_offsets) collapses into a _write_v6 that consumes SrasFile directly — verified byte-identical round-trip on v6 int8/int16 and legacy files. Eight print-and-exit pairs become _die(). - tools/make_test_sras imports the struct layouts from sras_format and the rotation matrix from sras_compute instead of re-declaring them (byte assembly stays independent of the reader). - sras_compute: block_mean_2d, pixel_pitch_mm, nominal_delta_deg made public (they were GUI-facing); registration_workers() and default_max_workers() wrap the remaining private reach-throughs from sras_workers. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
+1
-1
@@ -47,7 +47,7 @@ def read_header_sections(sras: SrasFile) -> bytes:
|
|||||||
lost in a re-encode."""
|
lost in a re-encode."""
|
||||||
with open(sras.path, "rb") as f:
|
with open(sras.path, "rb") as f:
|
||||||
f.seek(HDR_SIZE)
|
f.seek(HDR_SIZE)
|
||||||
return f.read(sras._data_offset - HDR_SIZE)
|
return f.read(sras.data_offset - HDR_SIZE)
|
||||||
|
|
||||||
|
|
||||||
def average_rows(block: np.ndarray, n: int, discard_remainder: bool) -> np.ndarray:
|
def average_rows(block: np.ndarray, n: int, discard_remainder: bool) -> np.ndarray:
|
||||||
|
|||||||
+20
-10
@@ -768,7 +768,7 @@ def _skimage_phase_cross_correlation():
|
|||||||
|
|
||||||
# ---- Geometry: local mm, ref mm, and the one affine builder ---------------
|
# ---- Geometry: local mm, ref mm, and the one affine builder ---------------
|
||||||
|
|
||||||
def _pixel_pitch_mm(sras: SrasFile, angle_idx: int) -> tuple[float, float]:
|
def pixel_pitch_mm(sras: SrasFile, angle_idx: int) -> tuple[float, float]:
|
||||||
"""(dx, dy) mm/pixel for one angle: dx is the file-wide constant
|
"""(dx, dy) mm/pixel for one angle: dx is the file-wide constant
|
||||||
pixel_x_mm; dy is this angle's own row spacing (assumed uniform, the same
|
pixel_x_mm; dy is this angle's own row spacing (assumed uniform, the same
|
||||||
assumption _redraw_image makes when it builds the display extent). dy keeps
|
assumption _redraw_image makes when it builds the display extent). dy keeps
|
||||||
@@ -803,7 +803,7 @@ def _local_half_extent_mm(sras: SrasFile, angle_idx: int) -> tuple[float, float]
|
|||||||
"""(half width, half height) in mm from this angle's array center to the
|
"""(half width, half height) in mm from this angle's array center to the
|
||||||
center of its outermost pixel."""
|
center of its outermost pixel."""
|
||||||
n_rows, n_frames = sras.image_shape(angle_idx)
|
n_rows, n_frames = sras.image_shape(angle_idx)
|
||||||
dx, dy = _pixel_pitch_mm(sras, angle_idx)
|
dx, dy = pixel_pitch_mm(sras, angle_idx)
|
||||||
return (n_frames - 1) / 2.0 * abs(dx), (n_rows - 1) / 2.0 * abs(dy)
|
return (n_frames - 1) / 2.0 * abs(dx), (n_rows - 1) / 2.0 * abs(dy)
|
||||||
|
|
||||||
|
|
||||||
@@ -813,7 +813,7 @@ def _rotation_matrix(theta_deg: float) -> np.ndarray:
|
|||||||
return np.array([[c, -s], [s, c]]) # CCW rotation acting on (x, y)
|
return np.array([[c, -s], [s, c]]) # CCW rotation acting on (x, y)
|
||||||
|
|
||||||
|
|
||||||
def _nominal_delta_deg(sras: SrasFile, angle_idx: int, ref_idx: int) -> float:
|
def nominal_delta_deg(sras: SrasFile, angle_idx: int, ref_idx: int) -> float:
|
||||||
"""The rotation-stage's own reported angle change between two angles.
|
"""The rotation-stage's own reported angle change between two angles.
|
||||||
|
|
||||||
Used only to *seed* the rotation search, never as the answer: the stage's
|
Used only to *seed* the rotation search, never as the answer: the stage's
|
||||||
@@ -888,7 +888,7 @@ def apply_alignment(result: AlignmentResult, angle_idx: int, img: np.ndarray,
|
|||||||
mode="constant", cval=0.0)
|
mode="constant", cval=0.0)
|
||||||
|
|
||||||
|
|
||||||
def _block_mean_2d(img: np.ndarray, fy: int, fx: int) -> np.ndarray:
|
def block_mean_2d(img: np.ndarray, fy: int, fx: int) -> np.ndarray:
|
||||||
"""Block-mean by independent row/column factors. Independent factors matter
|
"""Block-mean by independent row/column factors. Independent factors matter
|
||||||
because the raw grid is strongly anisotropic (5 µm along x, 50 µm along y):
|
because the raw grid is strongly anisotropic (5 µm along x, 50 µm along y):
|
||||||
a single square factor would either alias along x or throw away rows."""
|
a single square factor would either alias along x or throw away rows."""
|
||||||
@@ -944,10 +944,10 @@ def _prepare_reg_image(sras: SrasFile, angle_idx: int, img: np.ndarray,
|
|||||||
resampled onto the registration grid. Pre-averaging matters: the raw grid
|
resampled onto the registration grid. Pre-averaging matters: the raw grid
|
||||||
is 10x finer along x than along y, so sampling it directly at the (much
|
is 10x finer along x than along y, so sampling it directly at the (much
|
||||||
coarser) isotropic registration pitch would alias badly along x."""
|
coarser) isotropic registration pitch would alias badly along x."""
|
||||||
dx, dy = _pixel_pitch_mm(sras, angle_idx)
|
dx, dy = pixel_pitch_mm(sras, angle_idx)
|
||||||
fx = max(1, int(pitch_mm / abs(dx)))
|
fx = max(1, int(pitch_mm / abs(dx)))
|
||||||
fy = max(1, int(pitch_mm / abs(dy)))
|
fy = max(1, int(pitch_mm / abs(dy)))
|
||||||
small = _block_mean_2d(np.asarray(img, dtype=np.float32), fy, fx)
|
small = block_mean_2d(np.asarray(img, dtype=np.float32), fy, fx)
|
||||||
n_rows, n_frames = img.shape
|
n_rows, n_frames = img.shape
|
||||||
return _RegImage(
|
return _RegImage(
|
||||||
small, dx * fx, dy * fy,
|
small, dx * fx, dy * fy,
|
||||||
@@ -1087,7 +1087,7 @@ def _reg_pitch_and_size(sras: SrasFile, max_dim: int,
|
|||||||
span = diag * margin
|
span = diag * margin
|
||||||
native = float(np.sqrt(
|
native = float(np.sqrt(
|
||||||
abs(sras.pixel_x_mm)
|
abs(sras.pixel_x_mm)
|
||||||
* max(abs(_pixel_pitch_mm(sras, a)[1]) for a in range(sras.n_angles))))
|
* max(abs(pixel_pitch_mm(sras, a)[1]) for a in range(sras.n_angles))))
|
||||||
pitch = max(span / max_dim, native)
|
pitch = max(span / max_dim, native)
|
||||||
n = int(scipy_fft.next_fast_len(max(16, int(np.ceil(span / pitch)))))
|
n = int(scipy_fft.next_fast_len(max(16, int(np.ceil(span / pitch)))))
|
||||||
return pitch, n
|
return pitch, n
|
||||||
@@ -1111,6 +1111,16 @@ def _registration_workers(sras: SrasFile, fine_dim: int) -> int:
|
|||||||
_TOTAL_BYTES_BUDGET // max(1, per_worker))))
|
_TOTAL_BYTES_BUDGET // max(1, per_worker))))
|
||||||
|
|
||||||
|
|
||||||
|
def registration_workers(sras: SrasFile) -> int:
|
||||||
|
"""Public: concurrent-registration cap at the default fine grid."""
|
||||||
|
return _registration_workers(sras, _DEFAULT_FINE_DIM)
|
||||||
|
|
||||||
|
|
||||||
|
def default_max_workers() -> int:
|
||||||
|
"""Public: the module-wide worker cap (SRAS_MAX_WORKERS or cpu count)."""
|
||||||
|
return _MAX_WORKERS
|
||||||
|
|
||||||
|
|
||||||
def _rotation_candidates(nominal_deg: float, search_deg: float,
|
def _rotation_candidates(nominal_deg: float, search_deg: float,
|
||||||
step_deg: float) -> list[float]:
|
step_deg: float) -> list[float]:
|
||||||
"""Coarse rotation candidates: a window around *both* signs of the stage's
|
"""Coarse rotation candidates: a window around *both* signs of the stage's
|
||||||
@@ -1254,7 +1264,7 @@ def register_angle_to_reference(
|
|||||||
if not candidates:
|
if not candidates:
|
||||||
return RigidFit(0.0, (0.0, 0.0), -1.0, "none")
|
return RigidFit(0.0, (0.0, 0.0), -1.0, "none")
|
||||||
|
|
||||||
nominal = _nominal_delta_deg(sras, angle_idx, ref_angle_idx)
|
nominal = nominal_delta_deg(sras, angle_idx, ref_angle_idx)
|
||||||
thetas = _rotation_candidates(nominal, search_deg, coarse_step_deg)
|
thetas = _rotation_candidates(nominal, search_deg, coarse_step_deg)
|
||||||
|
|
||||||
# ---- Stage 1: coarse sweep, every source ------------------------------
|
# ---- Stage 1: coarse sweep, every source ------------------------------
|
||||||
@@ -1363,7 +1373,7 @@ def build_canvas_affine(sras: SrasFile, angle_idx: int, ref_angle_idx: int,
|
|||||||
(rows, cols) block-mean factor already applied to the image the caller will
|
(rows, cols) block-mean factor already applied to the image the caller will
|
||||||
resample — 1:1 for the raw image, coarser for the manual-alignment
|
resample — 1:1 for the raw image, coarser for the manual-alignment
|
||||||
preview's downsampled masks."""
|
preview's downsampled masks."""
|
||||||
dx_a, dy_a = _pixel_pitch_mm(sras, angle_idx)
|
dx_a, dy_a = pixel_pitch_mm(sras, angle_idx)
|
||||||
n_rows, n_frames = sras.image_shape(angle_idx)
|
n_rows, n_frames = sras.image_shape(angle_idx)
|
||||||
fy, fx = (max(1, int(v)) for v in src_downsample)
|
fy, fx = (max(1, int(v)) for v in src_downsample)
|
||||||
if (fy, fx) != (1, 1):
|
if (fy, fx) != (1, 1):
|
||||||
@@ -1398,7 +1408,7 @@ def _result_from_params(sras: SrasFile, ref_angle_idx: int,
|
|||||||
the shared canvas, then build each angle's canvas->raw affine. Pure matrix
|
the shared canvas, then build each angle's canvas->raw affine. Pure matrix
|
||||||
and bbox math, so it is cheap enough to call synchronously on the GUI
|
and bbox math, so it is cheap enough to call synchronously on the GUI
|
||||||
thread on every manual edit."""
|
thread on every manual edit."""
|
||||||
pitch = _pixel_pitch_mm(sras, ref_angle_idx)
|
pitch = pixel_pitch_mm(sras, ref_angle_idx)
|
||||||
canvas_origin_mm, canvas_shape = canvas_for_params(
|
canvas_origin_mm, canvas_shape = canvas_for_params(
|
||||||
sras, ref_angle_idx, pitch, params, snap=True)
|
sras, ref_angle_idx, pitch, params, snap=True)
|
||||||
|
|
||||||
|
|||||||
+36
-112
@@ -25,15 +25,17 @@ import struct
|
|||||||
import sys
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from sras_format import (
|
from sras_format import GEO_FMT_V6, HDR_FMT, HDR_FMT_V6, HDR_SIZE, SrasFile
|
||||||
GEO_FMT_V6, GEO_SIZE_V6, HDR_FMT, HDR_FMT_V6, HDR_SIZE, HDR_SIZE_V6,
|
|
||||||
SrasFile,
|
|
||||||
)
|
|
||||||
|
|
||||||
_LEGACY_VERSIONS = (2, 3, 4, 5)
|
_LEGACY_VERSIONS = (2, 3, 4, 5)
|
||||||
_V6_VERSIONS = (6, 7)
|
_V6_VERSIONS = (6, 7)
|
||||||
|
|
||||||
|
|
||||||
|
def _die(msg: str):
|
||||||
|
print(f"Error: {msg}", file=sys.stderr)
|
||||||
|
sys.exit(1)
|
||||||
|
|
||||||
|
|
||||||
def parse_args():
|
def parse_args():
|
||||||
p = argparse.ArgumentParser(
|
p = argparse.ArgumentParser(
|
||||||
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
description=__doc__, formatter_class=argparse.RawDescriptionHelpFormatter)
|
||||||
@@ -106,7 +108,7 @@ def _write_legacy(sras: SrasFile, keep: list[int], out_path: Path):
|
|||||||
angle_table_size = sras.n_angles * 4
|
angle_table_size = sras.n_angles * 4
|
||||||
with open(sras.path, "rb") as f:
|
with open(sras.path, "rb") as f:
|
||||||
f.seek(HDR_SIZE + angle_table_size)
|
f.seek(HDR_SIZE + angle_table_size)
|
||||||
shared_mid = f.read(sras._data_offset - (HDR_SIZE + angle_table_size))
|
shared_mid = f.read(sras.data_offset - (HDR_SIZE + angle_table_size))
|
||||||
|
|
||||||
angle_bytes = n_rows * n_ch * n_frames * spf * bps
|
angle_bytes = n_rows * n_ch * n_frames * spf * bps
|
||||||
|
|
||||||
@@ -115,128 +117,55 @@ def _write_legacy(sras: SrasFile, keep: list[int], out_path: Path):
|
|||||||
fout.write(sras.angles_deg[keep].astype(">f4").tobytes())
|
fout.write(sras.angles_deg[keep].astype(">f4").tobytes())
|
||||||
fout.write(shared_mid)
|
fout.write(shared_mid)
|
||||||
for a in keep:
|
for a in keep:
|
||||||
_copy_range(fin, fout, sras._data_offset + a * angle_bytes, angle_bytes)
|
_copy_range(fin, fout, sras.data_offset + a * angle_bytes, angle_bytes)
|
||||||
|
|
||||||
|
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
# v6/v7: per-angle geometry, ragged waveform blocks
|
# v6/v7: per-angle geometry, ragged waveform blocks
|
||||||
# ---------------------------------------------------------------------------
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
def _read_v6_sections(path: Path) -> dict:
|
def _write_v6(sras: SrasFile, keep: list[int], out_path: Path):
|
||||||
"""Raw, low-level read of everything before the waveform data.
|
|
||||||
|
|
||||||
SrasFile._parse_v6 reads and discards each angle's x_delta (it's not
|
|
||||||
part of the display-facing geometry it exposes), so a round-trip through
|
|
||||||
SrasFile would silently drop that field. Re-parsing here keeps every
|
|
||||||
byte of the Per-Angle Geometry Table intact.
|
|
||||||
"""
|
|
||||||
with open(path, "rb") as f:
|
|
||||||
hdr_raw = f.read(HDR_SIZE_V6)
|
|
||||||
(magic, ver, n_angles_declared, x_start_nom, y_start_nom, x_delta_nom,
|
|
||||||
y_delta_nom, row_spacing, vel, freq, spf, sr, bps,
|
|
||||||
n_ch) = struct.unpack(HDR_FMT_V6, hdr_raw)
|
|
||||||
|
|
||||||
angles_deg = list(struct.unpack(f">{n_angles_declared}f",
|
|
||||||
f.read(n_angles_declared * 4)))
|
|
||||||
|
|
||||||
geo = [struct.unpack(GEO_FMT_V6, f.read(GEO_SIZE_V6))
|
|
||||||
for _ in range(n_angles_declared)] # (x_start, x_delta, n_frames, n_rows)
|
|
||||||
|
|
||||||
row_table = [f.read(geo[a][3] * 4) for a in range(n_angles_declared)]
|
|
||||||
|
|
||||||
preamble_start = f.tell()
|
|
||||||
for _ in range(n_ch):
|
|
||||||
(length,) = struct.unpack(">H", f.read(2))
|
|
||||||
f.read(length)
|
|
||||||
preambles_raw = _reread_span(f, preamble_start)
|
|
||||||
|
|
||||||
bg_start = f.tell()
|
|
||||||
(n_bg,) = struct.unpack(">I", f.read(4))
|
|
||||||
f.read(n_bg)
|
|
||||||
background_raw = _reread_span(f, bg_start)
|
|
||||||
|
|
||||||
data_offset = f.tell()
|
|
||||||
|
|
||||||
return {
|
|
||||||
"version": ver, "n_angles_declared": n_angles_declared,
|
|
||||||
"x_start_nom": x_start_nom, "y_start_nom": y_start_nom,
|
|
||||||
"x_delta_nom": x_delta_nom, "y_delta_nom": y_delta_nom,
|
|
||||||
"row_spacing": row_spacing, "vel": vel, "freq": freq,
|
|
||||||
"spf": spf, "sr": sr, "bps": bps, "n_ch": n_ch,
|
|
||||||
"angles_deg": angles_deg, "geo": geo, "row_table": row_table,
|
|
||||||
"preambles_raw": preambles_raw, "background_raw": background_raw,
|
|
||||||
"data_offset": data_offset,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
def _reread_span(f, start: int) -> bytes:
|
|
||||||
end = f.tell()
|
|
||||||
f.seek(start)
|
|
||||||
span = f.read(end - start)
|
|
||||||
f.seek(end)
|
|
||||||
return span
|
|
||||||
|
|
||||||
|
|
||||||
def _v6_angle_offsets(sections: dict, file_size: int) -> list[tuple[int, int]]:
|
|
||||||
"""(offset, nbytes) of each declared angle's waveform block, stopping at
|
|
||||||
the first angle whose data isn't fully on disk (aborted scan)."""
|
|
||||||
n_ch, spf, bps = sections["n_ch"], sections["spf"], sections["bps"]
|
|
||||||
offsets = []
|
|
||||||
offset = sections["data_offset"]
|
|
||||||
for xs, xd, nf, nr in sections["geo"]:
|
|
||||||
nbytes = nr * n_ch * nf * spf * bps
|
|
||||||
if offset + nbytes > file_size:
|
|
||||||
break
|
|
||||||
offsets.append((offset, nbytes))
|
|
||||||
offset += nbytes
|
|
||||||
return offsets
|
|
||||||
|
|
||||||
|
|
||||||
def _write_v6(in_path: Path, sections: dict, keep: list[int], out_path: Path):
|
|
||||||
file_size = in_path.stat().st_size
|
|
||||||
offsets = _v6_angle_offsets(sections, file_size)
|
|
||||||
geo = sections["geo"]
|
|
||||||
|
|
||||||
header = struct.pack(
|
header = struct.pack(
|
||||||
HDR_FMT_V6, b"SRAS", sections["version"], len(keep),
|
HDR_FMT_V6, b"SRAS", sras.version, len(keep),
|
||||||
sections["x_start_nom"], sections["y_start_nom"],
|
sras.x_start_nominal_mm, sras.y_start_nominal_mm,
|
||||||
sections["x_delta_nom"], sections["y_delta_nom"],
|
sras.x_delta_nominal_mm, sras.y_delta_nominal_mm,
|
||||||
sections["row_spacing"], sections["vel"], sections["freq"],
|
sras.row_spacing_mm, sras.velocity_mm_s, sras.laser_freq_hz,
|
||||||
sections["spf"], sections["sr"], sections["bps"], sections["n_ch"],
|
sras.samples_per_frame, sras.sample_rate_hz,
|
||||||
|
sras.bytes_per_sample, sras.n_channels,
|
||||||
)
|
)
|
||||||
|
|
||||||
with open(in_path, "rb") as fin, open(out_path, "wb") as fout:
|
blocks = {a: (offset, nbytes) for a, offset, nbytes in sras.iter_angle_blocks()}
|
||||||
|
with open(sras.path, "rb") as fin, open(out_path, "wb") as fout:
|
||||||
fout.write(header)
|
fout.write(header)
|
||||||
fout.write(struct.pack(f">{len(keep)}f",
|
fout.write(sras.angles_deg[keep].astype(">f4").tobytes())
|
||||||
*[sections["angles_deg"][i] for i in keep]))
|
|
||||||
for i in keep:
|
for i in keep:
|
||||||
fout.write(struct.pack(GEO_FMT_V6, *geo[i]))
|
fout.write(struct.pack(
|
||||||
|
GEO_FMT_V6, float(sras.x_start_mm[i]),
|
||||||
|
float(sras.x_delta_mm_per_angle[i]),
|
||||||
|
int(sras.n_frames[i]), int(sras.n_rows[i])))
|
||||||
for i in keep:
|
for i in keep:
|
||||||
fout.write(sections["row_table"][i])
|
fout.write(sras.y_pos_per_angle[i].astype(">f4").tobytes())
|
||||||
fout.write(sections["preambles_raw"])
|
fout.write(sras.preambles_raw)
|
||||||
fout.write(sections["background_raw"])
|
fout.write(sras.background_raw)
|
||||||
for i in keep:
|
for i in keep:
|
||||||
off, nbytes = offsets[i]
|
offset, nbytes = blocks[i]
|
||||||
_copy_range(fin, fout, off, nbytes)
|
_copy_range(fin, fout, offset, nbytes)
|
||||||
|
|
||||||
|
|
||||||
def main():
|
def main():
|
||||||
args = parse_args()
|
args = parse_args()
|
||||||
in_path = Path(args.input)
|
in_path = Path(args.input)
|
||||||
if not in_path.exists():
|
if not in_path.exists():
|
||||||
print(f"Error: input file not found: {in_path}", file=sys.stderr)
|
_die(f"input file not found: {in_path}")
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
print(f"Reading {in_path} ...", flush=True)
|
print(f"Reading {in_path} ...", flush=True)
|
||||||
try:
|
try:
|
||||||
sras = SrasFile(str(in_path))
|
sras = SrasFile(str(in_path))
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
print(f"Error: {e}", file=sys.stderr)
|
_die(str(e))
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
if sras.version not in (*_LEGACY_VERSIONS, *_V6_VERSIONS):
|
if sras.version not in (*_LEGACY_VERSIONS, *_V6_VERSIONS):
|
||||||
print(f"Error: unsupported .sras version: {sras.version}", file=sys.stderr)
|
_die(f"unsupported .sras version: {sras.version}")
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
aborted_note = " (scan aborted; trailing angle(s) already excluded)" if sras.scan_aborted else ""
|
aborted_note = " (scan aborted; trailing angle(s) already excluded)" if sras.scan_aborted else ""
|
||||||
print(f" Version : v{sras.version}", flush=True)
|
print(f" Version : v{sras.version}", flush=True)
|
||||||
@@ -247,16 +176,13 @@ def main():
|
|||||||
return
|
return
|
||||||
|
|
||||||
if not args.output:
|
if not args.output:
|
||||||
print("Error: output path required unless --list is given.", file=sys.stderr)
|
_die("output path required unless --list is given.")
|
||||||
sys.exit(1)
|
|
||||||
if not (args.drop or args.keep):
|
if not (args.drop or args.keep):
|
||||||
print("Error: specify --drop or --keep (see --list for indices).", file=sys.stderr)
|
_die("specify --drop or --keep (see --list for indices).")
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
out_path = Path(args.output)
|
out_path = Path(args.output)
|
||||||
if out_path.resolve() == in_path.resolve():
|
if out_path.resolve() == in_path.resolve():
|
||||||
print("Error: output path must differ from input path.", file=sys.stderr)
|
_die("output path must differ from input path.")
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if args.drop:
|
if args.drop:
|
||||||
@@ -265,12 +191,10 @@ def main():
|
|||||||
else:
|
else:
|
||||||
keep = sorted(_parse_index_list(args.keep, sras.n_angles))
|
keep = sorted(_parse_index_list(args.keep, sras.n_angles))
|
||||||
except ValueError as e:
|
except ValueError as e:
|
||||||
print(f"Error: {e}", file=sys.stderr)
|
_die(str(e))
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
if not keep:
|
if not keep:
|
||||||
print("Error: at least one angle must remain.", file=sys.stderr)
|
_die("at least one angle must remain.")
|
||||||
sys.exit(1)
|
|
||||||
|
|
||||||
dropped = [a for a in range(sras.n_angles) if a not in keep]
|
dropped = [a for a in range(sras.n_angles) if a not in keep]
|
||||||
print(f"\nDropping angle(s): {dropped}")
|
print(f"\nDropping angle(s): {dropped}")
|
||||||
@@ -280,7 +204,7 @@ def main():
|
|||||||
if sras.version in _LEGACY_VERSIONS:
|
if sras.version in _LEGACY_VERSIONS:
|
||||||
_write_legacy(sras, keep, out_path)
|
_write_legacy(sras, keep, out_path)
|
||||||
else:
|
else:
|
||||||
_write_v6(in_path, _read_v6_sections(in_path), keep, out_path)
|
_write_v6(sras, keep, out_path)
|
||||||
|
|
||||||
in_mb = in_path.stat().st_size / 1024**2
|
in_mb = in_path.stat().st_size / 1024**2
|
||||||
out_mb = out_path.stat().st_size / 1024**2
|
out_mb = out_path.stat().st_size / 1024**2
|
||||||
|
|||||||
+52
-9
@@ -375,21 +375,34 @@ class SrasFile:
|
|||||||
angles = np.frombuffer(f.read(n_angles * 4), dtype=">f4").astype(np.float32)
|
angles = np.frombuffer(f.read(n_angles * 4), dtype=">f4").astype(np.float32)
|
||||||
|
|
||||||
x_start = np.empty(n_angles, dtype=np.float64)
|
x_start = np.empty(n_angles, dtype=np.float64)
|
||||||
|
x_delta = np.empty(n_angles, dtype=np.float64)
|
||||||
n_frames = np.empty(n_angles, dtype=np.int64)
|
n_frames = np.empty(n_angles, dtype=np.int64)
|
||||||
n_rows = np.empty(n_angles, dtype=np.int64)
|
n_rows = np.empty(n_angles, dtype=np.int64)
|
||||||
for a in range(n_angles):
|
for a in range(n_angles):
|
||||||
xs, xd, nf, nr = _read_struct(f, GEO_FMT_V6)
|
xs, xd, nf, nr = _read_struct(f, GEO_FMT_V6)
|
||||||
x_start[a], n_frames[a], n_rows[a] = xs, nf, nr
|
x_start[a], x_delta[a], n_frames[a], n_rows[a] = xs, xd, nf, nr
|
||||||
|
|
||||||
y_pos_per_angle = [
|
y_pos_per_angle = [
|
||||||
np.frombuffer(f.read(int(n_rows[a]) * 4), dtype=">f4").astype(np.float32)
|
np.frombuffer(f.read(int(n_rows[a]) * 4), dtype=">f4").astype(np.float32)
|
||||||
for a in range(n_angles)
|
for a in range(n_angles)
|
||||||
]
|
]
|
||||||
|
|
||||||
|
# Verbatim on-disk spans of the preamble and background sections,
|
||||||
|
# kept so file-rewriting tools (sras_edit_scans) can carry them
|
||||||
|
# over byte-for-byte without re-parsing.
|
||||||
|
span_start = f.tell()
|
||||||
self._set_calibration(_read_preambles(f, n_ch), n_ch)
|
self._set_calibration(_read_preambles(f, n_ch), n_ch)
|
||||||
self.background = _read_background(f)
|
span_end = f.tell()
|
||||||
|
f.seek(span_start)
|
||||||
|
self.preambles_raw = f.read(span_end - span_start)
|
||||||
|
|
||||||
data_offset = f.tell()
|
span_start = span_end
|
||||||
|
self.background = _read_background(f)
|
||||||
|
span_end = f.tell()
|
||||||
|
f.seek(span_start)
|
||||||
|
self.background_raw = f.read(span_end - span_start)
|
||||||
|
|
||||||
|
data_offset = span_end
|
||||||
|
|
||||||
self._data_offset = data_offset
|
self._data_offset = data_offset
|
||||||
|
|
||||||
@@ -428,6 +441,7 @@ class SrasFile:
|
|||||||
self.scan_aborted = n_complete < n_angles_declared
|
self.scan_aborted = n_complete < n_angles_declared
|
||||||
self.angles_deg = angles[:n_complete]
|
self.angles_deg = angles[:n_complete]
|
||||||
self.x_start_mm = x_start[:n_complete]
|
self.x_start_mm = x_start[:n_complete]
|
||||||
|
self.x_delta_mm_per_angle = x_delta[:n_complete]
|
||||||
self.n_frames = n_frames[:n_complete]
|
self.n_frames = n_frames[:n_complete]
|
||||||
self.n_rows = n_rows[:n_complete]
|
self.n_rows = n_rows[:n_complete]
|
||||||
self._y_pos_per_angle = y_pos_per_angle[:n_complete]
|
self._y_pos_per_angle = y_pos_per_angle[:n_complete]
|
||||||
@@ -436,6 +450,37 @@ class SrasFile:
|
|||||||
if self.version == 7 and offset < file_size:
|
if self.version == 7 and offset < file_size:
|
||||||
self._parse_cach_section(offset)
|
self._parse_cach_section(offset)
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
# Byte-layout accessors (public: used by file-rewriting tools)
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
@property
|
||||||
|
def data_offset(self) -> int:
|
||||||
|
"""File offset where the waveform data begins (headers end)."""
|
||||||
|
return self._data_offset
|
||||||
|
|
||||||
|
@property
|
||||||
|
def y_pos_per_angle(self) -> list[np.ndarray]:
|
||||||
|
"""Per-angle Y row positions (mm). The list and its arrays are the
|
||||||
|
live parsed state — tools that reproject may replace entries."""
|
||||||
|
return self._y_pos_per_angle
|
||||||
|
|
||||||
|
@y_pos_per_angle.setter
|
||||||
|
def y_pos_per_angle(self, value: list[np.ndarray]):
|
||||||
|
self._y_pos_per_angle = value
|
||||||
|
|
||||||
|
def iter_angle_blocks(self):
|
||||||
|
"""Yields (angle_idx, byte_offset, byte_count) for each complete
|
||||||
|
angle's waveform block. Works for every version: legacy files have
|
||||||
|
uniform per-angle geometry, so the same walk applies."""
|
||||||
|
offset = self._data_offset
|
||||||
|
for a in range(self.n_angles):
|
||||||
|
nbytes = (int(self.n_rows[a]) * self.n_channels
|
||||||
|
* int(self.n_frames[a]) * self.samples_per_frame
|
||||||
|
* self.bytes_per_sample)
|
||||||
|
yield a, offset, nbytes
|
||||||
|
offset += nbytes
|
||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
# v7 cache tail (CACH section: precomputed DC / FFT images)
|
# v7 cache tail (CACH section: precomputed DC / FFT images)
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
@@ -445,12 +490,10 @@ class SrasFile:
|
|||||||
start), derived purely from the header + Per-Angle Geometry Table —
|
start), derived purely from the header + Per-Angle Geometry Table —
|
||||||
independent of whether a cache tail is actually present. Used by
|
independent of whether a cache tail is actually present. Used by
|
||||||
both the parser and the in-place writer."""
|
both the parser and the in-place writer."""
|
||||||
waveform_bytes = sum(
|
end = self._data_offset
|
||||||
int(self.n_rows[a]) * self.n_channels * int(self.n_frames[a])
|
for _, offset, nbytes in self.iter_angle_blocks():
|
||||||
* self.samples_per_frame * self.bytes_per_sample
|
end = offset + nbytes
|
||||||
for a in range(self.n_angles)
|
return end
|
||||||
)
|
|
||||||
return self._data_offset + int(waveform_bytes)
|
|
||||||
|
|
||||||
def _read_cache_block(self, f, hdr_fmt: str, magic: bytes,
|
def _read_cache_block(self, f, hdr_fmt: str, magic: bytes,
|
||||||
stores: list[list]) -> int | None:
|
stores: list[list]) -> int | None:
|
||||||
|
|||||||
+4
-4
@@ -1127,7 +1127,7 @@ class ManualAlignmentDialog(QDialog):
|
|||||||
threshold = self.spin_mask_threshold_mv.value()
|
threshold = self.spin_mask_threshold_mv.value()
|
||||||
fy, fx = self._downsample
|
fy, fx = self._downsample
|
||||||
self._masks_small = {
|
self._masks_small = {
|
||||||
a: compute._block_mean_2d((img >= threshold).astype(np.float32), fy, fx)
|
a: compute.block_mean_2d((img >= threshold).astype(np.float32), fy, fx)
|
||||||
for a, img in self._dc4_mv.items()
|
for a, img in self._dc4_mv.items()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1143,7 +1143,7 @@ class ManualAlignmentDialog(QDialog):
|
|||||||
mask-threshold change, Auto De-rotate, a rotation nudge/edit of the
|
mask-threshold change, Auto De-rotate, a rotation nudge/edit of the
|
||||||
active angle. NOT triggered by a translation-only nudge — see
|
active angle. NOT triggered by a translation-only nudge — see
|
||||||
_refresh_active_preview_layer."""
|
_refresh_active_preview_layer."""
|
||||||
dx_ref, dy_ref = compute._pixel_pitch_mm(self._sras, self._ref_angle_idx)
|
dx_ref, dy_ref = compute.pixel_pitch_mm(self._sras, self._ref_angle_idx)
|
||||||
fy, fx = self._downsample
|
fy, fx = self._downsample
|
||||||
pitch = (dx_ref * fx, dy_ref * fy)
|
pitch = (dx_ref * fx, dy_ref * fy)
|
||||||
origin, shape = compute.canvas_for_params(
|
origin, shape = compute.canvas_for_params(
|
||||||
@@ -1288,7 +1288,7 @@ class ManualAlignmentDialog(QDialog):
|
|||||||
for a in range(self._sras.n_angles):
|
for a in range(self._sras.n_angles):
|
||||||
if a == self._ref_angle_idx:
|
if a == self._ref_angle_idx:
|
||||||
continue
|
continue
|
||||||
self._angle_params[a].rotation_deg = sign * compute._nominal_delta_deg(
|
self._angle_params[a].rotation_deg = sign * compute.nominal_delta_deg(
|
||||||
self._sras, a, self._ref_angle_idx)
|
self._sras, a, self._ref_angle_idx)
|
||||||
n_changed += 1
|
n_changed += 1
|
||||||
self._sync_active_spinboxes()
|
self._sync_active_spinboxes()
|
||||||
@@ -1364,7 +1364,7 @@ class ManualAlignmentDialog(QDialog):
|
|||||||
f"{worst[1][1]})."]
|
f"{worst[1][1]})."]
|
||||||
drifted = []
|
drifted = []
|
||||||
for a, _note in rows:
|
for a, _note in rows:
|
||||||
nominal = compute._nominal_delta_deg(self._sras, a, self._ref_angle_idx)
|
nominal = compute.nominal_delta_deg(self._sras, a, self._ref_angle_idx)
|
||||||
got = self._angle_params[a].rotation_deg
|
got = self._angle_params[a].rotation_deg
|
||||||
dev = min(abs(got - nominal), abs(got + nominal))
|
dev = min(abs(got - nominal), abs(got + nominal))
|
||||||
if dev > 1.0:
|
if dev > 1.0:
|
||||||
|
|||||||
+3
-2
@@ -253,7 +253,8 @@ class BatchCacheWorker(QObject):
|
|||||||
for path in paths:
|
for path in paths:
|
||||||
try:
|
try:
|
||||||
err = cache_file(path, self._mode, self._apply_bg_sub,
|
err = cache_file(path, self._mode, self._apply_bg_sub,
|
||||||
compute.get_fft_backend(), compute._MAX_WORKERS)
|
compute.get_fft_backend(),
|
||||||
|
compute.default_max_workers())
|
||||||
except Exception as exc:
|
except Exception as exc:
|
||||||
err = str(exc)
|
err = str(exc)
|
||||||
done += 1
|
done += 1
|
||||||
@@ -382,7 +383,7 @@ class CrossCorrelateWorker(_PooledWorker):
|
|||||||
self._search_deg = search_deg
|
self._search_deg = search_deg
|
||||||
|
|
||||||
def _plan(self) -> int:
|
def _plan(self) -> int:
|
||||||
return compute._registration_workers(self._sras, compute._DEFAULT_FINE_DIM)
|
return compute.registration_workers(self._sras)
|
||||||
|
|
||||||
def _items(self):
|
def _items(self):
|
||||||
return self._angles
|
return self._angles
|
||||||
|
|||||||
@@ -40,7 +40,7 @@ def mm_transform(sras: SrasFile, result, angle_idx: int) -> np.ndarray:
|
|||||||
per-angle pixel pitches; undoing both must leave something orthonormal, or
|
per-angle pixel pitches; undoing both must leave something orthonormal, or
|
||||||
the transform is smuggling in a scale or a shear.
|
the transform is smuggling in a scale or a shear.
|
||||||
"""
|
"""
|
||||||
dx_a, dy_a = compute._pixel_pitch_mm(sras, angle_idx)
|
dx_a, dy_a = compute.pixel_pitch_mm(sras, angle_idx)
|
||||||
A_out = np.array([[0.0, result.canvas_dx_mm], [result.canvas_dy_mm, 0.0]])
|
A_out = np.array([[0.0, result.canvas_dx_mm], [result.canvas_dy_mm, 0.0]])
|
||||||
D = np.array([[0.0, 1.0 / dy_a], [1.0 / dx_a, 0.0]])
|
D = np.array([[0.0, 1.0 / dy_a], [1.0 / dx_a, 0.0]])
|
||||||
return np.linalg.inv(D) @ result.per_angle[angle_idx].matrix @ np.linalg.inv(A_out)
|
return np.linalg.inv(D) @ result.per_angle[angle_idx].matrix @ np.linalg.inv(A_out)
|
||||||
@@ -99,7 +99,7 @@ def test_stage_coordinates_are_not_consulted(rig):
|
|||||||
moved = SrasFile(str(rig.path))
|
moved = SrasFile(str(rig.path))
|
||||||
for a in range(1, moved.n_angles):
|
for a in range(1, moved.n_angles):
|
||||||
moved.x_start_mm[a] += 13.5 * a
|
moved.x_start_mm[a] += 13.5 * a
|
||||||
moved._y_pos_per_angle[a] = moved._y_pos_per_angle[a] - 9.25 * a
|
moved.y_pos_per_angle[a] = moved.y_pos_per_angle[a] - 9.25 * a
|
||||||
moved_dc4 = dc4_images(moved)
|
moved_dc4 = dc4_images(moved)
|
||||||
moved_fits = {a: compute.register_angle_to_reference(
|
moved_fits = {a: compute.register_angle_to_reference(
|
||||||
moved, a, 0, moved_dc4, dc_threshold_mv=_THRESHOLD_MV)
|
moved, a, 0, moved_dc4, dc_threshold_mv=_THRESHOLD_MV)
|
||||||
@@ -118,7 +118,7 @@ def test_canvas_is_reference_grid_extended(rig):
|
|||||||
assert np.allclose(t0.offset, np.round(t0.offset)), \
|
assert np.allclose(t0.offset, np.round(t0.offset)), \
|
||||||
f"angle 0 does not land on whole canvas pixels: {t0.offset}"
|
f"angle 0 does not land on whole canvas pixels: {t0.offset}"
|
||||||
assert ((result.canvas_dx_mm, result.canvas_dy_mm)
|
assert ((result.canvas_dx_mm, result.canvas_dy_mm)
|
||||||
== compute._pixel_pitch_mm(sras, 0)), \
|
== compute.pixel_pitch_mm(sras, 0)), \
|
||||||
"canvas pitch is angle 0's own pitch"
|
"canvas pitch is angle 0's own pitch"
|
||||||
|
|
||||||
n_rows, n_cols = result.canvas_shape
|
n_rows, n_cols = result.canvas_shape
|
||||||
@@ -169,7 +169,7 @@ def test_downsampled_preview_lands_with_full_res(rig):
|
|||||||
result.canvas_origin_mm, result.canvas_shape)
|
result.canvas_origin_mm, result.canvas_shape)
|
||||||
fy, fx = 4, 16
|
fy, fx = 4, 16
|
||||||
small = compute.reproject_mask(
|
small = compute.reproject_mask(
|
||||||
sras, a, 0, compute._block_mean_2d(full_mask, fy, fx),
|
sras, a, 0, compute.block_mean_2d(full_mask, fy, fx),
|
||||||
p.rotation_deg, p.shift_mm, (pitch[0] * fx, pitch[1] * fy),
|
p.rotation_deg, p.shift_mm, (pitch[0] * fx, pitch[1] * fy),
|
||||||
result.canvas_origin_mm,
|
result.canvas_origin_mm,
|
||||||
(result.canvas_shape[0] // fy, result.canvas_shape[1] // fx),
|
(result.canvas_shape[0] // fy, result.canvas_shape[1] // fx),
|
||||||
|
|||||||
+3
-3
@@ -261,7 +261,7 @@ def test_manual_alignment_geometry(ctx):
|
|||||||
assert np.allclose(compute._center_idx(s, 0),
|
assert np.allclose(compute._center_idx(s, 0),
|
||||||
[(n_rows - 1) / 2, (n_frames - 1) / 2]), \
|
[(n_rows - 1) / 2, (n_frames - 1) / 2]), \
|
||||||
"array center is the geometric center of the pixel grid"
|
"array center is the geometric center of the pixel grid"
|
||||||
dx0, dy0 = compute._pixel_pitch_mm(s, 0)
|
dx0, dy0 = compute.pixel_pitch_mm(s, 0)
|
||||||
assert np.allclose(compute._local_half_extent_mm(s, 0),
|
assert np.allclose(compute._local_half_extent_mm(s, 0),
|
||||||
[(n_frames - 1) / 2 * abs(dx0), (n_rows - 1) / 2 * abs(dy0)]), \
|
[(n_frames - 1) / 2 * abs(dx0), (n_rows - 1) / 2 * abs(dy0)]), \
|
||||||
"local half-extent is derived from shape and pitch alone"
|
"local half-extent is derived from shape and pitch alone"
|
||||||
@@ -270,7 +270,7 @@ def test_manual_alignment_geometry(ctx):
|
|||||||
moved = SrasFile(str(ctx.path))
|
moved = SrasFile(str(ctx.path))
|
||||||
for a in range(1, moved.n_angles):
|
for a in range(1, moved.n_angles):
|
||||||
moved.x_start_mm[a] += 7.5
|
moved.x_start_mm[a] += 7.5
|
||||||
moved._y_pos_per_angle[a] = moved._y_pos_per_angle[a] + 3.25
|
moved.y_pos_per_angle[a] = moved.y_pos_per_angle[a] + 3.25
|
||||||
origin_b, shape_b = compute.canvas_for_params(moved, 0, (dx0, dy0), identity)
|
origin_b, shape_b = compute.canvas_for_params(moved, 0, (dx0, dy0), identity)
|
||||||
assert shape_a == shape_b and np.allclose(origin_a, origin_b), \
|
assert shape_a == shape_b and np.allclose(origin_a, origin_b), \
|
||||||
("moving every non-reference angle's scan window must leave the canvas "
|
("moving every non-reference angle's scan window must leave the canvas "
|
||||||
@@ -356,7 +356,7 @@ def test_auto_derotate(ctx):
|
|||||||
dlg, s, active = ctx.dlg, ctx.s, ctx.active
|
dlg, s, active = ctx.dlg, ctx.s, ctx.active
|
||||||
shift_before_derotate = dlg._angle_params[active].shift_mm
|
shift_before_derotate = dlg._angle_params[active].shift_mm
|
||||||
dlg._on_auto_derotate()
|
dlg._on_auto_derotate()
|
||||||
nominal = compute._nominal_delta_deg(s, active, dlg._ref_angle_idx)
|
nominal = compute.nominal_delta_deg(s, active, dlg._ref_angle_idx)
|
||||||
assert abs(dlg._angle_params[active].rotation_deg - nominal) < 1e-6, \
|
assert abs(dlg._angle_params[active].rotation_deg - nominal) < 1e-6, \
|
||||||
"auto de-rotate seeded rotation from the stage's reported angle"
|
"auto de-rotate seeded rotation from the stage's reported angle"
|
||||||
assert dlg._angle_params[active].shift_mm == shift_before_derotate, \
|
assert dlg._angle_params[active].shift_mm == shift_before_derotate, \
|
||||||
|
|||||||
@@ -47,8 +47,8 @@ def row_slice(sras: SrasFile, angle_idx: int, n_rows: int) -> SrasFile:
|
|||||||
view.n_rows[angle_idx] = n
|
view.n_rows[angle_idx] = n
|
||||||
view.data = list(sras.data)
|
view.data = list(sras.data)
|
||||||
view.data[angle_idx] = sras.data[angle_idx][:n]
|
view.data[angle_idx] = sras.data[angle_idx][:n]
|
||||||
view._y_pos_per_angle = list(sras._y_pos_per_angle)
|
view.y_pos_per_angle = list(sras.y_pos_per_angle)
|
||||||
view._y_pos_per_angle[angle_idx] = sras._y_pos_per_angle[angle_idx][:n]
|
view.y_pos_per_angle[angle_idx] = sras.y_pos_per_angle[angle_idx][:n]
|
||||||
return view
|
return view
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -11,12 +11,19 @@ Usage:
|
|||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
import struct
|
import struct
|
||||||
|
import sys
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
|
|
||||||
HDR_FMT_V6 = ">4sBHfffffffIdBB"
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||||
GEO_FMT_V6 = ">ffIH"
|
|
||||||
|
# Single source of truth for the byte layout: the reader's own constants.
|
||||||
|
# The byte *assembly* below stays independent, so a writer bug can't be
|
||||||
|
# masked by a matching reader bug.
|
||||||
|
from sras_format import HDR_FMT as HDR_FMT_LEGACY # noqa: E402
|
||||||
|
from sras_format import GEO_FMT_V6, HDR_FMT_V6 # noqa: E402
|
||||||
|
from sras_compute import _rotation_matrix as _rot # noqa: E402
|
||||||
|
|
||||||
# Per-angle (n_rows, n_frames) — deliberately different per angle so ragged
|
# Per-angle (n_rows, n_frames) — deliberately different per angle so ragged
|
||||||
# geometry handling is actually exercised.
|
# geometry handling is actually exercised.
|
||||||
@@ -171,12 +178,6 @@ def _sample_shape_mv(u: np.ndarray, v: np.ndarray) -> np.ndarray:
|
|||||||
return img
|
return img
|
||||||
|
|
||||||
|
|
||||||
def _rot(theta_deg: float) -> np.ndarray:
|
|
||||||
t = np.radians(theta_deg)
|
|
||||||
c, s = np.cos(t), np.sin(t)
|
|
||||||
return np.array([[c, -s], [s, c]])
|
|
||||||
|
|
||||||
|
|
||||||
def write_rotating(path: Path, n_angles: int = 5, samples_per_frame: int = 4,
|
def write_rotating(path: Path, n_angles: int = 5, samples_per_frame: int = 4,
|
||||||
seed: int = 0) -> dict:
|
seed: int = 0) -> dict:
|
||||||
"""Write a v6 file whose CH4 DC image is one sample seen at n_angles known
|
"""Write a v6 file whose CH4 DC image is one sample seen at n_angles known
|
||||||
@@ -260,7 +261,6 @@ def write_rotating(path: Path, n_angles: int = 5, samples_per_frame: int = 4,
|
|||||||
"y_starts": y_starts, "dx_mm": _ROT_DX_MM, "dy_mm": _ROT_DY_MM}
|
"y_starts": y_starts, "dx_mm": _ROT_DX_MM, "dy_mm": _ROT_DY_MM}
|
||||||
|
|
||||||
|
|
||||||
HDR_FMT_LEGACY = ">4sBHHffffIIdBB"
|
|
||||||
|
|
||||||
|
|
||||||
def write_legacy(path: Path, version: int = 4, n_angles: int = 2,
|
def write_legacy(path: Path, version: int = 4, n_angles: int = 2,
|
||||||
|
|||||||
Reference in New Issue
Block a user