Files
Jungfraujoch/reader/JFJochMarCCDReader.h
T
leonarski_fandClaude Opus 5 a1889c45e9 rugnux reads marCCD sweeps natively
A decade of deposited CCD data is archived as marCCD - what Rayonix MX-series and
mar Mosaic detectors write - and rugnux could not open any of it. reader/ had two
formats, NXmx/HDF5 and PILATUS miniCBF, and rugnux_cli dispatched on the one
CanRead(); this adds the third.

The format needs no new dependency: a marCCD file is an ordinary uncompressed TIFF
whose 3072-byte instrument header sits in the gap between the TIFF header and the
pixels, so libtiff - already fetched for JFJochPreview in every build mode - reads
the image, and the header is a fixed-offset block of little-endian int32.

Two things differ from the miniCBF path and are worth naming:

* The pixel size is NOT rounded to whole micrometres. A PILATUS pixel is exactly
  172 um so the existing reader can afford lround(); a MAR300 pixel is 73.242 um,
  and rounding it to 73 is a 0.33% scale error on every cell edge reported.
* The sweep template is the last run of digits in the whole file name rather than
  in the stem, which covers both schemes these detectors use - a numbered stem
  (xtal_1_00042.mccd) and the frame number as the extension (D1.042).

A CCD frame marks no untrusted pixels, so the sweep starts with nothing masked, and
the format has nowhere to state the rotation axis' direction, so the run settles its
sign from the data exactly as it does for a miniCBF carrying no axis table.

Measured on one deposited 300-frame Rayonix MX-300 sweep, de novo with no flags:
100% indexing, the deposited point group, cell within 0.045%, 99.5% complete at
multiplicity 3.4, in 26 s. The chosen sweep is confirmed against the instrument
header before it is opened, so a directory of ordinary TIFFs is refused rather than
read with a pixel size of zero - a unit test covers that, both naming schemes, and
the geometry conversion.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-09-16 19:31:58 +02:00

58 lines
2.5 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 "MarCCD.h"
// Reads a rotation sweep straight from a directory of marCCD files - what Rayonix MX-series and mar
// Mosaic detectors write, and what a decade of deposited CCD data is archived as - with no
// conversion step.
//
// The same shape as JFJochCBFReader: 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 so any number of
// workers can read at once. A raw marCCD file carries no analysis results, so there are no spots
// and no reflections here either.
//
// The one thing a CCD needs that a pixel detector does not: its pixels are unsigned 16-bit with no
// negative marker for untrusted ones, so the mask is what the header calls saturated and nothing
// else. A CCD's point spread and read-out noise are left to the analysis, which measures the
// background from the image rather than assuming a detector.
class JFJochMarCCDReader : public JFJochReader {
std::vector<std::string> files_;
std::shared_ptr<JFJochReaderDataset> dataset_;
marccd::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:
~JFJochMarCCDReader() 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;
bool ReadRawImage(int64_t image_number, JFJochReaderRawImage &image) override;
[[nodiscard]] std::vector<SpotToSave> ReadSpots(int64_t image) const override;
};