Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
103 commits
Select commit Hold shift + click to select a range
86af513
Create base arg_parser as common infrastructure
dimitrivlachos Jul 18, 2025
b213f80
Refactor argument parsing by creating SpotfinderArgumentParser class
dimitrivlachos Jul 18, 2025
920e7f3
Add integrator module with CMake configuration and basic file layout
dimitrivlachos May 20, 2025
87d758c
Include common libraries
dimitrivlachos Jun 4, 2025
692ab9b
Add basic arg parsing and file handling
dimitrivlachos Jun 4, 2025
056827e
Refactor arg parsing for distinct modules
dimitrivlachos Jun 16, 2025
deedc34
Implement CPU Kabsch coordinates computation
dimitrivlachos Jun 23, 2025
a132f51
Add DeviceBuffer class for managing CUDA device memory with RAII
dimitrivlachos Jul 25, 2025
bc8e8f5
Add Vector3D class for CUDA-compatible 3D vector mathematics
dimitrivlachos Jul 25, 2025
3d81b11
Add CUDA implementation and header for Kabsch coordinate transformations
dimitrivlachos Jul 25, 2025
0e98c69
Refactor integrator to use CUDA Kabsch implementation
dimitrivlachos Jul 25, 2025
7bd08d4
Implement CPU bounding box extent computation
dimitrivlachos Jul 29, 2025
b408e38
Refactor main to test transform and bbox computations
dimitrivlachos Jul 29, 2025
596ecd7
Refactor and optimise vector3d.cuh to utilise cuda native vector types
dimitrivlachos Jul 31, 2025
7e6a6b0
Add centralized precision configuration for CUDA device code
dimitrivlachos Aug 4, 2025
c7f2079
Add common math utility functions for CUDA
dimitrivlachos Aug 4, 2025
83eabaa
Update type configuration
dimitrivlachos Aug 4, 2025
0e60648
Add CUDA implementation for Kabsch bounding box extent computation
dimitrivlachos Aug 4, 2025
4188737
Rename kabsch functions for clarity
dimitrivlachos Aug 5, 2025
fe7549f
Refactor integrator to utilise CUDA bbox computation
dimitrivlachos Aug 11, 2025
e1fdc82
Add baseline integrator with CPU algorithms from testing
dimitrivlachos Aug 14, 2025
5c4a5c1
Update from main
dimitrivlachos Oct 3, 2025
600442f
Add missing Eigen dependency
dimitrivlachos Oct 6, 2025
255adde
Refactor arg parser to require app specific hdf5 implementation
dimitrivlachos Oct 6, 2025
fad7c47
Merge remote-tracking branch 'origin' into integration
dimitrivlachos Oct 13, 2025
eb767d9
Apply precommit code format
dimitrivlachos Oct 13, 2025
67de660
Update reader to require flags
dimitrivlachos Oct 13, 2025
2aeb113
Add basic integration test
dimitrivlachos Oct 13, 2025
5ec1191
Fix formatting in Docker Image CI workflow
dimitrivlachos Oct 13, 2025
2eb399c
Add initial framework for multi-threaded image read and decompression
dimitrivlachos Oct 24, 2025
9c315f4
Merge branch 'main' into integration
dimitrivlachos Nov 18, 2025
15756ed
Re-create basic CPU extent calculation
dimitrivlachos Nov 18, 2025
9f4b7c6
Refactor xy_mm to optional
dimitrivlachos Nov 18, 2025
a9282a0
Update dx2 to latest
dimitrivlachos Nov 18, 2025
1750679
Create integration calculation framework
dimitrivlachos Nov 18, 2025
664a164
Merge branch 'main' into integration
dimitrivlachos Nov 18, 2025
f045b53
Refactor build script artifacts for clarity
dimitrivlachos Nov 20, 2025
d062e18
Add unit test framework
dimitrivlachos Nov 21, 2025
ef3688e
Fix bbox datatype and path
dimitrivlachos Nov 24, 2025
3fb480c
Disable read-readiness check
dimitrivlachos Nov 27, 2025
674a361
Rename and extend extent test
dimitrivlachos Nov 27, 2025
a3088f9
Add test data
dimitrivlachos Nov 27, 2025
918b025
Specify units in type descriptor
dimitrivlachos Nov 28, 2025
7bc61e9
Refactor math utility to allow for non-CUDA usage
dimitrivlachos Nov 28, 2025
aa636a5
Fix z-index by accounting for image indexing starting at 1
dimitrivlachos Nov 28, 2025
fb71ba6
Fix sigma values by converting to radians
dimitrivlachos Nov 28, 2025
4a6a554
Rename test files for clarity
dimitrivlachos Nov 28, 2025
5fd93cc
Enhance extent calculation test with mismatch report
dimitrivlachos Nov 28, 2025
271c44f
Comment out logging of available columns in integrated reflection file
dimitrivlachos Nov 28, 2025
070ec91
Update dx2 to latest
dimitrivlachos Dec 2, 2025
e79e975
Add framework for kabsch coord transformation test
dimitrivlachos Dec 2, 2025
8ed81a5
Merge branch 'main' into integration
dimitrivlachos Dec 8, 2025
6cd2b20
Remove redundant Eigen dependency
dimitrivlachos Dec 9, 2025
9f07234
Add clarifying comments
dimitrivlachos Dec 9, 2025
e4a90fa
Update baseline sigma calculations to use radians
dimitrivlachos Dec 9, 2025
cf30248
Move radian conversion to main function
dimitrivlachos Dec 9, 2025
eb924ab
Remove unecessary wait for read
dimitrivlachos Dec 9, 2025
0b355e1
Add compile-time option for calculation precision selection
dimitrivlachos Dec 9, 2025
d209f6c
Merge integration foundational changes from main back into integration
dimitrivlachos Dec 9, 2025
56028f3
Implement per-thread whole image transfer using pinned memory
dimitrivlachos Dec 10, 2025
e5a2897
Add comments to clarify sigma_m and sigma_b parameters in radians
dimitrivlachos Dec 11, 2025
6f4d0d8
Add file brief to 'integrator.cc'
dimitrivlachos Dec 11, 2025
6ae1a60
Merge branch 'main' into integration
dimitrivlachos Jan 26, 2026
085890b
Remove simple sigma_m and sigma_b variables from main function
dimitrivlachos Jan 26, 2026
61ddf0a
Add integer ceiling division function
dimitrivlachos Jan 26, 2026
b160dcf
Outline updated kabsch transformation wrapper
dimitrivlachos Jan 28, 2026
0836902
Add data prep and threading regions
dimitrivlachos Jan 28, 2026
a82f94a
Add conversion helpers for Eigen and mdspan to fastvec::Vector3D
dimitrivlachos Jan 28, 2026
c0c54ea
Include observed-calculated offsets in sigma calculation for integrat…
jbeilstenedmands Jan 26, 2026
0bdc67d
Remove unused conversions
dimitrivlachos Jan 28, 2026
9373e5d
Update block dimensions for kabsch transformation kernel
dimitrivlachos Jan 28, 2026
7914edb
Merge branch 'main' into integration_update
dimitrivlachos Jan 28, 2026
5cb3b66
Refactor ceil_div to allow function call while remaining type-safe
dimitrivlachos Jan 28, 2026
302e425
Update kabsch grid dimension calculation
dimitrivlachos Jan 28, 2026
ecab819
Deprecate old kabsch function for temporary test compatability
dimitrivlachos Jan 29, 2026
225d2fc
Update kernel params
dimitrivlachos Jan 29, 2026
b2eb748
refactor kabsch_transform to use 2D grid
dimitrivlachos Jan 29, 2026
36488bf
Refactor BoundingBoxExtents to use integers internally and update cas…
dimitrivlachos Jan 30, 2026
4bcdb9c
Add bounding box check for reflections in kabsch transformation step
dimitrivlachos Jan 30, 2026
f01566f
Refactor bounding box check to include upper bounds and calculate shi…
dimitrivlachos Feb 2, 2026
c7cd802
Compute s_pixel from corner coords
dimitrivlachos Feb 2, 2026
512854d
Compute phi_pixel from image number
dimitrivlachos Feb 2, 2026
3bcbad2
Call kabsch transformation function on updated data
dimitrivlachos Feb 2, 2026
582acd6
Update kabsch wrapper parameter type
dimitrivlachos Feb 2, 2026
498d081
Remove half-pixel offset and convert to radians
dimitrivlachos Feb 3, 2026
650adc3
Create basic summation implementation plan
dimitrivlachos Feb 5, 2026
f99b999
Implement summation integration parameters and buffers
dimitrivlachos Feb 5, 2026
359855e
Add foreground/background classifier
dimitrivlachos Feb 5, 2026
3595316
Classify and sum intensities
dimitrivlachos Feb 5, 2026
d5817a5
Finalise summation integration with simplified host-side reduction an…
dimitrivlachos Feb 5, 2026
f9ab689
Add CUDA architecture configuration with GPU compute capability
dimitrivlachos Feb 5, 2026
d8065ed
Implement dynamic accumulator precision
dimitrivlachos Feb 9, 2026
f81eb83
Update intensity computation with background variance and sigma estim…
dimitrivlachos Feb 9, 2026
df5e8e9
Obliterate CUDA extent calculation
dimitrivlachos Feb 9, 2026
dfd104a
Refactor Kabsch trasnformation to use pixel_t type and PitchedArray2D
dimitrivlachos Feb 9, 2026
7c26f0a
Dials summation fix
dimitrivlachos Feb 11, 2026
279a673
Remove notes
dimitrivlachos Feb 12, 2026
e7542b7
Refactor Kabsch kernel to query 8 corners in shared mem
dimitrivlachos Feb 12, 2026
c401f2a
Update todo comment
dimitrivlachos Feb 12, 2026
9a2e494
Update accumulator type to uint32 and document
dimitrivlachos Feb 12, 2026
69776f0
Add inter-z-slice inclusion condition
dimitrivlachos Feb 27, 2026
d378147
Update foreground classification to only include pixel containing int…
dimitrivlachos Feb 27, 2026
13e352b
Neaten forground condition to make it more readable
dimitrivlachos Feb 27, 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
52 changes: 52 additions & 0 deletions CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,28 @@ include(ResolveGitVersion)

set(CMAKE_CXX_STANDARD 20)
set(CMAKE_CUDA_STANDARD 20)

# Set CUDA compute capability target
# Override with just the number: cmake -DCUDA_ARCH=86 ..
# Set default to 7.5 (Turing)
set(CUDA_ARCH "75 - Turing (RTX 20xx, GTX 16xx)" CACHE STRING "CUDA compute capability target architecture")
set_property(CACHE CUDA_ARCH PROPERTY STRINGS
"52 - Maxwell (GTX 9xx, Titan X)"
"60 - Pascal (GTX 10xx, P100)"
"61 - Pascal (GTX 1050/1030)"
"70 - Volta (V100, Titan V100)"
"75 - Turing (RTX 20xx, GTX 16xx)"
"80 - Ampere (A100)"
"86 - Ampere (RTX 30xx)"
"89 - Ada Lovelace (RTX 40xx)"
"90 - Hopper (H100)"
)

# Extract just the number from the selection (first two characters)
string(SUBSTRING "${CUDA_ARCH}" 0 2 CUDA_ARCH_NUMBER)
set(CMAKE_CUDA_ARCHITECTURES ${CUDA_ARCH_NUMBER})
message(STATUS "CUDA architecture set to: sm_${CMAKE_CUDA_ARCHITECTURES}")

project(fast-feedback-service LANGUAGES CXX VERSION ${FFS_VERSION_CMAKE})

set(CMAKE_EXPORT_COMPILE_COMMANDS yes)
Expand Down Expand Up @@ -111,6 +133,36 @@ find_package(CUDAToolkit)
if (CUDAToolkit_FOUND)
message(STATUS "CUDA found: Building CUDA components.")
set(FFS_ENABLE_CUDA ON)

# Try to detect GPU compute capability
execute_process(
COMMAND nvidia-smi --query-gpu=compute_cap --format=csv,noheader
OUTPUT_VARIABLE DETECTED_GPU_CAPS
OUTPUT_STRIP_TRAILING_WHITESPACE
ERROR_QUIET
)

if(DETECTED_GPU_CAPS)
# nvidia-smi returns "7.5" format, convert to "75"
string(REPLACE "." "" DETECTED_GPU_ARCH "${DETECTED_GPU_CAPS}")
# Get first GPU if multiple
string(REGEX MATCH "[0-9]+" DETECTED_GPU_ARCH "${DETECTED_GPU_ARCH}")
message(STATUS "Detected GPU compute capability: sm_${DETECTED_GPU_ARCH}")

# Warn if configured architecture doesn't match detected GPU
if(NOT CUDA_ARCH_NUMBER STREQUAL DETECTED_GPU_ARCH)
message(WARNING
"Target CUDA architecture (sm_${CUDA_ARCH_NUMBER}) differs from detected GPU (sm_${DETECTED_GPU_ARCH}).\n"
" The compiled code may not run optimally or at all on this system.\n"
" To build for the detected GPU: cmake -DCUDA_ARCH=${DETECTED_GPU_ARCH} ..\n"
" To build for a different target: ensure the target system has sm_${CUDA_ARCH_NUMBER} or newer."
)
else()
message(STATUS "✓ Configured architecture matches detected GPU")
endif()
else()
message(STATUS "Could not detect GPU compute capability (nvidia-smi not available or no GPU present)")
endif()
else()
message(STATUS "CUDA not found: Skipping CUDA components.")
set(FFS_ENABLE_CUDA OFF)
Expand Down
13 changes: 12 additions & 1 deletion include/math/device_precision.cuh
Original file line number Diff line number Diff line change
Expand Up @@ -34,4 +34,15 @@ using scalar_t = float;
#define CUDA_ABS fabsf
#define CUDA_SQRT sqrtf
#define CUDA_EXP expf
#endif
#endif

// Accumulator type for summation/reduction operations.
// Integer accumulation is used because:
// 1. atomicAdd on uint32_t is a single native hardware instruction (fastest atomic)
// 2. Maximum possible sum is well within uint32_t range (~20M << 4.3G)
// 3. Final reduction to double is performed on the host
//
// NOTE: If pixel_t can be negative (pedestal-subtracted data), change to int32_t.
// NOTE: If pixel values are non-integer (e.g. gain-corrected floats), revert to:
// using accumulator_t = scalar_t;
using accumulator_t = uint32_t;
21 changes: 21 additions & 0 deletions include/math/math_utils.cuh
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@
* constexpr scalar_t RAD_TO_DEG = 180.0 / cuda::std::numbers::pi_v<scalar_t>;
*/
#include <cmath>
#include <concepts>

#ifdef __CUDACC__
#include "math/device_precision.cuh"
Expand Down Expand Up @@ -43,4 +44,24 @@ DEVICE_HOST scalar_type degrees_to_radians(scalar_type degrees) {
return degrees * DEG_TO_RAD;
}

/**
* @brief Integer ceiling division
*
* Computes ceil(n/d) using integer arithmetic only, avoiding
* floating-point conversion. Equivalent to (n + d - 1) / d, which
* rounds up to the nearest integer quotient.
*
* @tparam T Integer type
* @param n Numerator
* @param d Denominator (must be > 0)
* @return Ceiling of n/d
*
* @example ceil_div(15, 4) returns 4, whereas 15/4 returns 3
*/
template <typename T>
DEVICE_HOST constexpr T ceil_div(T n, T d) {
static_assert(std::is_integral_v<T>, "ceil_div requires an integer type");
return (n + d - 1) / d;
}

#undef DEVICE_HOST
8 changes: 7 additions & 1 deletion integrator/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@ find_package(Bitshuffle REQUIRED)

add_executable(integrator
integrator.cc
integrator.cu
kabsch.cu
extent.cc
../spotfinder/shmread.cc
../spotfinder/cbfread.cc
)
Expand Down Expand Up @@ -39,4 +42,7 @@ target_link_libraries(integrator
# Allow CUDA to use multi-file device variables
set_property(TARGET integrator PROPERTY CUDA_SEPARABLE_COMPILATION ON)
target_compile_options(integrator PRIVATE "$<$<AND:$<CONFIG:Debug>,$<COMPILE_LANGUAGE:CUDA>>:-G>")
target_compile_options(integrator PRIVATE "$<$<AND:$<COMPILE_LANGUAGE:CUDA>,$<OR:$<CONFIG:Debug>,$<CONFIG:RelWithDebInfo>>>:--generate-line-info>")
target_compile_options(integrator PRIVATE "$<$<AND:$<COMPILE_LANGUAGE:CUDA>,$<OR:$<CONFIG:Debug>,$<CONFIG:RelWithDebInfo>>>:--generate-line-info>")

# Add tests subdirectory
add_subdirectory(tests)
191 changes: 191 additions & 0 deletions integrator/extent.cc
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
/**
* @file extent.cc
* @brief Extent and bounding box algorithms for baseline CPU implementation
*/

#include "extent.hpp"

#include <algorithm>
#include <array>
#include <cmath>

#include "ffs_logger.hpp"

std::vector<BoundingBoxExtents> compute_kabsch_bounding_boxes(
const Eigen::Vector3d &s0,
const Eigen::Vector3d &rot_axis,
const mdspan_2d_double &s1_vectors,
const mdspan_2d_double &phi_positions,
const size_t num_reflections,
const double sigma_b,
const double sigma_m,
const Panel &panel,
const Scan &scan,
const MonochromaticBeam &beam,
const double n_sigma,
const double sigma_b_multiplier) {
std::vector<BoundingBoxExtents> extents;
extents.reserve(num_reflections);

/*
* Tolerance for detecting when a reflection is nearly parallel to
* the rotation axis. When ζ = m₂ · e₁ approaches zero, it indicates
* the reflection's scattering plane is nearly parallel to the
* goniometer rotation axis, making the φ-to-image conversion
* numerically unstable. This threshold (1e-10) is chosen based on
* geometric considerations rather than pure floating-point precision
* - it represents a practical limit for "nearly parallel" geometry
* where the standard bounding box calculation should be bypassed in
* favor of spanning the entire image range.
*/
static constexpr double ZETA_TOLERANCE = 1e-10;

// Calculate the angular divergence parameters:
// Δb = nσ × σb × m (beam divergence extent)
// Δm = nσ × σm (mosaicity extent)
double delta_b = n_sigma * sigma_b * sigma_b_multiplier;
double delta_m = n_sigma * sigma_m;

// Extract experimental parameters needed for coordinate transformations
const auto [osc_start, osc_width] = scan.get_oscillation();
int image_range_start = scan.get_image_range()[0];
int image_range_end = scan.get_image_range()[1];
double wl = beam.get_wavelength();
Matrix3d d_matrix_inv = panel.get_d_matrix().inverse();

// Process each reflection individually
for (size_t i = 0; i < num_reflections; ++i) {
// Extract reflection centroid data
Eigen::Vector3d s1_c(
s1_vectors(i, 0), s1_vectors(i, 1), s1_vectors(i, 2)); // s₁ᶜ from s1_vectors
double phi_c = (phi_positions(i, 2)); // φᶜ from xyzcal.mm column

// Construct the Kabsch coordinate system for this reflection
// e1 = s₁ᶜ × s₀ / |s₁ᶜ × s₀| (perpendicular to scattering plane)
Eigen::Vector3d e1 = s1_c.cross(s0).normalized();
// e2 = s₁ᶜ × e₁ / |s₁ᶜ × e₁| (within scattering plane, orthogonal to e1)
Eigen::Vector3d e2 = s1_c.cross(e1).normalized();

double s1_len = s1_c.norm();

// Calculate s′ vectors at the four corners of the integration region
// These correspond to the extremes: (±Δb, ±Δb) in Kabsch coordinates
std::vector<Eigen::Vector3d> s_prime_vectors;
static constexpr std::array<std::pair<int, int>, 4> corner_signs = {
{{1, 1}, {1, -1}, {-1, 1}, {-1, -1}}};

for (auto [e1_sign, e2_sign] : corner_signs) {
// Project Δb divergences onto Kabsch basis vectors
// p represents the displacement in reciprocal space
Eigen::Vector3d p =
(e1_sign * delta_b * e1 / s1_len) + (e2_sign * delta_b * e2 / s1_len);

// Debug output for the Ewald sphere calculation
double p_magnitude = p.norm();
logger.trace(
"Reflection {}, corner ({},{}): p.norm()={:.6f}, s1_len={:.6f}, "
"delta_b={:.6f}",
i,
e1_sign,
e2_sign,
p_magnitude,
s1_len,
delta_b);

// Ensure the resulting s′ vector lies on the Ewald sphere
// This involves solving: |s′|² = |s₁ᶜ|² for the correct magnitude
double b = s1_len * s1_len - p.dot(p);
if (b < 0) {
logger.error(
"Negative b value: {:.6f} for reflection {} (p.dot(p)={:.6f}, "
"s1_len²={:.6f})",
b,
i,
p.dot(p),
s1_len * s1_len);
logger.error(
"This means the displacement vector is too large for the Ewald "
"sphere");
// Skip this corner or use a fallback approach
continue;
}
double d = -(p.dot(s1_c) / s1_len) + std::sqrt(b);

logger.trace("Reflection {}: b={:.6f}, d={:.6f}", i, b, d);

// Construct the s′ vector: s′ = (d × ŝ₁ᶜ) + p
Eigen::Vector3d s_prime = (d * s1_c / s1_len) + p;
s_prime_vectors.push_back(s_prime);
}

// Transform s′ vectors back to detector coordinates using Panel's get_ray_intersection
std::vector<std::pair<double, double>> detector_coords;
for (const auto &s_prime : s_prime_vectors) {
// Direct conversion from s′ vector to detector coordinates
// get_ray_intersection returns coordinates in mm
auto xy_mm_opt = panel.get_ray_intersection(s_prime);
if (!xy_mm_opt) {
continue; // Skip this corner if no intersection
}
std::array<double, 2> xy_mm = *xy_mm_opt;

// Convert from mm to pixels using the new mm_to_px function
std::array<double, 2> xy_pixels = panel.mm_to_px(xy_mm[0], xy_mm[1]);

detector_coords.push_back({xy_pixels[0], xy_pixels[1]});
}

// Determine the bounding box in detector coordinates
// Find minimum and maximum coordinates from the four corners
auto [min_x_it, max_x_it] = std::minmax_element(
detector_coords.begin(),
detector_coords.end(),
[](const auto &a, const auto &b) { return a.first < b.first; });
auto [min_y_it, max_y_it] = std::minmax_element(
detector_coords.begin(),
detector_coords.end(),
[](const auto &a, const auto &b) { return a.second < b.second; });

BoundingBoxExtents bbox;
// Use floor/ceil as specified in the paper: xmin = floor(min([x1,x2,x3,x4]))
bbox.x_min = static_cast<int>(std::floor(min_x_it->first));
bbox.x_max = static_cast<int>(std::ceil(max_x_it->first));
bbox.y_min = static_cast<int>(std::floor(min_y_it->second));
bbox.y_max = static_cast<int>(std::ceil(max_y_it->second));

// Calculate the image range (z-direction) using mosaicity parameter Δm
// The extent in φ depends on the geometry factor ζ = m₂ · e₁
double zeta = rot_axis.dot(e1);
if (std::abs(zeta) > ZETA_TOLERANCE) { // Avoid division by zero
// Convert angular extents to rotation angles: φ′ = φᶜ ± Δm/ζ
double phi_plus = phi_c + delta_m / zeta;
double phi_minus = phi_c - delta_m / zeta;

// Convert phi angles from radians to degrees before using scan parameters
double phi_plus_deg = phi_plus * 180.0 / M_PI;
double phi_minus_deg = phi_minus * 180.0 / M_PI;

// Transform rotation angles to image numbers using scan parameters
double z_plus =
image_range_start - 1 + ((phi_plus_deg - osc_start) / osc_width);
double z_minus =
image_range_start - 1 + ((phi_minus_deg - osc_start) / osc_width);

// Clamp to the actual image range and use floor/ceil for integer bounds
bbox.z_min =
std::max(image_range_start - 1,
static_cast<int>(std::floor(std::min(z_plus, z_minus))));
bbox.z_max = std::min(
image_range_end, static_cast<int>(std::ceil(std::max(z_plus, z_minus))));
} else {
// Handle degenerate case where reflection is parallel to rotation axis
// In this case, the reflection spans the entire image range
bbox.z_min = image_range_start;
bbox.z_max = image_range_end;
}

extents.push_back(bbox);
}

return extents;
}
Loading