libfs
Header-only C++11 library for accessing FreeSurfer neuroimaging data
Loading...
Searching...
No Matches
libfs.h
Go to the documentation of this file.
1#pragma once
2
3#include <iostream>
4#include <climits>
5#include <stdio.h>
6#include <vector>
7#include <fstream>
8#include <cassert>
9#include <sstream>
10#include <stdexcept>
11#include <map>
12#include <tuple>
13#include <unordered_set>
14#include <unordered_map>
15#include <cmath>
16#include <algorithm>
17#include <chrono>
18#include <cstdint>
19#include <cstring>
20
21// -- Optional MGZ / NIfTI-gz support via zlib -------------------------------------
22// When LIBFS_HAS_ZLIB is #defined before including this header, the following
23// become available:
24// - read_mgz() / write_mgz()
25// - read_nifti_gz() / write_nifti_gz() (and .nii.gz support in read_nifti() /
26// write_nifti() / read_desc_data())
27// Just link with -lz.
28//
29// There is NO auto-detection — you must explicitly opt in:
30// #define LIBFS_HAS_ZLIB
31// #include "libfs.h"
32//
33// If LIBFS_HAS_ZLIB is not defined, the MGZ / NIfTI-gz functions are simply
34// absent. Attempting to use them results in a compile error, and attempting
35// to read/write a .gz file via the generic read_nifti() / write_nifti() /
36// read_desc_data() functions throws a runtime_error at runtime.
37//
38// This is all compile-time; there is zero runtime overhead when zlib support
39// is not enabled.
40#ifdef LIBFS_HAS_ZLIB
41#include <zlib.h>
42#endif
43// -- End optional MGZ support -----------------------------------------------------
44
51#define LIBFS_VERSION "0.6.1"
52
59#define LIBFS_VERSION_MAJOR 0
60
65#define LIBFS_VERSION_MINOR 6
66
71#define LIBFS_VERSION_PATCH 1
72
73// -- Security / defensive hardening configuration -------------------------------------
74// Users can #define any of these BEFORE including libfs.h to override the defaults.
75
78#define LIBFS_MAX_ALLOC_BYTES_DEFAULT (2ULL * 1024ULL * 1024ULL * 1024ULL)
79
81#ifndef LIBFS_MAX_ALLOC_BYTES
82#define LIBFS_MAX_ALLOC_BYTES LIBFS_MAX_ALLOC_BYTES_DEFAULT
83#endif
84
86#ifndef LIBFS_MAX_STRING_LENGTH
87#define LIBFS_MAX_STRING_LENGTH 4096
88#endif
89
91#ifndef LIBFS_MAX_COLORTABLE_ENTRIES
92#define LIBFS_MAX_COLORTABLE_ENTRIES 10000
93#endif
94
97#ifndef LIBFS_MAX_OBJ_LINE_LENGTH
98#define LIBFS_MAX_OBJ_LINE_LENGTH 1048576 // 1 MiB
99#endif
100
102#ifndef LIBFS_MAX_OBJ_LINES
103#define LIBFS_MAX_OBJ_LINES 100000000 // 100M lines
104#endif
105
108#ifndef LIBFS_MAX_OBJ_FILE_SIZE
109#define LIBFS_MAX_OBJ_FILE_SIZE LIBFS_MAX_ALLOC_BYTES
110#endif
111
113#ifndef LIBFS_MAX_OFF_LINE_LENGTH
114#define LIBFS_MAX_OFF_LINE_LENGTH 1048576 // 1 MiB
115#endif
116
118#ifndef LIBFS_MAX_OFF_LINES
119#define LIBFS_MAX_OFF_LINES 100000000 // 100M lines
120#endif
121
123#ifndef LIBFS_MAX_OFF_FILE_SIZE
124#define LIBFS_MAX_OFF_FILE_SIZE LIBFS_MAX_ALLOC_BYTES
125#endif
126
128#ifndef LIBFS_MAX_PLY_LINE_LENGTH
129#define LIBFS_MAX_PLY_LINE_LENGTH 1048576 // 1 MiB
130#endif
131
133#ifndef LIBFS_MAX_PLY_LINES
134#define LIBFS_MAX_PLY_LINES 100000000 // 100M lines
135#endif
136
138#ifndef LIBFS_MAX_PLY_FILE_SIZE
139#define LIBFS_MAX_PLY_FILE_SIZE LIBFS_MAX_ALLOC_BYTES
140#endif
141// -- End security configuration --------------------------------------------------------
142
145
215#ifndef LIBFS_APPTAG
216#define LIBFS_APPTAG "[libfs] "
217#endif
218
226#define LIBFS_DBG_WARNING
227
228// If the user wants something below our default, remove our default.
229#ifdef LIBFS_DBG_NONE
230#undef LIBFS_DBG_WARNING
231#endif
232
233#ifdef LIBFS_DBG_CRITICAL
234#undef LIBFS_DBG_WARNING
235#endif
236
237#ifdef LIBFS_DBG_ERROR
238#undef LIBFS_DBG_WARNING
239#endif
240
241// Ensure that the user does not have to define all debug levels
242// up to the one they actually want, by defining all lower ones for them.
243#ifdef LIBFS_DBG_EXCESSIVE
244#define LIBFS_DBG_VERBOSE
245#endif
246
247#ifdef LIBFS_DBG_VERBOSE
248#define LIBFS_DBG_INFO
249#endif
250
251#ifdef LIBFS_DBG_INFO
252#define LIBFS_DBG_WARNING
253#endif
254
262#ifdef LIBFS_DBG_WARNING
263#define LIBFS_DBG_ERROR
264#endif
265
272#ifdef LIBFS_DBG_ERROR
273#define LIBFS_DBG_CRITICAL
274#endif
275
276// End of debug handling.
277
278namespace fs
279{
280
281 namespace util
282 {
283
287 tm _localtime(const std::time_t &time)
288 {
289 std::tm tm_snapshot;
290#if (defined(WIN32) || defined(_WIN32) || defined(__WIN32__))
291 ::localtime_s(&tm_snapshot, &time);
292#else
293 ::localtime_r(&time, &tm_snapshot); // POSIX
294#endif
295 return tm_snapshot;
296 }
297
306 std::string time_tag(std::chrono::system_clock::time_point t)
307 {
308 auto as_time_t = std::chrono::system_clock::to_time_t(t);
309 struct tm tm;
310 char time_buffer[64];
311 // if (::gmtime_r(&as_time_t, &tm)) {
312 tm = _localtime(as_time_t);
313 if (std::strftime(time_buffer, sizeof(time_buffer), "%F %T", &tm))
314 {
315 return std::string{time_buffer};
316 }
317 throw std::runtime_error("Failed to get current date as string");
318 }
319
321 const std::string LOGTAG_CRITICAL = "CRITICAL";
322
324 const std::string LOGTAG_ERROR = "ERROR";
325
327 const std::string LOGTAG_WARNING = "WARNING";
328
330 const std::string LOGTAG_INFO = "INFO";
331
333 const std::string LOGTAG_VERBOSE = "VERBOSE";
334
336 const std::string LOGTAG_EXCESSIVE = "EXCESSIVE";
337
341 inline void log(std::string const &message, std::string const loglevel = "INFO")
342 {
343#ifdef LIBFS_DBG_ERROR
344 std::cout << LIBFS_APPTAG << "[" << loglevel << "] [" << fs::util::time_tag(std::chrono::system_clock::now()) << "] " << message << "\n";
345#endif
346 }
347
348 // -- Security / defensive hardening helpers -----------------------------------------
349
352 inline bool safe_multiply(size_t a, size_t b, size_t &result)
353 {
354 if (a == 0 || b == 0)
355 {
356 result = 0;
357 return true;
358 }
359 if (a > std::numeric_limits<size_t>::max() / b)
360 {
361 return false;
362 }
363 result = a * b;
364 return true;
365 }
366
371 inline bool check_alloc(size_t num_elements, size_t bytes_per_element)
372 {
373 size_t total_bytes = 0;
374 if (!safe_multiply(num_elements, bytes_per_element, total_bytes))
375 {
376 return false;
377 }
378 if (total_bytes > LIBFS_MAX_ALLOC_BYTES)
379 {
380 return false;
381 }
382 return true;
383 }
384
387 inline size_t get_file_size(const std::string &filename)
388 {
389 std::ifstream ifs(filename, std::ios::binary | std::ios::ate);
390 if (!ifs.is_open())
391 {
392 return 0;
393 }
394 std::streampos end = ifs.tellg();
395 if (end < 0)
396 {
397 return 0;
398 }
399 return static_cast<size_t>(end);
400 }
401
404 inline bool is_finite_float(float value)
405 {
406 return !std::isnan(value) && !std::isinf(value);
407 }
408
409 // -- End security helpers ---------------------------------------------------------
410
419 inline bool ends_with(std::string const &value, std::string const &suffix)
420 {
421 if (suffix.size() > value.size())
422 return false;
423 return std::equal(suffix.rbegin(), suffix.rend(), value.rbegin());
424 }
425
434 inline bool ends_with(std::string const &value, std::initializer_list<std::string> suffixes)
435 {
436 for (auto suffix : suffixes)
437 {
438 if (ends_with(value, suffix))
439 {
440 return true;
441 }
442 }
443 return false;
444 }
445
458 template <typename T>
459 std::vector<std::vector<T>> v2d(std::vector<T> values, size_t num_cols)
460 {
461 std::vector<std::vector<T>> result;
462 for (std::size_t i = 0; i < values.size(); ++i)
463 {
464 if (i % num_cols == 0)
465 {
466 result.resize(result.size() + 1);
467 }
468 result[i / num_cols].push_back(values[i]);
469 }
470 return result;
471 }
472
483 template <typename T>
484 std::vector<T> vflatten(std::vector<std::vector<T>> values)
485 {
486 size_t total_size = 0;
487 for (std::size_t i = 0; i < values.size(); i++)
488 {
489 total_size += values[i].size();
490 }
491
492 std::vector<T> result = std::vector<T>(total_size);
493 size_t cur_idx = 0;
494 for (std::size_t i = 0; i < values.size(); i++)
495 {
496 for (std::size_t j = 0; j < values[i].size(); j++)
497 {
498 result[cur_idx] = values[i][j];
499 cur_idx++;
500 }
501 }
502 return result;
503 }
504
515 inline bool starts_with(std::string const &value, std::string const &prefix)
516 {
517 if (prefix.length() > value.length())
518 return false;
519 return value.rfind(prefix, 0) == 0;
520 }
521
532 inline bool starts_with(std::string const &value, std::initializer_list<std::string> prefixes)
533 {
534 for (auto prefix : prefixes)
535 {
536 if (starts_with(value, prefix))
537 {
538 return true;
539 }
540 }
541 return false;
542 }
543
555 inline bool file_exists(const std::string &name)
556 {
557 if (FILE *file = fopen(name.c_str(), "r"))
558 {
559 fclose(file);
560 return true;
561 }
562 else
563 {
564 return false;
565 }
566 }
567
583 std::string fullpath(std::initializer_list<std::string> path_components, std::string path_sep = std::string("/"))
584 {
585 std::string fp;
586 if (path_components.size() == 0)
587 {
588 throw std::invalid_argument("The 'path_components' must not be empty.");
589 }
590
591 std::string comp;
592 std::string comp_mod;
593 size_t idx = 0;
594 for (auto comp : path_components)
595 {
596 comp_mod = comp;
597 if (idx != 0)
598 { // We keep a leading slash intact for the first element (absolute path).
599 if (starts_with(comp, path_sep))
600 {
601 comp_mod = comp.substr(1, comp.size() - 1);
602 }
603 }
604
605 if (ends_with(comp_mod, path_sep))
606 {
607 comp_mod = comp_mod.substr(0, comp_mod.size() - 1);
608 }
609
610 fp += comp_mod;
611 if (idx < path_components.size() - 1)
612 {
613 fp += path_sep;
614 }
615 idx++;
616 }
617 return fp;
618 }
619
630 void str_to_file(const std::string &filename, const std::string rep)
631 {
632 std::ofstream ofs;
633 ofs.open(filename, std::ofstream::out);
634#ifdef LIBFS_DBG_VERBOSE
635 std::cout << LIBFS_APPTAG << "Opening file '" << filename << "' for writing.\n";
636#endif
637 if (ofs.is_open())
638 {
639 ofs << rep;
640 ofs.close();
641 }
642 else
643 {
644 throw std::runtime_error("Unable to open file '" + filename + "' for writing.\n");
645 }
646 }
647
686 std::vector<uint8_t> viridis(const std::vector<float> &data, float vmin = NAN, float vmax = NAN, uint8_t nan_r = 255, uint8_t nan_g = 255, uint8_t nan_b = 255)
687 {
688 std::vector<uint8_t> colors;
689 if (data.empty())
690 {
691 return colors;
692 }
693 colors.reserve(data.size() * 3);
694
695 // The official 256-entry Viridis colormap (RGB, floats in [0, 1]), identical to the
696 // matplotlib viridis lookup table. Linearly interpolated between samples below.
697 static const float lut[768] = {
698 0.267004, 0.004874, 0.329415, 0.26851, 0.009605, 0.335427, 0.269944, 0.014625,
699 0.341379, 0.271305, 0.019942, 0.347269, 0.272594, 0.025563, 0.353093, 0.273809,
700 0.031497, 0.358853, 0.274952, 0.037752, 0.364543, 0.276022, 0.044167, 0.370164,
701 0.277018, 0.050344, 0.375715, 0.277941, 0.056324, 0.381191, 0.278791, 0.062145,
702 0.386592, 0.279566, 0.067836, 0.391917, 0.280267, 0.073417, 0.397163, 0.280894,
703 0.078907, 0.402329, 0.281446, 0.08432, 0.407414, 0.281924, 0.089666, 0.412415,
704 0.282327, 0.094955, 0.417331, 0.282656, 0.100196, 0.42216, 0.28291, 0.105393,
705 0.426902, 0.283091, 0.110553, 0.431554, 0.283197, 0.11568, 0.436115, 0.283229,
706 0.120777, 0.440584, 0.283187, 0.125848, 0.44496, 0.283072, 0.130895, 0.449241,
707 0.282884, 0.13592, 0.453427, 0.282623, 0.140926, 0.457517, 0.28229, 0.145912,
708 0.46151, 0.281887, 0.150881, 0.465405, 0.281412, 0.155834, 0.469201, 0.280868,
709 0.160771, 0.472899, 0.280255, 0.165693, 0.476498, 0.279574, 0.170599, 0.479997,
710 0.278826, 0.17549, 0.483397, 0.278012, 0.180367, 0.486697, 0.277134, 0.185228,
711 0.489898, 0.276194, 0.190074, 0.493001, 0.275191, 0.194905, 0.496005, 0.274128,
712 0.199721, 0.498911, 0.273006, 0.20452, 0.501721, 0.271828, 0.209303, 0.504434,
713 0.270595, 0.214069, 0.507052, 0.269308, 0.218818, 0.509577, 0.267968, 0.223549,
714 0.512008, 0.26658, 0.228262, 0.514349, 0.265145, 0.232956, 0.516599, 0.263663,
715 0.237631, 0.518762, 0.262138, 0.242286, 0.520837, 0.260571, 0.246922, 0.522828,
716 0.258965, 0.251537, 0.524736, 0.257322, 0.25613, 0.526563, 0.255645, 0.260703,
717 0.528312, 0.253935, 0.265254, 0.529983, 0.252194, 0.269783, 0.531579, 0.250425,
718 0.27429, 0.533103, 0.248629, 0.278775, 0.534556, 0.246811, 0.283237, 0.535941,
719 0.244972, 0.287675, 0.53726, 0.243113, 0.292092, 0.538516, 0.241237, 0.296485,
720 0.539709, 0.239346, 0.300855, 0.540844, 0.237441, 0.305202, 0.541921, 0.235526,
721 0.309527, 0.542944, 0.233603, 0.313828, 0.543914, 0.231674, 0.318106, 0.544834,
722 0.229739, 0.322361, 0.545706, 0.227802, 0.326594, 0.546532, 0.225863, 0.330805,
723 0.547314, 0.223925, 0.334994, 0.548053, 0.221989, 0.339161, 0.548752, 0.220057,
724 0.343307, 0.549413, 0.21813, 0.347432, 0.550038, 0.21621, 0.351535, 0.550627,
725 0.214298, 0.355619, 0.551184, 0.212395, 0.359683, 0.55171, 0.210503, 0.363727,
726 0.552206, 0.208623, 0.367752, 0.552675, 0.206756, 0.371758, 0.553117, 0.204903,
727 0.375746, 0.553533, 0.203063, 0.379716, 0.553925, 0.201239, 0.38367, 0.554294,
728 0.19943, 0.387607, 0.554642, 0.197636, 0.391528, 0.554969, 0.19586, 0.395433,
729 0.555276, 0.1941, 0.399323, 0.555565, 0.192357, 0.403199, 0.555836, 0.190631,
730 0.407061, 0.556089, 0.188923, 0.41091, 0.556326, 0.187231, 0.414746, 0.556547,
731 0.185556, 0.41857, 0.556753, 0.183898, 0.422383, 0.556944, 0.182256, 0.426184,
732 0.55712, 0.180629, 0.429975, 0.557282, 0.179019, 0.433756, 0.55743, 0.177423,
733 0.437527, 0.557565, 0.175841, 0.44129, 0.557685, 0.174274, 0.445044, 0.557792,
734 0.172719, 0.448791, 0.557885, 0.171176, 0.45253, 0.557965, 0.169646, 0.456262,
735 0.55803, 0.168126, 0.459988, 0.558082, 0.166617, 0.463708, 0.558119, 0.165117,
736 0.467423, 0.558141, 0.163625, 0.471133, 0.558148, 0.162142, 0.474838, 0.55814,
737 0.160665, 0.47854, 0.558115, 0.159194, 0.482237, 0.558073, 0.157729, 0.485932,
738 0.558013, 0.15627, 0.489624, 0.557936, 0.154815, 0.493313, 0.55784, 0.153364,
739 0.497, 0.557724, 0.151918, 0.500685, 0.557587, 0.150476, 0.504369, 0.55743,
740 0.149039, 0.508051, 0.55725, 0.147607, 0.511733, 0.557049, 0.14618, 0.515413,
741 0.556823, 0.144759, 0.519093, 0.556572, 0.143343, 0.522773, 0.556295, 0.141935,
742 0.526453, 0.555991, 0.140536, 0.530132, 0.555659, 0.139147, 0.533812, 0.555298,
743 0.13777, 0.537492, 0.554906, 0.136408, 0.541173, 0.554483, 0.135066, 0.544853,
744 0.554029, 0.133743, 0.548535, 0.553541, 0.132444, 0.552216, 0.553018, 0.131172,
745 0.555899, 0.552459, 0.129933, 0.559582, 0.551864, 0.128729, 0.563265, 0.551229,
746 0.127568, 0.566949, 0.550556, 0.126453, 0.570633, 0.549841, 0.125394, 0.574318,
747 0.549086, 0.124395, 0.578002, 0.548287, 0.123463, 0.581687, 0.547445, 0.122606,
748 0.585371, 0.546557, 0.121831, 0.589055, 0.545623, 0.121148, 0.592739, 0.544641,
749 0.120565, 0.596422, 0.543611, 0.120092, 0.600104, 0.54253, 0.119738, 0.603785,
750 0.5414, 0.119512, 0.607464, 0.540218, 0.119423, 0.611141, 0.538982, 0.119483,
751 0.614817, 0.537692, 0.119699, 0.61849, 0.536347, 0.120081, 0.622161, 0.534946,
752 0.120638, 0.625828, 0.533488, 0.12138, 0.629492, 0.531973, 0.122312, 0.633153,
753 0.530398, 0.123444, 0.636809, 0.528763, 0.12478, 0.640461, 0.527068, 0.126326,
754 0.644107, 0.525311, 0.128087, 0.647749, 0.523491, 0.130067, 0.651384, 0.521608,
755 0.132268, 0.655014, 0.519661, 0.134692, 0.658636, 0.517649, 0.137339, 0.662252,
756 0.515571, 0.14021, 0.665859, 0.513427, 0.143303, 0.669459, 0.511215, 0.146616,
757 0.67305, 0.508936, 0.150148, 0.676631, 0.506589, 0.153894, 0.680203, 0.504172,
758 0.157851, 0.683765, 0.501686, 0.162016, 0.687316, 0.499129, 0.166383, 0.690856,
759 0.496502, 0.170948, 0.694384, 0.493803, 0.175707, 0.6979, 0.491033, 0.180653,
760 0.701402, 0.488189, 0.185783, 0.704891, 0.485273, 0.19109, 0.708366, 0.482284,
761 0.196571, 0.711827, 0.479221, 0.202219, 0.715272, 0.476084, 0.20803, 0.718701,
762 0.472873, 0.214, 0.722114, 0.469588, 0.220124, 0.725509, 0.466226, 0.226397,
763 0.728888, 0.462789, 0.232815, 0.732247, 0.459277, 0.239374, 0.735588, 0.455688,
764 0.24607, 0.73891, 0.452024, 0.252899, 0.742211, 0.448284, 0.259857, 0.745492,
765 0.444467, 0.266941, 0.748751, 0.440573, 0.274149, 0.751988, 0.436601, 0.281477,
766 0.755203, 0.432552, 0.288921, 0.758394, 0.428426, 0.296479, 0.761561, 0.424223,
767 0.304148, 0.764704, 0.419943, 0.311925, 0.767822, 0.415586, 0.319809, 0.770914,
768 0.411152, 0.327796, 0.77398, 0.40664, 0.335885, 0.777018, 0.402049, 0.344074,
769 0.780029, 0.397381, 0.35236, 0.783011, 0.392636, 0.360741, 0.785964, 0.387814,
770 0.369214, 0.788888, 0.382914, 0.377779, 0.791781, 0.377939, 0.386433, 0.794644,
771 0.372886, 0.395174, 0.797475, 0.367757, 0.404001, 0.800275, 0.362552, 0.412913,
772 0.803041, 0.357269, 0.421908, 0.805774, 0.35191, 0.430983, 0.808473, 0.346476,
773 0.440137, 0.811138, 0.340967, 0.449368, 0.813768, 0.335384, 0.458674, 0.816363,
774 0.329727, 0.468053, 0.818921, 0.323998, 0.477504, 0.821444, 0.318195, 0.487026,
775 0.823929, 0.312321, 0.496615, 0.826376, 0.306377, 0.506271, 0.828786, 0.300362,
776 0.515992, 0.831158, 0.294279, 0.525776, 0.833491, 0.288127, 0.535621, 0.835785,
777 0.281908, 0.545524, 0.838039, 0.275626, 0.555484, 0.840254, 0.269281, 0.565498,
778 0.84243, 0.262877, 0.575563, 0.844566, 0.256415, 0.585678, 0.846661, 0.249897,
779 0.595839, 0.848717, 0.243329, 0.606045, 0.850733, 0.236712, 0.616293, 0.852709,
780 0.230052, 0.626579, 0.854645, 0.223353, 0.636902, 0.856542, 0.21662, 0.647257,
781 0.8584, 0.209861, 0.657642, 0.860219, 0.203082, 0.668054, 0.861999, 0.196293,
782 0.678489, 0.863742, 0.189503, 0.688944, 0.865448, 0.182725, 0.699415, 0.867117,
783 0.175971, 0.709898, 0.868751, 0.169257, 0.720391, 0.87035, 0.162603, 0.730889,
784 0.871916, 0.156029, 0.741388, 0.873449, 0.149561, 0.751884, 0.874951, 0.143228,
785 0.762373, 0.876424, 0.137064, 0.772852, 0.877868, 0.131109, 0.783315, 0.879285,
786 0.125405, 0.79376, 0.880678, 0.120005, 0.804182, 0.882046, 0.114965, 0.814576,
787 0.883393, 0.110347, 0.82494, 0.88472, 0.106217, 0.83527, 0.886029, 0.102646,
788 0.845561, 0.887322, 0.099702, 0.85581, 0.888601, 0.097452, 0.866013, 0.889868,
789 0.095953, 0.876168, 0.891125, 0.09525, 0.886271, 0.892374, 0.095374, 0.89632,
790 0.893616, 0.096335, 0.906311, 0.894855, 0.098125, 0.916242, 0.896091, 0.100717,
791 0.926106, 0.89733, 0.104071, 0.935904, 0.89857, 0.108131, 0.945636, 0.899815,
792 0.112838, 0.9553, 0.901065, 0.118128, 0.964894, 0.902323, 0.123941, 0.974417,
793 0.90359, 0.130215, 0.983868, 0.904867, 0.136897, 0.993248, 0.906157, 0.143936,
794 };
795
796 const int n = 256;
797
798 bool auto_min = std::isnan(vmin);
799 bool auto_max = std::isnan(vmax);
800
801 // Determine the finite (non-NaN) min/max of the data, used for auto range.
802 float data_min = NAN;
803 float data_max = NAN;
804 bool have_finite = false;
805 for (size_t i = 0; i < data.size(); i++)
806 {
807 if (std::isnan(data[i]))
808 {
809 continue;
810 }
811 if (!have_finite)
812 {
813 data_min = data[i];
814 data_max = data[i];
815 have_finite = true;
816 }
817 else
818 {
819 if (data[i] < data_min)
820 {
821 data_min = data[i];
822 }
823 if (data[i] > data_max)
824 {
825 data_max = data[i];
826 }
827 }
828 }
829
830 float lo = auto_min ? data_min : vmin;
831 float hi = auto_max ? data_max : vmax;
832
833 if (!auto_min && !auto_max)
834 {
835 if (vmin > vmax)
836 {
837 throw std::invalid_argument("In viridis(): 'vmin' must not be greater than 'vmax'.");
838 }
839 }
840
841 if (!have_finite)
842 {
843 // All input values are NaN: map the whole vector to the configured NaN color.
844 for (size_t i = 0; i < data.size(); i++)
845 {
846 colors.push_back(nan_r);
847 colors.push_back(nan_g);
848 colors.push_back(nan_b);
849 }
850 return colors;
851 }
852
853 bool constant = (hi <= lo);
854
855 for (size_t i = 0; i < data.size(); i++)
856 {
857 if (std::isnan(data[i]))
858 {
859 colors.push_back(nan_r);
860 colors.push_back(nan_g);
861 colors.push_back(nan_b);
862 continue;
863 }
864
865 float t;
866 if (constant)
867 {
868 t = 0.5f;
869 }
870 else
871 {
872 t = (data[i] - lo) / (hi - lo);
873 if (t < 0.0f) { t = 0.0f; }
874 if (t > 1.0f) { t = 1.0f; }
875 }
876
877 float pos = t * (n - 1);
878 int idx0 = static_cast<int>(pos);
879 if (idx0 < 0) { idx0 = 0; }
880 if (idx0 > n - 2) { idx0 = n - 2; }
881 int idx1 = idx0 + 1;
882 float frac = pos - static_cast<float>(idx0);
883
884 for (int c = 0; c < 3; c++)
885 {
886 float val = lut[idx0 * 3 + c] * (1.0f - frac) + lut[idx1 * 3 + c] * frac;
887 int iv = static_cast<int>(val * 255.0f + 0.5f);
888 if (iv < 0) { iv = 0; }
889 if (iv > 255) { iv = 255; }
890 colors.push_back(static_cast<uint8_t>(iv));
891 }
892 }
893 return colors;
894 }
895 } // End namespace util.
896
897 // MRI data types, used by the MGH functions.
898
900 const int MRI_UCHAR = 0;
901
903 const int MRI_INT = 1;
904
906 const int MRI_FLOAT = 3;
907
909 const int MRI_SHORT = 4;
910
911 // Forward declarations.
912 int _fread3(std::istream &);
913 template <typename T>
914 T _freadt(std::istream &);
915 std::string _freadstringnewline(std::istream &);
916 std::string _freadfixedlengthstring(std::istream &, size_t, bool, size_t);
917 bool _ends_with(std::string const &fullString, std::string const &ending);
918 size_t _vidx_2d(size_t, size_t, size_t);
919 struct MghHeader;
920 struct Mgh;
921
922 // NIfTI-1 forward declarations (needed by read_desc_data).
923 void read_nifti(Mgh *, std::istream *, bool force_standard = false);
924 void read_nifti(Mgh *, const std::string &, bool force_standard = false);
925#ifdef LIBFS_HAS_ZLIB
926 inline void read_nifti_gz(Mgh *, const std::string &, bool force_standard = false);
927 inline void write_nifti_gz(const Mgh &, const std::string &);
928#endif
929
949 struct Mesh
950 {
951
953 Mesh(std::vector<float> cvertices, std::vector<int32_t> cfaces)
954 {
955 vertices = cvertices;
956 faces = cfaces;
957 }
958
968 Mesh(std::vector<std::vector<float>> cvertices, std::vector<std::vector<int32_t>> cfaces)
969 {
970 vertices = util::vflatten(cvertices);
971 faces = util::vflatten(cfaces);
972 }
973
975 Mesh() {}
976
977 std::vector<float> vertices;
978 std::vector<int32_t> faces;
979 std::vector<uint8_t> vertex_colors;
980 std::vector<float> vertex_normals;
981 std::vector<float> vertex_texcoords;
982
985 bool has_normals() const { return !vertex_normals.empty(); }
986
989 bool has_texcoords() const { return !vertex_texcoords.empty(); }
990
1002 {
1003 fs::Mesh mesh;
1004 mesh.vertices = {1.0, 1.0, 1.0,
1005 1.0, 1.0, -1.0,
1006 1.0, -1.0, 1.0,
1007 1.0, -1.0, -1.0,
1008 -1.0, 1.0, 1.0,
1009 -1.0, 1.0, -1.0,
1010 -1.0, -1.0, 1.0,
1011 -1.0, -1.0, -1.0};
1012 mesh.faces = {0, 2, 3,
1013 3, 1, 0,
1014 4, 7, 6,
1015 7, 4, 5,
1016 0, 5, 4,
1017 5, 0, 1,
1018 2, 6, 7,
1019 7, 3, 2,
1020 0, 4, 6,
1021 6, 2, 0,
1022 1, 7, 5,
1023 7, 1, 3};
1024 return mesh;
1025 }
1026
1039 {
1040 fs::Mesh mesh;
1041 mesh.vertices = {0.0, 0.0, 0.0, // start with 4x base
1042 0.0, 1.0, 0.0,
1043 1.0, 1.0, 0.0,
1044 1.0, 0.0, 0.0,
1045 0.5, 0.5, 1.0}; // apex
1046 mesh.faces = {0, 1, 2, // start with 2 base faces
1047 0, 2, 3,
1048 0, 4, 1, // now the 4 wall faces
1049 1, 4, 2,
1050 3, 2, 4,
1051 0, 3, 4};
1052 return mesh;
1053 }
1054
1071 static fs::Mesh construct_grid(const size_t nx = 4, const size_t ny = 5, const float distx = 1.0, const float disty = 1.0)
1072 {
1073 if (nx < 2 || ny < 2)
1074 {
1075 throw std::runtime_error("Parameters nx and ny must be at least 2.");
1076 }
1077 fs::Mesh mesh;
1078 size_t num_vertices = nx * ny;
1079 size_t num_faces = ((nx - 1) * (ny - 1)) * 2;
1080 std::vector<float> vertices;
1081 vertices.reserve(num_vertices * 3);
1082 std::vector<int> faces;
1083 faces.reserve(num_faces * 3);
1084
1085 // Create vertices.
1086 float cur_x, cur_y, cur_z;
1087 cur_x = cur_y = cur_z = 0.0;
1088 for (size_t i = 0; i < nx; i++)
1089 {
1090 cur_y = 0.0;
1091 for (size_t j = 0; j < ny; j++)
1092 {
1093 vertices.push_back(cur_x);
1094 vertices.push_back(cur_y);
1095 vertices.push_back(cur_z);
1096 cur_y += disty;
1097 }
1098 cur_x += distx;
1099 }
1100
1101 // Create faces.
1102 for (size_t i = 0; i < num_vertices; i++)
1103 {
1104 if ((i + 1) % ny == 0 || i >= num_vertices - ny)
1105 {
1106 // Do not use the last ones in row or column as source.
1107 continue;
1108 }
1109 // Add the upper left triangle of this grid cell.
1110 faces.push_back(int(i));
1111 faces.push_back(int(i + ny + 1));
1112 faces.push_back(int(i + 1));
1113 // Add the lower right triangle of this grid cell.
1114 faces.push_back(int(i));
1115 faces.push_back(int(i + ny + 1));
1116 faces.push_back(int(i + ny));
1117 }
1118
1119 mesh.vertices = vertices;
1120 mesh.faces = faces;
1121 return mesh;
1122 }
1123
1134 std::string to_obj() const
1135 {
1136 std::vector<uint8_t> empty_col;
1137 return (this->to_obj(empty_col));
1138 }
1139
1154 std::string to_obj(const std::vector<uint8_t> col) const
1155 {
1156 bool use_vertex_colors = col.size() != 0;
1157 bool use_normals = this->has_normals();
1158 bool use_texcoords = this->has_texcoords();
1159
1160 std::stringstream objs;
1161 for (size_t vidx = 0; vidx < this->vertices.size(); vidx += 3)
1162 { // vertex coords
1163 objs << "v " << vertices[vidx] << " " << vertices[vidx + 1] << " " << vertices[vidx + 2];
1164 if (use_vertex_colors)
1165 {
1166 if (col.size() != this->vertices.size())
1167 {
1168 throw std::invalid_argument("Number of vertex coordinates and vertex colors must match when writing OBJ file, but got " + std::to_string(this->vertices.size()) + " and " + std::to_string(col.size()) + ".");
1169 }
1170 objs << " " << (col[vidx] / 255.0f) << " " << (col[vidx + 1] / 255.0f) << " " << (col[vidx + 2] / 255.0f);
1171 }
1172 objs << "\n";
1173 }
1174
1175 // Emit texture coordinates if present.
1176 if (use_texcoords)
1177 {
1178 for (size_t vidx = 0; vidx < this->vertex_texcoords.size(); vidx += 2)
1179 {
1180 objs << "vt " << vertex_texcoords[vidx] << " " << vertex_texcoords[vidx + 1] << "\n";
1181 }
1182 }
1183
1184 // Emit vertex normals if present.
1185 if (use_normals)
1186 {
1187 for (size_t vidx = 0; vidx < this->vertex_normals.size(); vidx += 3)
1188 {
1189 objs << "vn " << vertex_normals[vidx] << " " << vertex_normals[vidx + 1] << " " << vertex_normals[vidx + 2] << "\n";
1190 }
1191 }
1192
1193 for (size_t fidx = 0; fidx < this->faces.size(); fidx += 3)
1194 { // faces: vertex indices, 1-based
1195 int v0 = faces[fidx] + 1;
1196 int v1 = faces[fidx + 1] + 1;
1197 int v2 = faces[fidx + 2] + 1;
1198
1199 objs << "f ";
1200 if (use_texcoords && use_normals)
1201 {
1202 objs << v0 << "/" << v0 << "/" << v0 << " "
1203 << v1 << "/" << v1 << "/" << v1 << " "
1204 << v2 << "/" << v2 << "/" << v2;
1205 }
1206 else if (use_texcoords)
1207 {
1208 objs << v0 << "/" << v0 << " "
1209 << v1 << "/" << v1 << " "
1210 << v2 << "/" << v2;
1211 }
1212 else if (use_normals)
1213 {
1214 objs << v0 << "//" << v0 << " "
1215 << v1 << "//" << v1 << " "
1216 << v2 << "//" << v2;
1217 }
1218 else
1219 {
1220 objs << v0 << " " << v1 << " " << v2;
1221 }
1222 objs << "\n";
1223 }
1224 return (objs.str());
1225 }
1226
1238 std::vector<std::vector<bool>> as_adjmatrix() const
1239 {
1240 std::vector<std::vector<bool>> adjm = std::vector<std::vector<bool>>(this->num_vertices(), std::vector<bool>(this->num_vertices(), false));
1241 for (size_t fidx = 0; fidx < this->faces.size(); fidx += 3)
1242 { // faces: vertex indices
1243 adjm[faces[fidx]][faces[fidx + 1]] = true;
1244 adjm[faces[fidx + 1]][faces[fidx]] = true;
1245 adjm[faces[fidx + 1]][faces[fidx + 2]] = true;
1246 adjm[faces[fidx + 2]][faces[fidx + 1]] = true;
1247 adjm[faces[fidx + 2]][faces[fidx]] = true;
1248 adjm[faces[fidx]][faces[fidx + 2]] = true;
1249 }
1250 return adjm;
1251 }
1252
1254 struct _tupleHashFunction
1255 {
1256 size_t operator()(const std::tuple<size_t, size_t> &x) const
1257 {
1258 size_t a = std::get<0>(x);
1259 size_t b = std::get<1>(x);
1260 return a ^ (b << 1) ^ (b >> (sizeof(size_t) * 8 - 1));
1261 }
1262 };
1263
1266 typedef std::unordered_set<std::tuple<size_t, size_t>, _tupleHashFunction> edge_set;
1267
1280 {
1281 edge_set edges;
1282 for (size_t fidx = 0; fidx < this->faces.size(); fidx += 3)
1283 { // faces: vertex indices
1284 edges.insert(std::make_tuple(faces[fidx], faces[fidx + 1]));
1285 edges.insert(std::make_tuple(faces[fidx + 1], faces[fidx]));
1286
1287 edges.insert(std::make_tuple(faces[fidx + 1], faces[fidx + 2]));
1288 edges.insert(std::make_tuple(faces[fidx + 2], faces[fidx + 1]));
1289
1290 edges.insert(std::make_tuple(faces[fidx], faces[fidx + 2]));
1291 edges.insert(std::make_tuple(faces[fidx + 2], faces[fidx]));
1292 }
1293 return edges;
1294 }
1295
1308 std::vector<std::vector<size_t>> as_adjlist(const bool via_matrix = true) const
1309 {
1310 if (!via_matrix)
1311 {
1312 return (this->_as_adjlist_via_edgeset());
1313 }
1314 std::vector<std::vector<bool>> adjm = this->as_adjmatrix();
1315 std::vector<std::vector<size_t>> adjl = std::vector<std::vector<size_t>>(this->num_vertices(), std::vector<size_t>());
1316 size_t nv = adjm.size();
1317 for (size_t i = 0; i < nv; i++)
1318 {
1319 for (size_t j = i + 1; j < nv; j++)
1320 {
1321 if (adjm[i][j] == true)
1322 {
1323 adjl[i].push_back(j);
1324 adjl[j].push_back(i);
1325 }
1326 }
1327 }
1328 return adjl;
1329 }
1330
1341 std::vector<std::vector<size_t>> _as_adjlist_via_edgeset() const
1342 {
1343 edge_set edges = this->as_edgelist();
1344 std::vector<std::vector<size_t>> adjl = std::vector<std::vector<size_t>>(this->num_vertices(), std::vector<size_t>());
1345 for (const std::tuple<size_t, size_t> &e : edges)
1346 {
1347 adjl[std::get<0>(e)].push_back(std::get<1>(e));
1348 }
1349 return adjl;
1350 }
1351
1367 std::vector<float> smooth_pvd_nn(const std::vector<float> pvd, const size_t num_iter = 1, const bool via_matrix = true, const bool with_nan = true, const bool detect_nan = true) const
1368 {
1369
1370 const std::vector<std::vector<size_t>> adjlist = this->as_adjlist(via_matrix);
1371 return fs::Mesh::smooth_pvd_nn(adjlist, pvd, num_iter, with_nan, detect_nan);
1372 }
1373
1390 static std::vector<float> smooth_pvd_nn(const std::vector<std::vector<size_t>> mesh_adj, const std::vector<float> pvd, const size_t num_iter = 1, const bool with_nan = true, const bool detect_nan = true)
1391 {
1392 assert(pvd.size() == mesh_adj.size());
1393 bool final_with_nan = with_nan;
1394 if (detect_nan)
1395 {
1396 final_with_nan = false;
1397 for (size_t i = 0; i < pvd.size(); i++)
1398 {
1399 if (std::isnan(pvd[i]))
1400 {
1401 final_with_nan = true;
1402 break;
1403 }
1404 }
1405 }
1406 if (final_with_nan)
1407 {
1408 return fs::Mesh::_smooth_pvd_nn_nan(mesh_adj, pvd, num_iter);
1409 }
1410 std::vector<float> current_pvd_source;
1411 std::vector<float> current_pvd_smoothed = std::vector<float>(pvd.size());
1412
1413 float val_sum;
1414 size_t num_neigh;
1415 for (size_t i = 0; i < num_iter; i++)
1416 {
1417 if (i == 0)
1418 {
1419 current_pvd_source = pvd;
1420 }
1421 else
1422 {
1423 current_pvd_source = current_pvd_smoothed;
1424 }
1425 for (size_t v_idx = 0; v_idx < mesh_adj.size(); v_idx++)
1426 {
1427 num_neigh = mesh_adj[v_idx].size();
1428 val_sum = current_pvd_source[v_idx] / (num_neigh + 1);
1429 for (size_t neigh_rel_idx = 0; neigh_rel_idx < num_neigh; neigh_rel_idx++)
1430 {
1431 val_sum += current_pvd_source[mesh_adj[v_idx][neigh_rel_idx]] / (num_neigh + 1);
1432 }
1433 current_pvd_smoothed[v_idx] = val_sum;
1434 }
1435 }
1436 return current_pvd_smoothed;
1437 }
1438
1455 static std::vector<float> _smooth_pvd_nn_nan(const std::vector<std::vector<size_t>> mesh_adj, const std::vector<float> pvd, const size_t num_iter = 1)
1456 {
1457 std::vector<float> current_pvd_source;
1458 std::vector<float> current_pvd_smoothed = std::vector<float>(pvd.size());
1459
1460 float val_sum;
1461 size_t num_neigh;
1462 size_t num_non_nan_values;
1463 float neigh_val;
1464 for (size_t i = 0; i < num_iter; i++)
1465 {
1466
1467 if (i == 0)
1468 {
1469 current_pvd_source = pvd;
1470 }
1471 else
1472 {
1473 current_pvd_source = current_pvd_smoothed;
1474 }
1475
1476 for (size_t v_idx = 0; v_idx < mesh_adj.size(); v_idx++)
1477 {
1478 if (std::isnan(current_pvd_source[v_idx]))
1479 {
1480 current_pvd_smoothed[v_idx] = NAN;
1481 continue;
1482 }
1483 val_sum = current_pvd_source[v_idx];
1484 num_non_nan_values = 1; // If we get here, the source vertex value is not NAN.
1485 num_neigh = mesh_adj[v_idx].size();
1486 for (size_t neigh_rel_idx = 0; neigh_rel_idx < num_neigh; neigh_rel_idx++)
1487 {
1488 neigh_val = current_pvd_source[mesh_adj[v_idx][neigh_rel_idx]];
1489 if (std::isnan(neigh_val))
1490 {
1491 continue;
1492 }
1493 else
1494 {
1495 val_sum += neigh_val;
1496 num_non_nan_values++;
1497 }
1498 }
1499 current_pvd_smoothed[v_idx] = val_sum / (float)num_non_nan_values;
1500 }
1501 }
1502 return current_pvd_smoothed;
1503 }
1504
1511 static std::vector<std::vector<size_t>> extend_adj(const std::vector<std::vector<size_t>> mesh_adj, const size_t extend_by = 1, std::vector<std::vector<size_t>> mesh_adj_ext = std::vector<std::vector<size_t>>())
1512 {
1513 size_t num_vertices = mesh_adj.size();
1514 if (mesh_adj_ext.size() == 0)
1515 {
1516 mesh_adj_ext = mesh_adj;
1517 }
1518 std::vector<size_t> neighborhood;
1519 std::vector<size_t> ext_neighborhood;
1520 for (size_t ext_idx = 0; ext_idx < extend_by; ext_idx++)
1521 {
1522 for (size_t source_vert_idx = 0; source_vert_idx < num_vertices; source_vert_idx++)
1523 {
1524 neighborhood = mesh_adj_ext[source_vert_idx]; // copy needed so we do not modify during iteration.
1525 // Extension: add all neighbors in distance one for all vertices in the neighborhood.
1526 for (size_t neigh_vert_rel_idx = 0; neigh_vert_rel_idx < neighborhood.size(); neigh_vert_rel_idx++)
1527 {
1528 for (size_t canidate_rel_idx = 0; canidate_rel_idx < mesh_adj[neighborhood[neigh_vert_rel_idx]].size(); canidate_rel_idx++)
1529 {
1530 if (mesh_adj[neighborhood[neigh_vert_rel_idx]][canidate_rel_idx] != source_vert_idx)
1531 {
1532 mesh_adj_ext[source_vert_idx].push_back(mesh_adj[neighborhood[neigh_vert_rel_idx]][canidate_rel_idx]);
1533 }
1534 }
1535 }
1536 // We need to remove duplicates.
1537 std::sort(mesh_adj_ext[source_vert_idx].begin(), mesh_adj_ext[source_vert_idx].end());
1538 mesh_adj_ext[source_vert_idx].erase(std::unique(mesh_adj_ext[source_vert_idx].begin(), mesh_adj_ext[source_vert_idx].end()), mesh_adj_ext[source_vert_idx].end());
1539 }
1540 }
1541 return mesh_adj_ext;
1542 }
1543
1556 void to_obj_file(const std::string &filename) const
1557 {
1558 fs::util::str_to_file(filename, this->to_obj());
1559 }
1560
1563 void to_obj_file(const std::string &filename, const std::vector<uint8_t> col) const
1564 {
1565 fs::util::str_to_file(filename, this->to_obj(col));
1566 }
1567
1584 std::pair<std::unordered_map<int32_t, int32_t>, fs::Mesh> submesh_vertex(const std::vector<int32_t> &old_vertex_indices, const bool mapdir_fulltosubmesh = false) const
1585 {
1586 fs::Mesh submesh;
1587 std::vector<float> new_vertices;
1588 std::vector<int> new_faces;
1589 std::unordered_map<int32_t, int32_t> vertex_index_map_full2submesh;
1590 int32_t new_vertex_idx = 0;
1591 for (size_t i = 0; i < old_vertex_indices.size(); i++)
1592 {
1593 vertex_index_map_full2submesh[old_vertex_indices[i]] = new_vertex_idx;
1594 new_vertices.push_back(this->vertices[size_t(old_vertex_indices[i]) * 3]);
1595 new_vertices.push_back(this->vertices[size_t(old_vertex_indices[i]) * 3 + 1]);
1596 new_vertices.push_back(this->vertices[size_t(old_vertex_indices[i]) * 3 + 2]);
1597 new_vertex_idx++;
1598 }
1599 int face_v0;
1600 int face_v1;
1601 int face_v2;
1602 for (size_t i = 0; i < this->num_faces(); i++)
1603 {
1604 face_v0 = this->faces[i * 3];
1605 face_v1 = this->faces[i * 3 + 1];
1606 face_v2 = this->faces[i * 3 + 2];
1607 if ((vertex_index_map_full2submesh.find(face_v0) != vertex_index_map_full2submesh.end()) && (vertex_index_map_full2submesh.find(face_v1) != vertex_index_map_full2submesh.end()) && (vertex_index_map_full2submesh.find(face_v2) != vertex_index_map_full2submesh.end()))
1608 {
1609 new_faces.push_back(vertex_index_map_full2submesh[face_v0]);
1610 new_faces.push_back(vertex_index_map_full2submesh[face_v1]);
1611 new_faces.push_back(vertex_index_map_full2submesh[face_v2]);
1612 }
1613 }
1614 submesh.vertices = new_vertices;
1615 submesh.faces = new_faces;
1616
1617 std::pair<std::unordered_map<int32_t, int32_t>, fs::Mesh> result;
1618 if (!mapdir_fulltosubmesh)
1619 { // Compute the new2old (reverse) vertex index map:
1620 std::unordered_map<int32_t, int32_t> vertex_index_map_submesh2full;
1621 for (auto const &pair : vertex_index_map_full2submesh)
1622 {
1623 vertex_index_map_submesh2full[pair.second] = pair.first;
1624 }
1625 result = std::pair<std::unordered_map<int32_t, int32_t>, fs::Mesh>(vertex_index_map_submesh2full, submesh);
1626 }
1627 else
1628 {
1629 result = std::pair<std::unordered_map<int32_t, int32_t>, fs::Mesh>(vertex_index_map_full2submesh, submesh);
1630 }
1631
1632 return result;
1633 }
1634
1651 static std::vector<float> curv_data_for_orig_mesh(const std::vector<float> data_submesh, const std::unordered_map<int32_t, int32_t> submesh_to_orig_mapping, const int32_t orig_mesh_num_vertices, const float fill_value = std::numeric_limits<float>::quiet_NaN())
1652 {
1653
1654 if (submesh_to_orig_mapping.size() != data_submesh.size())
1655 {
1656 throw std::domain_error("The number of vertices of the submesh and the number of values in the submesh_to_orig_mapping do not match: got " + std::to_string(data_submesh.size()) + " and " + std::to_string(submesh_to_orig_mapping.size()) + ".");
1657 }
1658
1659 std::vector<float> data_orig_mesh(orig_mesh_num_vertices, fill_value);
1660 for (size_t i = 0; i < data_submesh.size(); i++)
1661 {
1662 auto got = submesh_to_orig_mapping.find(int(i));
1663 if (got != submesh_to_orig_mapping.end())
1664 {
1665 data_orig_mesh[got->second] = data_submesh[i];
1666 }
1667 }
1668 return (data_orig_mesh);
1669 }
1670
1686 static void from_obj(Mesh *mesh, std::istream *is, const bool preserve_vertex_indices = false)
1687 {
1688 // -- Security: null-pointer check (plan #8) --
1689 if (!mesh)
1690 {
1691 throw std::invalid_argument("mesh pointer must not be null");
1692 }
1693
1694 std::string line;
1695 int line_idx = -1;
1696 size_t total_lines_processed = 0;
1697
1698 std::vector<float> vertices;
1699 std::vector<int> faces;
1700 std::vector<uint8_t> vertex_colors;
1701 std::vector<float> raw_normals; // raw `vn` data: 3 floats per normal, 1-based indexed
1702 std::vector<float> raw_texcoords; // raw `vt` data: 2 floats per texcoord, 1-based indexed
1703 std::vector<int> face_vt_indices; // per-face-vertex texcoord index (0 = absent), parallel to `faces`
1704 std::vector<int> face_vn_indices; // per-face-vertex normal index (0 = absent), parallel to `faces`
1705 bool has_any_vt = false;
1706 bool has_any_vn = false;
1707 int detected_format = -1; // -1 = unknown, 0 = no vertex colors, 1 = has vertex colors (r g b after x y z)
1708
1709#ifdef LIBFS_DBG_INFO
1710 size_t num_lines_ignored = 0; // Not comments, but custom extensions or material data lines which are ignored by libfs.
1711#endif
1712
1713 while (total_lines_processed < LIBFS_MAX_OBJ_LINES)
1714 {
1715 // -- Use std::getline for performance (buffered I/O), then post-check length (plan #10) --
1716 if (!std::getline(*is, line))
1717 {
1718 break; // EOF or read error
1719 }
1720 total_lines_processed++;
1721 line_idx++;
1722
1723 if (line.size() > LIBFS_MAX_OBJ_LINE_LENGTH)
1724 {
1725 throw std::runtime_error("OBJ line " + std::to_string(line_idx + 1) +
1726 " exceeds maximum allowed line length of " +
1727 std::to_string(LIBFS_MAX_OBJ_LINE_LENGTH) + " bytes.\n");
1728 }
1729
1730 // -- Security: check allocation limits before parsing this line (plan #5) --
1731 if (!util::check_alloc(vertices.size() + 3, sizeof(float)) ||
1732 !util::check_alloc(faces.size() + 12, sizeof(int)) ||
1733 !util::check_alloc(vertex_colors.size() + 3, sizeof(uint8_t)))
1734 {
1735 throw std::runtime_error("OBJ data exceeds maximum allowed memory allocation (" + std::to_string(LIBFS_MAX_ALLOC_BYTES) + " bytes).\n");
1736 }
1737
1738 std::istringstream iss(line);
1739 if (fs::util::starts_with(line, "#"))
1740 {
1741 continue; // skip comment.
1742 }
1743 else
1744 {
1745 if (fs::util::starts_with(line, "v "))
1746 {
1747 std::string elem_type_identifier;
1748 float x, y, z;
1749 if (!(iss >> elem_type_identifier >> x >> y >> z))
1750 {
1751 throw std::domain_error("Could not parse vertex line " + std::to_string(line_idx + 1) + " of OBJ data, invalid format.\n");
1752 }
1753 assert(elem_type_identifier == "v");
1754
1755 // -- Security: validate finite coordinates --
1756 if (!util::is_finite_float(x) || !util::is_finite_float(y) || !util::is_finite_float(z))
1757 {
1758 throw std::domain_error("Non-finite vertex coordinate on line " + std::to_string(line_idx + 1) + " of OBJ data.\n");
1759 }
1760
1761 vertices.push_back(x);
1762 vertices.push_back(y);
1763 vertices.push_back(z);
1764
1765 // Check for optional per-vertex colors: 6-value lines (x y z r g b) have colors,
1766 // 3-value lines (x y z) and 4-value lines (x y z w) do not.
1767 // Detect the format from the first vertex line.
1768 if (detected_format == -1)
1769 {
1770 float vr, vg, vb;
1771 if ((iss >> vr >> vg >> vb))
1772 {
1773 // We read 3 more floats successfully. Check if there is even more data
1774 // (e.g., x y z w nx ny nz) — if so, treat as no-colors format.
1775 float extra;
1776 if (iss >> extra)
1777 {
1778 detected_format = 0;
1779 }
1780 else
1781 {
1782 detected_format = 1;
1783 // Store colors for the first vertex (already consumed from stream).
1784 int ri = static_cast<int>(vr * 255.0f + 0.5f);
1785 int gi = static_cast<int>(vg * 255.0f + 0.5f);
1786 int bi = static_cast<int>(vb * 255.0f + 0.5f);
1787 if (ri < 0) { ri = 0; }
1788 if (ri > 255) { ri = 255; }
1789 if (gi < 0) { gi = 0; }
1790 if (gi > 255) { gi = 255; }
1791 if (bi < 0) { bi = 0; }
1792 if (bi > 255) { bi = 255; }
1793 vertex_colors.push_back(static_cast<uint8_t>(ri));
1794 vertex_colors.push_back(static_cast<uint8_t>(gi));
1795 vertex_colors.push_back(static_cast<uint8_t>(bi));
1796 }
1797 }
1798 else
1799 {
1800 detected_format = 0;
1801 }
1802 }
1803 else if (detected_format == 1)
1804 {
1805 // Read colors for subsequent vertices.
1806 float vr, vg, vb;
1807 if (!(iss >> vr >> vg >> vb))
1808 {
1809 throw std::domain_error("Expected vertex colors (r g b) on line " + std::to_string(line_idx + 1) + " of OBJ data, but could not parse them.\n");
1810 }
1811 int ri = static_cast<int>(vr * 255.0f + 0.5f);
1812 int gi = static_cast<int>(vg * 255.0f + 0.5f);
1813 int bi = static_cast<int>(vb * 255.0f + 0.5f);
1814 if (ri < 0) { ri = 0; }
1815 if (ri > 255) { ri = 255; }
1816 if (gi < 0) { gi = 0; }
1817 if (gi > 255) { gi = 255; }
1818 if (bi < 0) { bi = 0; }
1819 if (bi > 255) { bi = 255; }
1820 vertex_colors.push_back(static_cast<uint8_t>(ri));
1821 vertex_colors.push_back(static_cast<uint8_t>(gi));
1822 vertex_colors.push_back(static_cast<uint8_t>(bi));
1823 }
1824 }
1825 else if (!preserve_vertex_indices && fs::util::starts_with(line, "vn "))
1826 {
1827 // -- Feature: parse vertex normals (plan #3) --
1828 std::string elem_type_identifier;
1829 float nx, ny, nz;
1830 if (!(iss >> elem_type_identifier >> nx >> ny >> nz))
1831 {
1832 throw std::domain_error("Could not parse vertex normal line " + std::to_string(line_idx + 1) + " of OBJ data, invalid format.\n");
1833 }
1834 assert(elem_type_identifier == "vn");
1835 if (!util::is_finite_float(nx) || !util::is_finite_float(ny) || !util::is_finite_float(nz))
1836 {
1837 throw std::domain_error("Non-finite vertex normal on line " + std::to_string(line_idx + 1) + " of OBJ data.\n");
1838 }
1839 raw_normals.push_back(nx);
1840 raw_normals.push_back(ny);
1841 raw_normals.push_back(nz);
1842 }
1843 else if (!preserve_vertex_indices && fs::util::starts_with(line, "vt "))
1844 {
1845 // -- Feature: parse texture coordinates (plan #4) --
1846 std::string elem_type_identifier;
1847 float u, v;
1848 if (!(iss >> elem_type_identifier >> u >> v))
1849 {
1850 throw std::domain_error("Could not parse texture coordinate line " + std::to_string(line_idx + 1) + " of OBJ data, invalid format.\n");
1851 }
1852 assert(elem_type_identifier == "vt");
1853 if (!util::is_finite_float(u) || !util::is_finite_float(v))
1854 {
1855 throw std::domain_error("Non-finite texture coordinate on line " + std::to_string(line_idx + 1) + " of OBJ data.\n");
1856 }
1857 raw_texcoords.push_back(u);
1858 raw_texcoords.push_back(v);
1859 // Ignore optional 3rd component (w), if present.
1860 }
1861 else if (fs::util::starts_with(line, "f "))
1862 {
1863 std::string elem_type_identifier;
1864
1865 // -- Feature: read all vertex tokens (plan #1) --
1866 if (!(iss >> elem_type_identifier))
1867 {
1868 throw std::domain_error("Could not parse face line " + std::to_string(line_idx + 1) + " of OBJ data, invalid format.\n");
1869 }
1870 assert(elem_type_identifier == "f");
1871
1872 std::vector<std::string> face_tokens;
1873 {
1874 std::string token;
1875 while (iss >> token)
1876 {
1877 face_tokens.push_back(token);
1878 }
1879 }
1880
1881 if (face_tokens.size() < 3)
1882 {
1883 throw std::domain_error("Face line " + std::to_string(line_idx + 1) + " has fewer than 3 vertices, invalid format.\n");
1884 }
1885
1886 // -- Warning for quads/n-gons (plan #13) --
1887#ifdef LIBFS_DBG_WARNING
1888 if (face_tokens.size() > 3)
1889 {
1890 std::cout << LIBFS_APPTAG << "[WARNING] Face line " << (line_idx + 1)
1891 << " has " << face_tokens.size() << " vertices; fan-triangulating.\n";
1892 }
1893#endif
1894
1895 // Parse all vertex indices from tokens, properly splitting v/vt/vn.
1896 std::vector<int> raw_indices;
1897 std::vector<int> tok_vt_indices;
1898 std::vector<int> tok_vn_indices;
1899 raw_indices.reserve(face_tokens.size());
1900 tok_vt_indices.reserve(face_tokens.size());
1901 tok_vn_indices.reserve(face_tokens.size());
1902 for (size_t ti = 0; ti < face_tokens.size(); ti++)
1903 {
1904 const std::string &token = face_tokens[ti];
1905
1906 // Split "v/vt/vn", "v//vn", "v/vt", or just "v".
1907 // Count slashes to determine format.
1908 size_t slash1 = token.find('/');
1909 std::string v_part, vt_part, vn_part;
1910 if (slash1 == std::string::npos)
1911 {
1912 // "v" only
1913 v_part = token;
1914 }
1915 else
1916 {
1917 v_part = token.substr(0, slash1);
1918 size_t slash2 = token.find('/', slash1 + 1);
1919 if (slash2 == std::string::npos)
1920 {
1921 // "v/vt" — texcoord only, no normal
1922 vt_part = token.substr(slash1 + 1);
1923 }
1924 else
1925 {
1926 // "v//vn" or "v/vt/vn"
1927 if (slash2 == slash1 + 1)
1928 {
1929 // "v//vn" — empty texcoord field
1930 vn_part = token.substr(slash2 + 1);
1931 }
1932 else
1933 {
1934 // "v/vt/vn"
1935 vt_part = token.substr(slash1 + 1, slash2 - slash1 - 1);
1936 vn_part = token.substr(slash2 + 1);
1937 }
1938 }
1939 }
1940
1941 // Parse vertex index.
1942 int vi;
1943 try { vi = std::stoi(v_part); }
1944 catch (const std::invalid_argument &) {
1945 throw std::domain_error("Invalid face vertex index '" + v_part + "' on line " + std::to_string(line_idx + 1) + " of OBJ data.\n");
1946 }
1947 catch (const std::out_of_range &) {
1948 throw std::domain_error("Face vertex index '" + v_part + "' out of integer range on line " + std::to_string(line_idx + 1) + " of OBJ data.\n");
1949 }
1950 raw_indices.push_back(vi);
1951
1952 // Parse optional texcoord index.
1953 if (!preserve_vertex_indices)
1954 {
1955 int vti = 0;
1956 if (!vt_part.empty())
1957 {
1958 try { vti = std::stoi(vt_part); }
1959 catch (const std::invalid_argument &) {
1960 throw std::domain_error("Invalid texcoord index '" + vt_part + "' on line " + std::to_string(line_idx + 1) + " of OBJ data.\n");
1961 }
1962 catch (const std::out_of_range &) {
1963 throw std::domain_error("Texcoord index '" + vt_part + "' out of integer range on line " + std::to_string(line_idx + 1) + " of OBJ data.\n");
1964 }
1965 has_any_vt = true;
1966 }
1967 tok_vt_indices.push_back(vti);
1968 }
1969
1970 // Parse optional normal index.
1971 if (!preserve_vertex_indices)
1972 {
1973 int vni = 0;
1974 if (!vn_part.empty())
1975 {
1976 try { vni = std::stoi(vn_part); }
1977 catch (const std::invalid_argument &) {
1978 throw std::domain_error("Invalid normal index '" + vn_part + "' on line " + std::to_string(line_idx + 1) + " of OBJ data.\n");
1979 }
1980 catch (const std::out_of_range &) {
1981 throw std::domain_error("Normal index '" + vn_part + "' out of integer range on line " + std::to_string(line_idx + 1) + " of OBJ data.\n");
1982 }
1983 has_any_vn = true;
1984 }
1985 tok_vn_indices.push_back(vni);
1986 }
1987 }
1988
1989 // -- Feature: fan triangulation for quads and n-gons (plan #1) --
1990 // Emit triangles: (v[0], v[1], v[2]), (v[0], v[2], v[3]), ...
1991 // Indices are stored as-is (1-based, possibly negative) and resolved in the post-parse pass.
1992 for (size_t ti = 1; ti + 1 < raw_indices.size(); ti++)
1993 {
1994 // Vertex indices
1995 faces.push_back(raw_indices[0]);
1996 faces.push_back(raw_indices[ti]);
1997 faces.push_back(raw_indices[ti + 1]);
1998 // Parallel texcoord indices
1999 if (!preserve_vertex_indices)
2000 {
2001 face_vt_indices.push_back(tok_vt_indices[0]);
2002 face_vt_indices.push_back(tok_vt_indices[ti]);
2003 face_vt_indices.push_back(tok_vt_indices[ti + 1]);
2004 }
2005 // Parallel normal indices
2006 if (!preserve_vertex_indices)
2007 {
2008 face_vn_indices.push_back(tok_vn_indices[0]);
2009 face_vn_indices.push_back(tok_vn_indices[ti]);
2010 face_vn_indices.push_back(tok_vn_indices[ti + 1]);
2011 }
2012 }
2013 }
2014 else
2015 {
2016#ifdef LIBFS_DBG_INFO
2017 num_lines_ignored++;
2018#endif
2019
2020 continue;
2021 }
2022 }
2023 }
2024
2025 // -- Security: check if we hit the max-lines limit (plan #11) --
2026 if (total_lines_processed >= LIBFS_MAX_OBJ_LINES && !is->eof())
2027 {
2028 throw std::runtime_error("OBJ file exceeds maximum allowed line count of " + std::to_string(LIBFS_MAX_OBJ_LINES) + ".\n");
2029 }
2030
2031#ifdef LIBFS_DBG_INFO
2032 if (num_lines_ignored > 0)
2033 {
2034 std::cout << LIBFS_APPTAG << "Ignored " << num_lines_ignored << " lines in Wavefront OBJ format mesh file.\n";
2035 }
2036#endif
2037
2038 // -- Post-parse: resolve negative indices and convert to 0-based (plan #2, #6, #12) --
2039 int32_t nv = static_cast<int32_t>(vertices.size() / 3);
2040 if (nv == 0)
2041 {
2042 throw std::domain_error("OBJ file contains no vertices.\n");
2043 }
2044
2045 for (size_t fi = 0; fi < faces.size(); fi++)
2046 {
2047 int idx = faces[fi];
2048 if (idx == 0)
2049 {
2050 throw std::domain_error("Face index 0 in OBJ data: OBJ indices are 1-based, index 0 is invalid.\n");
2051 }
2052 if (idx < 0)
2053 {
2054 // -- Security: guard against integer overflow in negative-index resolution (plan #12) --
2055 if (idx < -nv)
2056 {
2057 throw std::domain_error("Negative face index " + std::to_string(idx) + " exceeds vertex count " + std::to_string(nv) + ".\n");
2058 }
2059 // Convert negative 1-based-relative to 0-based: -1 → nv-1, -2 → nv-2, etc.
2060 idx = nv + idx;
2061 }
2062 else
2063 {
2064 // Convert positive 1-based to 0-based.
2065 idx = idx - 1;
2066 }
2067
2068 // -- Security: validate face index range (plan #6) --
2069 if (idx < 0 || idx >= nv)
2070 {
2071 throw std::domain_error("Face index " + std::to_string(idx) + " out of range [0, " + std::to_string(nv - 1) + "] after resolution.\n");
2072 }
2073
2074 faces[fi] = idx;
2075 }
2076
2077 // -- Integrity: validate face count is a multiple of 3 (plan #14) --
2078 if (faces.size() % 3 != 0)
2079 {
2080 throw std::domain_error("Internal error: parsed face count " + std::to_string(faces.size()) + " is not a multiple of 3.\n");
2081 }
2082
2083 // -- Post-parse: if vertex index preservation was requested, keep the
2084 // file's vertex list and resolved 0-based faces as-is (no expansion).
2085 if (preserve_vertex_indices)
2086 {
2087 // faces have already been resolved to 0-based indices above, and
2088 // vertices are still in file order, so this gives a 1:1 mapping:
2089 // file vertex at position N ends up at mesh index N-1.
2090 mesh->vertices = vertices;
2091 mesh->faces = faces;
2093 mesh->vertex_normals.clear();
2094 mesh->vertex_texcoords.clear();
2095 return;
2096 }
2097
2098 // -- Post-parse: vertex deduplication by (position, texcoord, normal) tuple --
2099 // OBJ's data model is per-face-corner: the same vertex position can appear
2100 // with different texcoords/normals in different faces (at texture seams and
2101 // sharp edges). We build a new vertex list where each unique combination
2102 // becomes its own vertex, correctly handling these cases.
2103 int32_t num_normals = static_cast<int32_t>(raw_normals.size() / 3);
2104 int32_t num_texcoords = static_cast<int32_t>(raw_texcoords.size() / 2);
2105
2106 if (has_any_vn && num_normals == 0)
2107 {
2108 throw std::domain_error("OBJ file references vertex normals in faces but contains no 'vn' lines.\n");
2109 }
2110 if (has_any_vt && num_texcoords == 0)
2111 {
2112 throw std::domain_error("OBJ file references texture coordinates in faces but contains no 'vt' lines.\n");
2113 }
2114
2115 // Tuple key: (vertex_index, texcoord_index, normal_index)
2116 // Use -1 for absent texcoord or normal.
2117 using CornerKey = std::tuple<int32_t, int32_t, int32_t>;
2118 std::map<CornerKey, int32_t> corner_to_new_vertex;
2119
2120 std::vector<float> dedup_vertices;
2121 std::vector<float> dedup_texcoords;
2122 std::vector<float> dedup_normals;
2123 std::vector<uint8_t> dedup_vertex_colors;
2124 std::vector<int> dedup_faces;
2125
2126 size_t num_face_corners = faces.size();
2127 dedup_faces.reserve(num_face_corners);
2128
2129 for (size_t fi = 0; fi < num_face_corners; fi++)
2130 {
2131 int32_t vidx = static_cast<int32_t>(faces[fi]); // already 0-based vertex index
2132
2133 // Resolve texcoord index: -1 means absent.
2134 int32_t tidx = -1;
2135 if (has_any_vt)
2136 {
2137 int vt_raw = face_vt_indices[fi];
2138 if (vt_raw != 0)
2139 {
2140 if (vt_raw < 0)
2141 {
2142 if (vt_raw < -num_texcoords)
2143 {
2144 throw std::domain_error("Negative texcoord index " + std::to_string(vt_raw) + " exceeds texcoord count " + std::to_string(num_texcoords) + ".\n");
2145 }
2146 tidx = num_texcoords + vt_raw; // -1 → num_texcoords-1
2147 }
2148 else
2149 {
2150 tidx = vt_raw - 1;
2151 }
2152 if (tidx < 0 || tidx >= num_texcoords)
2153 {
2154 throw std::domain_error("Texcoord index out of range [1, " + std::to_string(num_texcoords) + "].\n");
2155 }
2156 }
2157 }
2158
2159 // Resolve normal index: -1 means absent.
2160 int32_t nidx = -1;
2161 if (has_any_vn)
2162 {
2163 int vn_raw = face_vn_indices[fi];
2164 if (vn_raw != 0)
2165 {
2166 if (vn_raw < 0)
2167 {
2168 if (vn_raw < -num_normals)
2169 {
2170 throw std::domain_error("Negative normal index " + std::to_string(vn_raw) + " exceeds normal count " + std::to_string(num_normals) + ".\n");
2171 }
2172 nidx = num_normals + vn_raw; // -1 → num_normals-1
2173 }
2174 else
2175 {
2176 nidx = vn_raw - 1;
2177 }
2178 if (nidx < 0 || nidx >= num_normals)
2179 {
2180 throw std::domain_error("Normal index out of range [1, " + std::to_string(num_normals) + "].\n");
2181 }
2182 }
2183 }
2184
2185 CornerKey key(vidx, tidx, nidx);
2186 auto it = corner_to_new_vertex.find(key);
2187 if (it != corner_to_new_vertex.end())
2188 {
2189 // Existing combination: reuse index.
2190 dedup_faces.push_back(it->second);
2191 }
2192 else
2193 {
2194 // New combination: create vertex entry.
2195 int32_t new_idx = static_cast<int32_t>(dedup_vertices.size() / 3);
2196 corner_to_new_vertex[key] = new_idx;
2197 dedup_faces.push_back(new_idx);
2198
2199 // Copy vertex position.
2200 dedup_vertices.push_back(vertices[static_cast<size_t>(vidx) * 3]);
2201 dedup_vertices.push_back(vertices[static_cast<size_t>(vidx) * 3 + 1]);
2202 dedup_vertices.push_back(vertices[static_cast<size_t>(vidx) * 3 + 2]);
2203
2204 // Copy texcoord (or 0,0 if absent).
2205 if (tidx >= 0)
2206 {
2207 dedup_texcoords.push_back(raw_texcoords[static_cast<size_t>(tidx) * 2]);
2208 dedup_texcoords.push_back(raw_texcoords[static_cast<size_t>(tidx) * 2 + 1]);
2209 }
2210 else if (has_any_vt)
2211 {
2212 dedup_texcoords.push_back(0.0f);
2213 dedup_texcoords.push_back(0.0f);
2214 }
2215
2216 // Copy normal (or 0,0,0 if absent).
2217 if (nidx >= 0)
2218 {
2219 dedup_normals.push_back(raw_normals[static_cast<size_t>(nidx) * 3]);
2220 dedup_normals.push_back(raw_normals[static_cast<size_t>(nidx) * 3 + 1]);
2221 dedup_normals.push_back(raw_normals[static_cast<size_t>(nidx) * 3 + 2]);
2222 }
2223 else if (has_any_vn)
2224 {
2225 dedup_normals.push_back(0.0f);
2226 dedup_normals.push_back(0.0f);
2227 dedup_normals.push_back(0.0f);
2228 }
2229
2230 // Copy vertex color from the source position vertex (if colors exist).
2231 if (detected_format == 1)
2232 {
2233 dedup_vertex_colors.push_back(vertex_colors[static_cast<size_t>(vidx) * 3]);
2234 dedup_vertex_colors.push_back(vertex_colors[static_cast<size_t>(vidx) * 3 + 1]);
2235 dedup_vertex_colors.push_back(vertex_colors[static_cast<size_t>(vidx) * 3 + 2]);
2236 }
2237 }
2238 }
2239
2240 mesh->vertices = dedup_vertices;
2241 mesh->faces = dedup_faces;
2242 mesh->vertex_colors = dedup_vertex_colors;
2243 mesh->vertex_normals = dedup_normals;
2244 mesh->vertex_texcoords = dedup_texcoords;
2245 }
2246
2262 static void from_obj(Mesh *mesh, const std::string &filename, const bool preserve_vertex_indices = false)
2263 {
2264#ifdef LIBFS_DBG_INFO
2265 std::cout << LIBFS_APPTAG << "Reading brain mesh from Wavefront object format file " << filename << ".\n";
2266#endif
2267 // -- Security: file-size pre-check to reject huge files before parsing (plan #9) --
2268 size_t file_size = util::get_file_size(filename);
2269 if (file_size > LIBFS_MAX_OBJ_FILE_SIZE)
2270 {
2271 throw std::runtime_error("OBJ file '" + filename + "' size (" + std::to_string(file_size) +
2272 " bytes) exceeds maximum allowed (" + std::to_string(LIBFS_MAX_OBJ_FILE_SIZE) + " bytes).\n");
2273 }
2274
2275 std::ifstream input(filename, std::fstream::in);
2276 if (input.is_open())
2277 {
2278 Mesh::from_obj(mesh, &input, preserve_vertex_indices);
2279 input.close();
2280 }
2281 else
2282 {
2283 throw std::runtime_error("Could not open Wavefront object format mesh file '" + filename + "' for reading.\n");
2284 }
2285 }
2286
2293 static void from_off(Mesh *mesh, std::istream *is, const std::string &source_filename = "")
2294 {
2295 // -- Security: null-pointer check (O3) --
2296 if (!mesh)
2297 {
2298 throw std::invalid_argument("mesh pointer must not be null");
2299 }
2300
2301 std::string msg_source_file_part = source_filename.empty() ? "" : "'" + source_filename + "'";
2302
2303 std::string line;
2304 int line_idx = -1;
2305 int noncomment_line_idx = -1;
2306 size_t total_lines_processed = 0;
2307
2308 std::vector<float> vertices;
2309 std::vector<int> faces;
2310 size_t num_vertices = 0;
2311 size_t num_faces = 0;
2312 size_t num_edges = 0;
2313 size_t num_verts_parsed = 0;
2314 size_t num_faces_parsed = 0;
2315 bool has_vertex_colors = false;
2316 float x, y, z; // vertex xyz coords
2317 int r, g, b, a; // vertex colors
2318 int num_verts_this_face, v0, v1, v2; // face, defined by number of vertices and vertex indices.
2319 std::vector<uint8_t> vertex_colors;
2320
2321 while (total_lines_processed < LIBFS_MAX_OFF_LINES)
2322 {
2323 // -- Security: bounded line read (O4) --
2324 if (!std::getline(*is, line))
2325 {
2326 break; // EOF or read error
2327 }
2328 total_lines_processed++;
2329 line_idx++;
2330
2331 if (line.size() > LIBFS_MAX_OFF_LINE_LENGTH)
2332 {
2333 throw std::runtime_error("OFF line " + std::to_string(line_idx + 1) +
2334 " exceeds maximum allowed line length of " +
2335 std::to_string(LIBFS_MAX_OFF_LINE_LENGTH) + " bytes.\n");
2336 }
2337
2338 std::istringstream iss(line);
2339 if (fs::util::starts_with(line, "#"))
2340 {
2341 continue; // skip comment.
2342 }
2343 else
2344 {
2345 noncomment_line_idx++;
2346 if (noncomment_line_idx == 0)
2347 {
2348 std::string off_header_magic;
2349 if (!(iss >> off_header_magic))
2350 {
2351 throw std::domain_error("Could not parse first header line " + std::to_string(line_idx + 1) + " of OFF data, invalid format.\n");
2352 }
2353 if (!(off_header_magic == "OFF" || off_header_magic == "COFF"))
2354 {
2355 throw std::domain_error("OFF magic string invalid, file " + msg_source_file_part + " not in OFF format.\n");
2356 }
2357 has_vertex_colors = (off_header_magic == "COFF");
2358 }
2359 else if (noncomment_line_idx == 1)
2360 {
2361 if (!(iss >> num_vertices >> num_faces >> num_edges))
2362 {
2363 throw std::domain_error("Could not parse element count header line " + std::to_string(line_idx + 1) + " of OFF data " + msg_source_file_part + ", invalid format.\n");
2364 }
2365
2366 // -- Security: validate header counts against allocation limit (O1) --
2367 if (!util::check_alloc(num_vertices, 3 * sizeof(float)) ||
2368 !util::check_alloc(num_faces, 3 * sizeof(int)))
2369 {
2370 throw std::runtime_error("OFF data " + msg_source_file_part + " header declares more vertices/faces than allowed by allocation limit (" +
2371 std::to_string(LIBFS_MAX_ALLOC_BYTES) + " bytes).\n");
2372 }
2373 }
2374 else
2375 {
2376
2377 if (num_verts_parsed < num_vertices)
2378 {
2379 if (has_vertex_colors)
2380 {
2381 if (!(iss >> x >> y >> z >> r >> g >> b >> a))
2382 {
2383 throw std::domain_error("Could not parse vertex coordinate and color line " + std::to_string(line_idx + 1) + " of COFF data " + msg_source_file_part + ", invalid format.\n");
2384 }
2385 // -- Security: clamp vertex colors to [0,255] (O8) --
2386 if (r < 0) { r = 0; } if (r > 255) { r = 255; }
2387 if (g < 0) { g = 0; } if (g > 255) { g = 255; }
2388 if (b < 0) { b = 0; } if (b > 255) { b = 255; }
2389 vertex_colors.push_back(static_cast<uint8_t>(r));
2390 vertex_colors.push_back(static_cast<uint8_t>(g));
2391 vertex_colors.push_back(static_cast<uint8_t>(b));
2392 }
2393 else
2394 {
2395 if (!(iss >> x >> y >> z))
2396 {
2397 throw std::domain_error("Could not parse vertex coordinate line " + std::to_string(line_idx + 1) + " of OFF data " + msg_source_file_part + ", invalid format.\n");
2398 }
2399 }
2400
2401 // -- Security: validate finite coordinates (O7) --
2402 if (!util::is_finite_float(x) || !util::is_finite_float(y) || !util::is_finite_float(z))
2403 {
2404 throw std::domain_error("Non-finite vertex coordinate on line " + std::to_string(line_idx + 1) + " of OFF data " + msg_source_file_part + ".\n");
2405 }
2406
2407 vertices.push_back(x);
2408 vertices.push_back(y);
2409 vertices.push_back(z);
2410 num_verts_parsed++;
2411 }
2412 else
2413 {
2414 if (num_faces_parsed < num_faces)
2415 {
2416 if (!(iss >> num_verts_this_face >> v0 >> v1 >> v2))
2417 {
2418 throw std::domain_error("Could not parse face line " + std::to_string(line_idx + 1) + " of OFF data " + msg_source_file_part + ", invalid format.\n");
2419 }
2420 if (num_verts_this_face != 3)
2421 {
2422 throw std::domain_error("At OFF data " + msg_source_file_part + " line " + std::to_string(line_idx + 1) + ": only triangular meshes supported.\n");
2423 }
2424 faces.push_back(v0);
2425 faces.push_back(v1);
2426 faces.push_back(v2);
2427 num_faces_parsed++;
2428 }
2429 }
2430 }
2431 }
2432 }
2433
2434 // -- Security: max-lines exceeded (O5) --
2435 if (total_lines_processed >= LIBFS_MAX_OFF_LINES && !is->eof())
2436 {
2437 throw std::runtime_error("OFF file exceeds maximum allowed line count of " + std::to_string(LIBFS_MAX_OFF_LINES) + ".\n");
2438 }
2439
2440 if (num_verts_parsed < num_vertices)
2441 {
2442 throw std::domain_error("Vertex count mismatch between OFF data " + msg_source_file_part + " header (" + std::to_string(num_vertices) + ") and data (" + std::to_string(num_verts_parsed) + ").\n");
2443 }
2444 if (num_faces_parsed < num_faces)
2445 {
2446 throw std::domain_error("Face count mismatch between OFF data " + msg_source_file_part + " header (" + std::to_string(num_faces) + ") and data (" + std::to_string(num_faces_parsed) + ").\n");
2447 }
2448
2449 // -- Security: validate face indices against vertex count (O2) --
2450 int32_t nv = static_cast<int32_t>(num_vertices);
2451 for (size_t fi = 0; fi < faces.size(); fi++)
2452 {
2453 int idx = faces[fi];
2454 if (idx < 0 || idx >= nv)
2455 {
2456 throw std::domain_error("Face index " + std::to_string(idx) + " out of range [0, " + std::to_string(nv - 1) + "] in OFF data " + msg_source_file_part + ".\n");
2457 }
2458 }
2459
2460 mesh->vertices = vertices;
2461 mesh->faces = faces;
2463 }
2464
2479 static void from_off(Mesh *mesh, const std::string &filename)
2480 {
2481#ifdef LIBFS_DBG_INFO
2482 std::cout << LIBFS_APPTAG << "Reading brain mesh from OFF format file " << filename << ".\n";
2483#endif
2484 // -- Security: file-size pre-check (O6) --
2485 size_t file_size = util::get_file_size(filename);
2486 if (file_size > LIBFS_MAX_OFF_FILE_SIZE)
2487 {
2488 throw std::runtime_error("OFF file '" + filename + "' size (" + std::to_string(file_size) +
2489 " bytes) exceeds maximum allowed (" + std::to_string(LIBFS_MAX_OFF_FILE_SIZE) + " bytes).\n");
2490 }
2491
2492 std::ifstream input(filename, std::fstream::in);
2493 if (input.is_open())
2494 {
2495 Mesh::from_off(mesh, &input);
2496 input.close();
2497 }
2498 else
2499 {
2500 throw std::runtime_error("Could not open Object file format (OFF) mesh file '" + filename + "' for reading.\n");
2501 }
2502 }
2503
2509 static void from_ply(Mesh *mesh, std::istream *is)
2510 {
2511 // -- Security: null-pointer check (P4) --
2512 if (!mesh)
2513 {
2514 throw std::invalid_argument("mesh pointer must not be null");
2515 }
2516
2517 std::string line;
2518 int line_idx = -1;
2519 int noncomment_line_idx = -1;
2520 size_t total_lines_processed = 0;
2521
2522 std::vector<float> vertices;
2523 std::vector<int> faces;
2524 std::vector<uint8_t> vertex_colors;
2525 std::vector<float> ply_normals; // collected per-vertex normals (3 floats per vertex)
2526 std::vector<float> ply_texcoords; // collected per-vertex texcoords (2 floats per vertex)
2527
2528 bool in_header = true;
2529 size_t num_verts = 0;
2530 size_t num_faces = 0;
2531 bool have_num_verts = false;
2532 bool have_num_faces = false;
2533 bool in_vertex_element = false;
2534 std::vector<std::string> vertex_properties;
2535
2536 while (total_lines_processed < LIBFS_MAX_PLY_LINES)
2537 {
2538 // -- Security: bounded line read (P6) --
2539 if (!std::getline(*is, line))
2540 {
2541 break; // EOF or read error
2542 }
2543 total_lines_processed++;
2544 line_idx++;
2545
2546 if (line.size() > LIBFS_MAX_PLY_LINE_LENGTH)
2547 {
2548 throw std::runtime_error("PLY line " + std::to_string(line_idx + 1) +
2549 " exceeds maximum allowed line length of " +
2550 std::to_string(LIBFS_MAX_PLY_LINE_LENGTH) + " bytes.\n");
2551 }
2552
2553 std::istringstream iss(line);
2554 if (fs::util::starts_with(line, "comment"))
2555 {
2556 continue; // skip comment.
2557 }
2558 else
2559 {
2560 noncomment_line_idx++;
2561 if (in_header)
2562 {
2563 if (noncomment_line_idx == 0)
2564 {
2565 if (line != "ply")
2566 throw std::domain_error("Invalid PLY file");
2567 }
2568 else if (noncomment_line_idx == 1)
2569 {
2570 if (line != "format ascii 1.0")
2571 throw std::domain_error("Unsupported PLY file format, only format 'format ascii 1.0' is supported.");
2572 }
2573
2574 if (line == "end_header")
2575 {
2576 in_header = false;
2577
2578 // -- Security: validate header counts after header is complete --
2579 if (!have_num_verts || !have_num_faces)
2580 {
2581 throw std::domain_error("Invalid PLY file: missing element count lines in header.\n");
2582 }
2583
2584 // -- Security: validate header counts against allocation limit (P2) --
2585 if (!util::check_alloc(num_verts, 3 * sizeof(float)) ||
2586 !util::check_alloc(num_faces, 3 * sizeof(int)))
2587 {
2588 throw std::runtime_error("PLY header declares more vertices/faces than allowed by allocation limit (" +
2589 std::to_string(LIBFS_MAX_ALLOC_BYTES) + " bytes).\n");
2590 }
2591 }
2592 else if (fs::util::starts_with(line, "element vertex"))
2593 {
2594 std::string elem, elem_type_identifier;
2595
2596 // -- Security: parse count as long long to avoid int overflow (P1) --
2597 long long parsed_num_verts;
2598 if (!(iss >> elem >> elem_type_identifier >> parsed_num_verts))
2599 {
2600 throw std::domain_error("Could not parse element vertex line of PLY header, invalid format.\n");
2601 }
2602 if (parsed_num_verts < 0)
2603 {
2604 throw std::domain_error("Negative vertex count in PLY header.\n");
2605 }
2606 num_verts = static_cast<size_t>(parsed_num_verts);
2607 have_num_verts = true;
2608 in_vertex_element = true;
2609 }
2610 else if (fs::util::starts_with(line, "element face"))
2611 {
2612 std::string elem, elem_type_identifier;
2613
2614 // -- Security: parse count as long long to avoid int overflow (P1) --
2615 long long parsed_num_faces;
2616 if (!(iss >> elem >> elem_type_identifier >> parsed_num_faces))
2617 {
2618 throw std::domain_error("Could not parse element face line of PLY header, invalid format.\n");
2619 }
2620 if (parsed_num_faces < 0)
2621 {
2622 throw std::domain_error("Negative face count in PLY header.\n");
2623 }
2624 num_faces = static_cast<size_t>(parsed_num_faces);
2625 have_num_faces = true;
2626 in_vertex_element = false;
2627 }
2628 else if (fs::util::starts_with(line, "element "))
2629 {
2630 // Some other element (e.g., edges): stop tracking vertex properties.
2631 in_vertex_element = false;
2632 }
2633 else if (fs::util::starts_with(line, "property ") && in_vertex_element)
2634 {
2635 // Record property order for the vertex element so we can parse data lines correctly.
2636 std::string kw, type, name;
2637 if (iss >> kw >> type >> name)
2638 {
2639 vertex_properties.push_back(name);
2640 }
2641 }
2642 }
2643 else
2644 { // in data part.
2645 if (!have_num_verts || !have_num_faces)
2646 {
2647 throw std::domain_error("Invalid PLY file: missing element count lines of header.");
2648 }
2649 // Read vertices
2650 if (vertices.size() < num_verts * 3)
2651 {
2652 float x = 0.0f, y = 0.0f, z = 0.0f;
2653 float nx = 0.0f, ny = 0.0f, nz = 0.0f;
2654 float s = 0.0f, t = 0.0f;
2655 int r = 0, g = 0, b = 0;
2656 if (vertex_properties.empty())
2657 {
2658 // No property declarations tracked: fall back to default x y z order.
2659 if (!(iss >> x >> y >> z))
2660 {
2661 throw std::domain_error("Could not parse vertex line " + std::to_string(line_idx) + " of PLY data, invalid format.\n");
2662 }
2663 }
2664 else
2665 {
2666 for (size_t pi = 0; pi < vertex_properties.size(); pi++)
2667 {
2668 const std::string &pname = vertex_properties[pi];
2669 if (pname == "x") { iss >> x; }
2670 else if (pname == "y") { iss >> y; }
2671 else if (pname == "z") { iss >> z; }
2672 else if (pname == "nx") { iss >> nx; }
2673 else if (pname == "ny") { iss >> ny; }
2674 else if (pname == "nz") { iss >> nz; }
2675 else if (pname == "s" || pname == "u") { iss >> s; }
2676 else if (pname == "t" || pname == "v") { iss >> t; }
2677 else if (pname == "red") { iss >> r; }
2678 else if (pname == "green") { iss >> g; }
2679 else if (pname == "blue") { iss >> b; }
2680 else
2681 {
2682 // Skip unknown property.
2683 std::string dummy; iss >> dummy;
2684 }
2685 if (iss.fail())
2686 {
2687 throw std::domain_error("Could not parse vertex property '" + pname + "' at line " + std::to_string(line_idx) + " of PLY data.\n");
2688 }
2689 }
2690 if (iss.fail())
2691 {
2692 throw std::domain_error("Could not parse vertex line " + std::to_string(line_idx) + " of PLY data, invalid format.\n");
2693 }
2694 }
2695
2696 // -- Security: validate finite coordinates (P9) --
2697 if (!util::is_finite_float(x) || !util::is_finite_float(y) || !util::is_finite_float(z))
2698 {
2699 throw std::domain_error("Non-finite vertex coordinate on line " + std::to_string(line_idx) + " of PLY data.\n");
2700 }
2701
2702 vertices.push_back(x);
2703 vertices.push_back(y);
2704 vertices.push_back(z);
2705
2706 // Only store colors if red/green/blue were declared in the header.
2707 bool has_r = false, has_g = false, has_b = false;
2708 for (size_t pi = 0; pi < vertex_properties.size(); pi++)
2709 {
2710 if (vertex_properties[pi] == "red") has_r = true;
2711 if (vertex_properties[pi] == "green") has_g = true;
2712 if (vertex_properties[pi] == "blue") has_b = true;
2713 }
2714 if (has_r && has_g && has_b)
2715 {
2716 // -- Security: clamp vertex colors to [0,255] (P10) --
2717 if (r < 0) { r = 0; } if (r > 255) { r = 255; }
2718 if (g < 0) { g = 0; } if (g > 255) { g = 255; }
2719 if (b < 0) { b = 0; } if (b > 255) { b = 255; }
2720 vertex_colors.push_back(static_cast<uint8_t>(r));
2721 vertex_colors.push_back(static_cast<uint8_t>(g));
2722 vertex_colors.push_back(static_cast<uint8_t>(b));
2723 }
2724
2725 // Collect normals if declared in header.
2726 bool has_nx = false, has_ny = false, has_nz = false;
2727 for (size_t pi = 0; pi < vertex_properties.size(); pi++)
2728 {
2729 if (vertex_properties[pi] == "nx") has_nx = true;
2730 if (vertex_properties[pi] == "ny") has_ny = true;
2731 if (vertex_properties[pi] == "nz") has_nz = true;
2732 }
2733 if (has_nx && has_ny && has_nz)
2734 {
2735 if (!util::is_finite_float(nx) || !util::is_finite_float(ny) || !util::is_finite_float(nz))
2736 {
2737 throw std::domain_error("Non-finite normal on line " + std::to_string(line_idx) + " of PLY data.\n");
2738 }
2739 ply_normals.push_back(nx);
2740 ply_normals.push_back(ny);
2741 ply_normals.push_back(nz);
2742 }
2743
2744 // Collect texcoords if declared in header.
2745 bool has_s = false, has_t = false;
2746 for (size_t pi = 0; pi < vertex_properties.size(); pi++)
2747 {
2748 if (vertex_properties[pi] == "s" || vertex_properties[pi] == "u") has_s = true;
2749 if (vertex_properties[pi] == "t" || vertex_properties[pi] == "v") has_t = true;
2750 }
2751 if (has_s && has_t)
2752 {
2753 if (!util::is_finite_float(s) || !util::is_finite_float(t))
2754 {
2755 throw std::domain_error("Non-finite texcoord on line " + std::to_string(line_idx) + " of PLY data.\n");
2756 }
2757 ply_texcoords.push_back(s);
2758 ply_texcoords.push_back(t);
2759 }
2760 }
2761 else
2762 {
2763 if (faces.size() < num_faces * 3)
2764 {
2765 int verts_per_face, v0, v1, v2;
2766 if (!(iss >> verts_per_face >> v0 >> v1 >> v2))
2767 {
2768 throw std::domain_error("Could not parse face line " + std::to_string(line_idx) + " of PLY data, invalid format.\n");
2769 }
2770 if (verts_per_face != 3)
2771 {
2772 throw std::domain_error("Only triangular meshes are supported: PLY faces lines must contain exactly 3 vertex indices.\n");
2773 }
2774 faces.push_back(v0);
2775 faces.push_back(v1);
2776 faces.push_back(v2);
2777 }
2778 }
2779 }
2780 }
2781 }
2782
2783 // -- Security: max-lines exceeded (P7) --
2784 if (total_lines_processed >= LIBFS_MAX_PLY_LINES && !is->eof())
2785 {
2786 throw std::runtime_error("PLY file exceeds maximum allowed line count of " + std::to_string(LIBFS_MAX_PLY_LINES) + ".\n");
2787 }
2788
2789 // -- Throw if header was never terminated --
2790 if (in_header)
2791 {
2792 throw std::domain_error("Invalid PLY file: header not terminated with 'end_header'.\n");
2793 }
2794
2795 // -- Security: throw on count mismatch instead of just warning (P5) --
2796 if (vertices.size() != num_verts * 3)
2797 {
2798 throw std::domain_error("PLY vertex count mismatch: header declares " + std::to_string(num_verts) +
2799 " vertices, but found " + std::to_string(vertices.size() / 3) + ".\n");
2800 }
2801 if (faces.size() != num_faces * 3)
2802 {
2803 throw std::domain_error("PLY face count mismatch: header declares " + std::to_string(num_faces) +
2804 " faces, but found " + std::to_string(faces.size() / 3) + ".\n");
2805 }
2806
2807 // -- Security: validate face indices against vertex count (P3) --
2808 {
2809 int32_t nv = static_cast<int32_t>(num_verts);
2810 for (size_t fi = 0; fi < faces.size(); fi++)
2811 {
2812 int idx = faces[fi];
2813 if (idx < 0 || idx >= nv)
2814 {
2815 throw std::domain_error("Face index " + std::to_string(idx) + " out of range [0, " + std::to_string(nv - 1) + "] in PLY data.\n");
2816 }
2817 }
2818 }
2819
2820 mesh->vertices = vertices;
2821 mesh->faces = faces;
2823 mesh->vertex_normals = ply_normals;
2824 mesh->vertex_texcoords = ply_texcoords;
2825 }
2826
2840 static void from_ply(Mesh *mesh, const std::string &filename)
2841 {
2842#ifdef LIBFS_DBG_INFO
2843 std::cout << LIBFS_APPTAG << "Reading brain mesh from PLY format file " << filename << ".\n";
2844#endif
2845 // -- Security: file-size pre-check (P8) --
2846 size_t file_size = util::get_file_size(filename);
2847 if (file_size > LIBFS_MAX_PLY_FILE_SIZE)
2848 {
2849 throw std::runtime_error("PLY file '" + filename + "' size (" + std::to_string(file_size) +
2850 " bytes) exceeds maximum allowed (" + std::to_string(LIBFS_MAX_PLY_FILE_SIZE) + " bytes).\n");
2851 }
2852
2853 std::ifstream input(filename, std::fstream::in);
2854 if (input.is_open())
2855 {
2856 Mesh::from_ply(mesh, &input);
2857 input.close();
2858 }
2859 else
2860 {
2861 throw std::runtime_error("Could not open Stanford PLY format mesh file '" + filename + "' for reading.\n");
2862 }
2863 }
2864
2874 size_t num_vertices() const
2875 {
2876 return (this->vertices.size() / 3);
2877 }
2878
2888 size_t num_faces() const
2889 {
2890 return (this->faces.size() / 3);
2891 }
2892
2905 const int32_t &fm_at(const size_t i, const size_t j) const
2906 {
2907 size_t idx = _vidx_2d(i, j, 3);
2908 if (idx > this->faces.size() - 1)
2909 {
2910 throw std::range_error("Indices (" + std::to_string(i) + "," + std::to_string(j) + ") into Mesh.faces out of bounds. Hit " + std::to_string(idx) + " with max valid index " + std::to_string(this->faces.size() - 1) + ".\n");
2911 }
2912 return (this->faces[idx]);
2913 }
2914
2926 std::vector<int32_t> face_vertices(const size_t face) const
2927 {
2928 if (face > this->num_faces() - 1)
2929 {
2930 throw std::range_error("Index " + std::to_string(face) + " into Mesh.faces out of bounds, max valid index is " + std::to_string(this->num_faces() - 1) + ".\n");
2931 }
2932 std::vector<int32_t> fv(3);
2933 fv[0] = this->fm_at(face, 0);
2934 fv[1] = this->fm_at(face, 1);
2935 fv[2] = this->fm_at(face, 2);
2936 return (fv);
2937 }
2938
2950 std::vector<float> vertex_coords(const size_t vertex) const
2951 {
2952 if (vertex > this->num_vertices() - 1)
2953 {
2954 throw std::range_error("Index " + std::to_string(vertex) + " into Mesh.vertices out of bounds, max valid index is " + std::to_string(this->num_vertices() - 1) + ".\n");
2955 }
2956 std::vector<float> vc(3);
2957 vc[0] = this->vm_at(vertex, 0);
2958 vc[1] = this->vm_at(vertex, 1);
2959 vc[2] = this->vm_at(vertex, 2);
2960 return (vc);
2961 }
2962
2976 const float &vm_at(const size_t i, const size_t j) const
2977 {
2978 size_t idx = _vidx_2d(i, j, 3);
2979 if (idx > this->vertices.size() - 1)
2980 {
2981 throw std::range_error("Indices (" + std::to_string(i) + "," + std::to_string(j) + ") into Mesh.vertices out of bounds. Hit " + std::to_string(idx) + " with max valid index " + std::to_string(this->vertices.size() - 1) + ".\n");
2982 }
2983 return (this->vertices[idx]);
2984 }
2985
2994 std::string to_ply() const
2995 {
2996 std::vector<uint8_t> empty_col;
2997 return (this->to_ply(empty_col));
2998 }
2999
3010 std::string to_ply(const std::vector<uint8_t> col) const
3011 {
3012 bool use_vertex_colors = col.size() != 0;
3013 bool use_normals = this->has_normals();
3014 bool use_texcoords = this->has_texcoords();
3015 std::stringstream plys;
3016 plys << "ply\nformat ascii 1.0\n";
3017 plys << "element vertex " << this->num_vertices() << "\n";
3018 plys << "property float x\nproperty float y\nproperty float z\n";
3019 if (use_normals)
3020 {
3021 plys << "property float nx\nproperty float ny\nproperty float nz\n";
3022 }
3023 if (use_texcoords)
3024 {
3025 plys << "property float s\nproperty float t\n";
3026 }
3027 if (use_vertex_colors)
3028 {
3029 if (col.size() != this->vertices.size())
3030 {
3031 throw std::invalid_argument("Number of vertex coordinates and vertex colors must match when writing PLY file, but got " + std::to_string(this->vertices.size()) + " and " + std::to_string(col.size()) + ".");
3032 }
3033 plys << "property uchar red\nproperty uchar green\nproperty uchar blue\n";
3034 }
3035 plys << "element face " << this->num_faces() << "\n";
3036 plys << "property list uchar int vertex_index\n";
3037 plys << "end_header\n";
3038
3039#ifdef LIBFS_DBG_DEBUG
3040 fs::util::log("Writing " + std::to_string(this->vertices.size() / 3) + " PLY format vertices.", "INFO");
3041#endif
3042
3043 for (size_t vidx = 0; vidx < this->vertices.size(); vidx += 3)
3044 { // vertex coords
3045 plys << vertices[vidx] << " " << vertices[vidx + 1] << " " << vertices[vidx + 2];
3046 if (use_normals)
3047 {
3048 plys << " " << vertex_normals[vidx] << " " << vertex_normals[vidx + 1] << " " << vertex_normals[vidx + 2];
3049 }
3050 if (use_texcoords)
3051 {
3052 size_t tcidx = (vidx / 3) * 2;
3053 plys << " " << vertex_texcoords[tcidx] << " " << vertex_texcoords[tcidx + 1];
3054 }
3055 if (use_vertex_colors)
3056 {
3057 plys << " " << (int)col[vidx] << " " << (int)col[vidx + 1] << " " << (int)col[vidx + 2];
3058 }
3059 plys << "\n";
3060 }
3061
3062#ifdef LIBFS_DBG_DEBUG
3063 fs::util::log("Writing " + std::to_string(this->faces.size() / 3) + " PLY format faces.", "INFO");
3064#endif
3065
3066 const int num_vertices_per_face = 3;
3067 for (size_t fidx = 0; fidx < this->faces.size(); fidx += 3)
3068 { // faces: vertex indices, 0-based
3069 plys << num_vertices_per_face << " " << faces[fidx] << " " << faces[fidx + 1] << " " << faces[fidx + 2] << "\n";
3070 }
3071 return (plys.str());
3072 }
3073
3083 void to_ply_file(const std::string &filename) const
3084 {
3085#ifdef LIBFS_DBG_INFO
3086 fs::util::log("Writing mesh to PLY file '" + filename + "'.", "INFO");
3087#endif
3088 fs::util::str_to_file(filename, this->to_ply());
3089 }
3090
3093 void to_ply_file(const std::string &filename, const std::vector<uint8_t> col) const
3094 {
3095 fs::util::str_to_file(filename, this->to_ply(col));
3096 }
3097
3106 std::string to_off() const
3107 {
3108 std::vector<uint8_t> empty_col;
3109 return (this->to_off(empty_col));
3110 }
3111
3115 std::string to_off(const std::vector<uint8_t> col) const
3116 {
3117 bool use_vertex_colors = col.size() != 0;
3118 std::stringstream offs;
3119 if (use_vertex_colors)
3120 {
3121#ifdef LIBFS_DBG_INFO
3122 fs::util::log("Writing OFF representation of mesh with vertex colors.", "INFO");
3123#endif
3124 if (col.size() != this->vertices.size())
3125 {
3126 throw std::invalid_argument("Number of vertex coordinates and vertex colors must match when writing OFF file but got " + std::to_string(this->vertices.size()) + " and " + std::to_string(col.size()) + ".");
3127 }
3128 offs << "COFF\n";
3129 }
3130 else
3131 {
3132#ifdef LIBFS_DBG_INFO
3133 fs::util::log("Writing OFF representation of mesh without vertex colors.", "INFO");
3134#endif
3135 offs << "OFF\n";
3136 }
3137 offs << this->num_vertices() << " " << this->num_faces() << " 0\n";
3138
3139 for (size_t vidx = 0; vidx < this->vertices.size(); vidx += 3)
3140 { // vertex coords
3141 offs << vertices[vidx] << " " << vertices[vidx + 1] << " " << vertices[vidx + 2];
3142 if (use_vertex_colors)
3143 {
3144 offs << " " << (int)col[vidx] << " " << (int)col[vidx + 1] << " " << (int)col[vidx + 2] << " 255";
3145 }
3146 offs << "\n";
3147 }
3148
3149 const int num_vertices_per_face = 3;
3150 for (size_t fidx = 0; fidx < this->faces.size(); fidx += 3)
3151 { // faces: vertex indices, 0-based
3152 offs << num_vertices_per_face << " " << faces[fidx] << " " << faces[fidx + 1] << " " << faces[fidx + 2] << "\n";
3153 }
3154 return (offs.str());
3155 }
3156
3166 void to_off_file(const std::string &filename) const
3167 {
3168 fs::util::str_to_file(filename, this->to_off());
3169 }
3170
3173 void to_off_file(const std::string &filename, const std::vector<uint8_t> col) const
3174 {
3175 fs::util::str_to_file(filename, this->to_off(col));
3176 }
3177 };
3178
3180 struct Curv
3181 {
3182
3184 Curv(std::vector<float> curv_data) : num_faces(100000), num_vertices(0), num_values_per_vertex(1)
3185 {
3186 data = curv_data;
3187 num_vertices = int(data.size());
3188 }
3189
3192
3194 int32_t num_faces;
3195
3197 std::vector<float> data;
3198
3201
3204 };
3205
3208 {
3209 std::vector<int32_t> id;
3210 std::vector<std::string> name;
3211 std::vector<int32_t> r;
3212 std::vector<int32_t> g;
3213 std::vector<int32_t> b;
3214 std::vector<int32_t> a;
3215 std::vector<int32_t> label;
3216
3218 size_t num_entries() const
3219 {
3220 size_t num_ids = this->id.size();
3221 if (this->name.size() != num_ids || this->r.size() != num_ids || this->g.size() != num_ids || this->b.size() != num_ids || this->a.size() != num_ids || this->label.size() != num_ids)
3222 {
3223#ifdef LIBFS_DBG_ERROR
3224 std::cerr << "Inconsistent Colortable, vector sizes do not match.\n";
3225#endif
3226 }
3227 return num_ids;
3228 }
3229
3231 int32_t get_region_idx(const std::string &query_name) const
3232 {
3233 for (size_t i = 0; i < this->num_entries(); i++)
3234 {
3235 if (this->name[i] == query_name)
3236 {
3237 return (int32_t)i;
3238 }
3239 }
3240 return (-1);
3241 }
3242
3244 int32_t get_region_idx(int32_t query_label) const
3245 {
3246 for (size_t i = 0; i < this->num_entries(); i++)
3247 {
3248 if (this->label[i] == query_label)
3249 {
3250 return (int32_t)i;
3251 }
3252 }
3253 return (-1);
3254 }
3255 };
3256
3258 struct Annot
3259 {
3260 std::vector<int32_t> vertex_indices;
3261 std::vector<int32_t> vertex_labels;
3263
3265 std::vector<int32_t> region_vertices(const std::string &region_name) const
3266 {
3267 int32_t region_idx = this->colortable.get_region_idx(region_name);
3268 if (region_idx >= 0)
3269 {
3270 return (this->region_vertices(this->colortable.label[region_idx]));
3271 }
3272 else
3273 {
3274#ifdef LIBFS_DBG_ERROR
3275 std::cerr << "No such region in annot, returning empty vector.\n";
3276#endif
3277 std::vector<int32_t> empty;
3278 return (empty);
3279 }
3280 }
3281
3283 std::vector<int32_t> region_vertices(int32_t region_label) const
3284 {
3285 std::vector<int32_t> reg_verts;
3286 for (size_t i = 0; i < this->vertex_labels.size(); i++)
3287 {
3288 if (this->vertex_labels[i] == region_label)
3289 {
3290 reg_verts.push_back(int(i));
3291 }
3292 }
3293 return (reg_verts);
3294 }
3295
3298 std::vector<uint8_t> vertex_colors(bool alpha = false) const
3299 {
3300 int num_channels = alpha ? 4 : 3;
3301 std::vector<uint8_t> col;
3302 col.reserve(this->num_vertices() * num_channels);
3303 std::vector<size_t> vertex_region_indices = this->vertex_regions();
3304 for (size_t i = 0; i < this->num_vertices(); i++)
3305 {
3306 col.push_back(this->colortable.r[vertex_region_indices[i]]);
3307 col.push_back(this->colortable.g[vertex_region_indices[i]]);
3308 col.push_back(this->colortable.b[vertex_region_indices[i]]);
3309 if (alpha)
3310 {
3311 col.push_back(this->colortable.a[vertex_region_indices[i]]);
3312 }
3313 }
3314 return (col);
3315 }
3316
3319 size_t num_vertices() const
3320 {
3321 size_t nv = this->vertex_indices.size();
3322 if (this->vertex_labels.size() != nv)
3323 {
3324 throw std::runtime_error("Inconsistent annot, number of vertex indices and labels does not match.\n");
3325 }
3326 return nv;
3327 }
3328
3331 std::vector<size_t> vertex_regions() const
3332 {
3333 std::vector<size_t> vert_reg;
3334 for (size_t i = 0; i < this->num_vertices(); i++)
3335 {
3336 vert_reg.push_back(0); // init with zeros.
3337 }
3338 for (size_t region_idx = 0; region_idx < this->colortable.num_entries(); region_idx++)
3339 {
3340 std::vector<int32_t> reg_vertices = this->region_vertices(this->colortable.label[region_idx]);
3341 for (size_t region_vert_local_idx = 0; region_vert_local_idx < reg_vertices.size(); region_vert_local_idx++)
3342 {
3343 int32_t region_vert_idx = reg_vertices[region_vert_local_idx];
3344 vert_reg[region_vert_idx] = region_idx;
3345 }
3346 }
3347 return vert_reg;
3348 }
3349
3351 std::vector<std::string> vertex_region_names() const
3352 {
3353 std::vector<std::string> region_names;
3354 std::vector<size_t> vertex_region_indices = this->vertex_regions();
3355 for (size_t i = 0; i < this->num_vertices(); i++)
3356 {
3357 region_names.push_back(this->colortable.name[vertex_region_indices[i]]);
3358 }
3359 return (region_names);
3360 }
3361 };
3362
3365 {
3368 {
3369 dim1length = curv.data.size();
3370 dim2length = 1;
3371 dim3length = 1;
3372 dim4length = 1;
3374 }
3375 MghHeader(std::vector<float> curv_data)
3376 {
3377 dim1length = curv_data.size();
3378 dim2length = 1;
3379 dim3length = 1;
3380 dim4length = 1;
3382 }
3383 int32_t dim1length = 0;
3384 int32_t dim2length = 0;
3385 int32_t dim3length = 0;
3386 int32_t dim4length = 0;
3387
3388 int32_t dtype = 0;
3389 int32_t dof = 0;
3390 int16_t ras_good_flag = 0;
3391
3393 size_t num_values() const
3394 {
3395 return ((size_t)dim1length * dim2length * dim3length * dim4length);
3396 }
3397
3398 float xsize = 0.0;
3399 float ysize = 0.0;
3400 float zsize = 0.0;
3401 std::vector<float> Mdc;
3402 std::vector<float> Pxyz_c;
3403
3417 std::vector<float> compute_vox2ras() const
3418 {
3419 if (ras_good_flag != 1 || Mdc.size() < 9 || Pxyz_c.size() < 3)
3420 {
3421 return {};
3422 }
3423 const float sizes[3] = { xsize, ysize, zsize };
3424 const float c_crs[3] = { (float)(dim1length / 2), (float)(dim2length / 2), (float)(dim3length / 2) };
3425
3426 std::vector<float> a(16, 0.0f);
3427 // Linear part: affine[i][j] = sizes[j] * Mdc[j*3 + i] (row j of Mdc is the unit axis-j direction).
3428 for (int i = 0; i < 3; ++i)
3429 {
3430 for (int j = 0; j < 3; ++j)
3431 {
3432 a[i * 4 + j] = sizes[j] * Mdc[j * 3 + i];
3433 }
3434 }
3435 // Translation: RAS of voxel (0,0,0) = center voxel RAS (Pxyz_c) - linear part * center index.
3436 for (int i = 0; i < 3; ++i)
3437 {
3438 float sum = 0.0f;
3439 for (int j = 0; j < 3; ++j)
3440 {
3441 sum += a[i * 4 + j] * c_crs[j];
3442 }
3443 a[i * 4 + 3] = Pxyz_c[i] - sum;
3444 }
3445 a[15] = 1.0f;
3446 return a;
3447 }
3448
3460 void set_ras_from_vox2ras(const std::vector<float> &a)
3461 {
3462 Mdc.clear();
3463 Pxyz_c.clear();
3464 ras_good_flag = 0;
3465 if (a.size() < 16)
3466 {
3467 return;
3468 }
3469
3470 // Voxel sizes are the norms of the columns of the linear part.
3471 float sizes[3] = { 0.0f, 0.0f, 0.0f };
3472 for (int j = 0; j < 3; ++j)
3473 {
3474 float norm = 0.0f;
3475 for (int i = 0; i < 3; ++i)
3476 {
3477 norm += a[i * 4 + j] * a[i * 4 + j];
3478 }
3479 sizes[j] = std::sqrt(norm);
3480 }
3481
3482 // Mdc row j = direction of voxel axis j = normalized column j of the affine.
3483 for (int j = 0; j < 3; ++j)
3484 {
3485 for (int i = 0; i < 3; ++i)
3486 {
3487 if (sizes[j] > 0.0f)
3488 {
3489 Mdc.push_back(a[i * 4 + j] / sizes[j]);
3490 }
3491 else
3492 {
3493 // Degenerate column (zero voxel size): use a unit vector along the world axis.
3494 Mdc.push_back(i == j ? 1.0f : 0.0f);
3495 sizes[j] = 1.0f;
3496 }
3497 }
3498 }
3499 xsize = sizes[0];
3500 ysize = sizes[1];
3501 zsize = sizes[2];
3502
3503 // Pxyz_c = translation + linear part * center voxel index (integer division).
3504 const float c_crs[3] = { (float)(dim1length / 2), (float)(dim2length / 2), (float)(dim3length / 2) };
3505 for (int i = 0; i < 3; ++i)
3506 {
3507 float sum = 0.0f;
3508 for (int j = 0; j < 3; ++j)
3509 {
3510 sum += a[i * 4 + j] * c_crs[j];
3511 }
3512 Pxyz_c.push_back(a[i * 4 + 3] + sum);
3513 }
3514 ras_good_flag = 1;
3515 }
3516 };
3517
3519 struct MghData
3520 {
3521 MghData() {}
3522 MghData(std::vector<int32_t> curv_data) { data_mri_int = curv_data; }
3523 explicit MghData(std::vector<uint8_t> curv_data) { data_mri_uchar = curv_data; }
3524 explicit MghData(std::vector<short> curv_data) { data_mri_short = curv_data; }
3525 MghData(std::vector<float> curv_data) { data_mri_float = curv_data; }
3526 MghData(Curv curv) { data_mri_float = curv.data; }
3527 std::vector<int32_t> data_mri_int;
3528 std::vector<uint8_t> data_mri_uchar;
3529 std::vector<float> data_mri_float;
3530 std::vector<short> data_mri_short;
3531 };
3532
3534 struct Mgh
3535 {
3538 Mgh() {}
3539 Mgh(Curv curv)
3540 {
3541 header = MghHeader(curv);
3542 data = MghData(curv);
3543 }
3544 Mgh(std::vector<float> curv_data)
3545 {
3546 header = MghHeader(curv_data);
3547 data = MghData(curv_data);
3548 }
3549 };
3550
3553 template <class T>
3554 struct Array4D
3555 {
3560 Array4D(unsigned int d1, unsigned int d2, unsigned int d3, unsigned int d4) : d1(d1), d2(d2), d3(d3), d4(d4), data(_compute_4d_size(d1, d2, d3, d4)) {}
3561
3567 Array4D(MghHeader *mgh_header) : d1(_validate_mgh_dim(mgh_header->dim1length)), d2(_validate_mgh_dim(mgh_header->dim2length)), d3(_validate_mgh_dim(mgh_header->dim3length)), d4(_validate_mgh_dim(mgh_header->dim4length)), data(_compute_4d_size(d1, d2, d3, d4)) {}
3568
3573 Array4D(Mgh *mgh) : // This does NOT init the data atm.
3574 d1(_validate_mgh_dim(mgh->header.dim1length)), d2(_validate_mgh_dim(mgh->header.dim2length)), d3(_validate_mgh_dim(mgh->header.dim3length)), d4(_validate_mgh_dim(mgh->header.dim4length)), data(_compute_4d_size(d1, d2, d3, d4))
3575 {
3576 }
3577
3579 const T &at(const unsigned int i1, const unsigned int i2, const unsigned int i3, const unsigned int i4) const
3580 {
3581 return data[get_index(i1, i2, i3, i4)];
3582 }
3583
3585 unsigned int get_index(const unsigned int i1, const unsigned int i2, const unsigned int i3, const unsigned int i4) const
3586 {
3587 assert(i1 >= 0 && i1 < d1);
3588 assert(i2 >= 0 && i2 < d2);
3589 assert(i3 >= 0 && i3 < d3);
3590 assert(i4 >= 0 && i4 < d4);
3591 return (((i1 * d2 + i2) * d3 + i3) * d4 + i4);
3592 }
3593
3595 unsigned int num_values() const
3596 {
3597 return (d1 * d2 * d3 * d4);
3598 }
3599
3600 unsigned int d1;
3601 unsigned int d2;
3602 unsigned int d3;
3603 unsigned int d4;
3604 std::vector<T> data;
3605
3606 private:
3609 static unsigned int _validate_mgh_dim(int32_t dim)
3610 {
3611 if (dim <= 0)
3612 {
3613 throw std::domain_error("MGH dimension " + std::to_string(dim) + " is not positive.\n");
3614 }
3615 return static_cast<unsigned int>(dim);
3616 }
3617
3619 static size_t _compute_4d_size(unsigned int d1, unsigned int d2, unsigned int d3, unsigned int d4)
3620 {
3621 if (d1 == 0 || d2 == 0 || d3 == 0 || d4 == 0)
3622 {
3623 throw std::domain_error("Array4D dimensions must be positive.\n");
3624 }
3625 size_t s1, s2, s3;
3626 if (!fs::util::safe_multiply(d1, d2, s1) ||
3627 !fs::util::safe_multiply(s1, d3, s2) ||
3628 !fs::util::safe_multiply(s2, d4, s3))
3629 {
3630 throw std::overflow_error("Array4D dimensions cause size_t overflow.\n");
3631 }
3632 if (s3 > LIBFS_MAX_ALLOC_BYTES / sizeof(T))
3633 {
3634 throw std::runtime_error("Array4D size " + std::to_string(s3) +
3635 " elements exceeds maximum allowed allocation (" +
3636 std::to_string(LIBFS_MAX_ALLOC_BYTES) + " bytes).\n");
3637 }
3638 return s3;
3639 }
3640 };
3641
3642 // More declarations, should also go to separate header.
3643 void read_mgh_header(MghHeader *, const std::string &);
3644 void read_mgh_header(MghHeader *, std::istream *);
3645 template <typename T>
3646 std::vector<T> _read_mgh_data(MghHeader *, const std::string &);
3647 template <typename T>
3648 std::vector<T> _read_mgh_data(MghHeader *, std::istream *);
3649 std::vector<int32_t> _read_mgh_data_int(MghHeader *, const std::string &);
3650 std::vector<int32_t> _read_mgh_data_int(MghHeader *, std::istream *);
3651 std::vector<uint8_t> _read_mgh_data_uchar(MghHeader *, const std::string &);
3652 std::vector<uint8_t> _read_mgh_data_uchar(MghHeader *, std::istream *);
3653 std::vector<short> _read_mgh_data_short(MghHeader *, const std::string &);
3654 std::vector<short> _read_mgh_data_short(MghHeader *, std::istream *);
3655 std::vector<float> _read_mgh_data_float(MghHeader *, const std::string &);
3656 std::vector<float> _read_mgh_data_float(MghHeader *, std::istream *);
3657
3670 void read_mgh(Mgh *mgh, const std::string &filename)
3671 {
3672 MghHeader mgh_header;
3673 read_mgh_header(&mgh_header, filename);
3674 mgh->header = mgh_header;
3675 if (mgh->header.dtype == MRI_INT)
3676 {
3677 std::vector<int32_t> data = _read_mgh_data_int(&mgh_header, filename);
3678 mgh->data.data_mri_int = data;
3679 }
3680 else if (mgh->header.dtype == MRI_UCHAR)
3681 {
3682 std::vector<uint8_t> data = _read_mgh_data_uchar(&mgh_header, filename);
3683 mgh->data.data_mri_uchar = data;
3684 }
3685 else if (mgh->header.dtype == MRI_FLOAT)
3686 {
3687 std::vector<float> data = _read_mgh_data_float(&mgh_header, filename);
3688 mgh->data.data_mri_float = data;
3689 }
3690 else if (mgh->header.dtype == MRI_SHORT)
3691 {
3692 std::vector<short> data = _read_mgh_data_short(&mgh_header, filename);
3693 mgh->data.data_mri_short = data;
3694 }
3695 else
3696 {
3697#ifdef LIBFS_DBG_INFO
3698 if (fs::util::ends_with(filename, ".mgz"))
3699 {
3700#ifndef LIBFS_HAS_ZLIB
3701 std::cout << LIBFS_APPTAG << "Note: your MGH filename ends with '.mgz'. MGZ support requires zlib: link with -lz. If you already have zlib and see this, #define LIBFS_HAS_ZLIB before including libfs.h, or upgrade your compiler.\n";
3702#else
3703 std::cout << LIBFS_APPTAG << "Note: your MGH filename ends with '.mgz'. Did you mean to call read_mgz() instead of read_mgh()?\n";
3704#endif
3705 }
3706#endif
3707 throw std::runtime_error("Not reading MGH data from file '" + filename + "', data type " + std::to_string(mgh->header.dtype) + " not supported yet.\n");
3708 }
3709 }
3710
3720 std::vector<std::string> read_subjectsfile(const std::string &filename)
3721 {
3722 std::vector<std::string> subjects;
3723 std::ifstream input(filename, std::fstream::in);
3724 std::string line;
3725
3726 if (!input.is_open())
3727 {
3728 throw std::runtime_error("Could not open subjects file '" + filename + "'.\n");
3729 }
3730
3731 while (std::getline(input, line))
3732 {
3733 subjects.push_back(line);
3734 }
3735 return (subjects);
3736 }
3737
3749 void write_subjectsfile(const std::string &filename, const std::vector<std::string> &subjects)
3750 {
3751 std::ofstream ofs;
3752 ofs.open(filename, std::ofstream::out);
3753 if (ofs.is_open())
3754 {
3755 for (size_t i = 0; i < subjects.size(); i++)
3756 {
3757 ofs << subjects[i] << "\n";
3758 }
3759 ofs.close();
3760 }
3761 else
3762 {
3763 throw std::runtime_error("Unable to open subjects file '" + filename + "' for writing.\n");
3764 }
3765 }
3766
3772 void read_mgh(Mgh *mgh, std::istream *is)
3773 {
3774 MghHeader mgh_header;
3775 read_mgh_header(&mgh_header, is);
3776 mgh->header = mgh_header;
3777 if (mgh->header.dtype == MRI_INT)
3778 {
3779 std::vector<int32_t> data = _read_mgh_data_int(&mgh_header, is);
3780 mgh->data.data_mri_int = data;
3781 }
3782 else if (mgh->header.dtype == MRI_UCHAR)
3783 {
3784 std::vector<uint8_t> data = _read_mgh_data_uchar(&mgh_header, is);
3785 mgh->data.data_mri_uchar = data;
3786 }
3787 else if (mgh->header.dtype == MRI_FLOAT)
3788 {
3789 std::vector<float> data = _read_mgh_data_float(&mgh_header, is);
3790 mgh->data.data_mri_float = data;
3791 }
3792 else if (mgh->header.dtype == MRI_SHORT)
3793 {
3794 std::vector<short> data = _read_mgh_data_short(&mgh_header, is);
3795 mgh->data.data_mri_short = data;
3796 }
3797 else
3798 {
3799 throw std::runtime_error("Not reading data from MGH stream, data type " + std::to_string(mgh->header.dtype) + " not supported yet.\n");
3800 }
3801 }
3802
3808 void read_mgh_header(MghHeader *mgh_header, std::istream *is)
3809 {
3810 const int MGH_VERSION = 1;
3811
3812 int format_version = _freadt<int32_t>(*is);
3813 if (format_version != MGH_VERSION)
3814 {
3815 throw std::runtime_error("Invalid MGH file or unsupported file format version: expected version " + std::to_string(MGH_VERSION) + ", found " + std::to_string(format_version) + ".\n");
3816 }
3817 mgh_header->dim1length = _freadt<int32_t>(*is);
3818 mgh_header->dim2length = _freadt<int32_t>(*is);
3819 mgh_header->dim3length = _freadt<int32_t>(*is);
3820 mgh_header->dim4length = _freadt<int32_t>(*is);
3821
3822 // Validate dimensions: must be positive (negative would wrap to huge size_t).
3823 if (mgh_header->dim1length <= 0 || mgh_header->dim2length <= 0 ||
3824 mgh_header->dim3length <= 0 || mgh_header->dim4length <= 0)
3825 {
3826 throw std::domain_error("MGH header contains non-positive dimension(s): dims=(" +
3827 std::to_string(mgh_header->dim1length) + "," +
3828 std::to_string(mgh_header->dim2length) + "," +
3829 std::to_string(mgh_header->dim3length) + "," +
3830 std::to_string(mgh_header->dim4length) + ").\n");
3831 }
3832
3833 // Validate total number of values against allocation limit.
3834 if (!fs::util::check_alloc(static_cast<size_t>(mgh_header->dim1length) *
3835 static_cast<size_t>(mgh_header->dim2length) *
3836 static_cast<size_t>(mgh_header->dim3length),
3837 static_cast<size_t>(mgh_header->dim4length)))
3838 {
3839 throw std::runtime_error("MGH header volume size exceeds maximum allowed allocation (" +
3840 std::to_string(LIBFS_MAX_ALLOC_BYTES) + " bytes).\n");
3841 }
3842
3843 mgh_header->dtype = _freadt<int32_t>(*is);
3844 mgh_header->dof = _freadt<int32_t>(*is);
3845
3846 int unused_header_space_size_left = 256; // in bytes
3847 mgh_header->ras_good_flag = _freadt<int16_t>(*is);
3848 unused_header_space_size_left -= 2; // for the ras_good_flag
3849
3850 // Read the RAS part of the header.
3851 if (mgh_header->ras_good_flag == 1)
3852 {
3853 mgh_header->xsize = _freadt<float>(*is);
3854 mgh_header->ysize = _freadt<float>(*is);
3855 mgh_header->zsize = _freadt<float>(*is);
3856
3857 // Validate voxel sizes: must be finite and non-zero to prevent division-by-zero
3858 // and NaN/Inf propagation in spatial transform calculations.
3859 if (!fs::util::is_finite_float(mgh_header->xsize) ||
3860 !fs::util::is_finite_float(mgh_header->ysize) ||
3861 !fs::util::is_finite_float(mgh_header->zsize))
3862 {
3863 throw std::domain_error("MGH header contains NaN or Inf voxel size(s): x=" +
3864 std::to_string(mgh_header->xsize) + " y=" +
3865 std::to_string(mgh_header->ysize) + " z=" +
3866 std::to_string(mgh_header->zsize) + ".\n");
3867 }
3868 if (mgh_header->xsize == 0.0f || mgh_header->ysize == 0.0f || mgh_header->zsize == 0.0f)
3869 {
3870 throw std::domain_error("MGH header contains zero voxel size(s): x=" +
3871 std::to_string(mgh_header->xsize) + " y=" +
3872 std::to_string(mgh_header->ysize) + " z=" +
3873 std::to_string(mgh_header->zsize) + ".\n");
3874 }
3875
3876 for (int i = 0; i < 9; i++)
3877 {
3878 mgh_header->Mdc.push_back(_freadt<float>(*is));
3879 }
3880 for (int i = 0; i < 3; i++)
3881 {
3882 mgh_header->Pxyz_c.push_back(_freadt<float>(*is));
3883 }
3884
3885 // Validate the direction cosine matrix (Mdc) and center coordinates (Pxyz_c).
3886 for (size_t i = 0; i < mgh_header->Mdc.size(); i++)
3887 {
3888 if (!fs::util::is_finite_float(mgh_header->Mdc[i]))
3889 {
3890 throw std::domain_error("MGH header Mdc matrix contains NaN or Inf at index " +
3891 std::to_string(i) + ".\n");
3892 }
3893 }
3894 for (size_t i = 0; i < mgh_header->Pxyz_c.size(); i++)
3895 {
3896 if (!fs::util::is_finite_float(mgh_header->Pxyz_c[i]))
3897 {
3898 throw std::domain_error("MGH header Pxyz_c contains NaN or Inf at index " +
3899 std::to_string(i) + ".\n");
3900 }
3901 }
3902
3903 unused_header_space_size_left -= 60;
3904 }
3905
3906 // Advance to data part. We do not seek here because that is not
3907 // possible if the stream is gzip-wrapped with zstr, as in the read_mgz example.
3908 uint8_t discarded;
3909 while (unused_header_space_size_left > 0)
3910 {
3911 discarded = _freadt<uint8_t>(*is);
3912 unused_header_space_size_left -= 1;
3913 }
3914 (void)discarded; // Suppress warnings about unused variable.
3915 }
3916
3921 std::vector<int32_t> _read_mgh_data_int(MghHeader *mgh_header, const std::string &filename)
3922 {
3923 if (mgh_header->dtype != MRI_INT)
3924 {
3925#ifdef LIBFS_DBG_ERROR
3926 std::cerr << "Expected MRI data type " << MRI_INT << ", but found " << mgh_header->dtype << ".\n";
3927#endif
3928 }
3929 return (_read_mgh_data<int32_t>(mgh_header, filename));
3930 }
3931
3936 std::vector<int32_t> _read_mgh_data_int(MghHeader *mgh_header, std::istream *is)
3937 {
3938 if (mgh_header->dtype != MRI_INT)
3939 {
3940#ifdef LIBFS_DBG_ERROR
3941 std::cerr << "Expected MRI data type " << MRI_INT << ", but found " << mgh_header->dtype << ".\n";
3942#endif
3943 }
3944 return (_read_mgh_data<int32_t>(mgh_header, is));
3945 }
3946
3951 std::vector<short> _read_mgh_data_short(MghHeader *mgh_header, const std::string &filename)
3952 {
3953 if (mgh_header->dtype != MRI_SHORT)
3954 {
3955#ifdef LIBFS_DBG_ERROR
3956 std::cerr << "Expected MRI data type " << MRI_SHORT << ", but found " << mgh_header->dtype << ".\n";
3957#endif
3958 }
3959 return (_read_mgh_data<short>(mgh_header, filename));
3960 }
3961
3966 std::vector<short> _read_mgh_data_short(MghHeader *mgh_header, std::istream *is)
3967 {
3968 if (mgh_header->dtype != MRI_SHORT)
3969 {
3970#ifdef LIBFS_DBG_ERROR
3971 std::cerr << "Expected MRI data type " << MRI_SHORT << ", but found " << mgh_header->dtype << ".\n";
3972#endif
3973 }
3974 return (_read_mgh_data<short>(mgh_header, is));
3975 }
3976
3983 void read_mgh_header(MghHeader *mgh_header, const std::string &filename)
3984 {
3985 std::ifstream ifs;
3986 ifs.open(filename, std::ios_base::in | std::ios::binary);
3987 if (ifs.is_open())
3988 {
3989 read_mgh_header(mgh_header, &ifs);
3990 ifs.close();
3991 }
3992 else
3993 {
3994 throw std::runtime_error("Unable to open MGH file '" + filename + "'.\n");
3995 }
3996 }
3997
4003 template <typename T>
4004 std::vector<T> _read_mgh_data(MghHeader *mgh_header, const std::string &filename)
4005 {
4006 std::ifstream ifs;
4007 ifs.open(filename, std::ios_base::in | std::ios::binary);
4008 if (ifs.is_open())
4009 {
4010 size_t num_values = mgh_header->num_values();
4011
4012 // Cross-check: ensure the file has enough data after the 284-byte header.
4013 size_t file_size = fs::util::get_file_size(filename);
4014 if (file_size > 0)
4015 {
4016 const size_t HEADER_SIZE = 284;
4017 size_t expected_data_bytes = 0;
4018 if (!fs::util::safe_multiply(num_values, sizeof(T), expected_data_bytes))
4019 {
4020 throw std::overflow_error("MGH data size computation overflowed.\n");
4021 }
4022 if (file_size < HEADER_SIZE || (file_size - HEADER_SIZE) < expected_data_bytes)
4023 {
4024 throw std::runtime_error("MGH file '" + filename + "' is too small (" +
4025 std::to_string(file_size) + " bytes) for the data claimed in its header (" +
4026 std::to_string(HEADER_SIZE + expected_data_bytes) + " bytes).\n");
4027 }
4028 }
4029
4030 if (!fs::util::check_alloc(num_values, sizeof(T)))
4031 {
4032 throw std::runtime_error("MGH file data size exceeds maximum allowed allocation.\n");
4033 }
4034
4035 ifs.seekg(284, ifs.beg); // skip to end of header and beginning of data
4036
4037 std::vector<T> data;
4038 data.reserve(num_values);
4039 for (size_t i = 0; i < num_values; i++)
4040 {
4041 data.push_back(_freadt<T>(ifs));
4042 }
4043 ifs.close();
4044 return (data);
4045 }
4046 else
4047 {
4048 throw std::runtime_error("Unable to open MGH file '" + filename + "'.\n");
4049 }
4050 }
4051
4056 template <typename T>
4057 std::vector<T> _read_mgh_data(MghHeader *mgh_header, std::istream *is)
4058 {
4059 size_t num_values = mgh_header->num_values();
4060 if (!fs::util::check_alloc(num_values, sizeof(T)))
4061 {
4062 throw std::runtime_error("MGH stream data size exceeds maximum allowed allocation.\n");
4063 }
4064 std::vector<T> data;
4065 data.reserve(num_values);
4066 for (size_t i = 0; i < num_values; i++)
4067 {
4068 data.push_back(_freadt<T>(*is));
4069 }
4070 return (data);
4071 }
4072
4077 std::vector<float> _read_mgh_data_float(MghHeader *mgh_header, const std::string &filename)
4078 {
4079 if (mgh_header->dtype != MRI_FLOAT)
4080 {
4081#ifdef LIBFS_DBG_ERROR
4082 std::cerr << "Expected MRI data type " << MRI_FLOAT << ", but found " << mgh_header->dtype << ".\n";
4083#endif
4084 }
4085 return (_read_mgh_data<float>(mgh_header, filename));
4086 }
4087
4092 std::vector<float> _read_mgh_data_float(MghHeader *mgh_header, std::istream *is)
4093 {
4094 if (mgh_header->dtype != MRI_FLOAT)
4095 {
4096#ifdef LIBFS_DBG_ERROR
4097 std::cerr << "Expected MRI data type " << MRI_FLOAT << ", but found " << mgh_header->dtype << ".\n";
4098#endif
4099 }
4100 return (_read_mgh_data<float>(mgh_header, is));
4101 }
4102
4107 std::vector<uint8_t> _read_mgh_data_uchar(MghHeader *mgh_header, const std::string &filename)
4108 {
4109 if (mgh_header->dtype != MRI_UCHAR)
4110 {
4111#ifdef LIBFS_DBG_ERROR
4112 std::cerr << "Expected MRI data type " << MRI_UCHAR << ", but found " << mgh_header->dtype << ".\n";
4113#endif
4114 }
4115 return (_read_mgh_data<uint8_t>(mgh_header, filename));
4116 }
4117
4122 std::vector<uint8_t> _read_mgh_data_uchar(MghHeader *mgh_header, std::istream *is)
4123 {
4124 if (mgh_header->dtype != MRI_UCHAR)
4125 {
4126#ifdef LIBFS_DBG_ERROR
4127 std::cerr << "Expected MRI data type " << MRI_UCHAR << ", but found " << mgh_header->dtype << ".\n";
4128#endif
4129 }
4130 return (_read_mgh_data<uint8_t>(mgh_header, is));
4131 }
4132
4146 void read_surf(Mesh *surface, const std::string &filename)
4147 {
4148 const int SURF_TRIS_MAGIC = 16777214;
4149 std::ifstream is;
4150 is.open(filename, std::ios_base::in | std::ios::binary);
4151 if (is.is_open())
4152 {
4153 int magic = _fread3(is);
4154 if (magic != SURF_TRIS_MAGIC)
4155 {
4156 throw std::domain_error("Surf file '" + filename + "' magic code in header did not match: expected " + std::to_string(SURF_TRIS_MAGIC) + ", found " + std::to_string(magic) + ".\n");
4157 }
4158 std::string created_line = _freadstringnewline(is);
4159 std::string comment_line = _freadstringnewline(is);
4160 int num_verts = _freadt<int32_t>(is);
4161 int num_faces = _freadt<int32_t>(is);
4162
4163 // Validate header fields.
4164 if (num_verts <= 0)
4165 {
4166 throw std::domain_error("Surf file '" + filename + "' has invalid num_verts: " + std::to_string(num_verts) + ".\n");
4167 }
4168 if (num_faces < 0)
4169 {
4170 throw std::domain_error("Surf file '" + filename + "' has invalid num_faces: " + std::to_string(num_faces) + ".\n");
4171 }
4172
4173 // Safe multiplication: num_verts * 3 (x,y,z per vertex).
4174 size_t num_vert_coords = 0;
4175 if (!fs::util::safe_multiply(static_cast<size_t>(num_verts), 3, num_vert_coords))
4176 {
4177 throw std::overflow_error("Surf file '" + filename + "': num_verts * 3 overflowed.\n");
4178 }
4179 size_t num_face_indices = 0;
4180 if (!fs::util::safe_multiply(static_cast<size_t>(num_faces), 3, num_face_indices))
4181 {
4182 throw std::overflow_error("Surf file '" + filename + "': num_faces * 3 overflowed.\n");
4183 }
4184
4185 // Cross-check against file size.
4186 size_t file_size = fs::util::get_file_size(filename);
4187 if (file_size > 0)
4188 {
4189 size_t vert_bytes = 0, face_bytes = 0;
4190 if (!fs::util::safe_multiply(num_vert_coords, sizeof(float), vert_bytes) ||
4191 !fs::util::safe_multiply(num_face_indices, sizeof(int32_t), face_bytes))
4192 {
4193 throw std::overflow_error("Surf file '" + filename + "': expected data size overflowed.\n");
4194 }
4195 // Guard against addition overflow (paranoid, since each is already <= LIBFS_MAX_ALLOC_BYTES).
4196 if (vert_bytes > std::numeric_limits<size_t>::max() - face_bytes)
4197 {
4198 throw std::overflow_error("Surf file '" + filename + "': total data size overflowed.\n");
4199 }
4200 size_t expected_total = vert_bytes + face_bytes;
4201 // Header takes some space, so file_size > raw data size for any valid file.
4202 if (file_size < expected_total)
4203 {
4204 throw std::runtime_error("Surf file '" + filename + "' is too small (" +
4205 std::to_string(file_size) + " bytes) for the data claimed in its header.\n");
4206 }
4207 }
4208
4209 if (!fs::util::check_alloc(num_vert_coords, sizeof(float)) ||
4210 !fs::util::check_alloc(num_face_indices, sizeof(int32_t)))
4211 {
4212 throw std::runtime_error("Surf file '" + filename + "' data size exceeds maximum allowed allocation.\n");
4213 }
4214
4215#ifdef LIBFS_DBG_INFO
4216 std::cout << LIBFS_APPTAG << "Read surface file with " << num_verts << " vertices, " << num_faces << " faces.\n";
4217#endif
4218 std::vector<float> vdata;
4219 vdata.reserve(num_vert_coords);
4220 for (size_t i = 0; i < num_vert_coords; i++)
4221 {
4222 vdata.push_back(_freadt<float>(is));
4223 }
4224 std::vector<int> fdata;
4225 fdata.reserve(num_face_indices);
4226 for (size_t i = 0; i < num_face_indices; i++)
4227 {
4228 fdata.push_back(_freadt<int32_t>(is));
4229 }
4230 is.close();
4231 surface->vertices = vdata;
4232 surface->faces = fdata;
4233 }
4234 else
4235 {
4236 throw std::runtime_error("Unable to open surface file '" + filename + "'.\n");
4237 }
4238 }
4239
4252 void read_mesh(Mesh *surface, const std::string &filename)
4253 {
4254 if (fs::util::ends_with(filename, ".obj"))
4255 {
4256 fs::Mesh::from_obj(surface, filename);
4257 }
4258 else if (fs::util::ends_with(filename, ".ply"))
4259 {
4260 fs::Mesh::from_ply(surface, filename);
4261 }
4262 else if (fs::util::ends_with(filename, ".off"))
4263 {
4264 fs::Mesh::from_off(surface, filename);
4265 }
4266 else
4267 {
4268 read_surf(surface, filename);
4269 }
4270 }
4271
4277 bool _is_bigendian()
4278 {
4279 const short int number = 0x1;
4280 const char *numPtr = reinterpret_cast<const char *>(&number);
4281 return (numPtr[0] != 1);
4282 }
4283
4293 void read_curv(Curv *curv, std::istream *is, const std::string &source_filename = "")
4294 {
4295 const std::string msg_source_file_part = source_filename.empty() ? "" : "'" + source_filename + "' ";
4296 const int CURV_MAGIC = 16777215;
4297 int magic = _fread3(*is);
4298 if (magic != CURV_MAGIC)
4299 {
4300 throw std::domain_error("Curv file " + msg_source_file_part + "header magic did not match: expected " + std::to_string(CURV_MAGIC) + ", found " + std::to_string(magic) + ".\n");
4301 }
4302 curv->num_vertices = _freadt<int32_t>(*is);
4303 curv->num_faces = _freadt<int32_t>(*is);
4304 curv->num_values_per_vertex = _freadt<int32_t>(*is);
4305
4306 // Validate header fields.
4307 if (curv->num_vertices <= 0)
4308 {
4309 throw std::domain_error("Curv file " + msg_source_file_part + "has invalid num_vertices: " + std::to_string(curv->num_vertices) + ".\n");
4310 }
4311 if (curv->num_faces < 0)
4312 {
4313 throw std::domain_error("Curv file " + msg_source_file_part + "has invalid num_faces: " + std::to_string(curv->num_faces) + ".\n");
4314 }
4315
4316#ifdef LIBFS_DBG_INFO
4317 std::cout << LIBFS_APPTAG << "Read curv file with " << curv->num_vertices << " vertices, " << curv->num_faces << " faces and " << curv->num_values_per_vertex << " values per vertex.\n";
4318#endif
4319 if (curv->num_values_per_vertex != 1)
4320 { // Not supported, I know no case where this is used. Please submit a PR with a demo file if you have one, and let me know where it came from.
4321 throw std::domain_error("Curv file " + msg_source_file_part + "must contain exactly 1 value per vertex, found " + std::to_string(curv->num_values_per_vertex) + ".\n");
4322 }
4323
4324 // File-size cross-check (only when reading from a file, not a generic stream).
4325 if (!source_filename.empty())
4326 {
4327 size_t file_size = fs::util::get_file_size(source_filename);
4328 if (file_size > 0)
4329 {
4330 // Curv header: 3 (magic) + 12 (three int32) = 15 bytes.
4331 const size_t CURV_HEADER_SIZE = 15;
4332 size_t expected_data_bytes = 0;
4333 if (!fs::util::safe_multiply(static_cast<size_t>(curv->num_vertices), sizeof(float), expected_data_bytes))
4334 {
4335 throw std::overflow_error("Curv file " + msg_source_file_part + "data size computation overflowed.\n");
4336 }
4337 if (file_size < CURV_HEADER_SIZE || (file_size - CURV_HEADER_SIZE) < expected_data_bytes)
4338 {
4339 throw std::runtime_error("Curv file " + msg_source_file_part + "is too small (" +
4340 std::to_string(file_size) + " bytes) for the data claimed in its header (" +
4341 std::to_string(CURV_HEADER_SIZE + expected_data_bytes) + " bytes expected).\n");
4342 }
4343 }
4344 }
4345
4346 std::vector<float> data;
4347 if (!fs::util::check_alloc(static_cast<size_t>(curv->num_vertices), sizeof(float)))
4348 {
4349 throw std::runtime_error("Curv file " + msg_source_file_part + "data size exceeds maximum allowed allocation.\n");
4350 }
4351 data.reserve(static_cast<size_t>(curv->num_vertices));
4352 for (size_t i = 0; i < static_cast<size_t>(curv->num_vertices); i++)
4353 {
4354 data.push_back(_freadt<float>(*is));
4355 }
4356 curv->data = data;
4357 }
4358
4371 void read_curv(Curv *curv, const std::string &filename)
4372 {
4373 std::ifstream is(filename, std::fstream::in | std::fstream::binary);
4374 if (is.is_open())
4375 {
4376 read_curv(curv, &is, filename);
4377 is.close();
4378 }
4379 else
4380 {
4381 throw std::runtime_error("Could not open curv file '" + filename + "' for reading.\n");
4382 }
4383 }
4384
4387 void _read_annot_colortable(Colortable *colortable, std::istream *is, int32_t num_entries)
4388 {
4389 // Validate num_entries against a reasonable cap.
4390 if (num_entries < 0 || static_cast<size_t>(num_entries) > LIBFS_MAX_COLORTABLE_ENTRIES)
4391 {
4392 throw std::domain_error("Annot colortable num_entries " + std::to_string(num_entries) +
4393 " is invalid or exceeds maximum (" + std::to_string(LIBFS_MAX_COLORTABLE_ENTRIES) + ").\n");
4394 }
4395
4396 int32_t num_chars_orig_filename = _freadt<int32_t>(*is); // The number of characters of the file this annot was built from.
4397
4398 // Validate and cap the original filename length.
4399 if (num_chars_orig_filename < 0 || static_cast<size_t>(num_chars_orig_filename) > LIBFS_MAX_STRING_LENGTH)
4400 {
4401 throw std::domain_error("Annot colortable original filename length " + std::to_string(num_chars_orig_filename) +
4402 " exceeds maximum (" + std::to_string(LIBFS_MAX_STRING_LENGTH) + ").\n");
4403 }
4404
4405 // It follows the name of the file this annot was built from. This is development metadata and irrelevant afaik. We skip it.
4406 uint8_t discarded;
4407 for (int32_t i = 0; i < num_chars_orig_filename; i++)
4408 {
4409 discarded = _freadt<uint8_t>(*is);
4410 }
4411 (void)discarded; // Suppress warnings about unused variable.
4412
4413 int32_t num_entries_duplicated = _freadt<int32_t>(*is); // Yes, once more.
4414 if (num_entries != num_entries_duplicated)
4415 {
4416#ifdef LIBFS_DBG_ERROR
4417 std::cerr << "Warning: the two num_entries header fields of this annotation do not match. Use with care.\n";
4418#endif
4419 }
4420
4421 colortable->id.reserve(static_cast<size_t>(num_entries));
4422 colortable->name.reserve(static_cast<size_t>(num_entries));
4423 colortable->r.reserve(static_cast<size_t>(num_entries));
4424 colortable->g.reserve(static_cast<size_t>(num_entries));
4425 colortable->b.reserve(static_cast<size_t>(num_entries));
4426 colortable->a.reserve(static_cast<size_t>(num_entries));
4427 colortable->label.reserve(static_cast<size_t>(num_entries));
4428
4429 int32_t entry_num_chars;
4430 for (int32_t i = 0; i < num_entries; i++)
4431 {
4432 colortable->id.push_back(_freadt<int32_t>(*is));
4433 entry_num_chars = _freadt<int32_t>(*is);
4434 // Pass a tighter max_length for region names (256 chars should be plenty).
4435 colortable->name.push_back(_freadfixedlengthstring(*is, entry_num_chars, true, 256));
4436 colortable->r.push_back(_freadt<int32_t>(*is));
4437 colortable->g.push_back(_freadt<int32_t>(*is));
4438 colortable->b.push_back(_freadt<int32_t>(*is));
4439 colortable->a.push_back(_freadt<int32_t>(*is));
4440 colortable->label.push_back(static_cast<uint32_t>(colortable->r[i]) + static_cast<uint32_t>(colortable->g[i]) * 256u + static_cast<uint32_t>(colortable->b[i]) * 65536u + static_cast<uint32_t>(colortable->a[i]) * 16777216u);
4441 }
4442 }
4443
4446 size_t _vidx_2d(size_t row, size_t column, size_t row_length = 3)
4447 {
4448 return (row + 1) * row_length - row_length + column;
4449 }
4450
4456 void read_annot(Annot *annot, std::istream *is)
4457 {
4458
4459 int32_t num_vertices = _freadt<int32_t>(*is);
4460
4461 // Validate num_vertices.
4462 if (num_vertices <= 0)
4463 {
4464 throw std::domain_error("Annot file has invalid num_vertices: " + std::to_string(num_vertices) + ".\n");
4465 }
4466
4467 // Safe multiplication: num_vertices * 2 (vertex index + label per vertex).
4468 size_t num_entries = 0;
4469 if (!fs::util::safe_multiply(static_cast<size_t>(num_vertices), 2, num_entries))
4470 {
4471 throw std::overflow_error("Annot: num_vertices * 2 overflowed.\n");
4472 }
4473 if (!fs::util::check_alloc(num_entries, sizeof(int32_t)))
4474 {
4475 throw std::runtime_error("Annot vertex/label data size exceeds maximum allowed allocation.\n");
4476 }
4477
4478 std::vector<int32_t> vertices;
4479 std::vector<int32_t> labels;
4480 vertices.reserve(num_vertices);
4481 labels.reserve(num_vertices);
4482 for (size_t i = 0; i < num_entries; i++)
4483 { // The vertices and their labels are stored directly after one another: v1,v1_label,v2,v2_label,...
4484 if (i % 2 == 0)
4485 {
4486 vertices.push_back(_freadt<int32_t>(*is));
4487 }
4488 else
4489 {
4490 labels.push_back(_freadt<int32_t>(*is));
4491 }
4492 }
4493 annot->vertex_indices = vertices;
4494 annot->vertex_labels = labels;
4495 int32_t has_colortable = _freadt<int32_t>(*is);
4496 if (has_colortable == 1)
4497 {
4498 int32_t num_colortable_entries_old_format = _freadt<int32_t>(*is);
4499 if (num_colortable_entries_old_format > 0)
4500 {
4501 throw std::domain_error("Reading annotation in old format not supported. Please open an issue and supply an example file if you need this.\n");
4502 }
4503 else
4504 {
4505 int32_t colortable_format_version = -num_colortable_entries_old_format; // If the value is negative, we are in new format and its absolute value is the format version.
4506 if (colortable_format_version == 2)
4507 {
4508 int32_t num_colortable_entries = _freadt<int32_t>(*is); // This time for real.
4509 _read_annot_colortable(&annot->colortable, is, num_colortable_entries);
4510 }
4511 else
4512 {
4513 throw std::domain_error("Reading annotation in new format version !=2 not supported. Please open an issue and supply an example file if you need this.\n");
4514 }
4515 }
4516 }
4517 else
4518 {
4519 throw std::domain_error("Reading annotation without colortable not supported. Maybe invalid annotation file?\n");
4520 }
4521 }
4522
4536 void read_annot(Annot *annot, const std::string &filename)
4537 {
4538 std::ifstream is(filename, std::fstream::in | std::fstream::binary);
4539 if (is.is_open())
4540 {
4541 read_annot(annot, &is);
4542 is.close();
4543 }
4544 else
4545 {
4546 throw std::runtime_error("Could not open annot file '" + filename + "' for reading.\n");
4547 }
4548 }
4549
4562 std::vector<float> read_curv_data(const std::string &filename)
4563 {
4564 Curv curv;
4565 read_curv(&curv, filename);
4566 return (curv.data);
4567 }
4568
4584 inline std::vector<float> read_desc_data(const std::string &filename)
4585 {
4586 if (fs::util::ends_with(filename, {".MGH", ".mgh"}))
4587 {
4588 fs::Mgh mgh;
4589 fs::read_mgh(&mgh, filename);
4590 assert(mgh.header.dtype == fs::MRI_FLOAT);
4591 int num_gt_1 = 0;
4592 std::vector<int> dims = {mgh.header.dim1length, mgh.header.dim2length, mgh.header.dim3length, mgh.header.dim4length};
4593 for (size_t i = 0; i < dims.size(); i++)
4594 {
4595 if (dims[i] > 1)
4596 {
4597 num_gt_1++;
4598 }
4599 }
4600 if (num_gt_1 > 1)
4601 {
4602#ifdef LIBFS_DBG_ERROR
4603 std::cerr << "MGH file '" << filename << "' contains more than one non-empty dimension. Returning concatinated data.\n";
4604#endif
4605 }
4606 return mgh.data.data_mri_float;
4607 }
4608 else if (fs::util::ends_with(filename, {".NII", ".nii", ".NII.GZ", ".nii.gz"}))
4609 {
4610 fs::Mgh mgh;
4611 fs::read_nifti(&mgh, filename);
4612 if (mgh.header.dtype != fs::MRI_FLOAT)
4613 {
4614 throw std::runtime_error("read_desc_data currently only supports NIfTI files with FLOAT32 data.\n");
4615 }
4616 int num_gt_1 = 0;
4617 std::vector<int> dims = {mgh.header.dim1length, mgh.header.dim2length, mgh.header.dim3length, mgh.header.dim4length};
4618 for (size_t i = 0; i < dims.size(); i++)
4619 {
4620 if (dims[i] > 1)
4621 {
4622 num_gt_1++;
4623 }
4624 }
4625 if (num_gt_1 > 1)
4626 {
4627#ifdef LIBFS_DBG_ERROR
4628 std::cerr << "NIfTI file '" << filename << "' contains more than one non-empty dimension. Returning concatenated data.\n";
4629#endif
4630 }
4631 return mgh.data.data_mri_float;
4632 }
4633 else
4634 {
4635 Curv curv;
4636 read_curv(&curv, filename);
4637 return (curv.data);
4638 }
4639 }
4640
4648 template <typename T>
4649 T _swap_endian(T u)
4650 {
4651 static_assert(CHAR_BIT == 8, "CHAR_BIT != 8");
4652
4653 unsigned char src[sizeof(T)];
4654 unsigned char dst[sizeof(T)];
4655 std::memcpy(src, &u, sizeof(T));
4656
4657 for (size_t k = 0; k < sizeof(T); k++)
4658 {
4659 dst[k] = src[sizeof(T) - k - 1];
4660 }
4661
4662 T result;
4663 std::memcpy(&result, dst, sizeof(T));
4664 return result;
4665 }
4666
4671 template <typename T>
4672 T _freadt(std::istream &is)
4673 {
4674 T t;
4675 is.read(reinterpret_cast<char *>(&t), sizeof(t));
4676 if (static_cast<size_t>(is.gcount()) != sizeof(T))
4677 {
4678 if (is.gcount() == 0)
4679 {
4680 throw std::runtime_error("Unexpected end of binary stream: expected " + std::to_string(sizeof(T)) + " bytes, got EOF.\n");
4681 }
4682 throw std::runtime_error("Short read in binary stream: expected " + std::to_string(sizeof(T)) + " bytes, got " + std::to_string(is.gcount()) + ".\n");
4683 }
4684 if (!_is_bigendian())
4685 {
4686 t = _swap_endian<T>(t);
4687 }
4688 return (t);
4689 }
4690
4695 int _fread3(std::istream &is)
4696 {
4697 uint32_t i = 0;
4698 is.read(reinterpret_cast<char *>(&i), 3);
4699 if (static_cast<size_t>(is.gcount()) != 3)
4700 {
4701 if (is.gcount() == 0)
4702 {
4703 throw std::runtime_error("Unexpected end of binary stream: expected 3 bytes, got EOF.\n");
4704 }
4705 throw std::runtime_error("Short read in binary stream: expected 3 bytes, got " + std::to_string(is.gcount()) + ".\n");
4706 }
4707 if (!_is_bigendian())
4708 {
4709 i = _swap_endian<std::uint32_t>(i);
4710 }
4711 i = ((i >> 8) & 0xffffff);
4712 return (i);
4713 }
4714
4719 template <typename T>
4720 void _fwritet(std::ostream &os, T t)
4721 {
4722 if (!_is_bigendian())
4723 {
4724 t = _swap_endian<T>(t);
4725 }
4726 os.write(reinterpret_cast<const char *>(&t), sizeof(t));
4727 }
4728
4729 // Write big endian 24 bit integer to a stream, extracted from the first 3 bytes of an unsigned 32 bit integer.
4730 //
4731 // THIS FUNCTION IS INTERNAL AND SHOULD NOT BE CALLED BY API CLIENTS.
4733 void _fwritei3(std::ostream &os, uint32_t i)
4734 {
4735 unsigned char b1 = (i >> 16) & 255;
4736 unsigned char b2 = (i >> 8) & 255;
4737 unsigned char b3 = i & 255;
4738
4739 os.write(reinterpret_cast<const char *>(&b1), sizeof(b1));
4740 os.write(reinterpret_cast<const char *>(&b2), sizeof(b2));
4741 os.write(reinterpret_cast<const char *>(&b3), sizeof(b3));
4742 }
4743
4749 void _fwritefixedlengthstring(std::ostream &os, const std::string &str, size_t len)
4750 {
4751 std::string buf(len, '\0');
4752 size_t copy_len = str.size() < len ? str.size() : len;
4753 std::memcpy(&buf[0], str.data(), copy_len);
4754 os.write(buf.data(), static_cast<std::streamsize>(len));
4755 }
4756
4761 std::string _freadstringnewline(std::istream &is)
4762 {
4763 std::string s;
4764 std::getline(is, s, '\n');
4765 return s;
4766 }
4767
4772 std::string _freadfixedlengthstring(std::istream &is, size_t length, bool strip_last_char = true, size_t max_length = LIBFS_MAX_STRING_LENGTH)
4773 {
4774 if (length == 0)
4775 {
4776 throw std::domain_error("Fixed-length string read with zero length.\n");
4777 }
4778 if (length > max_length)
4779 {
4780 throw std::domain_error("Fixed-length string length " + std::to_string(length) + " exceeds maximum " + std::to_string(max_length) + ".\n");
4781 }
4782 std::string str;
4783 str.resize(length);
4784 is.read(&str[0], length);
4785 if (static_cast<size_t>(is.gcount()) != length)
4786 {
4787 if (is.gcount() == 0)
4788 {
4789 throw std::runtime_error("Unexpected end of binary stream while reading fixed-length string: expected " + std::to_string(length) + " bytes, got EOF.\n");
4790 }
4791 throw std::runtime_error("Short read in binary stream while reading fixed-length string: expected " + std::to_string(length) + " bytes, got " + std::to_string(is.gcount()) + ".\n");
4792 }
4793 if (strip_last_char)
4794 {
4795 str = str.substr(0, length - 1);
4796 }
4797 return str;
4798 }
4799
4805 void write_annot(const Annot &annot, std::ostream &os)
4806 {
4807 int32_t num_vertices = static_cast<int32_t>(annot.num_vertices());
4808 _fwritet<int32_t>(os, num_vertices);
4809
4810 // Interleaved vertex indices and labels.
4811 for (size_t i = 0; i < static_cast<size_t>(num_vertices); i++)
4812 {
4813 _fwritet<int32_t>(os, annot.vertex_indices[i]);
4814 _fwritet<int32_t>(os, annot.vertex_labels[i]);
4815 }
4816
4817 // Colortable presence flag + version tag (version 2, no old-format entries).
4818 _fwritet<int32_t>(os, 1); // has_colortable
4819 _fwritet<int32_t>(os, -2); // version tag: negative means new format, abs value is version
4820
4821 int32_t num_entries = static_cast<int32_t>(annot.colortable.num_entries());
4822 _fwritet<int32_t>(os, num_entries);
4823
4824 // Original filename (not meaningful when writing, write "unknown" as placeholder).
4825 std::string orig_filename = "unknown";
4826 int32_t orig_filename_len = static_cast<int32_t>(orig_filename.size());
4827 _fwritet<int32_t>(os, orig_filename_len);
4828 _fwritefixedlengthstring(os, orig_filename, static_cast<size_t>(orig_filename_len));
4829
4830 // Duplicate num_entries (yes, the format stores it twice).
4831 _fwritet<int32_t>(os, num_entries);
4832
4833 for (int32_t i = 0; i < num_entries; i++)
4834 {
4835 _fwritet<int32_t>(os, annot.colortable.id[i]);
4836 // Name length: strlen + 1 for the trailing null byte, matching _freadfixedlengthstring(strip_last_char=true).
4837 int32_t name_len = static_cast<int32_t>(annot.colortable.name[i].size()) + 1;
4838 _fwritet<int32_t>(os, name_len);
4839 _fwritefixedlengthstring(os, annot.colortable.name[i] + '\0', static_cast<size_t>(name_len));
4840 _fwritet<int32_t>(os, annot.colortable.r[i]);
4841 _fwritet<int32_t>(os, annot.colortable.g[i]);
4842 _fwritet<int32_t>(os, annot.colortable.b[i]);
4843 _fwritet<int32_t>(os, annot.colortable.a[i]);
4844 }
4845 }
4846
4861 void write_annot(const Annot &annot, const std::string &filename)
4862 {
4863 std::ofstream ofs;
4864 ofs.open(filename, std::ofstream::out | std::ofstream::binary);
4865 if (ofs.is_open())
4866 {
4867 write_annot(annot, ofs);
4868 ofs.close();
4869 }
4870 else
4871 {
4872 throw std::runtime_error("Unable to open annot file '" + filename + "' for writing.\n");
4873 }
4874 }
4875
4881 void write_curv(std::ostream &os, std::vector<float> curv_data, int32_t num_faces = 100000)
4882 {
4883 const uint32_t CURV_MAGIC = 16777215;
4884 _fwritei3(os, CURV_MAGIC);
4885 _fwritet<int32_t>(os, int(curv_data.size()));
4886 _fwritet<int32_t>(os, num_faces);
4887 _fwritet<int32_t>(os, 1); // Number of values per vertex.
4888 for (size_t i = 0; i < curv_data.size(); i++)
4889 {
4890 _fwritet<float>(os, curv_data[i]);
4891 }
4892 }
4893
4908 void write_curv(const std::string &filename, std::vector<float> curv_data, const int32_t num_faces = 100000)
4909 {
4910 std::ofstream ofs;
4911 ofs.open(filename, std::ofstream::out | std::ofstream::binary);
4912 if (ofs.is_open())
4913 {
4914 write_curv(ofs, curv_data, num_faces);
4915 ofs.close();
4916 }
4917 else
4918 {
4919 throw std::runtime_error("Unable to open curvature file '" + filename + "' for writing.\n");
4920 }
4921 }
4922
4928 void write_mgh(const Mgh &mgh, std::ostream &os)
4929 {
4930 _fwritet<int32_t>(os, 1); // MGH file format version
4931 _fwritet<int32_t>(os, mgh.header.dim1length);
4932 _fwritet<int32_t>(os, mgh.header.dim2length);
4933 _fwritet<int32_t>(os, mgh.header.dim3length);
4934 _fwritet<int32_t>(os, mgh.header.dim4length);
4935
4936 _fwritet<int32_t>(os, mgh.header.dtype);
4937 _fwritet<int32_t>(os, mgh.header.dof);
4938
4939 size_t unused_header_space_size_left = 256; // in bytes
4940 _fwritet<int16_t>(os, mgh.header.ras_good_flag);
4941 unused_header_space_size_left -= 2; // for RAS flag
4942
4943 // Write RAS part of of header if flag is 1.
4944 if (mgh.header.ras_good_flag == 1)
4945 {
4946 if (mgh.header.Mdc.size() < 9 || mgh.header.Pxyz_c.size() < 3)
4947 {
4948 throw std::logic_error("MGH header ras_good_flag set but Mdc and/or Pxyz_c vectors are undersized.\n");
4949 }
4950 _fwritet<float>(os, mgh.header.xsize);
4951 _fwritet<float>(os, mgh.header.ysize);
4952 _fwritet<float>(os, mgh.header.zsize);
4953
4954 for (int i = 0; i < 9; i++)
4955 {
4956 _fwritet<float>(os, mgh.header.Mdc[i]);
4957 }
4958 for (int i = 0; i < 3; i++)
4959 {
4960 _fwritet<float>(os, mgh.header.Pxyz_c[i]);
4961 }
4962
4963 unused_header_space_size_left -= 60;
4964 }
4965
4966 for (size_t i = 0; i < unused_header_space_size_left; i++)
4967 { // Fill rest of header space.
4968 _fwritet<uint8_t>(os, 0);
4969 }
4970
4971 // Write data
4972 size_t num_values = mgh.header.num_values();
4973 if (mgh.header.dtype == MRI_INT)
4974 {
4975 if (mgh.data.data_mri_int.size() != num_values)
4976 {
4977 throw std::logic_error("Detected mismatch of MRI_INT data size and MGH header dim length values.\n");
4978 }
4979 for (size_t i = 0; i < num_values; i++)
4980 {
4981 _fwritet<int32_t>(os, mgh.data.data_mri_int[i]);
4982 }
4983 }
4984 else if (mgh.header.dtype == MRI_FLOAT)
4985 {
4986 if (mgh.data.data_mri_float.size() != num_values)
4987 {
4988 throw std::logic_error("Detected mismatch of MRI_FLOAT data size and MGH header dim length values.\n");
4989 }
4990 for (size_t i = 0; i < num_values; i++)
4991 {
4992 _fwritet<float>(os, mgh.data.data_mri_float[i]);
4993 }
4994 }
4995 else if (mgh.header.dtype == MRI_UCHAR)
4996 {
4997 if (mgh.data.data_mri_uchar.size() != num_values)
4998 {
4999 throw std::logic_error("Detected mismatch of MRI_UCHAR data size and MGH header dim length values.\n");
5000 }
5001 for (size_t i = 0; i < num_values; i++)
5002 {
5003 _fwritet<uint8_t>(os, mgh.data.data_mri_uchar[i]);
5004 }
5005 }
5006 else if (mgh.header.dtype == MRI_SHORT)
5007 {
5008 if (mgh.data.data_mri_short.size() != num_values)
5009 {
5010 throw std::logic_error("Detected mismatch of MRI_SHORT data size and MGH header dim length values.\n");
5011 }
5012 for (size_t i = 0; i < num_values; i++)
5013 {
5014 _fwritet<short>(os, mgh.data.data_mri_short[i]);
5015 }
5016 }
5017 else
5018 {
5019 throw std::domain_error("Unsupported MRI data type " + std::to_string(mgh.header.dtype) + ", cannot write MGH data.\n");
5020 }
5021 }
5022
5038 void write_mgh(const Mgh &mgh, const std::string &filename)
5039 {
5040 std::ofstream ofs;
5041 ofs.open(filename, std::ofstream::out | std::ofstream::binary);
5042 if (ofs.is_open())
5043 {
5044 write_mgh(mgh, ofs);
5045 ofs.close();
5046 }
5047 else
5048 {
5049 throw std::runtime_error("Unable to open MGH file '" + filename + "' for writing.\n");
5050 }
5051 }
5052
5053#ifdef LIBFS_HAS_ZLIB
5054
5068 inline void read_mgz(Mgh *mgh, const std::string &filename)
5069 {
5070 gzFile gz = gzopen(filename.c_str(), "rb");
5071 if (!gz)
5072 {
5073 int errnum = 0;
5074 const char *errstr = gzerror(gz, &errnum);
5075 throw std::runtime_error("Could not open MGZ file '" + filename + "' for reading: " +
5076 (errstr ? std::string(errstr) : "unknown error") + "\n");
5077 }
5078 std::vector<char> buf;
5079 char chunk[131072];
5080 int n;
5081 while ((n = gzread(gz, chunk, sizeof(chunk))) > 0)
5082 {
5083 buf.insert(buf.end(), chunk, chunk + n);
5084 }
5085 if (n < 0)
5086 {
5087 int errnum = 0;
5088 const char *errstr = gzerror(gz, &errnum);
5089 gzclose(gz);
5090 throw std::runtime_error("Error decompressing MGZ file '" + filename + "': " +
5091 (errstr ? std::string(errstr) : "unknown error") + "\n");
5092 }
5093 gzclose(gz);
5094 std::istringstream iss(std::string(buf.data(), buf.size()));
5095 read_mgh(mgh, &iss);
5096 }
5097
5113 inline void write_mgz(const Mgh &mgh, const std::string &filename)
5114 {
5115 std::ostringstream oss;
5116 write_mgh(mgh, oss);
5117 std::string data = oss.str();
5118
5119 gzFile gz = gzopen(filename.c_str(), "wb");
5120 if (!gz)
5121 {
5122 int errnum = 0;
5123 const char *errstr = gzerror(gz, &errnum);
5124 throw std::runtime_error("Could not open MGZ file '" + filename + "' for writing: " +
5125 (errstr ? std::string(errstr) : "unknown error") + "\n");
5126 }
5127 z_size_t total_written = 0;
5128 while (total_written < data.size())
5129 {
5130 int written = gzwrite(gz, data.data() + total_written, static_cast<unsigned int>(data.size() - total_written));
5131 if (written <= 0)
5132 {
5133 int errnum = 0;
5134 const char *errstr = gzerror(gz, &errnum);
5135 gzclose(gz);
5136 throw std::runtime_error("Error writing MGZ file '" + filename + "': " +
5137 (errstr ? std::string(errstr) : "unknown error") + "\n");
5138 }
5139 total_written += static_cast<z_size_t>(written);
5140 }
5141 gzclose(gz);
5142 }
5143
5144#endif // LIBFS_HAS_ZLIB
5145
5146 // ========================================================================
5147 // NIfTI-1 Support
5148 // ========================================================================
5149
5160
5162 const int16_t NIFTI_DT_NONE = 0;
5163
5166 const int16_t NIFTI_DT_BINARY = 1;
5167
5170 const int16_t NIFTI_DT_UINT8 = 2;
5171
5175 const int16_t NIFTI_DT_INT16 = 4;
5176
5180 const int16_t NIFTI_DT_INT32 = 8;
5181
5185 const int16_t NIFTI_DT_FLOAT32 = 16;
5186
5190 const int16_t NIFTI_DT_COMPLEX64 = 32;
5191
5195 const int16_t NIFTI_DT_FLOAT64 = 64;
5196
5200 const int16_t NIFTI_DT_RGB24 = 128;
5201
5205 const int16_t NIFTI_DT_INT8 = 256;
5206
5209 const int16_t NIFTI_DT_UINT16 = 512;
5210
5213 const int16_t NIFTI_DT_UINT32 = 768;
5214
5218 const int16_t NIFTI_DT_INT64 = 1024;
5219
5222 const int16_t NIFTI_DT_UINT64 = 1280;
5223
5227 const int16_t NIFTI_DT_FLOAT128 = 1536;
5228
5231 const int16_t NIFTI_DT_COMPLEX128 = 1792;
5232
5237 const int16_t NIFTI_DT_COMPLEX256 = 2048;
5238
5240
5242#pragma pack(push, 1)
5244 {
5245 int32_t sizeof_hdr;
5246 char data_type[10];
5247 char db_name[18];
5248 int32_t extents;
5250 char regular;
5252 int16_t dim[8];
5256 int16_t intent_code;
5257 int16_t datatype;
5258 int16_t bitpix;
5259 int16_t slice_start;
5260 float pixdim[8];
5264 int16_t slice_end;
5267 float cal_max;
5268 float cal_min;
5270 float toffset;
5271 int32_t glmax;
5272 int32_t glmin;
5273 char descrip[80];
5274 char aux_file[24];
5275 int16_t qform_code;
5276 int16_t sform_code;
5283 float srow_x[4];
5284 float srow_y[4];
5285 float srow_z[4];
5286 char intent_name[16];
5287 char magic[4];
5288 };
5289#pragma pack(pop)
5290
5291 // --- Internal NIfTI helpers ---
5292
5296 inline int _nifti_dtype_to_mri(int16_t nifti_dtype)
5297 {
5298 switch (nifti_dtype)
5299 {
5300 case NIFTI_DT_UINT8: return MRI_UCHAR;
5301 case NIFTI_DT_INT16: return MRI_SHORT;
5302 case NIFTI_DT_INT32: return MRI_INT;
5303 case NIFTI_DT_FLOAT32: return MRI_FLOAT;
5304 default:
5305 throw std::runtime_error("Unsupported NIfTI data type " + std::to_string(nifti_dtype) +
5306 ". Supported types: UINT8 (2), INT16 (4), INT32 (8), FLOAT32 (16).\n");
5307 }
5308 }
5309
5313 inline int16_t _mri_dtype_to_nifti(int32_t mri_dtype)
5314 {
5315 switch (mri_dtype)
5316 {
5317 case MRI_UCHAR: return NIFTI_DT_UINT8;
5318 case MRI_SHORT: return NIFTI_DT_INT16;
5319 case MRI_INT: return NIFTI_DT_INT32;
5320 case MRI_FLOAT: return NIFTI_DT_FLOAT32;
5321 default:
5322 throw std::runtime_error("Unsupported MGH data type " + std::to_string(mri_dtype) +
5323 " for NIfTI output.\n");
5324 }
5325 }
5326
5333 inline Nifti1Header _read_nifti1_header(std::istream &is, bool &file_is_bigendian)
5334 {
5335 Nifti1Header hdr;
5336 is.read(reinterpret_cast<char *>(&hdr), sizeof(Nifti1Header));
5337 if (static_cast<size_t>(is.gcount()) != sizeof(Nifti1Header))
5338 {
5339 throw std::runtime_error("NIfTI file too small for header: expected " +
5340 std::to_string(sizeof(Nifti1Header)) + " bytes.\n");
5341 }
5342
5343 // Detect endianness: sizeof_hdr must be 348.
5344 if (hdr.sizeof_hdr != 348)
5345 {
5346 int32_t swapped = _swap_endian(hdr.sizeof_hdr);
5347 if (swapped == 348)
5348 {
5349 file_is_bigendian = true;
5350 }
5351 else
5352 {
5353 throw std::runtime_error("Invalid NIfTI file: sizeof_hdr = " +
5354 std::to_string(hdr.sizeof_hdr) + " (expected 348).\n");
5355 }
5356 }
5357 else
5358 {
5359 file_is_bigendian = false;
5360 }
5361
5362 // If file endianness differs from host, byte-swap the numeric fields.
5363 bool need_swap = (file_is_bigendian != _is_bigendian());
5364 if (need_swap)
5365 {
5366 hdr.sizeof_hdr = 348; // already correct, keep it
5367 hdr.extents = _swap_endian(hdr.extents);
5368 hdr.session_error = _swap_endian(hdr.session_error);
5369 // dim_info, regular are char — no swap
5370 for (int i = 0; i < 8; i++) hdr.dim[i] = _swap_endian(hdr.dim[i]);
5371 hdr.intent_p1 = _swap_endian(hdr.intent_p1);
5372 hdr.intent_p2 = _swap_endian(hdr.intent_p2);
5373 hdr.intent_p3 = _swap_endian(hdr.intent_p3);
5374 hdr.intent_code = _swap_endian(hdr.intent_code);
5375 hdr.datatype = _swap_endian(hdr.datatype);
5376 hdr.bitpix = _swap_endian(hdr.bitpix);
5377 hdr.slice_start = _swap_endian(hdr.slice_start);
5378 for (int i = 0; i < 8; i++) hdr.pixdim[i] = _swap_endian(hdr.pixdim[i]);
5379 hdr.vox_offset = _swap_endian(hdr.vox_offset);
5380 hdr.scl_slope = _swap_endian(hdr.scl_slope);
5381 hdr.scl_inter = _swap_endian(hdr.scl_inter);
5382 hdr.slice_end = _swap_endian(hdr.slice_end);
5383 // slice_code, xyzt_units are char — no swap
5384 hdr.cal_max = _swap_endian(hdr.cal_max);
5385 hdr.cal_min = _swap_endian(hdr.cal_min);
5386 hdr.slice_duration = _swap_endian(hdr.slice_duration);
5387 hdr.toffset = _swap_endian(hdr.toffset);
5388 hdr.glmax = _swap_endian(hdr.glmax);
5389 hdr.glmin = _swap_endian(hdr.glmin);
5390 hdr.qform_code = _swap_endian(hdr.qform_code);
5391 hdr.sform_code = _swap_endian(hdr.sform_code);
5392 hdr.quatern_b = _swap_endian(hdr.quatern_b);
5393 hdr.quatern_c = _swap_endian(hdr.quatern_c);
5394 hdr.quatern_d = _swap_endian(hdr.quatern_d);
5395 hdr.qoffset_x = _swap_endian(hdr.qoffset_x);
5396 hdr.qoffset_y = _swap_endian(hdr.qoffset_y);
5397 hdr.qoffset_z = _swap_endian(hdr.qoffset_z);
5398 for (int i = 0; i < 4; i++) hdr.srow_x[i] = _swap_endian(hdr.srow_x[i]);
5399 for (int i = 0; i < 4; i++) hdr.srow_y[i] = _swap_endian(hdr.srow_y[i]);
5400 for (int i = 0; i < 4; i++) hdr.srow_z[i] = _swap_endian(hdr.srow_z[i]);
5401 }
5402
5403 // Validate magic.
5404 if (std::memcmp(hdr.magic, "n+1\0", 4) != 0 &&
5405 std::memcmp(hdr.magic, "ni1\0", 4) != 0)
5406 {
5407 // The magic may also need swapping.
5408 throw std::runtime_error("NIfTI file has invalid magic string. "
5409 "Only single-file .nii (n+1) is supported.\n");
5410 }
5411
5412 return hdr;
5413 }
5414
5417 template <typename T>
5418 inline T _nifti_read_data_element(std::istream &is, bool file_is_bigendian)
5419 {
5420 T val;
5421 is.read(reinterpret_cast<char *>(&val), sizeof(T));
5422 if (static_cast<size_t>(is.gcount()) != sizeof(T))
5423 {
5424 throw std::runtime_error("Unexpected end of NIfTI data stream.\n");
5425 }
5426 if (file_is_bigendian != _is_bigendian())
5427 {
5428 val = _swap_endian(val);
5429 }
5430 return val;
5431 }
5432
5435 template <typename T>
5436 inline void _nifti_write_data_element(std::ostream &os, T val, bool file_is_bigendian)
5437 {
5438 if (file_is_bigendian != _is_bigendian())
5439 {
5440 val = _swap_endian(val);
5441 }
5442 os.write(reinterpret_cast<const char *>(&val), sizeof(T));
5443 }
5444
5456 inline void _nifti_mat33_to_quatern(float m00, float m01, float m02,
5457 float m10, float m11, float m12,
5458 float m20, float m21, float m22,
5459 float *b, float *c, float *d, float *qfac)
5460 {
5461 // Fold any reflection into the third column (NIfTI convention).
5462 float det = m00 * (m11 * m22 - m12 * m21) -
5463 m01 * (m10 * m22 - m12 * m20) +
5464 m02 * (m10 * m21 - m11 * m20);
5465 *qfac = (det < 0.0f) ? -1.0f : 1.0f;
5466 if (*qfac < 0.0f)
5467 {
5468 m02 = -m02;
5469 m12 = -m12;
5470 m22 = -m22;
5471 }
5472
5473 // Extract a unit quaternion (w, x, y, z) using Shepperd's method.
5474 float w, x, y, z;
5475 float trace = m00 + m11 + m22;
5476 if (trace > 0.0f)
5477 {
5478 float s = std::sqrt(trace + 1.0f) * 2.0f; // s = 4*w
5479 w = 0.25f * s;
5480 x = (m21 - m12) / s;
5481 y = (m02 - m20) / s;
5482 z = (m10 - m01) / s;
5483 }
5484 else if (m00 > m11 && m00 > m22)
5485 {
5486 float s = std::sqrt(1.0f + m00 - m11 - m22) * 2.0f; // s = 4*x
5487 w = (m21 - m12) / s;
5488 x = 0.25f * s;
5489 y = (m01 + m10) / s;
5490 z = (m02 + m20) / s;
5491 }
5492 else if (m11 > m22)
5493 {
5494 float s = std::sqrt(1.0f + m11 - m00 - m22) * 2.0f; // s = 4*y
5495 w = (m02 - m20) / s;
5496 x = (m01 + m10) / s;
5497 y = 0.25f * s;
5498 z = (m12 + m21) / s;
5499 }
5500 else
5501 {
5502 float s = std::sqrt(1.0f + m22 - m00 - m11) * 2.0f; // s = 4*z
5503 w = (m10 - m01) / s;
5504 x = (m02 + m20) / s;
5505 y = (m12 + m21) / s;
5506 z = 0.25f * s;
5507 }
5508
5509 // Normalize, and ensure the scalar part is non-negative so that the
5510 // implied `a = sqrt(1 - b² - c² - d²)` reproduces the same rotation.
5511 float n = std::sqrt(w * w + x * x + y * y + z * z);
5512 if (n > 0.0f)
5513 {
5514 w /= n;
5515 x /= n;
5516 y /= n;
5517 z /= n;
5518 }
5519 if (w < 0.0f)
5520 {
5521 w = -w;
5522 x = -x;
5523 y = -y;
5524 z = -z;
5525 }
5526
5527 *b = x;
5528 *c = y;
5529 *d = z;
5530 }
5531
5542 inline void _nifti_extract_ras(const Nifti1Header &hdr, MghHeader *mgh_header)
5543 {
5544 // Decode the voxel-to-RAS affine (16 floats, row-major), translation = RAS of voxel (0,0,0).
5545 std::vector<float> affine;
5546 if (hdr.sform_code > 0)
5547 {
5548 affine.assign(16, 0.0f);
5549 affine[0] = hdr.srow_x[0];
5550 affine[1] = hdr.srow_x[1];
5551 affine[2] = hdr.srow_x[2];
5552 affine[3] = hdr.srow_x[3];
5553 affine[4] = hdr.srow_y[0];
5554 affine[5] = hdr.srow_y[1];
5555 affine[6] = hdr.srow_y[2];
5556 affine[7] = hdr.srow_y[3];
5557 affine[8] = hdr.srow_z[0];
5558 affine[9] = hdr.srow_z[1];
5559 affine[10] = hdr.srow_z[2];
5560 affine[11] = hdr.srow_z[3];
5561 affine[15] = 1.0f;
5562 }
5563 else if (hdr.qform_code > 0)
5564 {
5565 // Compute the rotation matrix from the quaternion.
5566 float b = hdr.quatern_b;
5567 float c = hdr.quatern_c;
5568 float d = hdr.quatern_d;
5569 float a = std::sqrt(std::max(0.0f, 1.0f - (b * b + c * c + d * d)));
5570 float qfac = (hdr.pixdim[0] < 0.0f) ? -1.0f : 1.0f;
5571
5572 float R11 = a * a + b * b - c * c - d * d;
5573 float R12 = 2.0f * (b * c - a * d);
5574 float R13 = 2.0f * (b * d + a * c);
5575 float R21 = 2.0f * (b * c + a * d);
5576 float R22 = a * a + c * c - b * b - d * d;
5577 float R23 = 2.0f * (c * d - a * b);
5578 float R31 = 2.0f * (b * d - a * c);
5579 float R32 = 2.0f * (c * d + a * b);
5580 float R33 = a * a + d * d - b * b - c * c;
5581
5582 // Apply qfac (a possible reflection) to the 3rd column, then scale the columns by the voxel
5583 // sizes. The translation is the qoffset (RAS of voxel (0,0,0)).
5584 float R[3][3] = {
5585 { R11, R12, R13 * qfac },
5586 { R21, R22, R23 * qfac },
5587 { R31, R32, R33 * qfac }
5588 };
5589 affine.assign(16, 0.0f);
5590 for (int i = 0; i < 3; ++i)
5591 {
5592 for (int j = 0; j < 3; ++j)
5593 {
5594 affine[i * 4 + j] = R[i][j] * hdr.pixdim[j + 1];
5595 }
5596 }
5597 affine[3] = hdr.qoffset_x;
5598 affine[7] = hdr.qoffset_y;
5599 affine[11] = hdr.qoffset_z;
5600 affine[15] = 1.0f;
5601 }
5602
5603 if (affine.empty())
5604 {
5605 // No valid spatial transform — just store the voxel sizes.
5606 mgh_header->ras_good_flag = 0;
5607 mgh_header->xsize = hdr.pixdim[1];
5608 mgh_header->ysize = hdr.pixdim[2];
5609 mgh_header->zsize = hdr.pixdim[3];
5610 mgh_header->Mdc.clear();
5611 mgh_header->Pxyz_c.clear();
5612 }
5613 else
5614 {
5615 mgh_header->set_ras_from_vox2ras(affine);
5616 }
5617 }
5618
5619 // --- Public NIfTI read API ---
5620
5626 inline void read_nifti(Mgh *mgh, std::istream *is, bool force_standard)
5627 {
5628 // 1. Determine stream size (for FS hack recovery and validation).
5629 std::streampos start_pos = is->tellg();
5630 is->seekg(0, std::ios::end);
5631 std::streamsize total_file_size = is->tellg();
5632 is->seekg(start_pos, std::ios::beg);
5633
5634 // 2. Read and validate header.
5635 bool file_is_bigendian = false;
5636 Nifti1Header hdr = _read_nifti1_header(*is, file_is_bigendian);
5637
5638 // 3. Detect FreeSurfer hack.
5639 int64_t true_dim1 = hdr.dim[1];
5640 bool hack_detected = false;
5641
5642 // dim[1] is int16_t; values > 32767 wrap to negative via signed overflow.
5643 if (hdr.dim[1] < 0 && hdr.dim[2] == 1 && hdr.dim[3] == 1)
5644 {
5645 int bytes_per_element = hdr.bitpix / 8;
5646 // dim[4]: NIfTI convention says dim[i] for i>dim[0] should be 1,
5647 // but FreeSurfer files may set it to 0. Treat 0 and 1 both as 1 frame.
5648 int64_t frames = (hdr.dim[4] > 1) ? static_cast<int64_t>(hdr.dim[4]) : 1;
5649
5650 int64_t payload_bytes = total_file_size - static_cast<int64_t>(hdr.vox_offset);
5651 int64_t computed_x = payload_bytes / (bytes_per_element * frames);
5652
5653 // False-positive mitigation: only accept if the recovered vertex count
5654 // is in a plausible range for a FreeSurfer surface mesh (1K – 5M vertices).
5655 if (computed_x >= 1000 && computed_x <= 5000000)
5656 {
5657 hack_detected = true;
5658 true_dim1 = computed_x;
5659 }
5660 }
5661
5662 // If the caller requested strict conformance, reject the hack.
5663 if (force_standard && hack_detected)
5664 {
5665 throw std::runtime_error(
5666 "NIfTI file does not conform to the NIfTI-1 standard: "
5667 "dim[1] overflow detected (likely FreeSurfer hack). "
5668 "Re-run with force_standard=false to recover surface data.\n");
5669 }
5670
5671 // 4. Map dimensions (treat dim[i] <= 0 as 1).
5672 int32_t dim2 = (hdr.dim[2] > 0) ? hdr.dim[2] : 1;
5673 int32_t dim3 = (hdr.dim[3] > 0) ? hdr.dim[3] : 1;
5674 int32_t dim4 = (hdr.dim[4] > 0) ? hdr.dim[4] : 1;
5675 int bytes_per_element = hdr.bitpix / 8;
5676
5677 // 5. Overflow-safe size validation.
5678 uint64_t total_elements = static_cast<uint64_t>(true_dim1) *
5679 static_cast<uint64_t>(dim2) *
5680 static_cast<uint64_t>(dim3) *
5681 static_cast<uint64_t>(dim4);
5682 uint64_t expected_payload = total_elements * static_cast<uint64_t>(bytes_per_element);
5683
5684 if (!fs::util::check_alloc(static_cast<size_t>(total_elements), static_cast<size_t>(bytes_per_element)))
5685 {
5686 throw std::runtime_error("NIfTI dimensions exceed maximum allowed allocation (" +
5687 std::to_string(LIBFS_MAX_ALLOC_BYTES) + " bytes).\n");
5688 }
5689
5690 uint64_t available_bytes = static_cast<uint64_t>(total_file_size) - static_cast<uint64_t>(hdr.vox_offset);
5691 if (expected_payload > available_bytes)
5692 {
5693 throw std::runtime_error("Corrupted NIfTI file: dimensions require " +
5694 std::to_string(expected_payload) + " bytes but only " +
5695 std::to_string(available_bytes) + " available.\n");
5696 }
5697
5698 if (hdr.vox_offset < 348 || static_cast<uint64_t>(hdr.vox_offset) >= static_cast<uint64_t>(total_file_size))
5699 {
5700 throw std::runtime_error("Corrupted NIfTI file: invalid vox_offset " +
5701 std::to_string(hdr.vox_offset) + ".\n");
5702 }
5703
5704 // 6. Map data type and prepare MGH header.
5705 int mri_dtype = _nifti_dtype_to_mri(hdr.datatype);
5706 mgh->header.dim1length = static_cast<int32_t>(true_dim1);
5707 mgh->header.dim2length = dim2;
5708 mgh->header.dim3length = dim3;
5709 mgh->header.dim4length = dim4;
5710 mgh->header.dtype = mri_dtype;
5711 mgh->header.dof = 0;
5712
5713 // Extract spatial metadata.
5714 _nifti_extract_ras(hdr, &mgh->header);
5715
5716 // 7. Skip any extensions and seek to voxel data.
5717 is->seekg(start_pos + std::streamoff(static_cast<int64_t>(hdr.vox_offset)), std::ios::beg);
5718
5719 // 8. Read data and apply scaling.
5720 float slope = (hdr.scl_slope != 0.0f) ? hdr.scl_slope : 1.0f;
5721 float inter = hdr.scl_inter;
5722 size_t num_voxels = static_cast<size_t>(total_elements);
5723 // Suppress unused variable warning in builds without LIBFS_DBG_INFO
5724 (void)num_voxels;
5725
5726#ifdef LIBFS_DBG_INFO
5727 std::cout << LIBFS_APPTAG << "Reading NIfTI file: " << true_dim1 << "x" << dim2
5728 << "x" << dim3 << "x" << dim4 << " (" << num_voxels << " voxels), dtype="
5729 << hdr.datatype << (hack_detected ? " [FS hack]" : "") << "\n";
5730#endif
5731
5732 if (mri_dtype == MRI_INT)
5733 {
5734 mgh->data.data_mri_int.reserve(num_voxels);
5735 for (size_t i = 0; i < num_voxels; i++)
5736 {
5737 int32_t raw = _nifti_read_data_element<int32_t>(*is, file_is_bigendian);
5738 mgh->data.data_mri_int.push_back(static_cast<int32_t>(std::round(raw * slope + inter)));
5739 }
5740 }
5741 else if (mri_dtype == MRI_FLOAT)
5742 {
5743 mgh->data.data_mri_float.reserve(num_voxels);
5744 for (size_t i = 0; i < num_voxels; i++)
5745 {
5746 float raw = _nifti_read_data_element<float>(*is, file_is_bigendian);
5747 mgh->data.data_mri_float.push_back(raw * slope + inter);
5748 }
5749 }
5750 else if (mri_dtype == MRI_UCHAR)
5751 {
5752 mgh->data.data_mri_uchar.reserve(num_voxels);
5753 for (size_t i = 0; i < num_voxels; i++)
5754 {
5755 uint8_t raw = _nifti_read_data_element<uint8_t>(*is, file_is_bigendian);
5756 mgh->data.data_mri_uchar.push_back(static_cast<uint8_t>(std::max(0.0f, std::min(255.0f, std::round(raw * slope + inter)))));
5757 }
5758 }
5759 else if (mri_dtype == MRI_SHORT)
5760 {
5761 mgh->data.data_mri_short.reserve(num_voxels);
5762 for (size_t i = 0; i < num_voxels; i++)
5763 {
5764 int16_t raw = _nifti_read_data_element<int16_t>(*is, file_is_bigendian);
5765 mgh->data.data_mri_short.push_back(static_cast<short>(std::round(raw * slope + inter)));
5766 }
5767 }
5768 }
5769
5776 inline void read_nifti(Mgh *mgh, const std::string &filename, bool force_standard)
5777 {
5778 if (fs::util::ends_with(filename, ".nii.gz") || fs::util::ends_with(filename, ".NII.GZ"))
5779 {
5780#ifdef LIBFS_HAS_ZLIB
5781 read_nifti_gz(mgh, filename, force_standard);
5782 return;
5783#else
5784 throw std::runtime_error("Cannot read .nii.gz file '" + filename +
5785 "': zlib support not enabled. "
5786 "Link with -lz or decompress the file first.\n");
5787#endif
5788 }
5789
5790 std::ifstream ifs(filename, std::ios::binary);
5791 if (!ifs.is_open())
5792 {
5793 throw std::runtime_error("Could not open NIfTI file '" + filename + "' for reading.\n");
5794 }
5795 read_nifti(mgh, &ifs, force_standard);
5796 ifs.close();
5797 }
5798
5799 // --- NIfTI write API ---
5800
5807 inline void write_nifti(const Mgh &mgh, std::ostream &os)
5808 {
5809 // Validate dimensions: NIfTI-1 uses int16_t for dim[].
5810 if (mgh.header.dim1length > 32767 || mgh.header.dim2length > 32767 ||
5811 mgh.header.dim3length > 32767 || mgh.header.dim4length > 32767)
5812 {
5813 throw std::runtime_error("MGH dimensions exceed NIfTI-1 int16 limit (32767). "
5814 "Cannot write as NIfTI.\n");
5815 }
5816
5817 bool file_is_bigendian = true; // NIfTI standard is big-endian on disk.
5818
5819 // Build header (all zeroed first).
5820 Nifti1Header hdr;
5821 std::memset(&hdr, 0, sizeof(hdr));
5822 hdr.sizeof_hdr = 348;
5823 hdr.dim[0] = 4; // always 4D for our purposes
5824 hdr.dim[1] = static_cast<int16_t>(mgh.header.dim1length);
5825 hdr.dim[2] = static_cast<int16_t>(mgh.header.dim2length);
5826 hdr.dim[3] = static_cast<int16_t>(mgh.header.dim3length);
5827 hdr.dim[4] = static_cast<int16_t>(mgh.header.dim4length);
5828 hdr.dim[5] = 1;
5829 hdr.dim[6] = 1;
5830 hdr.dim[7] = 1;
5831
5832 hdr.datatype = _mri_dtype_to_nifti(mgh.header.dtype);
5833 hdr.bitpix = 0;
5834 switch (mgh.header.dtype)
5835 {
5836 case MRI_UCHAR: hdr.bitpix = 8; break;
5837 case MRI_SHORT: hdr.bitpix = 16; break;
5838 case MRI_INT: hdr.bitpix = 32; break;
5839 case MRI_FLOAT: hdr.bitpix = 32; break;
5840 }
5841
5842 // Voxel sizes and spatial transform.
5843 hdr.pixdim[0] = 1.0f;
5844 hdr.pixdim[1] = mgh.header.xsize > 0.0f ? mgh.header.xsize : 1.0f;
5845 hdr.pixdim[2] = mgh.header.ysize > 0.0f ? mgh.header.ysize : 1.0f;
5846 hdr.pixdim[3] = mgh.header.zsize > 0.0f ? mgh.header.zsize : 1.0f;
5847 hdr.pixdim[4] = 1.0f;
5848 hdr.pixdim[5] = 1.0f;
5849 hdr.pixdim[6] = 1.0f;
5850 hdr.pixdim[7] = 1.0f;
5851
5852 hdr.vox_offset = 352.0f; // 348-byte header + 4-byte extension indicator
5853 hdr.scl_slope = 1.0f;
5854 hdr.scl_inter = 0.0f;
5855
5856 // Compute the voxel-to-RAS affine (vox2ras) from the MGH header, if it carries RAS info.
5857 std::vector<float> vox2ras = mgh.header.compute_vox2ras();
5858 if (!vox2ras.empty())
5859 {
5860 // s-form: the vox2ras affine itself. Note that the translation (srow*[3]) is the RAS
5861 // coordinate of voxel (0,0,0), which is what the NIfTI s-form stores. This is NOT the MGH
5862 // center voxel Pxyz_c; converting between the two is handled by compute_vox2ras(). This
5863 // matches what FreeSurfer's mri_convert writes.
5864 hdr.sform_code = 1; // Scanner Anatomical
5865 hdr.srow_x[0] = vox2ras[0]; hdr.srow_x[1] = vox2ras[1]; hdr.srow_x[2] = vox2ras[2]; hdr.srow_x[3] = vox2ras[3];
5866 hdr.srow_y[0] = vox2ras[4]; hdr.srow_y[1] = vox2ras[5]; hdr.srow_y[2] = vox2ras[6]; hdr.srow_y[3] = vox2ras[7];
5867 hdr.srow_z[0] = vox2ras[8]; hdr.srow_z[1] = vox2ras[9]; hdr.srow_z[2] = vox2ras[10]; hdr.srow_z[3] = vox2ras[11];
5868
5869 // q-form: encode the same affine as a quaternion. Normalize the columns of the linear part
5870 // to obtain the (orthonormal) rotation; the q-offset is the affine translation
5871 // (voxel-(0,0,0) RAS).
5872 bool qform_ok = true;
5873 float R[3][3];
5874 for (int j = 0; j < 3 && qform_ok; ++j)
5875 {
5876 float norm = 0.0f;
5877 for (int i = 0; i < 3; ++i)
5878 {
5879 norm += vox2ras[i * 4 + j] * vox2ras[i * 4 + j];
5880 }
5881 float size = std::sqrt(norm);
5882 if (size == 0.0f || !fs::util::is_finite_float(size))
5883 {
5884 qform_ok = false;
5885 break;
5886 }
5887 for (int i = 0; i < 3; ++i)
5888 {
5889 R[i][j] = vox2ras[i * 4 + j] / size;
5890 }
5891 }
5892 if (qform_ok)
5893 {
5894 float qb, qc, qd, qfac;
5895 _nifti_mat33_to_quatern(R[0][0], R[0][1], R[0][2],
5896 R[1][0], R[1][1], R[1][2],
5897 R[2][0], R[2][1], R[2][2],
5898 &qb, &qc, &qd, &qfac);
5899 hdr.qform_code = 1;
5900 hdr.pixdim[0] = qfac; // qfac sign encodes reflections (NIfTI convention)
5901 hdr.quatern_b = qb;
5902 hdr.quatern_c = qc;
5903 hdr.quatern_d = qd;
5904 hdr.qoffset_x = vox2ras[3];
5905 hdr.qoffset_y = vox2ras[7];
5906 hdr.qoffset_z = vox2ras[11];
5907 }
5908 else
5909 {
5910 hdr.qform_code = 0;
5911 }
5912 }
5913 else
5914 {
5915 hdr.sform_code = 0;
5916 hdr.qform_code = 0;
5917 }
5918
5919 // Set magic for single-file NIfTI.
5920 std::memcpy(hdr.magic, "n+1\0", 4);
5921
5922 // Write header.
5923 bool need_swap = (file_is_bigendian != _is_bigendian());
5924 if (need_swap)
5925 {
5926 Nifti1Header hdr_swapped = hdr;
5927 hdr_swapped.sizeof_hdr = _swap_endian(hdr.sizeof_hdr);
5928 hdr_swapped.extents = _swap_endian(hdr.extents);
5929 hdr_swapped.session_error = _swap_endian(hdr.session_error);
5930 for (int i = 0; i < 8; i++) hdr_swapped.dim[i] = _swap_endian(hdr.dim[i]);
5931 hdr_swapped.intent_p1 = _swap_endian(hdr.intent_p1);
5932 hdr_swapped.intent_p2 = _swap_endian(hdr.intent_p2);
5933 hdr_swapped.intent_p3 = _swap_endian(hdr.intent_p3);
5934 hdr_swapped.intent_code = _swap_endian(hdr.intent_code);
5935 hdr_swapped.datatype = _swap_endian(hdr.datatype);
5936 hdr_swapped.bitpix = _swap_endian(hdr.bitpix);
5937 hdr_swapped.slice_start = _swap_endian(hdr.slice_start);
5938 for (int i = 0; i < 8; i++) hdr_swapped.pixdim[i] = _swap_endian(hdr.pixdim[i]);
5939 hdr_swapped.vox_offset = _swap_endian(hdr.vox_offset);
5940 hdr_swapped.scl_slope = _swap_endian(hdr.scl_slope);
5941 hdr_swapped.scl_inter = _swap_endian(hdr.scl_inter);
5942 hdr_swapped.slice_end = _swap_endian(hdr.slice_end);
5943 hdr_swapped.cal_max = _swap_endian(hdr.cal_max);
5944 hdr_swapped.cal_min = _swap_endian(hdr.cal_min);
5945 hdr_swapped.slice_duration = _swap_endian(hdr.slice_duration);
5946 hdr_swapped.toffset = _swap_endian(hdr.toffset);
5947 hdr_swapped.glmax = _swap_endian(hdr.glmax);
5948 hdr_swapped.glmin = _swap_endian(hdr.glmin);
5949 hdr_swapped.qform_code = _swap_endian(hdr.qform_code);
5950 hdr_swapped.sform_code = _swap_endian(hdr.sform_code);
5951 hdr_swapped.quatern_b = _swap_endian(hdr.quatern_b);
5952 hdr_swapped.quatern_c = _swap_endian(hdr.quatern_c);
5953 hdr_swapped.quatern_d = _swap_endian(hdr.quatern_d);
5954 hdr_swapped.qoffset_x = _swap_endian(hdr.qoffset_x);
5955 hdr_swapped.qoffset_y = _swap_endian(hdr.qoffset_y);
5956 hdr_swapped.qoffset_z = _swap_endian(hdr.qoffset_z);
5957 for (int i = 0; i < 4; i++) hdr_swapped.srow_x[i] = _swap_endian(hdr.srow_x[i]);
5958 for (int i = 0; i < 4; i++) hdr_swapped.srow_y[i] = _swap_endian(hdr.srow_y[i]);
5959 for (int i = 0; i < 4; i++) hdr_swapped.srow_z[i] = _swap_endian(hdr.srow_z[i]);
5960 os.write(reinterpret_cast<const char *>(&hdr_swapped), sizeof(Nifti1Header));
5961 }
5962 else
5963 {
5964 os.write(reinterpret_cast<const char *>(&hdr), sizeof(Nifti1Header));
5965 }
5966
5967 // Write 4-byte extension indicator (0 = no extensions).
5968 int32_t ext_indicator = 0;
5969 if (file_is_bigendian != _is_bigendian())
5970 {
5971 ext_indicator = _swap_endian(ext_indicator);
5972 }
5973 os.write(reinterpret_cast<const char *>(&ext_indicator), 4);
5974
5975 // Write voxel data.
5976 size_t num_values = mgh.header.num_values();
5977 if (mgh.header.dtype == MRI_INT)
5978 {
5979 for (size_t i = 0; i < num_values; i++)
5980 {
5981 _nifti_write_data_element<int32_t>(os, mgh.data.data_mri_int[i], file_is_bigendian);
5982 }
5983 }
5984 else if (mgh.header.dtype == MRI_FLOAT)
5985 {
5986 for (size_t i = 0; i < num_values; i++)
5987 {
5988 _nifti_write_data_element<float>(os, mgh.data.data_mri_float[i], file_is_bigendian);
5989 }
5990 }
5991 else if (mgh.header.dtype == MRI_UCHAR)
5992 {
5993 for (size_t i = 0; i < num_values; i++)
5994 {
5995 _nifti_write_data_element<uint8_t>(os, mgh.data.data_mri_uchar[i], file_is_bigendian);
5996 }
5997 }
5998 else if (mgh.header.dtype == MRI_SHORT)
5999 {
6000 for (size_t i = 0; i < num_values; i++)
6001 {
6002 _nifti_write_data_element<short>(os, mgh.data.data_mri_short[i], file_is_bigendian);
6003 }
6004 }
6005 else
6006 {
6007 throw std::domain_error("Unsupported MRI data type " + std::to_string(mgh.header.dtype) +
6008 " for NIfTI output.\n");
6009 }
6010 }
6011
6016 inline void write_nifti(const Mgh &mgh, const std::string &filename)
6017 {
6018 if (fs::util::ends_with(filename, ".nii.gz") || fs::util::ends_with(filename, ".NII.GZ"))
6019 {
6020#ifdef LIBFS_HAS_ZLIB
6021 write_nifti_gz(mgh, filename);
6022 return;
6023#else
6024 throw std::runtime_error("Cannot write .nii.gz file '" + filename +
6025 "': zlib support not enabled. Link with -lz.\n");
6026#endif
6027 }
6028
6029 std::ofstream ofs(filename, std::ofstream::out | std::ofstream::binary);
6030 if (!ofs.is_open())
6031 {
6032 throw std::runtime_error("Unable to open NIfTI file '" + filename + "' for writing.\n");
6033 }
6034 write_nifti(mgh, ofs);
6035 ofs.close();
6036 }
6037
6038 // --- Gzip-compressed NIfTI (.nii.gz) ---
6039
6040#ifdef LIBFS_HAS_ZLIB
6041
6047 inline void read_nifti_gz(Mgh *mgh, const std::string &filename, bool force_standard)
6048 {
6049 gzFile gz = gzopen(filename.c_str(), "rb");
6050 if (!gz)
6051 {
6052 int errnum = 0;
6053 const char *errstr = gzerror(gz, &errnum);
6054 throw std::runtime_error("Could not open NIfTI.GZ file '" + filename + "' for reading: " +
6055 (errstr ? std::string(errstr) : "unknown error") + "\n");
6056 }
6057 std::vector<char> buf;
6058 char chunk[131072];
6059 int n;
6060 while ((n = gzread(gz, chunk, sizeof(chunk))) > 0)
6061 {
6062 buf.insert(buf.end(), chunk, chunk + n);
6063 }
6064 if (n < 0)
6065 {
6066 int errnum = 0;
6067 const char *errstr = gzerror(gz, &errnum);
6068 gzclose(gz);
6069 throw std::runtime_error("Error decompressing NIfTI.GZ file '" + filename + "': " +
6070 (errstr ? std::string(errstr) : "unknown error") + "\n");
6071 }
6072 gzclose(gz);
6073 std::istringstream iss(std::string(buf.data(), buf.size()));
6074 read_nifti(mgh, &iss, force_standard);
6075 }
6076
6081 inline void write_nifti_gz(const Mgh &mgh, const std::string &filename)
6082 {
6083 std::ostringstream oss;
6084 write_nifti(mgh, oss);
6085 std::string data = oss.str();
6086
6087 gzFile gz = gzopen(filename.c_str(), "wb");
6088 if (!gz)
6089 {
6090 int errnum = 0;
6091 const char *errstr = gzerror(gz, &errnum);
6092 throw std::runtime_error("Could not open NIfTI.GZ file '" + filename + "' for writing: " +
6093 (errstr ? std::string(errstr) : "unknown error") + "\n");
6094 }
6095 z_size_t total_written = 0;
6096 while (total_written < data.size())
6097 {
6098 z_size_t remaining = data.size() - total_written;
6099 z_size_t chunk = (remaining > 131072) ? 131072 : remaining;
6100 int written = gzwrite(gz, data.data() + total_written, static_cast<unsigned int>(chunk));
6101 if (written <= 0)
6102 {
6103 int errnum = 0;
6104 const char *errstr = gzerror(gz, &errnum);
6105 gzclose(gz);
6106 throw std::runtime_error("Error writing NIfTI.GZ file '" + filename + "': " +
6107 (errstr ? std::string(errstr) : "unknown error") + "\n");
6108 }
6109 total_written += static_cast<z_size_t>(written);
6110 }
6111 gzclose(gz);
6112 }
6113
6114#endif // LIBFS_HAS_ZLIB (NIfTI GZ support)
6115
6116 // --- NIfTI ↔ MGH conversion helpers ---
6117
6125 inline Mgh nifti_to_mgh(const std::string &filename)
6126 {
6127 Mgh mgh;
6128 read_nifti(&mgh, filename);
6129 return mgh;
6130 }
6131
6132 // ========================================================================
6133 // End NIfTI-1 Support
6134 // ========================================================================
6135
6142 struct Label
6143 {
6144
6147
6149 Label(std::vector<int> vertices, std::vector<float> values)
6150 {
6151 assert(vertices.size() == values.size());
6152 vertex = vertices;
6153 value = values;
6154 coord_x = std::vector<float>(vertices.size(), 0.0f);
6155 coord_y = std::vector<float>(vertices.size(), 0.0f);
6156 coord_z = std::vector<float>(vertices.size(), 0.0f);
6157 }
6158
6160 Label(std::vector<int> vertices)
6161 {
6162 vertex = vertices;
6163 value = std::vector<float>(vertices.size(), 0.0f);
6164 coord_x = std::vector<float>(vertices.size(), 0.0f);
6165 coord_y = std::vector<float>(vertices.size(), 0.0f);
6166 coord_z = std::vector<float>(vertices.size(), 0.0f);
6167 }
6168
6169 std::vector<int> vertex;
6170 std::vector<float> coord_x;
6171 std::vector<float> coord_y;
6172 std::vector<float> coord_z;
6173 std::vector<float> value;
6174
6176 std::vector<bool> vert_in_label(size_t surface_num_verts) const
6177 {
6178 if (surface_num_verts < this->vertex.size())
6179 { // nonsense, so we warn (but don't throw, maybe the user really wants this).
6180#ifdef LIBFS_DBG_ERROR
6181 std::cerr << "Invalid number of vertices for surface, must be at least " << this->vertex.size() << "\n";
6182#endif
6183 }
6184 std::vector<bool> is_in = std::vector<bool>(surface_num_verts, false);
6185
6186 for (size_t i = 0; i < this->vertex.size(); i++)
6187 {
6188 is_in[this->vertex[i]] = true;
6189 }
6190 return (is_in);
6191 }
6192
6194 size_t num_entries() const
6195 {
6196 size_t num_ent = this->vertex.size();
6197 if (this->coord_x.size() != num_ent || this->coord_y.size() != num_ent || this->coord_z.size() != num_ent || this->value.size() != num_ent)
6198 {
6199#ifdef LIBFS_DBG_ERROR
6200 std::cerr << "Inconsistent label: sizes of property vectors do not match.\n";
6201#endif
6202 }
6203 return (num_ent);
6204 }
6205 };
6206
6213 void write_surf(std::vector<float> vertices, std::vector<int32_t> faces, std::ostream &os)
6214 {
6215 const uint32_t SURF_TRIS_MAGIC = 16777214;
6216 _fwritei3(os, SURF_TRIS_MAGIC);
6217 std::string created_and_comment_lines = "Created by fslib\n\n";
6218 os << created_and_comment_lines;
6219 _fwritet<int32_t>(os, int(vertices.size() / 3)); // number of vertices
6220 _fwritet<int32_t>(os, int(faces.size() / 3)); // number of faces
6221 for (size_t i = 0; i < vertices.size(); i++)
6222 {
6223 _fwritet<float>(os, vertices[i]);
6224 }
6225 for (size_t i = 0; i < faces.size(); i++)
6226 {
6227 _fwritet<int32_t>(os, faces[i]);
6228 }
6229 }
6230
6244 void write_surf(std::vector<float> vertices, std::vector<int32_t> faces, const std::string &filename)
6245 {
6246 std::ofstream ofs;
6247 ofs.open(filename, std::ofstream::out | std::ofstream::binary);
6248 if (ofs.is_open())
6249 {
6250 write_surf(vertices, faces, ofs);
6251 ofs.close();
6252 }
6253 else
6254 {
6255 throw std::runtime_error("Unable to open surf file '" + filename + "' for writing.\n");
6256 }
6257 }
6258
6271 void write_surf(const Mesh &mesh, const std::string &filename)
6272 {
6273 std::ofstream ofs;
6274 ofs.open(filename, std::ofstream::out | std::ofstream::binary);
6275 if (ofs.is_open())
6276 {
6277 write_surf(mesh.vertices, mesh.faces, ofs);
6278 ofs.close();
6279 }
6280 else
6281 {
6282 throw std::runtime_error("Unable to open surf file '" + filename + "' for writing.\n");
6283 }
6284 }
6285
6292 void read_label(Label *label, std::istream *is)
6293 {
6294 std::string line;
6295 int line_idx = -1;
6296 size_t num_entries_header = 0; // number of vertices/voxels according to header
6297 size_t num_entries = 0; // number of vertices/voxels for which the file contains label entries.
6298 while (std::getline(*is, line))
6299 {
6300 line_idx += 1;
6301 std::istringstream iss(line);
6302 if (line_idx == 0)
6303 {
6304 continue; // skip comment.
6305 }
6306 else
6307 {
6308 if (line_idx == 1)
6309 {
6310 if (!(iss >> num_entries_header))
6311 {
6312 throw std::domain_error("Could not parse entry count from label file, invalid format.\n");
6313 }
6314 }
6315 else
6316 {
6317 int vertex;
6318 float x, y, z, value;
6319 if (!(iss >> vertex >> x >> y >> z >> value))
6320 {
6321 throw std::domain_error("Could not parse line " + std::to_string(line_idx + 1) + " of label file, invalid format.\n");
6322 }
6323 label->vertex.push_back(vertex);
6324 label->coord_x.push_back(x);
6325 label->coord_y.push_back(y);
6326 label->coord_z.push_back(z);
6327 label->value.push_back(value);
6328 num_entries++;
6329 }
6330 }
6331 }
6332 if (num_entries != num_entries_header)
6333 {
6334 throw std::domain_error("Expected " + std::to_string(num_entries_header) + " entries from label file header, but found " + std::to_string(num_entries) + " in file, invalid label file.\n");
6335 }
6336 if (label->vertex.size() != num_entries || label->coord_x.size() != num_entries || label->coord_y.size() != num_entries || label->coord_z.size() != num_entries || label->value.size() != num_entries)
6337 {
6338 throw std::domain_error("Expected " + std::to_string(num_entries) + " entries in all Label vectors, but some did not match.\n");
6339 }
6340 }
6341
6355 void read_label(Label *label, const std::string &filename)
6356 {
6357 std::ifstream infile(filename, std::fstream::in);
6358 if (infile.is_open())
6359 {
6360 read_label(label, &infile);
6361 infile.close();
6362 }
6363 else
6364 {
6365 throw std::runtime_error("Could not open label file '" + filename + "' for reading.\n");
6366 }
6367 }
6368
6373 void write_label(const Label &label, std::ostream &os)
6374 {
6375 const size_t num_entries = label.num_entries();
6376 os << "#!ascii label from subject anonymous\n"
6377 << num_entries << "\n";
6378 for (size_t i = 0; i < num_entries; i++)
6379 {
6380 os << label.vertex[i] << " " << label.coord_x[i] << " " << label.coord_y[i] << " " << label.coord_z[i] << " " << label.value[i] << "\n";
6381 }
6382 }
6383
6397 void write_label(const Label &label, const std::string &filename)
6398 {
6399 std::ofstream ofs;
6400 ofs.open(filename, std::ofstream::out);
6401 if (ofs.is_open())
6402 {
6403 write_label(label, ofs);
6404 ofs.close();
6405 }
6406 else
6407 {
6408 throw std::runtime_error("Unable to open label file '" + filename + "' for writing.\n");
6409 }
6410 }
6411
6427 void write_mesh(const Mesh &mesh, const std::string &filename)
6428 {
6429 if (fs::util::ends_with(filename, {".ply", ".PLY"}))
6430 {
6431 mesh.to_ply_file(filename);
6432 }
6433 else if (fs::util::ends_with(filename, {".obj", ".OBJ"}))
6434 {
6435 mesh.to_obj_file(filename);
6436 }
6437 else if (fs::util::ends_with(filename, {".off", ".OFF"}))
6438 {
6439 mesh.to_off_file(filename);
6440 }
6441 else
6442 {
6443 fs::write_surf(mesh, filename);
6444 }
6445 }
6446
6460 void write_mesh(const Mesh &mesh, const std::string &filename, const std::vector<uint8_t> col)
6461 {
6462 if (fs::util::ends_with(filename, {".ply", ".PLY"}))
6463 {
6464 mesh.to_ply_file(filename, col);
6465 }
6466 else if (fs::util::ends_with(filename, {".obj", ".OBJ"}))
6467 {
6468 mesh.to_obj_file(filename, col);
6469 }
6470 else if (fs::util::ends_with(filename, {".off", ".OFF"}))
6471 {
6472 mesh.to_off_file(filename, col);
6473 }
6474 else
6475 {
6476 fs::write_surf(mesh, filename);
6477 }
6478 }
6479
6480} // End namespace fs
void read_mesh(Mesh *surface, const std::string &filename)
Read a triangular mesh from a surf, obj, or ply file into the given Mesh instance.
Definition libfs.h:4252
#define LIBFS_MAX_PLY_FILE_SIZE
Maximum file size for a PLY file checked before parsing.
Definition libfs.h:139
Mgh nifti_to_mgh(const std::string &filename)
Convert a NIfTI-1 file directly to MGH by reading it.
Definition libfs.h:6125
#define LIBFS_MAX_OBJ_LINES
Maximum number of lines parsed from an OBJ file (prevents many-tiny-lines CPU-exhaustion DoS).
Definition libfs.h:103
void write_nifti(const Mgh &mgh, std::ostream &os)
Write MGH data to a NIfTI-1 file (stream overload).
Definition libfs.h:5807
std::vector< float > read_desc_data(const std::string &filename)
Read per-vertex brain morphometry data from a FreeSurfer curv, MGH, or NIfTI format file.
Definition libfs.h:4584
std::string fullpath(std::initializer_list< std::string > path_components, std::string path_sep=std::string("/"))
Construct a UNIX file system path from the given path_components.
Definition libfs.h:583
void read_mgh(Mgh *mgh, const std::string &filename)
Read a FreeSurfer volume file in MGH format into the given Mgh struct.
Definition libfs.h:3670
const std::string LOGTAG_EXCESSIVE
Logging threshold for warning messages.
Definition libfs.h:336
void read_nifti(Mgh *, std::istream *, bool force_standard=false)
Read a NIfTI-1 file into an Mgh struct (stream overload).
Definition libfs.h:5626
#define LIBFS_MAX_OFF_FILE_SIZE
Maximum file size for an OFF file checked before parsing.
Definition libfs.h:124
const std::string LOGTAG_ERROR
Logging threshold for error messages.
Definition libfs.h:324
void write_label(const Label &label, std::ostream &os)
Write label data to a stream.
Definition libfs.h:6373
const int16_t NIFTI_DT_UINT32
Definition libfs.h:5213
const int16_t NIFTI_DT_BINARY
Definition libfs.h:5166
const int16_t NIFTI_DT_COMPLEX128
Definition libfs.h:5231
#define LIBFS_MAX_ALLOC_BYTES
Maximum memory allocation limit.
Definition libfs.h:82
const std::string LOGTAG_INFO
Logging threshold for warning messages.
Definition libfs.h:330
const int16_t NIFTI_DT_FLOAT64
Definition libfs.h:5195
void read_curv(Curv *curv, std::istream *is, const std::string &source_filename="")
Read per-vertex brain morphometry data from a FreeSurfer curv stream.
Definition libfs.h:4293
const int16_t NIFTI_DT_UINT64
Definition libfs.h:5222
std::vector< uint8_t > viridis(const std::vector< float > &data, float vmin=NAN, float vmax=NAN, uint8_t nan_r=255, uint8_t nan_g=255, uint8_t nan_b=255)
Map per-vertex numeric data to RGB colors using the Viridis perceptually-uniform colormap.
Definition libfs.h:686
const int16_t NIFTI_DT_COMPLEX64
Definition libfs.h:5190
const int MRI_UCHAR
MRI data type representing an 8 bit unsigned integer.
Definition libfs.h:900
const int16_t NIFTI_DT_NONE
No data / unknown type (value 0).
Definition libfs.h:5162
void write_mesh(const Mesh &mesh, const std::string &filename)
Write a mesh to a file in different formats.
Definition libfs.h:6427
std::vector< T > vflatten(std::vector< std::vector< T > > values)
Flatten 2D vector.
Definition libfs.h:484
void read_label(Label *label, std::istream *is)
Read a FreeSurfer ASCII label from a stream.
Definition libfs.h:6292
#define LIBFS_MAX_OFF_LINES
Maximum number of lines parsed from an OFF file (prevents many-tiny-lines CPU-exhaustion DoS).
Definition libfs.h:119
void log(std::string const &message, std::string const loglevel="INFO")
Log a message, goes to stdout.
Definition libfs.h:341
#define LIBFS_MAX_STRING_LENGTH
Maximum length for fixed-length strings read from binary headers (e.g., filenames in annot colortable...
Definition libfs.h:87
std::vector< float > read_curv_data(const std::string &filename)
Read per-vertex brain morphometry data from a FreeSurfer curv format file.
Definition libfs.h:4562
#define LIBFS_MAX_OBJ_FILE_SIZE
Definition libfs.h:109
#define LIBFS_MAX_PLY_LINE_LENGTH
Maximum line length when reading PLY files (prevents single-line memory-exhaustion DoS).
Definition libfs.h:129
void write_mgh(const Mgh &mgh, std::ostream &os)
Write MGH data to a stream.
Definition libfs.h:4928
#define LIBFS_MAX_OFF_LINE_LENGTH
Maximum line length when reading OFF files (prevents single-line memory-exhaustion DoS).
Definition libfs.h:114
const int MRI_SHORT
MRI data type representing a 16 bit signed integer.
Definition libfs.h:909
void write_surf(std::vector< float > vertices, std::vector< int32_t > faces, std::ostream &os)
Write a mesh to a stream in FreeSurfer surf format.
Definition libfs.h:6213
const int16_t NIFTI_DT_INT32
Definition libfs.h:5180
bool file_exists(const std::string &name)
Check whether a file exists (can be read) at given path.
Definition libfs.h:555
const std::string LOGTAG_CRITICAL
Logging threshold for critical messages.
Definition libfs.h:321
void write_curv(std::ostream &os, std::vector< float > curv_data, int32_t num_faces=100000)
Write curv data to a stream.
Definition libfs.h:4881
const int16_t NIFTI_DT_COMPLEX256
Definition libfs.h:5237
const int16_t NIFTI_DT_UINT8
Definition libfs.h:5170
const int16_t NIFTI_DT_RGB24
Definition libfs.h:5200
const int16_t NIFTI_DT_INT64
Definition libfs.h:5218
void write_subjectsfile(const std::string &filename, const std::vector< std::string > &subjects)
Write a vector of subject identifiers to a FreeSurfer subjects file.
Definition libfs.h:3749
std::vector< std::string > read_subjectsfile(const std::string &filename)
Read a vector of subject identifiers from a FreeSurfer subjects file.
Definition libfs.h:3720
void read_annot(Annot *annot, std::istream *is)
Read a FreeSurfer annotation or brain surface parcellation from an annot stream.
Definition libfs.h:4456
const int16_t NIFTI_DT_FLOAT32
Definition libfs.h:5185
const int16_t NIFTI_DT_FLOAT128
Definition libfs.h:5227
#define LIBFS_MAX_PLY_LINES
Maximum number of lines parsed from a PLY file (prevents many-tiny-lines CPU-exhaustion DoS).
Definition libfs.h:134
const std::string LOGTAG_WARNING
Logging threshold for warning messages.
Definition libfs.h:327
const std::string LOGTAG_VERBOSE
Logging threshold for warning messages.
Definition libfs.h:333
const int MRI_INT
MRI data type representing a 32 bit signed integer.
Definition libfs.h:903
const int16_t NIFTI_DT_INT8
Definition libfs.h:5205
void read_mgh_header(MghHeader *, const std::string &)
Read the header of a FreeSurfer volume file in MGH format into the given MghHeader struct.
Definition libfs.h:3983
std::string time_tag(std::chrono::system_clock::time_point t)
Get current time as string, e.g. for log messages.
Definition libfs.h:306
const int16_t NIFTI_DT_UINT16
Definition libfs.h:5209
void write_annot(const Annot &annot, std::ostream &os)
Write a FreeSurfer annotation (brain surface parcellation) to a stream.
Definition libfs.h:4805
void read_surf(Mesh *surface, const std::string &filename)
Read a brain mesh from a file in binary FreeSurfer 'surf' format into the given Mesh instance.
Definition libfs.h:4146
#define LIBFS_APPTAG
Application tag prepended to every debug message from libfs.
Definition libfs.h:216
void str_to_file(const std::string &filename, const std::string rep)
Write the given text representation (any string) to a file.
Definition libfs.h:630
const int MRI_FLOAT
MRI data type representing a 32 bit float.
Definition libfs.h:906
#define LIBFS_MAX_COLORTABLE_ENTRIES
Maximum number of entries in an annotation colortable.
Definition libfs.h:92
#define LIBFS_MAX_OBJ_LINE_LENGTH
Definition libfs.h:98
const int16_t NIFTI_DT_INT16
Definition libfs.h:5175
An annotation, also known as a brain surface parcellation. Assigns to each vertex a region,...
Definition libfs.h:3259
std::vector< std::string > vertex_region_names() const
Compute the region names in the Colortable for all vertices in this brain surface parcellation.
Definition libfs.h:3351
std::vector< uint8_t > vertex_colors(bool alpha=false) const
Get the vertex colors as an array of uchar values, 3 consecutive values are the red,...
Definition libfs.h:3298
std::vector< int32_t > vertex_indices
Indices of the vertices, these always go from 0 to N-1 (where N is the number of vertices in the resp...
Definition libfs.h:3260
std::vector< size_t > vertex_regions() const
Compute the region indices in the Colortable for all vertices in this brain surface parcellation....
Definition libfs.h:3331
std::vector< int32_t > vertex_labels
The label code for each vertex, defining the region it belongs to. Check in the Colortable for a regi...
Definition libfs.h:3261
std::vector< int32_t > region_vertices(const std::string &region_name) const
Get all vertices of a region given by name in the brain surface parcellation. Returns an integer vect...
Definition libfs.h:3265
Colortable colortable
A Colortable defining the regions (most importantly, the region name and visualization color).
Definition libfs.h:3262
size_t num_vertices() const
Get the number of vertices of this parcellation (or the associated surface).
Definition libfs.h:3319
std::vector< int32_t > region_vertices(int32_t region_label) const
Get all vertices of a region given by label in the brain surface parcellation. Returns an integer vec...
Definition libfs.h:3283
A simple 4D array datastructure, useful for representing volume data.
Definition libfs.h:3555
Array4D(unsigned int d1, unsigned int d2, unsigned int d3, unsigned int d4)
Definition libfs.h:3560
unsigned int num_values() const
Get number of values/voxels.
Definition libfs.h:3595
unsigned int d4
size of data along 4th dimension
Definition libfs.h:3603
unsigned int d1
size of data along 1st dimension
Definition libfs.h:3600
const T & at(const unsigned int i1, const unsigned int i2, const unsigned int i3, const unsigned int i4) const
Get the value at the given 4D position.
Definition libfs.h:3579
Array4D(Mgh *mgh)
Definition libfs.h:3573
Array4D(MghHeader *mgh_header)
Definition libfs.h:3567
unsigned int get_index(const unsigned int i1, const unsigned int i2, const unsigned int i3, const unsigned int i4) const
Get the index in the vector for the given 4D position.
Definition libfs.h:3585
unsigned int d2
size of data along 2nd dimension
Definition libfs.h:3601
std::vector< T > data
the data, as a 1D vector. Use fs::Array4D::at for easy access in 4D.
Definition libfs.h:3604
unsigned int d3
size of data along 3rd dimension
Definition libfs.h:3602
The colortable from an Annot file, can be used for parcellations and integer labels....
Definition libfs.h:3208
std::vector< int32_t > b
green channel of RGBA color
Definition libfs.h:3213
std::vector< int32_t > id
internal region index
Definition libfs.h:3209
std::vector< std::string > name
region name
Definition libfs.h:3210
std::vector< int32_t > a
alpha channel of RGBA color
Definition libfs.h:3214
size_t num_entries() const
Get the number of enties (regions) in this Colortable.
Definition libfs.h:3218
int32_t get_region_idx(int32_t query_label) const
Get the index of a region in the Colortable by label. Returns a negative value if the region is not f...
Definition libfs.h:3244
std::vector< int32_t > label
label integer computed from rgba values. Maps to the Annot.vertex_label field.
Definition libfs.h:3215
std::vector< int32_t > g
blue channel of RGBA color
Definition libfs.h:3212
std::vector< int32_t > r
red channel of RGBA color
Definition libfs.h:3211
int32_t get_region_idx(const std::string &query_name) const
Get the index of a region in the Colortable by region name. Returns a negative value if the region is...
Definition libfs.h:3231
Models a FreeSurfer curv file that contains per-vertex float data.
Definition libfs.h:3181
std::vector< float > data
The curvature data, one value per vertex. Something like the cortical thickness at each vertex.
Definition libfs.h:3197
Curv()
Construct an empty Curv instance.
Definition libfs.h:3191
int32_t num_values_per_vertex
The number of values per vertex, stored in this file. Almost all apps (including FreeSurfer itself) o...
Definition libfs.h:3203
int32_t num_vertices
The number of vertices of the mesh to which this belongs. Can be deduced from length of 'data'.
Definition libfs.h:3200
Curv(std::vector< float > curv_data)
Construct a Curv instance from the given per-vertex data.
Definition libfs.h:3184
int32_t num_faces
The number of faces of the mesh to which this belongs, typically irrelevant and ignored.
Definition libfs.h:3194
Definition libfs.h:6143
size_t num_entries() const
Return the number of entries (vertices/voxels) in this label.
Definition libfs.h:6194
std::vector< float > coord_y
y coordinates of the vertices in case of a surface label, or voxels coordinates for a volume label.
Definition libfs.h:6171
Label(std::vector< int > vertices)
Construct a Label from the given vertices / voxel numbers.
Definition libfs.h:6160
std::vector< float > coord_x
x coordinates of the vertices in case of a surface label, or voxels coordinates for a volume label.
Definition libfs.h:6170
std::vector< float > value
the value of the label, can represent continuous data like a p-value, or sometimes simply 1....
Definition libfs.h:6173
std::vector< int > vertex
vertex indices for the data in this label if it is a surface label. These are indices into the vertic...
Definition libfs.h:6169
std::vector< float > coord_z
z coordinates of the vertices in case of a surface label, or voxels coordinates for a volume label.
Definition libfs.h:6172
Label(std::vector< int > vertices, std::vector< float > values)
Construct a Label from the given vertices / voxel numbers and values.
Definition libfs.h:6149
Label()
Default constructor for a label.
Definition libfs.h:6146
std::vector< bool > vert_in_label(size_t surface_num_verts) const
Compute for each vertex of the surface whether it is inside the label.
Definition libfs.h:6176
Models a triangular mesh, used for brain surface meshes.
Definition libfs.h:950
edge_set as_edgelist() const
Return edge list representation of this mesh.
Definition libfs.h:1279
void to_off_file(const std::string &filename) const
Export this mesh to a file in OFF format.
Definition libfs.h:3166
std::pair< std::unordered_map< int32_t, int32_t >, fs::Mesh > submesh_vertex(const std::vector< int32_t > &old_vertex_indices, const bool mapdir_fulltosubmesh=false) const
Compute a new mesh that is a submesh of this mesh, based on a subset of the vertices of this mesh.
Definition libfs.h:1584
std::vector< float > vertex_texcoords
n x 2 vector of texture coordinates (u,v), one per vertex in the same order as vertices....
Definition libfs.h:981
const float & vm_at(const size_t i, const size_t j) const
Retrieve a single (x, y, or z) coordinate of a vertex, treating the vertices vector as an nx3 matrix.
Definition libfs.h:2976
bool has_normals() const
Return whether this mesh has per-vertex normals.
Definition libfs.h:985
std::unordered_set< std::tuple< size_t, size_t >, _tupleHashFunction > edge_set
Datastructure for storing, and quickly querying the existence of, mesh edges.
Definition libfs.h:1266
static void from_obj(Mesh *mesh, const std::string &filename, const bool preserve_vertex_indices=false)
Read a brainmesh from a Wavefront object format mesh file.
Definition libfs.h:2262
std::string to_ply(const std::vector< uint8_t > col) const
Return string representing the mesh in PLY format.
Definition libfs.h:3010
size_t num_vertices() const
Return the number of vertices in this mesh.
Definition libfs.h:2874
std::string to_obj() const
Return string representing the mesh in Wavefront Object (.obj) format.
Definition libfs.h:1134
std::vector< float > smooth_pvd_nn(const std::vector< float > pvd, const size_t num_iter=1, const bool via_matrix=true, const bool with_nan=true, const bool detect_nan=true) const
Smooth given per-vertex data using nearest neighbor smoothing.
Definition libfs.h:1367
std::string to_obj(const std::vector< uint8_t > col) const
Return string representing the mesh in Wavefront Object (.obj) format with vertex colors.
Definition libfs.h:1154
std::vector< float > vertices
n x 3 vector of the x,y,z coordinates for the n vertices. The x,y,z coordinates for a single vertex f...
Definition libfs.h:977
static fs::Mesh construct_pyramid()
Construct and return a simple pyramidal mesh.
Definition libfs.h:1038
std::vector< std::vector< size_t > > as_adjlist(const bool via_matrix=true) const
Return adjacency list representation of this mesh.
Definition libfs.h:1308
static fs::Mesh construct_cube()
Construct and return a simple cube mesh.
Definition libfs.h:1001
std::vector< int32_t > face_vertices(const size_t face) const
Get all vertex indices of the face, given by its index.
Definition libfs.h:2926
static void from_ply(Mesh *mesh, const std::string &filename)
Read a brainmesh from a Stanford PLY format mesh file.
Definition libfs.h:2840
void to_obj_file(const std::string &filename) const
Export this mesh to a file in Wavefront OBJ format.
Definition libfs.h:1556
static std::vector< float > smooth_pvd_nn(const std::vector< std::vector< size_t > > mesh_adj, const std::vector< float > pvd, const size_t num_iter=1, const bool with_nan=true, const bool detect_nan=true)
Smooth given per-vertex data using nearest neighbor smoothing based on adjacency list mesh represenat...
Definition libfs.h:1390
static std::vector< std::vector< size_t > > extend_adj(const std::vector< std::vector< size_t > > mesh_adj, const size_t extend_by=1, std::vector< std::vector< size_t > > mesh_adj_ext=std::vector< std::vector< size_t > >())
Extend mesh neighborhoods based on mesh adjacency representation.
Definition libfs.h:1511
static fs::Mesh construct_grid(const size_t nx=4, const size_t ny=5, const float distx=1.0, const float disty=1.0)
Construct and return a simple planar grid mesh.
Definition libfs.h:1071
std::vector< std::vector< bool > > as_adjmatrix() const
Return adjacency matrix representation of this mesh.
Definition libfs.h:1238
static std::vector< float > curv_data_for_orig_mesh(const std::vector< float > data_submesh, const std::unordered_map< int32_t, int32_t > submesh_to_orig_mapping, const int32_t orig_mesh_num_vertices, const float fill_value=std::numeric_limits< float >::quiet_NaN())
Given per-vertex data for a submesh, expand it back to full mesh size.
Definition libfs.h:1651
Mesh(std::vector< float > cvertices, std::vector< int32_t > cfaces)
Construct a Mesh from the given vertices and faces.
Definition libfs.h:953
std::vector< int32_t > faces
n x 3 vector of the 3 vertex indices for the n triangles or faces. The 3 vertices of a single face fo...
Definition libfs.h:978
std::vector< float > vertex_normals
n x 3 vector of normal vectors (nx,ny,nz), one per vertex in the same order as vertices....
Definition libfs.h:980
size_t num_faces() const
Return the number of faces in this mesh.
Definition libfs.h:2888
const int32_t & fm_at(const size_t i, const size_t j) const
Retrieve a vertex index of a face, treating the faces vector as an nx3 matrix.
Definition libfs.h:2905
Mesh()
Construct an empty Mesh.
Definition libfs.h:975
void to_off_file(const std::string &filename, const std::vector< uint8_t > col) const
Export this mesh to a file in OFF format with vertex colors (COFF).
Definition libfs.h:3173
static void from_off(Mesh *mesh, std::istream *is, const std::string &source_filename="")
Read a brainmesh from an Object File format (OFF) stream.
Definition libfs.h:2293
std::string to_off(const std::vector< uint8_t > col) const
Return string representing the mesh in PLY format.
Definition libfs.h:3115
std::string to_ply() const
Return string representing the mesh in PLY format. Overload that works without passing a color vector...
Definition libfs.h:2994
bool has_texcoords() const
Return whether this mesh has per-vertex texture coordinates.
Definition libfs.h:989
void to_ply_file(const std::string &filename) const
Export this mesh to a file in Stanford PLY format.
Definition libfs.h:3083
void to_ply_file(const std::string &filename, const std::vector< uint8_t > col) const
Export this mesh to a file in Stanford PLY format with vertex colors.
Definition libfs.h:3093
static void from_off(Mesh *mesh, const std::string &filename)
Read a brainmesh from an OFF format mesh file.
Definition libfs.h:2479
static void from_ply(Mesh *mesh, std::istream *is)
Read a brainmesh from a Stanford PLY format stream.
Definition libfs.h:2509
std::string to_off() const
Return string representing the mesh in OFF format. Overload that works without passing a color vector...
Definition libfs.h:3106
void to_obj_file(const std::string &filename, const std::vector< uint8_t > col) const
Export this mesh to a file in Wavefront OBJ format with vertex colors.
Definition libfs.h:1563
std::vector< uint8_t > vertex_colors
n x 3 vector of RGB color values, 3 per vertex (v0_r, v0_g, v0_b, v1_r, ...). Empty if no vertex colo...
Definition libfs.h:979
static void from_obj(Mesh *mesh, std::istream *is, const bool preserve_vertex_indices=false)
Read a brainmesh from a Wavefront object format stream.
Definition libfs.h:1686
std::vector< float > vertex_coords(const size_t vertex) const
Get all coordinates of the vertex, given by its index.
Definition libfs.h:2950
Mesh(std::vector< std::vector< float > > cvertices, std::vector< std::vector< int32_t > > cfaces)
Construct a Mesh from 2-D vertex and face lists.
Definition libfs.h:968
Models the data of an MGH file. Currently these are 1D vectors, but one can compute the 4D array usin...
Definition libfs.h:3520
std::vector< float > data_mri_float
data of type MRI_FLOAT, check the dtype to see whether this is relevant for this instance.
Definition libfs.h:3529
MghData(std::vector< short > curv_data)
constructor to create MghData from MRI_SHORT (short) data.
Definition libfs.h:3524
MghData(std::vector< int32_t > curv_data)
constructor to create MghData from MRI_INT (int32_t) data.
Definition libfs.h:3522
std::vector< int32_t > data_mri_int
data of type MRI_INT, check the dtype to see whether this is relevant for this instance.
Definition libfs.h:3527
std::vector< uint8_t > data_mri_uchar
data of type MRI_UCHAR, check the dtype to see whether this is relevant for this instance.
Definition libfs.h:3528
MghData(std::vector< uint8_t > curv_data)
constructor to create MghData from MRI_UCHAR (uint8_t) data.
Definition libfs.h:3523
std::vector< short > data_mri_short
data of type MRI_SHORT, check the dtype to see whether this is relevant for this instance.
Definition libfs.h:3530
MghData(Curv curv)
constructor to create MghData from a Curv instance
Definition libfs.h:3526
MghData(std::vector< float > curv_data)
constructor to create MghData from MRI_FLOAT (float) data.
Definition libfs.h:3525
Models the header of an MGH file.
Definition libfs.h:3365
int32_t dim2length
size of data along 2nd dimension
Definition libfs.h:3384
std::vector< float > Pxyz_c
x,y,z coordinates of central vertex
Definition libfs.h:3402
size_t num_values() const
Compute the number of values based on the dim*length header fields.
Definition libfs.h:3393
MghHeader(Curv curv)
Definition libfs.h:3367
float ysize
size of voxels along 2nd axis (y or a)
Definition libfs.h:3399
int16_t ras_good_flag
flag indicating whether the data in the RAS fields (Mdc, Pxyz_c) are valid. 1 means valid,...
Definition libfs.h:3390
float xsize
size of voxels along 1st axis (x or r)
Definition libfs.h:3398
int32_t dof
typically ignored
Definition libfs.h:3389
int32_t dim4length
size of data along 4th dimension
Definition libfs.h:3386
std::vector< float > Mdc
matrix
Definition libfs.h:3401
int32_t dim1length
size of data along 1st dimension
Definition libfs.h:3383
MghHeader()
Empty default constuctor.
Definition libfs.h:3366
std::vector< float > compute_vox2ras() const
Compute the 4x4 voxel-to-RAS (vox2ras) matrix from the RAS header fields, if available.
Definition libfs.h:3417
int32_t dim3length
size of data along 3rd dimension
Definition libfs.h:3385
MghHeader(std::vector< float > curv_data)
Definition libfs.h:3375
int32_t dtype
the MRI data type
Definition libfs.h:3388
float zsize
size of voxels along 3rd axis (z or s)
Definition libfs.h:3400
Models a whole MGH file.
Definition libfs.h:3535
Mgh(std::vector< float > curv_data)
Definition libfs.h:3544
MghHeader header
Header for this MGH instance.
Definition libfs.h:3536
MghData data
4D data for this MGH instance.
Definition libfs.h:3537
Mgh()
Empty default constuctor.
Definition libfs.h:3538
Mgh(Curv curv)
Definition libfs.h:3539
NIfTI-1 header structure (348 bytes, packed).
Definition libfs.h:5244
float quatern_c
quaternion c param
Definition libfs.h:5278
int32_t sizeof_hdr
must be 348
Definition libfs.h:5245
float qoffset_x
quaternion x shift
Definition libfs.h:5280
int16_t slice_end
last slice index
Definition libfs.h:5264
float scl_slope
scaling slope
Definition libfs.h:5262
int16_t slice_start
first slice index
Definition libfs.h:5259
float pixdim[8]
voxel dimensions (mm)
Definition libfs.h:5260
float srow_z[4]
affine transform row z
Definition libfs.h:5285
int16_t session_error
unused
Definition libfs.h:5249
char descrip[80]
description
Definition libfs.h:5273
char db_name[18]
unused
Definition libfs.h:5247
float cal_max
calibrated max
Definition libfs.h:5267
char dim_info
MRI slice ordering.
Definition libfs.h:5251
char data_type[10]
unused
Definition libfs.h:5246
char aux_file[24]
auxiliary filename
Definition libfs.h:5274
int16_t dim[8]
dim[0]=ndim, dim[1..7]=dimensions
Definition libfs.h:5252
int16_t sform_code
affine transform code (>0 = valid)
Definition libfs.h:5276
int16_t datatype
NIfTI data type code.
Definition libfs.h:5257
float vox_offset
byte offset to data from header start
Definition libfs.h:5261
float qoffset_z
quaternion z shift
Definition libfs.h:5282
char slice_code
slice timing code
Definition libfs.h:5265
float qoffset_y
quaternion y shift
Definition libfs.h:5281
float quatern_d
quaternion d param
Definition libfs.h:5279
int16_t bitpix
bits per voxel
Definition libfs.h:5258
float toffset
time offset
Definition libfs.h:5270
char regular
unused
Definition libfs.h:5250
char intent_name[16]
intent name
Definition libfs.h:5286
int32_t glmax
global max (unused)
Definition libfs.h:5271
float intent_p3
intent parameter 3
Definition libfs.h:5255
float intent_p2
intent parameter 2
Definition libfs.h:5254
float srow_x[4]
affine transform row x
Definition libfs.h:5283
float slice_duration
slice timing duration
Definition libfs.h:5269
float cal_min
calibrated min
Definition libfs.h:5268
float scl_inter
scaling intercept
Definition libfs.h:5263
char xyzt_units
units for pixdim[] dimensions
Definition libfs.h:5266
char magic[4]
"n+1\0" (single file) or "ni1\0" (header/img pair)
Definition libfs.h:5287
int32_t glmin
global min (unused)
Definition libfs.h:5272
float srow_y[4]
affine transform row y
Definition libfs.h:5284
int16_t intent_code
NIfTI intent code.
Definition libfs.h:5256
int32_t extents
unused
Definition libfs.h:5248
int16_t qform_code
quaternion transform code (>0 = valid)
Definition libfs.h:5275
float intent_p1
intent parameter 1
Definition libfs.h:5253
float quatern_b
quaternion b param
Definition libfs.h:5277