-
Notifications
You must be signed in to change notification settings - Fork 0
/
libfs.h
2955 lines (2672 loc) · 128 KB
/
libfs.h
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
#pragma once
#include <iostream>
#include <climits>
#include <stdio.h>
#include <vector>
#include <fstream>
#include <cassert>
#include <sstream>
#include <stdexcept>
#include <map>
#include <unordered_set>
#include <unordered_map>
#include <cmath>
#include <algorithm>
#include <chrono>
#include <cstdint>
#define LIBFS_VERSION "0.3.4"
/// @file
///
/*! \mainpage The libfs API documentation
*
* \section intro_sec Introduction
*
* Welcome to the API documentation for libfs, a header-only C++11 library to read and write FreeSurfer neuroimaging data.
*
* All relevant functions are in the file include/libfs.h and only a few utility functions are class
* members, so the best place to start is to open the documentation for libfs.h in the Files section above.
*
* \subsection intro-examples A note on the API doc examples
*
* The examples in the doc strings of the libfs.h functions usually only show data preparation and the function call itself. Typically you
* will get a working program out of them by wrapping them into something like:
*
* @code
* #include "libfs.h"
* #include <string>
* #include <iostream>
* #include <vector>
* // maybe more includes for some examples here.
*
* int main(int argc, char** argv) {
* // Demo code goes here
* }
* @endcode
*
* To see full demo programs and compilation instructions, check the <a href="https://github.com/dfsp-spirit/libfs/tree/main/examples">examples/ directory</a> in the GitHub repository linked below.
*
* \subsection logging Logging with libfs
*
* You can define the output produced by libfs from your application. To do so,
* `#define` _one_ of the following debug levels in your application, *before* including 'libfs.h':
*
* - `LIBFS_DBG_CRITICAL` // print only critical errors that will raise an expection and most likely cause application to stop (unless caught).
* - `LIBFS_DBG_ERROR` // prints errors (and more severe things).
* - `LIBFS_DBG_WARNING` // the default, prints warnings (and more severe things).
* - `LIBFS_DBG_INFO` // prints info messages, like what is currently being done.
* - `LIBFS_DBG_VERBOSE` // prints info messages inside loops, may considerable slow down apps and litter stdout.
* - `LIBFS_DBG_EXCESSIVE` // prints info messages in nested loops, will considerable slow down apps and quickly litter stdout.
*
*
* Things you should know about logging and controlling libfs output:
*
* - The debug levels are ordered in the list above, and defining a single one will automatically enable
* all levels of higher importance (e.g., defining `LIBFS_DBG_WARNING` also enables `LIBFS_DBG_ERROR` and `LIBFS_DBG_CRITICAL`).
* - If you define nothing at all, libfs defaults to `LIBFS_DBG_WARNING`.
* - If you do not want any ouput from libfs, define `LIBFS_DBG_NONE`. This is not recommended though, as it completely disables
* all output, including critical error messages. This means that your application may terminate without any message,
* and is only advisable if you are very sure that you catch all possible exceptions and then produce an error message
* for users in your application code.
* - Currently all debug output goes to `stdout`, i.e., typically to the terminal.
*
*
* \subsection intro-website The libfs project website
*
* The project page for libfs can be found at https://github.com/dfsp-spirit/libfs. It contains information on all documentation available for libfs.
*
*
*/
// Set apptag (printed as prefix of debug messages) for debug messages.
// Users can overwrite this by defining LIBFS_APPTAG before including 'libfs.h'.
#ifndef LIBFS_APPTAG
#define LIBFS_APPTAG "[libfs] "
#endif
// Set default.
#define LIBFS_DBG_WARNING
// If the user wants something below our default, remove our default.
#ifdef LIBFS_DBG_NONE
#undef LIBFS_DBG_WARNING
#endif
#ifdef LIBFS_DBG_CRITICAL
#undef LIBFS_DBG_WARNING
#endif
#ifdef LIBFS_DBG_ERROR
#undef LIBFS_DBG_WARNING
#endif
// Ensure that the user does not have to define all debug levels
// up to the one they actually want, by defining all lower ones for them.
#ifdef LIBFS_DBG_EXCESSIVE
#define LIBFS_DBG_VERBOSE
#endif
#ifdef LIBFS_DBG_VERBOSE
#define LIBFS_DBG_INFO
#endif
#ifdef LIBFS_DBG_INFO
#define LIBFS_DBG_WARNING
#endif
#ifdef LIBFS_DBG_WARNING
#define LIBFS_DBG_ERROR
#endif
#ifdef LIBFS_DBG_ERROR
#define LIBFS_DBG_CRITICAL
#endif
// End of debug handling.
namespace fs {
namespace util {
/// @brief Cross-platform wrapper for localtime_r and localtime_s.
/// @param time the time to convert to an std::tm struct.
/// @return the std::tm struct.
tm _localtime(const std::time_t& time) {
std::tm tm_snapshot;
#if (defined(WIN32) || defined(_WIN32) || defined(__WIN32__))
::localtime_s(&tm_snapshot, &time);
#else
::localtime_r(&time, &tm_snapshot); // POSIX
#endif
return tm_snapshot;
}
/// @brief Get current time as string, e.g. for log messages.
/// @param t the timepoint to format as a string, typically `std::system_clock::now()`.
/// @return the formatted time string.
/// #### Examples
///
/// @code
/// std::string time_rep = fs::util::time_tag(std::chrono::system_clock::now());
/// @endcode
std::string time_tag(std::chrono::system_clock::time_point t) {
auto as_time_t = std::chrono::system_clock::to_time_t(t);
struct tm tm;
char time_buffer[64];
//if (::gmtime_r(&as_time_t, &tm)) {
tm = _localtime(as_time_t);
if (std::strftime(time_buffer, sizeof(time_buffer), "%F %T", &tm)) {
return std::string{time_buffer};
}
throw std::runtime_error("Failed to get current date as string");
}
/// Logging threshold for critical messages.
const std::string LOGTAG_CRITICAL = "CRITICAL";
/// Logging threshold for error messages.
const std::string LOGTAG_ERROR = "ERROR";
/// Logging threshold for warning messages.
const std::string LOGTAG_WARNING = "WARNING";
/// Logging threshold for warning messages.
const std::string LOGTAG_INFO = "INFO";
/// Logging threshold for warning messages.
const std::string LOGTAG_VERBOSE = "VERBOSE";
/// Logging threshold for warning messages.
const std::string LOGTAG_EXCESSIVE = "EXCESSIVE";
/// @brief Log a message, goes to stdout.
/// @param message the message to be logged.
/// @param loglevel the log level, one of `fs::util::LOGTAG_*`.
inline void log(std::string const & message, std::string const loglevel = "INFO") {
std::cout << LIBFS_APPTAG << "[" << loglevel << "] [" << fs::util::time_tag(std::chrono::system_clock::now()) << "] " << message << "\n";
}
/// @brief Check whether a string ends with the given suffix.
/// @private
///
/// #### Examples
///
/// @code
/// bool ev = fs::util::ends_with("freesurfer", "surfer"); // true
/// @endcode
inline bool ends_with(std::string const & value, std::string const & suffix) {
if (suffix.size() > value.size()) return false;
return std::equal(suffix.rbegin(), suffix.rend(), value.rbegin());
}
/// @brief Check whether a string ends with one of the given suffixes.
/// @private
///
/// #### Examples
///
/// @code
/// bool ev = fs::util::ends_with("freesurfer", {"surfer", "not"}); // true
/// @endcode
inline bool ends_with(std::string const & value, std::initializer_list<std::string> suffixes) {
for (auto suffix : suffixes) {
if (ends_with(value, suffix)) {
return true;
}
}
return false;
}
/// @brief Turn 1D vector into 2D vector.
/// @param values the input 1D vector.
/// @param num_cols number of columns for the returned 2D vector.
/// @return 2D vector with `num_cols` columns.
/// @private
///
/// #### Examples
///
/// @code
/// std::vector<float> input = { 1.0, 2.0, 3.0, 4.0, 5.0, 6.0 };
/// std::vector<std::vector<float>> res = fs::util::v2d(input, 2);
/// @endcode
template <typename T>
std::vector<std::vector <T>> v2d(std::vector<T> values, size_t num_cols) {
std::vector<std::vector <T>> result;
for (std::size_t i = 0; i < values.size(); ++i) {
if (i % num_cols == 0) {
result.resize(result.size() + 1);
}
result[i / num_cols].push_back(values[i]);
}
return result;
}
/// @brief Flatten 2D vector.
/// @param values the input 2D vector.
/// @return 1D vector.
///
/// #### Examples
///
/// @code
/// std::vector<float> input = { 1.0, 2.0, 3.0, 4.0, 5.0, 6.0 };
/// std::vector<std::vector<float>> res = fs::util::v2d(input, 2);
/// @endcode
template <typename T>
std::vector<T> vflatten(std::vector<std::vector <T>> values) {
size_t total_size = 0;
for (std::size_t i = 0; i < values.size(); i++) {
total_size += values[i].size();
}
std::vector <T> result = std::vector<T>(total_size);
size_t cur_idx = 0;
for (std::size_t i = 0; i < values.size(); i++) {
for (std::size_t j = 0; j < values[i].size(); j++) {
result[cur_idx] = values[i][j];
cur_idx++;
}
}
return result;
}
/// @brief Check whether a string starts with the given prefix.
/// @private
/// @note This is a private function, users should call the overloaded version that accepts
/// a vector of prefixes instead.
///
/// #### Examples
///
/// @code
/// bool ev = fs::util::starts_with("freesurfer", "free"); // true
/// @endcode
inline bool starts_with(std::string const & value, std::string const & prefix) {
if (prefix.length() > value.length()) return false;
return value.rfind(prefix, 0) == 0;
}
/// @brief Check whether a string starts with one of the given prefixes.
/// @param value the string for which to check whether it starts with any of the prefixes
/// @param prefixes the prefixes to consider
/// @returns whether the string starts with one of the prefixes.
///
/// #### Examples
///
/// @code
/// bool ev = fs::util::starts_with("freesurfer", {"free", "not"}); // true
/// @endcode
inline bool starts_with(std::string const & value, std::initializer_list<std::string> prefixes) {
for (auto prefix : prefixes) {
if (starts_with(value, prefix)) {
return true;
}
}
return false;
}
/// @brief Check whether a file exists (can be read) at given path.
/// @details You should not rely on this as a pre-check when considering to open a file due
/// to race conditions, just try-catch open in that case. This is intended to check
/// whether a certain software run succeeded, by checking whether the key expected
/// output files exist.
/// @param name the filename that should be checked.
/// #### Examples
///
/// @code
/// bool exists = fs::util::file_exists("./study1/subject1/label/lh.aparc.annot");
/// @endcode
inline bool file_exists(const std::string& name) {
if (FILE *file = fopen(name.c_str(), "r")) {
fclose(file);
return true;
} else {
return false;
}
}
/// @brief Construct a UNIX file system path from the given path_components.
/// @details Any trailing or leading slash (path_sep) will be stripped from the individual components and replaced with a single one between two components. If the first path component started with a slash, that slash will be kept (absolute paths are left intact).
/// @param path_components init list of strings, the path components
/// @param path_sep path separator to use, typically `/` on Unix-based system.
/// @throws std::invalid_argument on empty
/// @returns string representation of the path, using the `path_sep`.
///
/// #### Examples
///
/// @code
/// std::string p = fs::util::fullpath({"path", "to", "file.txt"});
/// // Gives: "path/to/file.txt"
/// std::string p = fs::util::fullpath({"/path", "to", "file.txt"});
/// // Gives: "/path/to/file.txt"
/// @endcode
std::string fullpath( std::initializer_list<std::string> path_components, std::string path_sep = std::string("/") ) {
std::string fp;
if(path_components.size() == 0) {
throw std::invalid_argument("The 'path_components' must not be empty.");
}
std::string comp;
std::string comp_mod;
size_t idx = 0;
for(auto comp : path_components) {
comp_mod = comp;
if(idx != 0) { // We keep a leading slash intact for the first element (absolute path).
if (starts_with(comp, path_sep)) {
comp_mod = comp.substr(1, comp.size()-1);
}
}
if(ends_with(comp_mod, path_sep)) {
comp_mod = comp_mod.substr(0, comp_mod.size()-1);
}
fp += comp_mod;
if(idx < path_components.size()-1) {
fp += path_sep;
}
idx++;
}
return fp;
}
/// @brief Write the given text representation (any string) to a file.
/// @param filename the file to which to write, will be overwritten if exists
/// @param rep the string to write to the file
/// @throws std::runtime_error if the file cannot be opened.
///
/// #### Examples
///
/// @code
/// fs::util::str_to_file("thoughts.txt", "blah, blah, blah");
/// @endcode
void str_to_file(const std::string& filename, const std::string rep) {
std::ofstream ofs;
ofs.open(filename, std::ofstream::out);
#ifdef LIBFS_DBG_VERBOSE
std::cout << LIBFS_APPTAG << "Opening file '" << filename << "' for writing.\n";
#endif
if(ofs.is_open()) {
ofs << rep;
ofs.close();
} else {
throw std::runtime_error("Unable to open file '" + filename + "' for writing.\n");
}
}
} // End namespace util.
// MRI data types, used by the MGH functions.
/// MRI data type representing an 8 bit unsigned integer.
const int MRI_UCHAR = 0;
/// MRI data type representing a 32 bit signed integer.
const int MRI_INT = 1;
/// MRI data type representing a 32 bit float.
const int MRI_FLOAT = 3;
/// MRI data type representing a 16 bit signed integer.
const int MRI_SHORT = 4;
// Forward declarations.
int _fread3(std::istream&);
template <typename T> T _freadt(std::istream&);
std::string _freadstringnewline(std::istream&);
std::string _freadfixedlengthstring(std::istream&, size_t, bool);
bool _ends_with(std::string const &fullString, std::string const &ending);
size_t _vidx_2d(size_t, size_t, size_t);
struct MghHeader;
/// @brief Models a triangular mesh, used for brain surface meshes.
///
/// @details Represents a vertex-indexed mesh. The `n` vertices are stored as 3D point coordinates (x,y,z) in a vector
/// of length `3n`, in which 3 consecutive values represent the x, y and z coordinate of the same vertex.
/// The `m` faces are stored as a vector of `3m` integers, where 3 consecutive values represent the 3 vertices (by index)
/// making up the respective face. Vertex indices are 0-based.
/// #### Examples
///
/// @code
/// fs::Mesh surface = fs::Mesh::construct_cube();
/// size_t nv = surface.num_vertices(); // 8
/// auto first_face_verts = surface.face_vertices(0);
/// int first_face_third_vert = surface.fm_at(0, 2);
/// size_t nf = surface.num_faces();
/// size_t nv = surface.num_vertices();
/// surface.to_obj("cube_out.obj");
/// @endcode
struct Mesh {
/// Construct a Mesh from the given vertices and faces.
Mesh(std::vector<float> cvertices, std::vector<int32_t> cfaces) {
vertices = cvertices; faces = cfaces;
}
// Construct from 2D vectors (Nx3).
Mesh(std::vector<std::vector<float>> cvertices, std::vector<std::vector<int32_t>> cfaces) {
vertices = util::vflatten(cvertices); faces = util::vflatten(cfaces);
}
/// Construct an empty Mesh.
Mesh() {}
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 form consecutive entries.
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 form consecutive entries.
/// @brief Construct and return a simple cube mesh.
/// @return fs::Mesh instance representing a cube.
///
/// #### Examples
///
/// @code
/// fs::Mesh surface = fs::Mesh::construct_cube();
/// size_t nv = surface.num_vertices(); // 8
/// size_t nf = surface.num_faces(); // 12
/// @endcode
static fs::Mesh construct_cube() {
fs::Mesh mesh;
mesh.vertices = { 1.0, 1.0, 1.0,
1.0, 1.0, -1.0,
1.0, -1.0, 1.0,
1.0, -1.0, -1.0,
-1.0, 1.0, 1.0,
-1.0, 1.0, -1.0,
-1.0, -1.0, 1.0,
-1.0, -1.0, -1.0 };
mesh.faces = { 0, 2, 3,
3 ,1, 0,
4, 6, 7,
7, 5, 4,
0, 4, 5,
5, 1, 0,
2, 6, 7,
7, 3, 2,
0, 4, 6,
6, 2, 0,
1, 5, 7,
7, 3, 1 };
return mesh;
}
/// @brief Construct and return a simple pyramidal mesh.
/// @details This constructs a right square pyramid with base edge length 1 and height 1. Think of the Great Pyramid of Giza.
/// @return fs::Mesh instance representing a 4-sided pyramid.
///
/// #### Examples
///
/// @code
/// fs::Mesh surface = fs::Mesh::construct_pyramid();
/// size_t nv = surface.num_vertices(); // 5
/// size_t nf = surface.num_faces(); // 6
/// @endcode
static fs::Mesh construct_pyramid() {
fs::Mesh mesh;
mesh.vertices = { 0.0, 0.0, 0.0, // start with 4x base
0.0, 1.0, 0.0,
1.0, 1.0, 0.0,
1.0, 0.0, 0.0,
0.5, 0.5, 1.0 }; // apex
mesh.faces = { 0, 2, 1, // start with 2 base faces
0, 3, 2,
0, 4, 1, // now the 4 wall faces
1, 4, 2,
3, 2, 4,
0, 3, 4 };
return mesh;
}
/// @brief Construct and return a simple planar grid mesh.
/// @details This is a 2D rectangular grid embedded in 3D. Each rectangular cell consists of 2 triangular faces. The height (z coordinate) for all vertices is `0.0`.
/// @param nx number of vertices in x direction
/// @param ny number of vertices in y direction
/// @param distx distance between vertices in x direction
/// @param disty distance between vertices in y direction
/// @return fs::Mesh instance representing a flat grid.
/// @throws std::invalid_argument error if `nx` or `ny` are `< 2`.
///
/// #### Examples
///
/// @code
/// fs::Mesh surface = fs::Mesh::construct_grid(4, 5);
/// size_t nv = surface.num_vertices(); // 4*5 = 20
/// size_t nf = surface.num_faces(); // (4-1)*(5-1)*2 = 24;
/// @endcode
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) {
if(nx < 2 || ny < 2) {
throw std::runtime_error("Parameters nx and ny must be at least 2.");
}
fs::Mesh mesh;
size_t num_vertices = nx * ny;
size_t num_faces = ((nx - 1) * (ny - 1)) * 2;
std::vector<float> vertices;
vertices.reserve(num_vertices * 3);
std::vector<int> faces;
faces.reserve(num_faces * 3);
// Create vertices.
float cur_x, cur_y, cur_z;
cur_x = cur_y = cur_z = 0.0;
for(size_t i = 0; i < nx; i++) {
for(size_t j = 0; j < ny; j++) {
vertices.push_back(cur_x);
vertices.push_back(cur_y);
vertices.push_back(cur_z);
cur_y += disty;
}
cur_x += distx;
}
// Create faces.
for(size_t i = 0; i < num_vertices; i++) {
if((i+1) % ny == 0 || i >= num_vertices - ny) {
// Do not use the last ones in row or column as source.
continue;
}
// Add the upper left triangle of this grid cell.
faces.push_back(int(i));
faces.push_back(int(i + ny + 1));
faces.push_back(int(i + 1));
// Add the lower right triangle of this grid cell.
faces.push_back(int(i));
faces.push_back(int(i + ny + 1));
faces.push_back(int(i + ny));
}
mesh.vertices = vertices;
mesh.faces = faces;
return mesh;
}
/// @brief Return string representing the mesh in Wavefront Object (.obj) format.
/// @return Wavefront Object string representation of the mesh, including vertices and faces.
/// @see fs::Mesh::to_obj_file is a shortcut if you want to export the string representation to a file.
///
/// #### Examples
///
/// @code
/// fs::Mesh surface = fs::Mesh::construct_cube();
/// std::str mesh_repr_off = surface.to_obj();
/// @endcode
std::string to_obj() const {
std::stringstream objs;
for(size_t vidx=0; vidx<this->vertices.size(); vidx+=3) { // vertex coords
objs << "v " << vertices[vidx] << " " << vertices[vidx+1] << " " << vertices[vidx+2] << "\n";
}
for(size_t fidx=0; fidx<this->faces.size(); fidx+=3) { // faces: vertex indices, 1-based
objs << "f " << faces[fidx]+1 << " " << faces[fidx+1]+1 << " " << faces[fidx+2]+1 << "\n";
}
return(objs.str());
}
/// @brief Return adjacency matrix representation of this mesh.
/// @return boolean 2D matrix, where true means an edge between the respective vertex pair exists, and false mean it does not.
/// @see fs::Mesh::to_rep_adjlist gives you an adjacency list instead.
/// @note This requires a lot of memory for large meshes.
///
/// #### Examples
///
/// @code
/// fs::Mesh surface = fs::Mesh::construct_cube();
/// std::vector<std::vector<bool>> adjm = surface.as_adjmatrix();
/// @endcode
std::vector<std::vector<bool>> as_adjmatrix() const {
std::vector<std::vector<bool>> adjm = std::vector<std::vector<bool>>(this->num_vertices(), std::vector<bool>(this->num_vertices(), false));
for(size_t fidx=0; fidx<this->faces.size(); fidx+=3) { // faces: vertex indices
adjm[faces[fidx]][faces[fidx+1]] = true;
adjm[faces[fidx+1]][faces[fidx]] = true;
adjm[faces[fidx+1]][faces[fidx+2]] = true;
adjm[faces[fidx+2]][faces[fidx+1]] = true;
adjm[faces[fidx+2]][faces[fidx]] = true;
adjm[faces[fidx]][faces[fidx+2]] = true;
}
return adjm;
}
/// @brief Hash function for 2-tuples of `<size_t, sizt_t>`, used to hash an edge of a graph or mesh.
struct _tupleHashFunction {
size_t operator()(const std::tuple<size_t , size_t>&x) const {
return std::get<0>(x) ^ std::get<1>(x);
}
};
/// @brief Datastructure for storing, and quickly querying the existence of, mesh edges.
/// @details This is an unordered set of 2-tuples, where each tuple represents an edge, given as a pair of vertex indices. Each edge occurs twice in the list, once as `make_tuple(i,j)` and once as `make_tuple(j,i)`. Use the API of `std::unordered_set` to interact with it.
typedef std::unordered_set<std::tuple<size_t, size_t>, _tupleHashFunction> edge_set;
/// @brief Return edge list representation of this mesh.
/// @note While this mesh or graph representation is typically known as an edge list, this function actually returns a set.
/// @return an `fs::Mesh::edge_set`, i.e., an unordered set of 2-tuples, where each tuple represents an edge, given as a pair of vertex indices. Each edge occurs twice in the list, once as `make_tuple(i,j)` and once as `make_tuple(j,i)`.
///
/// #### Examples
///
/// @code
/// fs::Mesh surface = fs::Mesh::construct_cube();
/// edge_set edges = surface.as_edgelist();
/// size_t num_undirected_edges = edg es.size() / 2;
/// @endcode
edge_set as_edgelist() const {
edge_set edges;
for(size_t fidx=0; fidx<this->faces.size(); fidx+=3) { // faces: vertex indices
edges.insert(std::make_tuple(faces[fidx], faces[fidx+1]));
edges.insert(std::make_tuple(faces[fidx+1], faces[fidx]));
edges.insert(std::make_tuple(faces[fidx+1], faces[fidx+2]));
edges.insert(std::make_tuple(faces[fidx+2], faces[fidx+1]));
edges.insert(std::make_tuple(faces[fidx], faces[fidx+2]));
edges.insert(std::make_tuple(faces[fidx+2], faces[fidx]));
}
return edges;
}
/// @brief Return adjacency list representation of this mesh.
/// @param via_matrix whether the computation should be done via an step involving an adjacency matrix, or via an edge set. Leaving this at `true` temporarily requires a lot of memory for large meshes, but is faster.
/// @return vector of vectors, where the outer vector has size this->num_vertices. The inner vector at index N contains the M neighbors of vertex n, as vertex indices.
/// @see fs::Mesh::to_rep_adjmatrix gives you an adjacency matrix instead.
///
/// #### Examples
///
/// @code
/// fs::Mesh surface = fs::Mesh::construct_cube();
/// std::vector<std::vector<size_t>> adjl = surface.as_adjlist();
/// std::vector<std::vector<size_t>> adjl1 = surface.as_adjlist(true);
/// @endcode
std::vector<std::vector<size_t>> as_adjlist(const bool via_matrix=true) const {
if(! via_matrix) {
return(this->_as_adjlist_via_edgeset());
}
std::vector<std::vector<bool>> adjm = this->as_adjmatrix();
std::vector<std::vector<size_t>> adjl = std::vector<std::vector<size_t>>(this->num_vertices(), std::vector<size_t>());
size_t nv = adjm.size();
for (size_t i = 0; i < nv; i++) {
for (size_t j = i+1; j < nv; j++) {
if (adjm[i][j] == true) {
adjl[i].push_back(j);
adjl[j].push_back(i);
}
}
}
return adjl;
}
/// @brief Return adjacency list representation of this mesh via edge list.
/// @return vector of vectors, where the outer vector has size this->num_vertices. The inner vector at index N contains the M neighbors of vertex n, as vertex indices.
/// @see fs::Mesh::to_rep_adjmatrix gives you an adjacency matrix instead.
///
/// #### Examples
///
/// @code
/// fs::Mesh surface = fs::Mesh::construct_cube();
/// std::vector<std::vector<size_t>> adjl = surface.as_adjlist();
/// @endcode
std::vector<std::vector<size_t>> _as_adjlist_via_edgeset() const {
edge_set edges = this->as_edgelist();
std::vector<std::vector<size_t>> adjl = std::vector<std::vector<size_t>>(this->num_vertices(), std::vector<size_t>());
for (const std::tuple<size_t, size_t> &e: edges) {
adjl[std::get<0>(e)].push_back(std::get<1>(e));
}
return adjl;
}
/// @brief Smooth given per-vertex data using nearest neighbor smoothing.
/// @param pvd vector of per-vertex data values, one value per mesh vertex.
/// @param num_iter number of iterations of smoothing to perform.
/// @param via_matrix passed on to `this->as_asjlist()`, whether to construct the adjacency list of the mesh using an intermediate step involving an adjacency matrix, as opposed to using an edge set. The latter is slower but requires less memory.
/// @param with_nan whether you need support for NAN values in `pvd`. A bit slower if active. Ignored if `detectnan` is `true`.
/// @param detect_nan whether to auto-detect presence of NAN values, ignoring the setting of `with_nan`.
/// @return vector of smoothed per-vertex data values, same length as `pvd` param.
///
/// #### Examples
///
/// @code
/// fs::Mesh surface = fs::Mesh::construct_cube();
/// std::vector<float> pvd = {1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7};
/// std::vector<float> pvd_smooth = surface.smooth_pvd_nn(pvd, 2);
/// @endcode
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 {
const std::vector<std::vector<size_t>> adjlist = this->as_adjlist(via_matrix);
return fs::Mesh::smooth_pvd_nn(adjlist, pvd, num_iter, with_nan, detect_nan);
}
/// @brief Smooth given per-vertex data using nearest neighbor smoothing based on adjacency list mesh represenation.
/// @param mesh_adj the mesh, given as an adjacency list. The outer vector has size num_vertices, and the inner vectors sizes are the number of neighbors of the respective vertex.
/// @param pvd vector of per-vertex data values, one value per mesh vertex. Must not include NAN values. See `smooth_pvd_nn_nan` if you need support for NAN values.
/// @param num_iter number of iterations of smoothing to perform.
/// @param with_nan whether you need support for NAN values in `pvd`. A bit slower if active. Ignored if `detectnan` is `true`.
/// @param detect_nan whether to auto-detect presence of NAN values, ignoring the setting of `with_nan`.
/// @return vector of smoothed per-vertex data values, same length as `pvd` param.
///
/// #### Examples
///
/// @code
/// fs::Mesh surface = fs::Mesh::construct_cube();
/// std::vector<std::vector<size_t>> mesh_adj = surface.as_adjlist();
/// std::vector<float> pvd = {1.0, 1.1, 1.2, 1.3, 1.4, 1.5, 1.6, 1.7};
/// std::vector<float> pvd_smooth = fs::Mesh::smooth_pvd_nn(mesh_adj, pvd, 2);
/// @endcode
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) {
assert(pvd.size() == mesh_adj.size());
bool final_with_nan = with_nan;
if (detect_nan) {
final_with_nan = false;
for(size_t i=0; i<pvd.size(); i++) {
if(std::isnan(pvd[i])) {
final_with_nan = true;
break;
}
}
}
if (final_with_nan) {
return fs::Mesh::_smooth_pvd_nn_nan(mesh_adj, pvd, num_iter);
}
std::vector<float> current_pvd_source;
std::vector<float> current_pvd_smoothed = std::vector<float>(pvd.size());
float val_sum;
size_t num_neigh;
for(size_t i = 0; i < num_iter; i++) {
if(i == 0) {
current_pvd_source = pvd;
} else {
current_pvd_source = current_pvd_smoothed;
}
for(size_t v_idx = 0; v_idx < mesh_adj.size(); v_idx++) {
num_neigh = mesh_adj[v_idx].size();
val_sum = current_pvd_source[v_idx] / (num_neigh+1);
for(size_t neigh_rel_idx = 0; neigh_rel_idx < num_neigh; neigh_rel_idx++) {
val_sum += current_pvd_source[mesh_adj[v_idx][neigh_rel_idx]] / (num_neigh+1);
}
current_pvd_smoothed[v_idx] = val_sum;
}
}
return current_pvd_smoothed;
}
/// @brief Smooth given per-vertex data including NAN values using nearest neighbor smoothing based on adjacency list mesh representation.
/// @private
/// @param mesh_adj the mesh, given as an adjacency list. The outer vector has size num_vertices, and the inner vectors sizes are the number of neighbors of the respective vertex.
/// @param pvd vector of per-vertex data values, one value per mesh vertex. Must not include NAN values. See `smooth_pvd_nn_nan` if you need support for NAN values.
/// @param num_iter number of iterations of smoothing to perform.
/// @return vector of smoothed per-vertex data values, same length as `pvd` param.
/// @note This function is private, users should call `fs::Mesh::smooth_pvd_nn` instead.
///
/// #### Examples
///
/// @code
/// fs::Mesh surface = fs::Mesh::construct_cube();
/// std::vector<std::vector<size_t>> mesh_adj = surface.as_adjlist();
/// std::vector<float> pvd = {1.0, 1.1, 1.2, NAN, 1.4, 1.5, 1.6, 1.7};
/// std::vector<float> pvd_smooth = fs::Mesh::smooth_pvd_nn(mesh_adj, pvd, 2);
/// @endcode
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) {
std::vector<float> current_pvd_source;
std::vector<float> current_pvd_smoothed = std::vector<float>(pvd.size());
float val_sum;
size_t num_neigh;
size_t num_non_nan_values;
float neigh_val;
for(size_t i = 0; i < num_iter; i++) {
if(i == 0) {
current_pvd_source = pvd;
} else {
current_pvd_source = current_pvd_smoothed;
}
for(size_t v_idx = 0; v_idx < mesh_adj.size(); v_idx++) {
if(std::isnan(current_pvd_source[v_idx])) {
current_pvd_smoothed[v_idx] = NAN;
continue;
}
val_sum = current_pvd_source[v_idx];
num_non_nan_values = 1; // If we get here, the source vertex value is not NAN.
num_neigh = mesh_adj[v_idx].size();
for(size_t neigh_rel_idx = 0; neigh_rel_idx < num_neigh; neigh_rel_idx++) {
neigh_val = current_pvd_source[mesh_adj[v_idx][neigh_rel_idx]];
if(std::isnan(neigh_val)) {
continue;
} else {
val_sum += neigh_val;
num_non_nan_values++;
}
}
current_pvd_smoothed[v_idx] = val_sum / (float)num_non_nan_values;
}
}
return current_pvd_smoothed;
}
/// @brief Extend mesh neighborhoods based on mesh adjacency representation.
/// @details This function is mainly extended to extend a source neighborhood representation (typically the mesh's `k=1` neighborhood, i.e., the adjacency list of the mesh) to a higher `k`. In a `k=3` neighborhood, the neighorhood around a source vertex includes all vertices in edge distance up to 3 from the source vertex (but not the source vertex itself).
/// @param mesh_adj The adjacency list representation of the underlying mesh, the outer vector must have size `N` for a mesh with `N` vertices.
/// @param extend_by the number of edges to hop to extend the neighborhoods.
/// @param mesh_adj_ext the starting neighborhoods to extend, same representation as `mesh_adj`. The outer vector must have size `N` or `0`. If passed as an empty vector, this will be ignored and a copy of the `mesh_adj` is used as the `start_neighborhoods`.
/// @return extended neighborhoods
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>>()) {
size_t num_vertices = mesh_adj.size();
if(mesh_adj_ext.size() == 0) {
mesh_adj_ext = mesh_adj;
}
std::vector<size_t> neighborhood;
std::vector<size_t> ext_neighborhood;
for(size_t ext_idx = 0; ext_idx < extend_by; ext_idx++) {
for(size_t source_vert_idx = 0; source_vert_idx < num_vertices; source_vert_idx++) {
neighborhood = mesh_adj_ext[source_vert_idx]; // copy needed so we do not modify during iteration.
// Extension: add all neighbors in distance one for all vertices in the neighborhood.
for(size_t neigh_vert_rel_idx = 0; neigh_vert_rel_idx < neighborhood.size(); neigh_vert_rel_idx++) {
for(size_t canidate_rel_idx = 0; canidate_rel_idx < mesh_adj[neighborhood[neigh_vert_rel_idx]].size(); canidate_rel_idx++) {
if(mesh_adj[neighborhood[neigh_vert_rel_idx]][canidate_rel_idx] != source_vert_idx) {
mesh_adj_ext[source_vert_idx].push_back(mesh_adj[neighborhood[neigh_vert_rel_idx]][canidate_rel_idx]);
}
}
}
// We need to remove duplicates.
std::sort(mesh_adj_ext[source_vert_idx].begin(), mesh_adj_ext[source_vert_idx].end());
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());
}
}
return mesh_adj_ext;
}
/// @brief Export this mesh to a file in Wavefront OBJ format.
/// @param filename path to the output file, will be overwritten if existing.
/// @throws std::runtime_error if the target file cannot be opened.
/// @see fs::Mesh::to_obj if you want the string representation (without writing it to a file).
///
/// #### Examples
///
/// @code
/// fs::Mesh surface = fs::Mesh::construct_cube();
/// const std::string out_path = fs::util::fullpath({"/tmp", "mesh.obj"});
/// surface.to_obj_file(out_path);
/// @endcode
void to_obj_file(const std::string& filename) const {
fs::util::str_to_file(filename, this->to_obj());
}
/// @brief Compute a new mesh that is a submesh of this mesh, based on a subset of the vertices of this mesh.
/// @param old_vertex_indices vector of vertex indices of this mesh, which should be included in the submesh.
/// @param mapdir_fulltosubmesh whether to return a map from the old (full mesh) to the new (submesh) vertex indices (`true`), or the other way around (`false`, the default) as the first element of the returned pair.
/// @return a pair of the vertex index map (direction 'fullmesh to submesh' by default, but see 'mapdir_fulltosubmesh' parameter) and the submesh.
///
/// #### Examples
///
/// @code
/// fs::Mesh surface;
/// fs::read_surf(&surface, "examples/read_surf/lh.white");
/// fs::Label label;
/// fs::read_label(&label, "examples/read_label/lh.cortex.label");
/// std::pair <std::unordered_map<int32_t, int32_t>, fs::Mesh> result = surface.submesh_vertex(label.vertex);
/// fs::Mesh patch = result.second;
/// auto vertexindexmap_submesh2full = result.first; // or '<std::unordered_map<int32_t, int32_t>' instead of 'auto'.
/// @endcode
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 {
fs::Mesh submesh;
std::vector<float> new_vertices;
std::vector<int> new_faces;
std::unordered_map<int32_t, int32_t> vertex_index_map_full2submesh;
int32_t new_vertex_idx = 0;
for(size_t i = 0; i < old_vertex_indices.size(); i++) {
vertex_index_map_full2submesh[old_vertex_indices[i]] = new_vertex_idx;
new_vertices.push_back(this->vertices[size_t(old_vertex_indices[i])*3]);
new_vertices.push_back(this->vertices[size_t(old_vertex_indices[i])*3+1]);
new_vertices.push_back(this->vertices[size_t(old_vertex_indices[i])*3+2]);
new_vertex_idx++;
}
int face_v0;
int face_v1;
int face_v2;
for(size_t i = 0; i < this->num_faces(); i++) {
face_v0 = this->faces[i*3];
face_v1 = this->faces[i*3+1];
face_v2 = this->faces[i*3+2];
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())) {
new_faces.push_back(vertex_index_map_full2submesh[face_v0]);
new_faces.push_back(vertex_index_map_full2submesh[face_v1]);
new_faces.push_back(vertex_index_map_full2submesh[face_v2]);
}
}
submesh.vertices = new_vertices;
submesh.faces = new_faces;
std::pair <std::unordered_map<int32_t, int32_t>, fs::Mesh> result;
if(! mapdir_fulltosubmesh) { // Compute the new2old (reverse) vertex index map:
std::unordered_map<int32_t, int32_t> vertex_index_map_submesh2full;
for (auto const& pair: vertex_index_map_full2submesh) {
vertex_index_map_submesh2full[pair.second] = pair.first;
}
result = std::pair <std::unordered_map<int32_t, int32_t>, fs::Mesh>(vertex_index_map_submesh2full, submesh);
} else {
result = std::pair <std::unordered_map<int32_t, int32_t>, fs::Mesh>(vertex_index_map_full2submesh, submesh);
}
return result;
}
/// @brief Given per-vertex data for a submesh, add NAN values inbetween to restore the original mesh size.
/// @param data_submesh vector of per-vertex data values, one value per mesh vertex of the submesh.
/// @param submesh_to_orig_mapping map<int, int>, mapping vertex indices of the submesh to vertex indices of the original, full mesh.
/// @param orig_mesh_num_vertices number of vertices of the original, full mesh.
/// @see `fs::Mesh::submesh_vertex` for how to get the `submesh_to_orig_mapping` parameter.
/// @return vector of per-vertex data values, one value per mesh vertex of the original mesh. Values for vertices that are not part of the submesh are set to NAN.
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()) {
if(submesh_to_orig_mapping.size() != data_submesh.size()) {
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()) + ".");
}
std::vector<float> data_orig_mesh(orig_mesh_num_vertices, fill_value);
for(size_t i=0; i<data_submesh.size(); i++) {
auto got = submesh_to_orig_mapping.find(int(i));
if (got != submesh_to_orig_mapping.end()) {
data_orig_mesh[got->second] = data_submesh[i];
}
}
return(data_orig_mesh);
}
/// @brief Read a brainmesh from a Wavefront object format stream.
/// @details This only reads the geometry, optional format extensions like materials are ignored (but files including them should parse fine).
/// @param mesh pointer to fs:Mesh instance to be filled.
/// @param is stream holding a text representation of a mesh in Wavefront object format.
/// @see There exists an overloaded version that reads from a file.
/// @throws std::domain_error if the file format is invalid.
///
/// #### Examples
///
/// @code
/// fs::Mesh surface;
/// const std::string in_path = fs::util::fullpath({"/tmp", "mesh.obj"});
/// fs::Mesh::from_obj(&surface, in_path);
/// @endcode
static void from_obj(Mesh* mesh, std::istream* is) {
std::string line;
int line_idx = -1;
std::vector<float> vertices;
std::vector<int> faces;
#ifdef LIBFS_DBG_INFO