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>
67 lines
2.9 KiB
C++
67 lines
2.9 KiB
C++
// SPDX-FileCopyrightText: 2026 Filip Leonarski, Paul Scherrer Institute <filip.leonarski@psi.ch>
|
|
// SPDX-License-Identifier: GPL-3.0-only
|
|
|
|
#pragma once
|
|
|
|
#include <cstdint>
|
|
#include <string>
|
|
#include <vector>
|
|
|
|
// marCCD: an ordinary uncompressed TIFF whose 3072-byte instrument header sits in the gap between
|
|
// the TIFF header and the pixels. The image is read through libtiff, which the build already
|
|
// carries; the instrument header is a fixed-offset block of little-endian int32, so it is read
|
|
// directly. Rayonix MX-series and mar Mosaic detectors both write it, under half a dozen file
|
|
// extensions (.mccd, .img, .tif, or a bare frame number).
|
|
//
|
|
// Every value the header stores is an integer times a fixed scale: lengths in micrometres or
|
|
// nanometres, angles in millidegrees, the wavelength in 1e-5 A. They are converted here, so the
|
|
// rest of the code sees metres, degrees and angstroms like every other reader.
|
|
namespace marccd {
|
|
|
|
// Byte offset of the instrument header when the file does not say. Every writer seen puts it here
|
|
// and states it in TIFF tag 34710 as well.
|
|
inline constexpr uint32_t DEFAULT_HEADER_OFFSET = 1024;
|
|
|
|
// The private TIFF tag in which marCCD states where its own header begins.
|
|
inline constexpr uint16_t TIFFTAG_MARCCD_HEADER_OFFSET = 34710;
|
|
|
|
struct Header {
|
|
std::string detector; // from the TIFF Model/Software tags where they say anything
|
|
int64_t nx = 0; // fast dimension (columns)
|
|
int64_t ny = 0; // slow dimension (rows)
|
|
double pixel_x_m = 0;
|
|
double pixel_y_m = 0;
|
|
double distance_m = 0;
|
|
double beam_x_px = 0;
|
|
double beam_y_px = 0;
|
|
double wavelength_A = 0;
|
|
double start_angle_deg = 0;
|
|
double angle_increment_deg = 0;
|
|
double two_theta_deg = 0;
|
|
double exposure_s = 0;
|
|
int64_t saturated_value = 0;
|
|
std::string axis_name = "phi"; // the circle that moved between start and end
|
|
};
|
|
|
|
// True if the path names something this reader can open: a marCCD file, or a directory holding at
|
|
// least one. Reads a few kB at most.
|
|
bool CanRead(const std::string &path);
|
|
|
|
// The files of one sweep, in collection order. path is a directory, or one frame inside the sweep
|
|
// to take the whole sweep from. Empty where there is none.
|
|
std::vector<std::string> CollectSweep(const std::string &path);
|
|
|
|
// Header only - reads the first few kB of the file. Cheap enough to call per frame for the
|
|
// rotation angles.
|
|
Header ReadHeader(const std::string &path);
|
|
|
|
// Header + pixels of one file. out is resized to nx * ny.
|
|
Header Read(const std::string &path, std::vector<int32_t> &out);
|
|
|
|
// The same, widening the stored uint16 into memory the caller already has. scratch carries the
|
|
// 16-bit strip between frames, so a worker walking a sweep allocates once. Throws if the image
|
|
// does not fit in capacity pixels.
|
|
Header ReadInto(const std::string &path, int32_t *out, size_t capacity, std::vector<uint8_t> &scratch);
|
|
|
|
} // namespace marccd
|