Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
26 commits
Select commit Hold shift + click to select a range
6da25ce
feat: enable pixel, line and frame marker processing for BH_RECORD_TY…
cqian89 Jan 20, 2026
2cd8942
Add BH SPC preprocessing helpers and tests
cqian89 Jan 20, 2026
55804fd
feat: implement pixel marker-based binning support in CLSMImage
cqian89 Jan 20, 2026
1fadc21
fix(clsm): handle last pixel in marker binning and optimize memory
cqian89 Jan 20, 2026
2693ade
Add BH CLSM helper functions for Frame 1 adjustment
cqian89 Jan 20, 2026
a8c8542
Add integration tests for BH pixel marker-based binning and fix bug i…
cqian89 Jan 20, 2026
5973d23
style(clsm): revert whitespace changes from e993834, keep functional …
cqian89 Jan 20, 2026
9c84069
Add CLSM_BH_SPC130 to ReadingRoutine and fix SP8 comment typo
cqian89 Jan 20, 2026
7278d35
Implement read_bh_set_file() in TTTRHeader to parse BH .set files
cqian89 Jan 20, 2026
c410db8
Auto-find and parse .set files for BH_SPC130_CONTAINER in TTTR::read_…
cqian89 Jan 20, 2026
16594d7
feat(clsm): apply BH defaults in CLSMImage constructor
cqian89 Jan 20, 2026
8538756
fix: refine BH SPC-130 default setting logic in CLSMImage
cqian89 Jan 20, 2026
373eaf4
Implement BH SPC-130 dimension inference, frame 1 adjustment, and det…
cqian89 Jan 20, 2026
00b8237
Revert BH-specific truncation in remove_incomplete_frames and add und…
cqian89 Jan 20, 2026
50fce02
Remove deprecated bh_helpers.py and update BH .set file parsing
cqian89 Jan 20, 2026
f751139
style: reduce non-functional diffs
cqian89 Jan 20, 2026
88abb8a
style: remove trailing whitespace in SPC-130 reader
cqian89 Jan 20, 2026
a1ca5bd
style: align whitespace with upstream
cqian89 Jan 20, 2026
eecd6e7
fix(clsm): fix BH SPC-130 line marker defaults and unify in Python
cqian89 Jan 20, 2026
0e47aea
fix(bh): recover truncated recordings by appending synthetic markers
cqian89 Jan 21, 2026
df40b20
Revert "fix(bh): recover truncated recordings by appending synthetic …
cqian89 Jan 21, 2026
a188a4d
fix(clsm): recover truncated BH SPC-130 recordings at CLSMImage level
cqian89 Jan 21, 2026
0674bbf
Fix core logic and API issues in CLSMImage
cqian89 Jan 21, 2026
75a3773
Improve safety and robustness
cqian89 Jan 21, 2026
03f91e7
Cleanup and documentation
cqian89 Jan 21, 2026
a083807
fix(clsm): default n_pixel_per_line to 0 to enable auto-detection
cqian89 Jan 21, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 24 additions & 3 deletions ext/python/CLSMImage.py
Original file line number Diff line number Diff line change
Expand Up @@ -303,6 +303,8 @@ def __init__(
skip_before_first_frame_marker=False,
skip_after_last_frame_marker=False,
split_by_channel=False,
use_pixel_markers=False,
marker_pixel=1,
filename=None,
channel_luts=None,
channel_shifts=None,
Expand Down Expand Up @@ -381,7 +383,8 @@ def __init__(
rt = {
'SP8': CLSM_SP8,
'SP5': CLSM_SP5,
'default': CLSM_DEFAULT
'BH_SPC130': CLSM_BH_SPC130,
'default': CLSM_DEFAULT,
}

# Always include bidirectional_scan=False by default
Expand All @@ -396,6 +399,8 @@ def __init__(
"n_pixel_per_line": n_pixel_per_line,
"bidirectional_scan": False, # <-- new default
"split_by_channel": bool(split_by_channel),
"use_pixel_markers": bool(use_pixel_markers),
"marker_pixel": int(marker_pixel),
}

# Override CLSM settings from JSON if provided
Expand All @@ -405,7 +410,8 @@ def __init__(
'marker_line_start', 'marker_line_stop', 'marker_frame_start',
'marker_event_type', 'n_pixel_per_line', 'n_lines',
'skip_before_first_frame_marker', 'skip_after_last_frame_marker',
'bidirectional_scan', 'split_by_channel', 'reading_routine'
'bidirectional_scan', 'split_by_channel', 'reading_routine',
'use_pixel_markers', 'marker_pixel'
]
for key in clsm_keys:
if key in json_settings:
Expand Down Expand Up @@ -447,6 +453,10 @@ def __init__(
settings_kwargs['marker_event_type'] = marker_event_type
if isinstance(n_pixel_per_line, int):
settings_kwargs['n_pixel_per_line'] = n_pixel_per_line
if isinstance(use_pixel_markers, bool):
settings_kwargs['use_pixel_markers'] = use_pixel_markers
if isinstance(marker_pixel, int):
settings_kwargs['marker_pixel'] = marker_pixel

# Consume our special parameters before passing to C++ constructor
kwargs.pop('settings_file', None)
Expand Down Expand Up @@ -475,7 +485,18 @@ def __init__(
settings_kwargs["bidirectional_scan"] = (bd != 0)
except:
pass

elif reading_routine == 'BH_SPC130':
# BH SPC-130 specific defaults
# These mirror the BH SPC-130 defaults implemented in CLSMImage.cpp
settings_kwargs["marker_event_type"] = 1
settings_kwargs["marker_frame_start"] = [4] # BH frame marker channel
settings_kwargs["marker_line_start"] = 2 # BH line marker channel
settings_kwargs["marker_line_stop"] = 255 # CLSM_MARKER_NO_STOP: BH uses start-only marker pairing
settings_kwargs["skip_before_first_frame_marker"] = True
settings_kwargs["skip_after_last_frame_marker"] = False

# Remove None values so CLSMSettings uses its C++ defaults
settings_kwargs = {k: v for k, v in settings_kwargs.items() if v is not None}

clsm_settings = CLSMSettings(**settings_kwargs)

Expand Down
52 changes: 46 additions & 6 deletions include/CLSMImage.h
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,17 @@
#include "Correlator.h"


/// Sentinel value indicating no stop marker (BH SPC-130 start-only mode)
/// When marker_line_stop equals this value, lines are paired start-to-start
constexpr int CLSM_MARKER_NO_STOP = 255;


/// Different types of distances between two accessible volumes
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The comment on line 29 says "Different types of distances between two accessible volumes" but this enum actually defines reading routines for different CLSM hardware types. This is misleading documentation that was already present, but since the enum is being modified, the comment should be corrected to accurately describe what ReadingRoutine represents.

Suggested change
/// Different types of distances between two accessible volumes
/// Reading routines corresponding to different CLSM hardware types

Copilot uses AI. Check for mistakes.
typedef enum{
CLSM_DEFAULT, /// Default reading compute_icsroutine
CLSM_DEFAULT, /// Default reading routine
CLSM_SP5, /// Leica SP5
CLSM_SP8 /// Leica SP5
CLSM_SP8, /// Leica SP8
CLSM_BH_SPC130 /// Becker & Hickl SPC-130
} ReadingRoutine;


Expand Down Expand Up @@ -83,7 +89,7 @@ static std::pair<int, int> find_clsm_start_stop(
class CLSMSettings {
friend class CLSMImage;

protected:
public:
Copy link

Copilot AI Jan 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changing CLSMSettings members from protected to public changes the class interface. This could expose internal state that was previously encapsulated. Consider whether all these members need to be public or if only specific accessors should be provided.

Suggested change
public:
protected:

Copilot uses AI. Check for mistakes.
/// Skip events before the first frame marker?
bool skip_before_first_frame_marker = false;

Expand Down Expand Up @@ -121,6 +127,12 @@ class CLSMSettings {
/// When false (default), keep a flat single-channel view regardless of flips.
bool split_by_channel = false;

/// If true, use markers to determine pixel edges (Becker & Hickl mode).
bool use_pixel_markers = false;

/// The routing channel of the pixel markers (Becker & Hickl mode).
int marker_pixel = 1;

public:
/*!
* \brief CLSMSettings Constructor.
Expand All @@ -143,6 +155,8 @@ class CLSMSettings {
* @param n_lines Number of lines per frame. If -1, auto‐detect from the first frame (default: -1).
* @param bidirectional_scan If true, every odd line was scanned in reverse direction (default: false).
* @param split_by_channel If true, split frames into multiple channels using channel-flip markers (default: false).
* @param use_pixel_markers If true, use markers to determine pixel edges (default: false).
* @param marker_pixel Routing channel of pixel markers (default: 1).
*/
explicit CLSMSettings(
bool skip_before_first_frame_marker = false,
Expand All @@ -152,10 +166,12 @@ class CLSMSettings {
int marker_line_stop = 2,
std::vector<int> marker_frame_start = std::vector<int>({1}),
int marker_event_type = 1,
int n_pixel_per_line = 1,
int n_pixel_per_line = 0,
int n_lines = -1,
bool bidirectional_scan = false,
bool split_by_channel = false
bool split_by_channel = false,
bool use_pixel_markers = false,
int marker_pixel = 1
) {
this->skip_before_first_frame_marker = skip_before_first_frame_marker;
this->skip_after_last_frame_marker = skip_after_last_frame_marker;
Expand All @@ -168,6 +184,8 @@ class CLSMSettings {
this->n_lines = n_lines;
this->bidirectional_scan = bidirectional_scan;
this->split_by_channel = split_by_channel;
this->use_pixel_markers = use_pixel_markers;
this->marker_pixel = marker_pixel;
}
};

Expand Down Expand Up @@ -1050,7 +1068,9 @@ class CLSMImage {
int marker_event_type = 15,
int reading_routine = CLSM_SP8,
bool skip_before_first_frame_marker = false,
bool skip_after_last_frame_marker = false
bool skip_after_last_frame_marker = false,
int marker_line_start = 0,
int expected_n_lines = 0
);

/*!
Expand Down Expand Up @@ -1112,6 +1132,26 @@ class CLSMImage {
int reading_routine = CLSM_SP8
);

/*!
* \brief Detect if BH SPC-130 Frame 1 has an extra initialization line.
*
* For BH SPC data, Frame 1 often has an extra partial line at the start
* that should be skipped. This compares line marker counts between Frame 1 and Frame 2.
*
* @param tttr Pointer to TTTR data
* @param frame_marker Frame marker routing channel (default 4)
* @param line_marker Line marker routing channel (default 2)
* @param marker_event_type Marker event type (default 1)
* @return true if Frame 1 has one extra line marker compared to Frame 2
*/
static bool detect_bh_frame1_extra_line(
TTTR* tttr,
int frame_marker = 4,
int line_marker = 2,
int marker_event_type = 1,
int expected_n_lines = 0
);


/*!
* \brief Obtains the duration of a line (in milliseconds) for a specified frame and line.
Expand Down
15 changes: 15 additions & 0 deletions include/TTTRHeader.h
Original file line number Diff line number Diff line change
Expand Up @@ -358,6 +358,21 @@ class TTTRHeader {
bool rewind = true
);

/*!
* @brief Reads a Becker & Hickl .set file and extracts imaging parameters.
*
* Parses the BH .set file to extract SP_IMG_X (pixels per line),
* SP_IMG_Y (lines per frame), and SP_PIX_CLK (pixel clock mode).
* These values are stored in json_data under tags:
* - ImgHdr_PixX
* - ImgHdr_PixY
* - BH_UsePixelClock
*
* @param filename Path to the .set file
* @return true if parsing succeeded, false otherwise
*/
bool read_bh_set_file(const std::string& filename);

/*!
* @brief Reads the header of a Carl Zeiss (CZ) Confocor3 file and sets the reading routing.
*
Expand Down
19 changes: 18 additions & 1 deletion include/TTTRRecordReader.h
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,7 @@ struct RecordProcessor<BH_RECORD_TYPE_SPC130> {
bh_spc130_record_t rec;
rec.allbits = TTTRRecord;

// Case 1: Valid photon record (INVALID=0)
if (!rec.bits.invalid) {
overflow_counter += rec.bits.mtov;
true_nsync = rec.bits.mt + overflow_counter * 4096;
Expand All @@ -260,13 +261,29 @@ struct RecordProcessor<BH_RECORD_TYPE_SPC130> {
return true;
}

if (rec.bits.invalid && rec.bits.mtov) {
// Case 2: Marker record (INVALID=1, MARK=1)
// Routing bits encode marker type:
// rout bit 0 (value 1) = Pixel marker
// rout bit 1 (value 2) = Line marker
// rout bit 2 (value 4) = Frame marker
if (rec.bits.mark) {
overflow_counter += rec.bits.mtov; // Handle overflow for markers too
true_nsync = rec.bits.mt + overflow_counter * 4096;
micro_time = rec.bits.rout; // Store marker type in micro_time
channel = static_cast<int16_t>(rec.bits.rout); // Marker type as channel
record_type = RECORD_MARKER;
return true;
}

// Case 3: Overflow record (INVALID=1, MARK=0, MTOV=1)
if (rec.bits.mtov) {
bh_overflow_t overflow_record;
overflow_record.allbits = TTTRRecord;
overflow_counter += overflow_record.bits.cnt;
return false;
}

// Case 4: Invalid record (INVALID=1, MARK=0, MTOV=0) - skip
return false;
}
};
Expand Down
Loading
Loading