From 832a4060f65efbcb55b7c06abdf9a856d2a9ca8f Mon Sep 17 00:00:00 2001 From: Amin Khosrozadeh Date: Wed, 20 May 2026 18:16:14 +0200 Subject: [PATCH] tomo: respect _rlnTomoVisibleFrames in particle reconstruction reconstruct_particle determined per-tilt particle visibility from geometry only (Tomogram::determineVisiblity on the projection trajectory). This ignored the explicit visibility mask written by subtomo at extract time (rlnTomoVisibleFrames in particles.star). When Extract runs with --max_dose, the 2D-stacks contain only the dose-filtered frames; ml_optimiser (Refine3D / Class3D) aligns only those frames (exp_model.cpp 'Add only the visible images for this particle' loop); but reconstruct_particle was silently bringing high-dose frames back. The per-particle CTF / dose-weight math then used a different frame set than the data on disk, producing cloud-of-points-looking merged maps despite data_*.mrc (the un-Wiener-divided numerator) looking healthy. Fix: when EMDL_TOMO_VISIBLE_FRAMES is present on particleSet.partTable, intersect the geometric visibility with the explicit mask before extractAt3D_Fourier. Backwards-compatible: behavior unchanged for inputs without the column. Tested on a 4-tomogram 2D-stack project with --max_dose 50: - pre-fix: 39.2 mean visible frames per particle at reconstruction time - post-fix: 16.5 mean visible frames per particle, matching what Extract baked into the 2D stacks and what Refine3D used for alignment. --- .../programs/reconstruct_particle.cpp | 35 ++++++++++++++++++- 1 file changed, 34 insertions(+), 1 deletion(-) diff --git a/src/jaz/tomography/programs/reconstruct_particle.cpp b/src/jaz/tomography/programs/reconstruct_particle.cpp index 04b4e85da..126d02551 100644 --- a/src/jaz/tomography/programs/reconstruct_particle.cpp +++ b/src/jaz/tomography/programs/reconstruct_particle.cpp @@ -198,6 +198,8 @@ void ReconstructParticleProgram::processTomograms( const int s = dataImgFS[0].ydim; const int sh = s/2 + 1; const int tc = tomoIndices.size(); + const bool has_explicit_visible_frames = + particleSet.partTable.containsLabel(EMDL_TOMO_VISIBLE_FRAMES); if (verbosity > 0 && !per_tomogram_progress) { @@ -343,7 +345,38 @@ void ReconstructParticleProgram::processTomograms( part_id, fc, tomogram.centre, tomogram.optics.pixelSize); std::vector projCut(fc), projPart(fc); - const std::vector isVisible = tomogram.determineVisiblity(traj, s/2.0); + std::vector isVisible = tomogram.determineVisiblity(traj, s/2.0); + + // Intersect geometric visibility with the explicit per-particle + // _rlnTomoVisibleFrames mask that subtomo writes when --max_dose + // (or any frame filter) was used at extraction. Without this, + // reconstruction silently includes frames that were excluded + // from the 2D stacks and from Refine3D alignment, so the + // per-particle CTF + dose-weight math uses a different frame + // set than the data on disk. exp_model.cpp's "Add only the + // visible images for this particle" loop is the authoritative + // pattern that this matches. + if (has_explicit_visible_frames) + { + const std::vector explicitVisible = + particleSet.getVisibleFrames(part_id); + + if ((int)explicitVisible.size() != fc) + { + REPORT_ERROR_STR( + "reconstruct_particle: bad " + << EMDL::label2Str(EMDL_TOMO_VISIBLE_FRAMES) + << " length for particle " + << particleSet.getName(part_id) + << "; expected " << fc + << " frames, found " << explicitVisible.size()); + } + + for (int f = 0; f < fc; f++) + { + isVisible[f] = isVisible[f] && (explicitVisible[f] != 0); + } + } const bool circle_crop = do_circle_crop;