A series of one file per image was laid out end to end: the rotation start came from the first file and the step from the difference between the first two, so a series with frames missing came out compressed - one 179.8 degree deposited sweep of 1108 files out of 1800 was read as 111 degrees, and every frame past the first gap was analysed at the wrong spindle angle. Indexing then found a lattice that took 4% of the validation spots, and two other gapped series aborted outright with "it is not this crystal's lattice". Every one of these formats writes each image's own start angle in its own header, so the sweep is fully recoverable. The new reader/SweepLayout places each frame at the slot its own angle puts it in and leaves a missing frame as a gap - a slot with no file, which ReadRawImage reports as nothing to read, which every image loop in the pipeline already passes over. The goniometer's start + increment * image_number is then the true angle of every image, and the sweep range, the per-10-degree delta-CC1/2 batches and the sweep-quality ledger all read the rotation the headers describe. The rotation step is the smallest move between two frames that really are adjacent, not the first pair. The three readers shared this code by duplication; it is now written once. The same place refuses what is not a sweep rather than averaging it into one: headers that disagree about the detector distance, the beam centre, the wavelength or the oscillation width, angles that do not sit on a single step (a folder of screening shots), or two frames claiming the same angle - each naming the frames. A series that does not turn at all is left exactly as it was. A directory holding fewer files than its own numbering spans is also reported, with both counts: that is the signal that a sweep was not unpacked or copied whole, which otherwise shows up only as a resolution nobody can explain. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_013nW6FNRP1bBJJ8pfHiByAT
64 lines
2.8 KiB
C++
64 lines
2.8 KiB
C++
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
#pragma once
|
|
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
#include "JFJochReader.h"
|
|
#include "SMV.h"
|
|
|
|
// Reads a rotation sweep straight from a directory of SMV files - what ADSC Quantum detectors
|
|
// wrote and what Rayonix and others still write, so most archived CCD data from the 2000s is in
|
|
// this format.
|
|
//
|
|
// The same shape as JFJochMarCCDReader, which it is otherwise a sibling of: the sweep's geometry
|
|
// comes from the first file's header, the rotation angle of each image from its own, and images
|
|
// are decoded on demand. A raw SMV file carries no analysis results, so no spots and no
|
|
// reflections.
|
|
//
|
|
// SMV states no saturation value and marks no untrusted pixels, so nothing is masked and no pixel
|
|
// is called an overload from the file alone - unlike marCCD, which at least carries a saturated
|
|
// value. The beam centre it states is in millimetres and its convention varies between writers;
|
|
// see the note in smv::Header.
|
|
class JFJochSMVReader : public JFJochReader {
|
|
std::vector<std::string> files_;
|
|
std::shared_ptr<JFJochReaderDataset> dataset_;
|
|
smv::Header header0_;
|
|
|
|
bool LoadImage_i(std::shared_ptr<JFJochReaderDataset> &dataset,
|
|
DataMessage &message,
|
|
std::vector<uint8_t> &buffer,
|
|
int64_t image_number,
|
|
bool update_dataset) override;
|
|
|
|
// Decodes one image into the caller's byte buffer and returns the image that points at it. The
|
|
// stored 16-bit strip is read through scratch, which the caller keeps between frames.
|
|
template <class Buffer>
|
|
CompressedImage DecodeInto(int64_t image_number, Buffer &buffer, std::vector<uint8_t> &scratch) const;
|
|
|
|
public:
|
|
~JFJochSMVReader() override = default;
|
|
|
|
// True if the path names something this reader can open: a marCCD file, or a directory holding
|
|
// at least one. Cheap - it reads a few kB at most.
|
|
static bool CanRead(const std::string &path);
|
|
|
|
// path is a directory of marCCD frames, or one frame inside the sweep to take the whole sweep
|
|
// from.
|
|
void ReadFiles(const std::string &path);
|
|
|
|
[[nodiscard]] uint64_t GetNumberOfImages() const override;
|
|
void Close() override;
|
|
|
|
|
|
// Whether the sweep really has an image at this point. A deposited series can be missing frames,
|
|
// and they are kept as gaps so that every image keeps the spindle angle its own header states;
|
|
// ReadRawImage hands such a slot back as "nothing to read".
|
|
[[nodiscard]] bool HasImage(int64_t image_number) const;
|
|
|
|
bool ReadRawImage(int64_t image_number, JFJochReaderRawImage &image) override;
|
|
[[nodiscard]] std::vector<SpotToSave> ReadSpots(int64_t image) const override;
|
|
};
|