├── .github └── workflows │ └── run_test.yml ├── .miss_hit ├── AUTHORS.txt ├── LICENSE.txt ├── README.md ├── brain1020.m ├── brain2mesh.m ├── closestnode.m ├── examples ├── SPM_example_brain.m ├── SPM_example_wholehead.m ├── check_brain2mesh_dependency.m └── jnii │ ├── c1ANTS19-5Years_head.jnii │ ├── c1ANTS40-44Years_head.jnii │ ├── c2ANTS19-5Years_head.jnii │ ├── c2ANTS40-44Years_head.jnii │ ├── c3ANTS19-5Years_head.jnii │ ├── c3ANTS40-44Years_head.jnii │ ├── c4ANTS19-5Years_head.jnii │ ├── c4ANTS40-44Years_head.jnii │ ├── c5ANTS19-5Years_head.jnii │ ├── c5ANTS40-44Years_head.jnii │ ├── c6ANTS19-5Years_head.jnii │ └── c6ANTS40-44Years_head.jnii ├── intriangulation.m ├── label2tpm.m ├── layersurf.m ├── polylineinterp.m ├── polylinelen.m ├── polylinesimplify.m ├── ray2surf.m ├── slicesurf.m ├── slicesurf3.m └── tpm2label.m /.github/workflows/run_test.yml: -------------------------------------------------------------------------------- 1 | name: Brain2Mesh CI 2 | 3 | on: [push, pull_request] 4 | 5 | jobs: 6 | octave_test: 7 | name: Octave tests 8 | strategy: 9 | # provided octave versions: ubuntu-20.04 = 5.2, ubuntu-22.04 = 6.4, macos-13 = 8.1, windows-2019 = 7.3 10 | matrix: 11 | os: [ubuntu-20.04, ubuntu-22.04, macos-13, windows-2019] 12 | runs-on: ${{ matrix.os }} 13 | defaults: 14 | run: 15 | shell: bash 16 | 17 | steps: 18 | - name: Checkout repo 19 | uses: actions/checkout@v3 20 | with: 21 | submodules: 'recursive' 22 | - name: Install dependencies 23 | run: | 24 | [[ "$RUNNER_OS" == "Linux" ]] && sudo apt-get update && sudo apt-get install -y octave 25 | if [[ "$RUNNER_OS" == "macOS" ]]; then 26 | brew install octave 27 | fi 28 | if [[ "$RUNNER_OS" == "Windows" ]]; then 29 | curl --retry 3 -kL http://cdimage.debian.org/mirror/gnu.org/gnu/octave/windows/octave-9.2.0-w64-64.7z --output octave-9.2.0-w64-64.7z 30 | 7z x octave-9.2.0-w64-64.7z -ooctave -y 31 | echo "$PWD/octave/octave-9.2.0-w64-64/mingw64/bin" >> $GITHUB_PATH 32 | fi 33 | - name: Setup iso2mesh 34 | run: | 35 | curl --retry 3 -kL https://github.com/fangq/iso2mesh/archive/refs/heads/master.zip -o master.zip 36 | unzip master.zip && rm -rf master.zip 37 | if [[ "$RUNNER_OS" == "Linux" ]]; then 38 | curl --retry 3 -kL https://github.com/NeuroJSON/zmat/archive/refs/heads/master.zip -o master.zip 39 | unzip master.zip && rm -rf master.zip 40 | fi 41 | - name: Run octave test 42 | run: | 43 | octave-cli --version 44 | octave-cli --eval "addpath(pwd, [pwd filesep 'iso2mesh-master'], [pwd filesep 'zmat-master']); cd examples; SPM_example_brain" 45 | octave-cli --eval "addpath(pwd, [pwd filesep 'iso2mesh-master'], [pwd filesep 'zmat-master']); cd examples; SPM_example_wholehead" 46 | 47 | matlab_test: 48 | name: MATLAB test 49 | strategy: 50 | matrix: 51 | os: [ubuntu-20.04, macos-13, windows-2019] 52 | runs-on: ${{ matrix.os }} 53 | steps: 54 | - name: Checkout repo 55 | uses: actions/checkout@v3 56 | with: 57 | submodules: 'recursive' 58 | - name: Set up MATLAB 59 | uses: matlab-actions/setup-matlab@v1 60 | with: 61 | release: R2022a 62 | - name: Setup iso2mesh 63 | run: | 64 | curl --retry 3 -kL https://github.com/fangq/iso2mesh/archive/refs/heads/master.zip -o master.zip 65 | unzip master.zip 66 | - name: Run MATLAB brain example 67 | uses: matlab-actions/run-command@v1 68 | with: 69 | command: addpath(pwd, [pwd filesep 'iso2mesh-master']); cd examples; SPM_example_brain 70 | - name: Run MATLAB wholehead example 71 | uses: matlab-actions/run-command@v1 72 | with: 73 | command: addpath(pwd, [pwd filesep 'iso2mesh-master']); cd examples; SPM_example_wholehead 74 | -------------------------------------------------------------------------------- /.miss_hit: -------------------------------------------------------------------------------- 1 | project_root 2 | 3 | suppress_rule: "redundant_brackets" 4 | suppress_rule: "line_length" 5 | suppress_rule: "file_length" 6 | suppress_rule: "copyright_notice" 7 | suppress_rule: "naming_functions" 8 | suppress_rule: "naming_parameters" 9 | suppress_rule: "naming_scripts" 10 | suppress_rule: "unicode" 11 | indent_function_file_body: false 12 | -------------------------------------------------------------------------------- /AUTHORS.txt: -------------------------------------------------------------------------------- 1 | Brain2Mesh is written by 2 | 3 | ============================================================== 4 | Copyright (C) 2011,2017-2020 Qianqian Fang 5 | 6 | Dr. Qianqian is currently an Assistant Professor in the 7 | Department of Bioengineering at Northeastern University. 8 | 9 | Address: Dept. of Bioengineering 10 | Northeastern University 11 | 360 Huntington Ave, ISEC 206, Boston, MA 02115, USA 12 | URL: http://fanglab.org 13 | Email: 14 | 15 | Brain2Mesh Website : http://mcx.space/brain2mesh 16 | 17 | =============================================================== 18 | Copyright (C) 2017-2018 Anh Phong Tran 19 | 20 | As a former PhD student in Dr. Fang's lab, Phong wrote the initial 21 | version of the brain2mesh, adapted from an earlier script written by 22 | Dr. Fang 23 | 24 | https://github.com/fangq/Colin27BrainMesh/blob/master/colinsmesh_v2.m 25 | http://mcx.space/wiki/index.cgi?MMC/Colin27AtlasMesh/Version2 26 | 27 | Email: 28 | 29 | -------------------------------------------------------------------------------- /LICENSE.txt: -------------------------------------------------------------------------------- 1 | =============================================================================== 2 | = Brain2Mesh = 3 | = A one-liner for human brain 3D mesh generation = 4 | =============================================================================== 5 | 6 | Copyright (C) 2011,2017-2020 Qianqian Fang 7 | Copyright (C) 2017-2018 Anh Phong Tran 8 | 9 | ------------------------------------------------------------------------------- 10 | 11 | This program is free software: you can redistribute it and/or modify 12 | it under the terms of the GNU General Public License as published by 13 | the Free Software Foundation, either version 3 of the License, or 14 | (at your option) any later version. 15 | 16 | This program is distributed in the hope that it will be useful, 17 | but WITHOUT ANY WARRANTY; without even the implied warranty of 18 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 19 | GNU General Public License for more details. 20 | 21 | You should have received a copy of the GNU General Public License 22 | along with this program. If not, see . 23 | 24 | ------------------------------------------------------------------------ 25 | 26 | GNU GENERAL PUBLIC LICENSE 27 | Version 3, 29 June 2007 28 | 29 | Copyright (C) 2007 Free Software Foundation, Inc. 30 | Everyone is permitted to copy and distribute verbatim copies 31 | of this license document, but changing it is not allowed. 32 | 33 | Preamble 34 | 35 | The GNU General Public License is a free, copyleft license for 36 | software and other kinds of works. 37 | 38 | The licenses for most software and other practical works are designed 39 | to take away your freedom to share and change the works. By contrast, 40 | the GNU General Public License is intended to guarantee your freedom to 41 | share and change all versions of a program--to make sure it remains free 42 | software for all its users. We, the Free Software Foundation, use the 43 | GNU General Public License for most of our software; it applies also to 44 | any other work released this way by its authors. You can apply it to 45 | your programs, too. 46 | 47 | When we speak of free software, we are referring to freedom, not 48 | price. Our General Public Licenses are designed to make sure that you 49 | have the freedom to distribute copies of free software (and charge for 50 | them if you wish), that you receive source code or can get it if you 51 | want it, that you can change the software or use pieces of it in new 52 | free programs, and that you know you can do these things. 53 | 54 | To protect your rights, we need to prevent others from denying you 55 | these rights or asking you to surrender the rights. Therefore, you have 56 | certain responsibilities if you distribute copies of the software, or if 57 | you modify it: responsibilities to respect the freedom of others. 58 | 59 | For example, if you distribute copies of such a program, whether 60 | gratis or for a fee, you must pass on to the recipients the same 61 | freedoms that you received. You must make sure that they, too, receive 62 | or can get the source code. And you must show them these terms so they 63 | know their rights. 64 | 65 | Developers that use the GNU GPL protect your rights with two steps: 66 | (1) assert copyright on the software, and (2) offer you this License 67 | giving you legal permission to copy, distribute and/or modify it. 68 | 69 | For the developers' and authors' protection, the GPL clearly explains 70 | that there is no warranty for this free software. For both users' and 71 | authors' sake, the GPL requires that modified versions be marked as 72 | changed, so that their problems will not be attributed erroneously to 73 | authors of previous versions. 74 | 75 | Some devices are designed to deny users access to install or run 76 | modified versions of the software inside them, although the manufacturer 77 | can do so. This is fundamentally incompatible with the aim of 78 | protecting users' freedom to change the software. The systematic 79 | pattern of such abuse occurs in the area of products for individuals to 80 | use, which is precisely where it is most unacceptable. Therefore, we 81 | have designed this version of the GPL to prohibit the practice for those 82 | products. If such problems arise substantially in other domains, we 83 | stand ready to extend this provision to those domains in future versions 84 | of the GPL, as needed to protect the freedom of users. 85 | 86 | Finally, every program is threatened constantly by software patents. 87 | States should not allow patents to restrict development and use of 88 | software on general-purpose computers, but in those that do, we wish to 89 | avoid the special danger that patents applied to a free program could 90 | make it effectively proprietary. To prevent this, the GPL assures that 91 | patents cannot be used to render the program non-free. 92 | 93 | The precise terms and conditions for copying, distribution and 94 | modification follow. 95 | 96 | TERMS AND CONDITIONS 97 | 98 | 0. Definitions. 99 | 100 | "This License" refers to version 3 of the GNU General Public License. 101 | 102 | "Copyright" also means copyright-like laws that apply to other kinds of 103 | works, such as semiconductor masks. 104 | 105 | "The Program" refers to any copyrightable work licensed under this 106 | License. Each licensee is addressed as "you". "Licensees" and 107 | "recipients" may be individuals or organizations. 108 | 109 | To "modify" a work means to copy from or adapt all or part of the work 110 | in a fashion requiring copyright permission, other than the making of an 111 | exact copy. The resulting work is called a "modified version" of the 112 | earlier work or a work "based on" the earlier work. 113 | 114 | A "covered work" means either the unmodified Program or a work based 115 | on the Program. 116 | 117 | To "propagate" a work means to do anything with it that, without 118 | permission, would make you directly or secondarily liable for 119 | infringement under applicable copyright law, except executing it on a 120 | computer or modifying a private copy. Propagation includes copying, 121 | distribution (with or without modification), making available to the 122 | public, and in some countries other activities as well. 123 | 124 | To "convey" a work means any kind of propagation that enables other 125 | parties to make or receive copies. Mere interaction with a user through 126 | a computer network, with no transfer of a copy, is not conveying. 127 | 128 | An interactive user interface displays "Appropriate Legal Notices" 129 | to the extent that it includes a convenient and prominently visible 130 | feature that (1) displays an appropriate copyright notice, and (2) 131 | tells the user that there is no warranty for the work (except to the 132 | extent that warranties are provided), that licensees may convey the 133 | work under this License, and how to view a copy of this License. If 134 | the interface presents a list of user commands or options, such as a 135 | menu, a prominent item in the list meets this criterion. 136 | 137 | 1. Source Code. 138 | 139 | The "source code" for a work means the preferred form of the work 140 | for making modifications to it. "Object code" means any non-source 141 | form of a work. 142 | 143 | A "Standard Interface" means an interface that either is an official 144 | standard defined by a recognized standards body, or, in the case of 145 | interfaces specified for a particular programming language, one that 146 | is widely used among developers working in that language. 147 | 148 | The "System Libraries" of an executable work include anything, other 149 | than the work as a whole, that (a) is included in the normal form of 150 | packaging a Major Component, but which is not part of that Major 151 | Component, and (b) serves only to enable use of the work with that 152 | Major Component, or to implement a Standard Interface for which an 153 | implementation is available to the public in source code form. A 154 | "Major Component", in this context, means a major essential component 155 | (kernel, window system, and so on) of the specific operating system 156 | (if any) on which the executable work runs, or a compiler used to 157 | produce the work, or an object code interpreter used to run it. 158 | 159 | The "Corresponding Source" for a work in object code form means all 160 | the source code needed to generate, install, and (for an executable 161 | work) run the object code and to modify the work, including scripts to 162 | control those activities. However, it does not include the work's 163 | System Libraries, or general-purpose tools or generally available free 164 | programs which are used unmodified in performing those activities but 165 | which are not part of the work. For example, Corresponding Source 166 | includes interface definition files associated with source files for 167 | the work, and the source code for shared libraries and dynamically 168 | linked subprograms that the work is specifically designed to require, 169 | such as by intimate data communication or control flow between those 170 | subprograms and other parts of the work. 171 | 172 | The Corresponding Source need not include anything that users 173 | can regenerate automatically from other parts of the Corresponding 174 | Source. 175 | 176 | The Corresponding Source for a work in source code form is that 177 | same work. 178 | 179 | 2. Basic Permissions. 180 | 181 | All rights granted under this License are granted for the term of 182 | copyright on the Program, and are irrevocable provided the stated 183 | conditions are met. This License explicitly affirms your unlimited 184 | permission to run the unmodified Program. The output from running a 185 | covered work is covered by this License only if the output, given its 186 | content, constitutes a covered work. This License acknowledges your 187 | rights of fair use or other equivalent, as provided by copyright law. 188 | 189 | You may make, run and propagate covered works that you do not 190 | convey, without conditions so long as your license otherwise remains 191 | in force. You may convey covered works to others for the sole purpose 192 | of having them make modifications exclusively for you, or provide you 193 | with facilities for running those works, provided that you comply with 194 | the terms of this License in conveying all material for which you do 195 | not control copyright. Those thus making or running the covered works 196 | for you must do so exclusively on your behalf, under your direction 197 | and control, on terms that prohibit them from making any copies of 198 | your copyrighted material outside their relationship with you. 199 | 200 | Conveying under any other circumstances is permitted solely under 201 | the conditions stated below. Sublicensing is not allowed; section 10 202 | makes it unnecessary. 203 | 204 | 3. Protecting Users' Legal Rights From Anti-Circumvention Law. 205 | 206 | No covered work shall be deemed part of an effective technological 207 | measure under any applicable law fulfilling obligations under article 208 | 11 of the WIPO copyright treaty adopted on 20 December 1996, or 209 | similar laws prohibiting or restricting circumvention of such 210 | measures. 211 | 212 | When you convey a covered work, you waive any legal power to forbid 213 | circumvention of technological measures to the extent such circumvention 214 | is effected by exercising rights under this License with respect to 215 | the covered work, and you disclaim any intention to limit operation or 216 | modification of the work as a means of enforcing, against the work's 217 | users, your or third parties' legal rights to forbid circumvention of 218 | technological measures. 219 | 220 | 4. Conveying Verbatim Copies. 221 | 222 | You may convey verbatim copies of the Program's source code as you 223 | receive it, in any medium, provided that you conspicuously and 224 | appropriately publish on each copy an appropriate copyright notice; 225 | keep intact all notices stating that this License and any 226 | non-permissive terms added in accord with section 7 apply to the code; 227 | keep intact all notices of the absence of any warranty; and give all 228 | recipients a copy of this License along with the Program. 229 | 230 | You may charge any price or no price for each copy that you convey, 231 | and you may offer support or warranty protection for a fee. 232 | 233 | 5. Conveying Modified Source Versions. 234 | 235 | You may convey a work based on the Program, or the modifications to 236 | produce it from the Program, in the form of source code under the 237 | terms of section 4, provided that you also meet all of these conditions: 238 | 239 | a) The work must carry prominent notices stating that you modified 240 | it, and giving a relevant date. 241 | 242 | b) The work must carry prominent notices stating that it is 243 | released under this License and any conditions added under section 244 | 7. This requirement modifies the requirement in section 4 to 245 | "keep intact all notices". 246 | 247 | c) You must license the entire work, as a whole, under this 248 | License to anyone who comes into possession of a copy. This 249 | License will therefore apply, along with any applicable section 7 250 | additional terms, to the whole of the work, and all its parts, 251 | regardless of how they are packaged. This License gives no 252 | permission to license the work in any other way, but it does not 253 | invalidate such permission if you have separately received it. 254 | 255 | d) If the work has interactive user interfaces, each must display 256 | Appropriate Legal Notices; however, if the Program has interactive 257 | interfaces that do not display Appropriate Legal Notices, your 258 | work need not make them do so. 259 | 260 | A compilation of a covered work with other separate and independent 261 | works, which are not by their nature extensions of the covered work, 262 | and which are not combined with it such as to form a larger program, 263 | in or on a volume of a storage or distribution medium, is called an 264 | "aggregate" if the compilation and its resulting copyright are not 265 | used to limit the access or legal rights of the compilation's users 266 | beyond what the individual works permit. Inclusion of a covered work 267 | in an aggregate does not cause this License to apply to the other 268 | parts of the aggregate. 269 | 270 | 6. Conveying Non-Source Forms. 271 | 272 | You may convey a covered work in object code form under the terms 273 | of sections 4 and 5, provided that you also convey the 274 | machine-readable Corresponding Source under the terms of this License, 275 | in one of these ways: 276 | 277 | a) Convey the object code in, or embodied in, a physical product 278 | (including a physical distribution medium), accompanied by the 279 | Corresponding Source fixed on a durable physical medium 280 | customarily used for software interchange. 281 | 282 | b) Convey the object code in, or embodied in, a physical product 283 | (including a physical distribution medium), accompanied by a 284 | written offer, valid for at least three years and valid for as 285 | long as you offer spare parts or customer support for that product 286 | model, to give anyone who possesses the object code either (1) a 287 | copy of the Corresponding Source for all the software in the 288 | product that is covered by this License, on a durable physical 289 | medium customarily used for software interchange, for a price no 290 | more than your reasonable cost of physically performing this 291 | conveying of source, or (2) access to copy the 292 | Corresponding Source from a network server at no charge. 293 | 294 | c) Convey individual copies of the object code with a copy of the 295 | written offer to provide the Corresponding Source. This 296 | alternative is allowed only occasionally and noncommercially, and 297 | only if you received the object code with such an offer, in accord 298 | with subsection 6b. 299 | 300 | d) Convey the object code by offering access from a designated 301 | place (gratis or for a charge), and offer equivalent access to the 302 | Corresponding Source in the same way through the same place at no 303 | further charge. You need not require recipients to copy the 304 | Corresponding Source along with the object code. If the place to 305 | copy the object code is a network server, the Corresponding Source 306 | may be on a different server (operated by you or a third party) 307 | that supports equivalent copying facilities, provided you maintain 308 | clear directions next to the object code saying where to find the 309 | Corresponding Source. Regardless of what server hosts the 310 | Corresponding Source, you remain obligated to ensure that it is 311 | available for as long as needed to satisfy these requirements. 312 | 313 | e) Convey the object code using peer-to-peer transmission, provided 314 | you inform other peers where the object code and Corresponding 315 | Source of the work are being offered to the general public at no 316 | charge under subsection 6d. 317 | 318 | A separable portion of the object code, whose source code is excluded 319 | from the Corresponding Source as a System Library, need not be 320 | included in conveying the object code work. 321 | 322 | A "User Product" is either (1) a "consumer product", which means any 323 | tangible personal property which is normally used for personal, family, 324 | or household purposes, or (2) anything designed or sold for incorporation 325 | into a dwelling. In determining whether a product is a consumer product, 326 | doubtful cases shall be resolved in favor of coverage. For a particular 327 | product received by a particular user, "normally used" refers to a 328 | typical or common use of that class of product, regardless of the status 329 | of the particular user or of the way in which the particular user 330 | actually uses, or expects or is expected to use, the product. A product 331 | is a consumer product regardless of whether the product has substantial 332 | commercial, industrial or non-consumer uses, unless such uses represent 333 | the only significant mode of use of the product. 334 | 335 | "Installation Information" for a User Product means any methods, 336 | procedures, authorization keys, or other information required to install 337 | and execute modified versions of a covered work in that User Product from 338 | a modified version of its Corresponding Source. The information must 339 | suffice to ensure that the continued functioning of the modified object 340 | code is in no case prevented or interfered with solely because 341 | modification has been made. 342 | 343 | If you convey an object code work under this section in, or with, or 344 | specifically for use in, a User Product, and the conveying occurs as 345 | part of a transaction in which the right of possession and use of the 346 | User Product is transferred to the recipient in perpetuity or for a 347 | fixed term (regardless of how the transaction is characterized), the 348 | Corresponding Source conveyed under this section must be accompanied 349 | by the Installation Information. But this requirement does not apply 350 | if neither you nor any third party retains the ability to install 351 | modified object code on the User Product (for example, the work has 352 | been installed in ROM). 353 | 354 | The requirement to provide Installation Information does not include a 355 | requirement to continue to provide support service, warranty, or updates 356 | for a work that has been modified or installed by the recipient, or for 357 | the User Product in which it has been modified or installed. Access to a 358 | network may be denied when the modification itself materially and 359 | adversely affects the operation of the network or violates the rules and 360 | protocols for communication across the network. 361 | 362 | Corresponding Source conveyed, and Installation Information provided, 363 | in accord with this section must be in a format that is publicly 364 | documented (and with an implementation available to the public in 365 | source code form), and must require no special password or key for 366 | unpacking, reading or copying. 367 | 368 | 7. Additional Terms. 369 | 370 | "Additional permissions" are terms that supplement the terms of this 371 | License by making exceptions from one or more of its conditions. 372 | Additional permissions that are applicable to the entire Program shall 373 | be treated as though they were included in this License, to the extent 374 | that they are valid under applicable law. If additional permissions 375 | apply only to part of the Program, that part may be used separately 376 | under those permissions, but the entire Program remains governed by 377 | this License without regard to the additional permissions. 378 | 379 | When you convey a copy of a covered work, you may at your option 380 | remove any additional permissions from that copy, or from any part of 381 | it. (Additional permissions may be written to require their own 382 | removal in certain cases when you modify the work.) You may place 383 | additional permissions on material, added by you to a covered work, 384 | for which you have or can give appropriate copyright permission. 385 | 386 | Notwithstanding any other provision of this License, for material you 387 | add to a covered work, you may (if authorized by the copyright holders of 388 | that material) supplement the terms of this License with terms: 389 | 390 | a) Disclaiming warranty or limiting liability differently from the 391 | terms of sections 15 and 16 of this License; or 392 | 393 | b) Requiring preservation of specified reasonable legal notices or 394 | author attributions in that material or in the Appropriate Legal 395 | Notices displayed by works containing it; or 396 | 397 | c) Prohibiting misrepresentation of the origin of that material, or 398 | requiring that modified versions of such material be marked in 399 | reasonable ways as different from the original version; or 400 | 401 | d) Limiting the use for publicity purposes of names of licensors or 402 | authors of the material; or 403 | 404 | e) Declining to grant rights under trademark law for use of some 405 | trade names, trademarks, or service marks; or 406 | 407 | f) Requiring indemnification of licensors and authors of that 408 | material by anyone who conveys the material (or modified versions of 409 | it) with contractual assumptions of liability to the recipient, for 410 | any liability that these contractual assumptions directly impose on 411 | those licensors and authors. 412 | 413 | All other non-permissive additional terms are considered "further 414 | restrictions" within the meaning of section 10. If the Program as you 415 | received it, or any part of it, contains a notice stating that it is 416 | governed by this License along with a term that is a further 417 | restriction, you may remove that term. If a license document contains 418 | a further restriction but permits relicensing or conveying under this 419 | License, you may add to a covered work material governed by the terms 420 | of that license document, provided that the further restriction does 421 | not survive such relicensing or conveying. 422 | 423 | If you add terms to a covered work in accord with this section, you 424 | must place, in the relevant source files, a statement of the 425 | additional terms that apply to those files, or a notice indicating 426 | where to find the applicable terms. 427 | 428 | Additional terms, permissive or non-permissive, may be stated in the 429 | form of a separately written license, or stated as exceptions; 430 | the above requirements apply either way. 431 | 432 | 8. Termination. 433 | 434 | You may not propagate or modify a covered work except as expressly 435 | provided under this License. Any attempt otherwise to propagate or 436 | modify it is void, and will automatically terminate your rights under 437 | this License (including any patent licenses granted under the third 438 | paragraph of section 11). 439 | 440 | However, if you cease all violation of this License, then your 441 | license from a particular copyright holder is reinstated (a) 442 | provisionally, unless and until the copyright holder explicitly and 443 | finally terminates your license, and (b) permanently, if the copyright 444 | holder fails to notify you of the violation by some reasonable means 445 | prior to 60 days after the cessation. 446 | 447 | Moreover, your license from a particular copyright holder is 448 | reinstated permanently if the copyright holder notifies you of the 449 | violation by some reasonable means, this is the first time you have 450 | received notice of violation of this License (for any work) from that 451 | copyright holder, and you cure the violation prior to 30 days after 452 | your receipt of the notice. 453 | 454 | Termination of your rights under this section does not terminate the 455 | licenses of parties who have received copies or rights from you under 456 | this License. If your rights have been terminated and not permanently 457 | reinstated, you do not qualify to receive new licenses for the same 458 | material under section 10. 459 | 460 | 9. Acceptance Not Required for Having Copies. 461 | 462 | You are not required to accept this License in order to receive or 463 | run a copy of the Program. Ancillary propagation of a covered work 464 | occurring solely as a consequence of using peer-to-peer transmission 465 | to receive a copy likewise does not require acceptance. However, 466 | nothing other than this License grants you permission to propagate or 467 | modify any covered work. These actions infringe copyright if you do 468 | not accept this License. Therefore, by modifying or propagating a 469 | covered work, you indicate your acceptance of this License to do so. 470 | 471 | 10. Automatic Licensing of Downstream Recipients. 472 | 473 | Each time you convey a covered work, the recipient automatically 474 | receives a license from the original licensors, to run, modify and 475 | propagate that work, subject to this License. You are not responsible 476 | for enforcing compliance by third parties with this License. 477 | 478 | An "entity transaction" is a transaction transferring control of an 479 | organization, or substantially all assets of one, or subdividing an 480 | organization, or merging organizations. If propagation of a covered 481 | work results from an entity transaction, each party to that 482 | transaction who receives a copy of the work also receives whatever 483 | licenses to the work the party's predecessor in interest had or could 484 | give under the previous paragraph, plus a right to possession of the 485 | Corresponding Source of the work from the predecessor in interest, if 486 | the predecessor has it or can get it with reasonable efforts. 487 | 488 | You may not impose any further restrictions on the exercise of the 489 | rights granted or affirmed under this License. For example, you may 490 | not impose a license fee, royalty, or other charge for exercise of 491 | rights granted under this License, and you may not initiate litigation 492 | (including a cross-claim or counterclaim in a lawsuit) alleging that 493 | any patent claim is infringed by making, using, selling, offering for 494 | sale, or importing the Program or any portion of it. 495 | 496 | 11. Patents. 497 | 498 | A "contributor" is a copyright holder who authorizes use under this 499 | License of the Program or a work on which the Program is based. The 500 | work thus licensed is called the contributor's "contributor version". 501 | 502 | A contributor's "essential patent claims" are all patent claims 503 | owned or controlled by the contributor, whether already acquired or 504 | hereafter acquired, that would be infringed by some manner, permitted 505 | by this License, of making, using, or selling its contributor version, 506 | but do not include claims that would be infringed only as a 507 | consequence of further modification of the contributor version. For 508 | purposes of this definition, "control" includes the right to grant 509 | patent sublicenses in a manner consistent with the requirements of 510 | this License. 511 | 512 | Each contributor grants you a non-exclusive, worldwide, royalty-free 513 | patent license under the contributor's essential patent claims, to 514 | make, use, sell, offer for sale, import and otherwise run, modify and 515 | propagate the contents of its contributor version. 516 | 517 | In the following three paragraphs, a "patent license" is any express 518 | agreement or commitment, however denominated, not to enforce a patent 519 | (such as an express permission to practice a patent or covenant not to 520 | sue for patent infringement). To "grant" such a patent license to a 521 | party means to make such an agreement or commitment not to enforce a 522 | patent against the party. 523 | 524 | If you convey a covered work, knowingly relying on a patent license, 525 | and the Corresponding Source of the work is not available for anyone 526 | to copy, free of charge and under the terms of this License, through a 527 | publicly available network server or other readily accessible means, 528 | then you must either (1) cause the Corresponding Source to be so 529 | available, or (2) arrange to deprive yourself of the benefit of the 530 | patent license for this particular work, or (3) arrange, in a manner 531 | consistent with the requirements of this License, to extend the patent 532 | license to downstream recipients. "Knowingly relying" means you have 533 | actual knowledge that, but for the patent license, your conveying the 534 | covered work in a country, or your recipient's use of the covered work 535 | in a country, would infringe one or more identifiable patents in that 536 | country that you have reason to believe are valid. 537 | 538 | If, pursuant to or in connection with a single transaction or 539 | arrangement, you convey, or propagate by procuring conveyance of, a 540 | covered work, and grant a patent license to some of the parties 541 | receiving the covered work authorizing them to use, propagate, modify 542 | or convey a specific copy of the covered work, then the patent license 543 | you grant is automatically extended to all recipients of the covered 544 | work and works based on it. 545 | 546 | A patent license is "discriminatory" if it does not include within 547 | the scope of its coverage, prohibits the exercise of, or is 548 | conditioned on the non-exercise of one or more of the rights that are 549 | specifically granted under this License. You may not convey a covered 550 | work if you are a party to an arrangement with a third party that is 551 | in the business of distributing software, under which you make payment 552 | to the third party based on the extent of your activity of conveying 553 | the work, and under which the third party grants, to any of the 554 | parties who would receive the covered work from you, a discriminatory 555 | patent license (a) in connection with copies of the covered work 556 | conveyed by you (or copies made from those copies), or (b) primarily 557 | for and in connection with specific products or compilations that 558 | contain the covered work, unless you entered into that arrangement, 559 | or that patent license was granted, prior to 28 March 2007. 560 | 561 | Nothing in this License shall be construed as excluding or limiting 562 | any implied license or other defenses to infringement that may 563 | otherwise be available to you under applicable patent law. 564 | 565 | 12. No Surrender of Others' Freedom. 566 | 567 | If conditions are imposed on you (whether by court order, agreement or 568 | otherwise) that contradict the conditions of this License, they do not 569 | excuse you from the conditions of this License. If you cannot convey a 570 | covered work so as to satisfy simultaneously your obligations under this 571 | License and any other pertinent obligations, then as a consequence you may 572 | not convey it at all. For example, if you agree to terms that obligate you 573 | to collect a royalty for further conveying from those to whom you convey 574 | the Program, the only way you could satisfy both those terms and this 575 | License would be to refrain entirely from conveying the Program. 576 | 577 | 13. Use with the GNU Affero General Public License. 578 | 579 | Notwithstanding any other provision of this License, you have 580 | permission to link or combine any covered work with a work licensed 581 | under version 3 of the GNU Affero General Public License into a single 582 | combined work, and to convey the resulting work. The terms of this 583 | License will continue to apply to the part which is the covered work, 584 | but the special requirements of the GNU Affero General Public License, 585 | section 13, concerning interaction through a network will apply to the 586 | combination as such. 587 | 588 | 14. Revised Versions of this License. 589 | 590 | The Free Software Foundation may publish revised and/or new versions of 591 | the GNU General Public License from time to time. Such new versions will 592 | be similar in spirit to the present version, but may differ in detail to 593 | address new problems or concerns. 594 | 595 | Each version is given a distinguishing version number. If the 596 | Program specifies that a certain numbered version of the GNU General 597 | Public License "or any later version" applies to it, you have the 598 | option of following the terms and conditions either of that numbered 599 | version or of any later version published by the Free Software 600 | Foundation. If the Program does not specify a version number of the 601 | GNU General Public License, you may choose any version ever published 602 | by the Free Software Foundation. 603 | 604 | If the Program specifies that a proxy can decide which future 605 | versions of the GNU General Public License can be used, that proxy's 606 | public statement of acceptance of a version permanently authorizes you 607 | to choose that version for the Program. 608 | 609 | Later license versions may give you additional or different 610 | permissions. However, no additional obligations are imposed on any 611 | author or copyright holder as a result of your choosing to follow a 612 | later version. 613 | 614 | 15. Disclaimer of Warranty. 615 | 616 | THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY 617 | APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT 618 | HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY 619 | OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, 620 | THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR 621 | PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM 622 | IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF 623 | ALL NECESSARY SERVICING, REPAIR OR CORRECTION. 624 | 625 | 16. Limitation of Liability. 626 | 627 | IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 628 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS 629 | THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY 630 | GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE 631 | USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF 632 | DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD 633 | PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS), 634 | EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF 635 | SUCH DAMAGES. 636 | 637 | 17. Interpretation of Sections 15 and 16. 638 | 639 | If the disclaimer of warranty and limitation of liability provided 640 | above cannot be given local legal effect according to their terms, 641 | reviewing courts shall apply local law that most closely approximates 642 | an absolute waiver of all civil liability in connection with the 643 | Program, unless a warranty or assumption of liability accompanies a 644 | copy of the Program in return for a fee. 645 | 646 | END OF TERMS AND CONDITIONS 647 | 648 | How to Apply These Terms to Your New Programs 649 | 650 | If you develop a new program, and you want it to be of the greatest 651 | possible use to the public, the best way to achieve this is to make it 652 | free software which everyone can redistribute and change under these terms. 653 | 654 | To do so, attach the following notices to the program. It is safest 655 | to attach them to the start of each source file to most effectively 656 | state the exclusion of warranty; and each file should have at least 657 | the "copyright" line and a pointer to where the full notice is found. 658 | 659 | 660 | Copyright (C) 661 | 662 | This program is free software: you can redistribute it and/or modify 663 | it under the terms of the GNU General Public License as published by 664 | the Free Software Foundation, either version 3 of the License, or 665 | (at your option) any later version. 666 | 667 | This program is distributed in the hope that it will be useful, 668 | but WITHOUT ANY WARRANTY; without even the implied warranty of 669 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 670 | GNU General Public License for more details. 671 | 672 | You should have received a copy of the GNU General Public License 673 | along with this program. If not, see . 674 | 675 | Also add information on how to contact you by electronic and paper mail. 676 | 677 | If the program does terminal interaction, make it output a short 678 | notice like this when it starts in an interactive mode: 679 | 680 | Copyright (C) 681 | This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 682 | This is free software, and you are welcome to redistribute it 683 | under certain conditions; type `show c' for details. 684 | 685 | The hypothetical commands `show w' and `show c' should show the appropriate 686 | parts of the General Public License. Of course, your program's commands 687 | might be different; for a GUI interface, you would use an "about box". 688 | 689 | You should also get your employer (if you work as a programmer) or school, 690 | if any, to sign a "copyright disclaimer" for the program, if necessary. 691 | For more information on this, and how to apply and follow the GNU GPL, see 692 | . 693 | 694 | The GNU General Public License does not permit incorporating your program 695 | into proprietary programs. If your program is a subroutine library, you 696 | may consider it more useful to permit linking proprietary applications with 697 | the library. If this is what you want to do, use the GNU Lesser General 698 | Public License instead of this License. But first, please read 699 | . 700 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Brain2mesh: a one-liner for 3D brain mesh generation 2 | 3 | * Version: 0.8 4 | * Project Maintainer: Qianqian Fang 5 | * Contributors: See AUTHORS.txt for details 6 | * Address: 7 | * Department of Bioengineering 8 | * Northeastern University 9 | * 360 Huntington Ave, Boston, MA 02115 10 | * License: GPL v3 or later, see LICENSE.txt 11 | * URL: http://mcx.space/brain2mesh 12 | 13 | ## Introduction 14 | 15 | The Brain2Mesh toolbox provides a streamlined matlab function to convert a segmented brain 16 | volumes and surfaces into a high-quality multi-layered tetrahedral brain/full head mesh. 17 | 18 | The details of this toolbox is described in the paper listed in the [Reference](#reference) section. 19 | 20 | This tool does not handle the segmentation of MRI scans, but examples of how commonly 21 | encountered segmented datasets can be used to create meshes are available under the `examples` folder. 22 | 23 | The Brain2Mesh toolbox is also extensively dependent on: 24 | 1. Iso2Mesh toolbox (http://iso2mesh.sf.net), not included, download at https://github.com/fangq/iso2mesh 25 | 2. MATLAB Image-Processing toolbox (such as `imfill`, `imdilate`) 26 | 3. `intriangulation.m` (by Adam Aitkenhead and Johannes Korsawe) 27 | 28 | ## Overview of the functions 29 | 30 | The function `brain2mesh` handles the conversion of segmented volumes into high-quality 3D meshes. 31 | It takes an 4D array as input, with different assumptions as to the number of layers. Typically, the layers 32 | are assumed to contain: white matter (WM), grey matter (GM), cerebrospinal fluid (CSF), bone and scalp. 33 | It also is able to handle inputs missing a bone segmentation, and missing bone+scalp segmentation. 34 | 35 | Patient-specific segmentations can be done using common neuroimaging tools such as FSL, SPM, 36 | FreeSurfer and BrainSuite. There also exists series of available atlas databases that offer segmented volumes. 37 | 38 | In a near future release, scripts will be made available to accomodate the combination of segmented surface meshes 39 | such as the ones produced in FreeSurfer and BrainSuite as part of the input data. 40 | 41 | Another function `brain1020` provides an automated interface to compute head landmarks (10-20/10-5 systems 42 | or user-customizable divisions). Users can either interactively select 5 initial landmarks (nasion, inion, 43 | left and right ear-lobes, i.e. LPA/RPA, and vertex, i.e. CZ), the function automatically computes all brain 44 | landmarks on the scalp surface using user-specified density. 45 | 46 | Your acknowledgement of Brain2Mesh in your publications or presentations 47 | would be greatly appreciated by the authors of this toolbox. The citation 48 | information can be found in the Introduction section. 49 | 50 | ## Reference 51 | 52 | If you use Brain2Mesh or Brain Mesh Library in your publications, the authors of this toolbox 53 | greatly appreciate if you can cite the below paper 54 | 55 | * Anh Phong Tran†, Shijie Yan†, Qianqian Fang*, (2020) "Improving model-based fNIRS analysis using mesh-based anatomical and light-transport models," Neurophotonics, 7(1), 015008, URL: http://dx.doi.org/10.1117/1.NPh.7.1.015008 56 | 57 | ## Acknowledgement 58 | 59 | This project is funded by the National Institutes of Health (NIH) / National Institute of General 60 | Medical Sciences (NIGMS) under the grant number R01-GM114365, and NIH/NINID/NIBIB under the grant 61 | number R01-EB026998. 62 | 63 | ### Copyright disclaimer for `intriangulation.m` 64 | 65 | Copyright (c) 2016, Johannes Korsawe 66 | Copyright (c) 2013, Adam H. Aitkenhead 67 | All rights reserved. 68 | 69 | Redistribution and use in source and binary forms, with or without 70 | modification, are permitted provided that the following conditions are met: 71 | 72 | * Redistributions of source code must retain the above copyright notice, this 73 | list of conditions and the following disclaimer. 74 | * Redistributions in binary form must reproduce the above copyright notice, 75 | this list of conditions and the following disclaimer in the documentation 76 | and/or other materials provided with the distribution 77 | * Neither the name of Volkswagen AG nor the names of its 78 | contributors may be used to endorse or promote products derived from this 79 | software without specific prior written permission. 80 | * Neither the name of The Christie NHS Foundation Trust nor the names of its 81 | contributors may be used to endorse or promote products derived from this 82 | software without specific prior written permission. 83 | 84 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 85 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 86 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 87 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE 88 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 89 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 90 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 91 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 92 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 93 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 94 | -------------------------------------------------------------------------------- /brain1020.m: -------------------------------------------------------------------------------- 1 | function [landmarks, curves, initpoints] = brain1020(node, face, initpoints, perc1, perc2, varargin) 2 | % 3 | % landmarks=brain1020(node, face) 4 | % or 5 | % landmarks=brain1020(node, face, [], perc1, perc2) 6 | % landmarks=brain1020(node, face, initpoints) 7 | % landmarks=brain1020(node, face, initpoints, perc1, perc2) 8 | % [landmarks, curves, initpoints]=brain1020(node, face, initpoints, perc1, perc2, options) 9 | % 10 | % compute 10-20-like scalp landmarks with user-specified density on a head mesh 11 | % 12 | % author: Qianqian Fang (q.fang at neu.edu) 13 | % 14 | % == Input == 15 | % node: full head mesh node list 16 | % face: full head mesh element list- a 3-column array defines face list 17 | % for the exterior (scalp) surface; a 4-column array defines the 18 | % tetrahedral mesh of the full head. 19 | % initpoints:(optional) one can provide the 3-D coordinates of the below 20 | % 5 landmarks: nz, iz, lpa, rpa, cz0 (cz0 is the initial guess of cz) 21 | % initpoints can be a struct with the above landmark names 22 | % as subfield, or a 5x3 array definining these points in the above 23 | % mentioned order (one can use the output landmarks as initpoints) 24 | % perc1:(optional) the percentage of geodesic distance towards the rim of 25 | % the landmarks; this is the first number of the 10-20 or 10-10 or 26 | % 10-5 systems, in this case, it is 10 (for 10%). default is 10. 27 | % perc2:(optional) the percentage of geodesic distance twoards the center 28 | % of the landmarks; this is the 2nd number of the 10-20 or 10-10 or 29 | % 10-5 systems, which are 20, 10, 5, respectively, default is 20 30 | % options: one can add additional 'name',value pairs to the function 31 | % call to provide additional control. Supported optional names 32 | % include 33 | % 'display' : [1] or 0, if set to 1, plot landmarks and curves 34 | % 'cztol' : [1e-6], the tolerance for searching cz that bisects 35 | % saggital and coronal reference curves 36 | % 'maxcziter' : [10] the maximum number of iterations to update 37 | % cz to bisect both cm and sm curves 38 | % 'baseplane' : [1] or 0, if set to 1, create the reference 39 | % curves along the primary control points (nz,iz,lpa,rpa) 40 | % 'minangle' : [0] if set to a positive number, this specifies 41 | % the minimum angle (radian) between adjacent segments in 42 | % the reference curves to avoid sharp turns (such as the 43 | % dips near ear canals), this parameter will be 44 | % passed to polylinesimplify to simplify the curve first. 45 | % Please be noted that the landmarks generated with 46 | % simplified curves may not land exactly on the surface. 47 | % 48 | % == Output == 49 | % landmarks: a structure storing all computed landmarks. The subfields 50 | % include two sections: 51 | % 1) 'nz','iz','lpa','rpa','cz': individual 3D positions defining 52 | % the 5 principle reference points: nasion (nz), inion (in), 53 | % left-pre-auricular-point (lpa), right-pre-auricular-point 54 | % (rpa) and vertex (cz) - cz is updated from initpoints to bisect 55 | % the saggital and coronal ref. curves. 56 | % 2) landmarks along specific cross-sections, each cross section 57 | % may contain more than 1 position. The cross-sections are 58 | % named in the below format: 59 | % 2.1: a fieldname starting from 'c', 's' and 'a' indicates 60 | % the cut is along coronal, saggital and axial directions, 61 | % respectively; 62 | % 2.2: a name starting from 'pa' indicates the cut is along the 63 | % axial plane acrossing principle reference points 64 | % 2.3: the following letter 'm', 'a','p' suggests the 'medial', 65 | % 'anterior' and 'posterior', respectively 66 | % 2.4: the last letter 'l' or 'r' suggests the 'left' and 67 | % 'right' side, respectively 68 | % 2.5: non-medial coronal cuts are divided into two groups, the 69 | % anterior group (ca{l,r}) and the posterior group 70 | % (cp{lr}), with a number indicates the node spacing 71 | % stepping away from the medial plane. 72 | % 73 | % for example, landmarks.cm refers to the landmarks along the 74 | % medial-coronal plane, anterior-to-posteior order 75 | % similarly, landmarks.cpl_3 refers to the landmarks along the 76 | % coronal (c) cut plane located in the posterior-left side 77 | % of the head, with 3 saggital landmark spacing from the 78 | % medial-coronal reference curve. 79 | % curves: a structure storing all computed cross-section curves. The 80 | % subfields are named similarly to landmarks, except that 81 | % landmarks stores the 10-? points, and curves stores the 82 | % detailed cross-sectional curves 83 | % initpoints: a 5x3 array storing the principle reference points in the 84 | % orders of 'nz','iz','lpa','rpa','cz' 85 | % 86 | % 87 | % == Example == 88 | % See brain2mesh/examples/SPM_example_brain.m for an example 89 | % https://github.com/fangq/brain2mesh/blob/master/examples/SPM_example_brain.m 90 | % 91 | % == Dependency == 92 | % This function requires a pre-installed Iso2Mesh Toolbox 93 | % Download URL: http://github.com/fangq/iso2mesh 94 | % Website: http://iso2mesh.sf.net 95 | % 96 | % == Reference == 97 | % If you use this function in your publication, the authors of this toolbox 98 | % apprecitate if you can cite the below paper 99 | % 100 | % Anh Phong Tran, Shijie Yan and Qianqian Fang, "Improving model-based 101 | % fNIRS analysis using mesh-based anatomical and light-transport models," 102 | % Neurophotonics, 7(1), 015008, 2020 103 | % URL: http://dx.doi.org/10.1117/1.NPh.7.1.015008 104 | % 105 | % 106 | % -- this function is part of brain2mesh toolbox (http://mcx.space/brain2mesh) 107 | % License: GPL v3 or later, see LICENSE.txt for details 108 | % 109 | 110 | if (nargin < 2) 111 | error('one must provide a head-mesh to call this function'); 112 | end 113 | 114 | if (isempty(node) || isempty(face) || size(face, 2) <= 2 || size(node, 2) < 3) 115 | error('input node must have 3 columns, face must have at least 3 columns'); 116 | end 117 | 118 | if (nargin < 5) 119 | perc2 = 20; 120 | if (nargin < 4) 121 | perc1 = 10; 122 | if (nargin < 3) 123 | initpoints = []; 124 | end 125 | end 126 | end 127 | 128 | % parse user options 129 | opt = varargin2struct(varargin{:}); 130 | 131 | showplot = jsonopt('display', 1, opt); 132 | baseplane = jsonopt('baseplane', 1, opt); 133 | tol = jsonopt('cztol', 1e-6, opt); 134 | dosimplify = jsonopt('minangle', 0, opt); 135 | maxcziter = jsonopt('maxcziter', 10, opt); 136 | 137 | if (nargin >= 2 && ... 138 | ((isstruct(initpoints) && ~isfield(initpoints, 'iz')) || ... 139 | (~isstruct(initpoints) && size(initpoints, 1) == 3))) 140 | 141 | if (isstruct(initpoints)) 142 | nz = initpoints.nz(:).'; 143 | lpa = initpoints.lpa(:).'; 144 | rpa = initpoints.rpa(:).'; 145 | else 146 | nz = initpoints(1, :); 147 | lpa = initpoints(2, :); 148 | rpa = initpoints(3, :); 149 | end 150 | % This assume nz, lpa, rpa, iz are on the same plane to find iz on the head 151 | % surface 152 | pa_mid = mean([lpa; rpa]); 153 | v0 = pa_mid - nz; 154 | [iz, e0] = ray2surf(node, face, nz, v0, '>'); 155 | 156 | % To find cz, we assume that the vector from iz nz midpoint to cz is perpendicular 157 | % to the plane defined by nz, lpa, and rpa. 158 | iznz_mid = (nz + iz) * 0.5; 159 | v0 = cross(nz - rpa, lpa - rpa); 160 | [cz, e0] = ray2surf(node, face, iznz_mid, v0, '>'); 161 | if (isstruct(initpoints)) 162 | initpoints.iz = iz; 163 | initpoints.cz = cz; 164 | else 165 | initpoints = [initpoints(1, :); iz; initpoints(2:3, :); cz]; 166 | end 167 | end 168 | 169 | % convert initpoints input to a 5x3 array 170 | if (isstruct(initpoints)) 171 | initpoints = struct('nz', initpoints.nz(:).', 'iz', initpoints.iz(:).', ... 172 | 'lpa', initpoints.lpa(:).', 'rpa', initpoints.rpa(:).', ... 173 | 'cz', initpoints.cz(:).'); 174 | landmarks = initpoints; 175 | if (exist('struct2array', 'file')) 176 | initpoints = struct2array(initpoints); 177 | initpoints = reshape(initpoints(:), 3, length(initpoints(:)) / 3)'; 178 | else 179 | initpoints = [initpoints.nz(:).'; initpoints.iz(:).'; initpoints.lpa(:).'; initpoints.rpa(:).'; initpoints.cz(:).']; 180 | end 181 | end 182 | 183 | % convert tetrahedral mesh into a surface mesh 184 | if (size(face, 2) >= 4) 185 | face = volface(face(:, 1:4)); 186 | end 187 | 188 | % remove nodes not located in the surface 189 | [node, face] = removeisolatednode(node, face); 190 | 191 | % if initpoints is not sufficient, ask user to interactively select nz, iz, lpa, rpa and cz first 192 | if (isempty(initpoints) || size(initpoints, 1) < 5) 193 | hf = figure; 194 | plotmesh(node, face); 195 | set(hf, 'userdata', initpoints); 196 | if (~isempty(initpoints)) 197 | hold on; 198 | plotmesh(initpoints, 'gs', 'LineWidth', 4); 199 | end 200 | idx = size(initpoints, 1) + 1; 201 | landmarkname = {'Nasion', 'Inion', 'Left-pre-auricular-point', 'Right-pre-auricular-point', 'Vertex/Cz', 'Done'}; 202 | title(sprintf('Rotate the mesh, select data cursor, click on P%d: %s', idx, landmarkname{idx})); 203 | rotate3d('on'); 204 | set(datacursormode(hf), 'UpdateFcn', @myupdatefcn); 205 | end 206 | 207 | % wait until all 5 points are defined 208 | if (exist('hf', 'var')) 209 | try 210 | while (size(get(hf, 'userdata'), 1) < 5) 211 | pause(0.1); 212 | end 213 | catch 214 | error('user aborted'); 215 | end 216 | datacursormode(hf, 'off'); 217 | initpoints = get(hf, 'userdata'); 218 | close(hf); 219 | end 220 | 221 | if (showplot) 222 | disp(initpoints); 223 | end 224 | 225 | % save input initpoints to landmarks output, cz is not finalized 226 | if (size(initpoints, 1) >= 5) 227 | landmarks = struct('nz', initpoints(1, :), 'iz', initpoints(2, :), ... 228 | 'lpa', initpoints(3, :), 'rpa', initpoints(4, :), ... 229 | 'cz', initpoints(5, :)); 230 | end 231 | 232 | % at this point, initpoints contains {nz, iz, lpa, rpa, cz0} 233 | % plot the head mesh 234 | if (showplot) 235 | figure; 236 | hp = plotmesh(node, face, 'facealpha', 0.6, 'facecolor', [1 0.8 0.7]); 237 | if (~isoctavemesh) 238 | set(hp, 'linestyle', 'none'); 239 | end 240 | camlight; 241 | lighting gouraud; 242 | hold on; 243 | end 244 | 245 | lastcz = [1 1 1] * inf; 246 | cziter = 0; 247 | 248 | %% Find cz that bisects cm and sm curves within a tolerance, using UI 10-10 approach 249 | while (norm(initpoints(5, :) - lastcz) > tol && cziter < maxcziter) 250 | %% Step 1: nz, iz and cz0 to determine saggital reference curve 251 | 252 | nsagg = slicesurf(node, face, initpoints([1, 2, 5], :)); 253 | %% Step 1.1: get cz1 as the mid-point between iz and nz 254 | [slen, nsagg] = polylinelen(nsagg, initpoints(1, :), initpoints(2, :), initpoints(5, :)); 255 | if (dosimplify) 256 | [nsagg, slen] = polylinesimplify(nsagg, dosimplify); 257 | end 258 | [idx, weight, cz] = polylineinterp(slen, sum(slen) * 0.5, nsagg); 259 | initpoints(5, :) = cz(1, :); 260 | 261 | %% Step 1.2: lpa, rpa and cz1 to determine coronal reference curve, update cz1 262 | curves.cm = slicesurf(node, face, initpoints([3, 4, 5], :)); 263 | [len, curves.cm] = polylinelen(curves.cm, initpoints(3, :), initpoints(4, :), initpoints(5, :)); 264 | if (dosimplify) 265 | [curves.cm, len] = polylinesimplify(curves.cm, dosimplify); 266 | end 267 | [idx, weight, coro] = polylineinterp(len, sum(len) * 0.5, curves.cm); 268 | lastcz = initpoints(5, :); 269 | initpoints(5, :) = coro(1, :); 270 | cziter = cziter + 1; 271 | if (showplot) 272 | fprintf('cz iteration %d error %e\n', cziter, norm(initpoints(5, :) - lastcz)); 273 | end 274 | end 275 | 276 | % set the finalized cz to output 277 | landmarks.cz = initpoints(5, :); 278 | 279 | if (showplot) 280 | disp(initpoints); 281 | end 282 | 283 | %% Step 2: subdivide saggital (sm) and coronal (cm) ref curves 284 | 285 | [idx, weight, coro] = polylineinterp(len, sum(len) * (perc1:perc2:(100 - perc1)) * 0.01, curves.cm); 286 | landmarks.cm = coro; % t7, c3, cz, c4, t8 287 | 288 | curves.sm = slicesurf(node, face, initpoints([1, 2, 5], :)); 289 | [slen, curves.sm] = polylinelen(curves.sm, initpoints(1, :), initpoints(2, :), initpoints(5, :)); 290 | if (dosimplify) 291 | [curves.sm, slen] = polylinesimplify(curves.sm, dosimplify); 292 | end 293 | [idx, weight, sagg] = polylineinterp(slen, sum(slen) * (perc1:perc2:(100 - perc1)) * 0.01, curves.sm); 294 | landmarks.sm = sagg; % fpz, fz, cz, pz, oz 295 | 296 | %% Step 3: fpz, t7 and oz to determine left 10% axial reference curve 297 | 298 | [landmarks.aal, curves.aal, landmarks.apl, curves.apl] = slicesurf3(node, face, landmarks.sm(1, :), landmarks.cm(1, :), landmarks.sm(end, :), perc2 * 2); 299 | 300 | %% Step 4: fpz, t8 and oz to determine right 10% axial reference curve 301 | 302 | [landmarks.aar, curves.aar, landmarks.apr, curves.apr] = slicesurf3(node, face, landmarks.sm(1, :), landmarks.cm(end, :), landmarks.sm(end, :), perc2 * 2); 303 | 304 | %% show plots of the landmarks 305 | if (showplot) 306 | plotmesh(curves.sm, 'r-', 'LineWidth', 1); 307 | plotmesh(curves.cm, 'g-', 'LineWidth', 1); 308 | plotmesh(curves.aal, 'k-', 'LineWidth', 1); 309 | plotmesh(curves.aar, 'k-', 'LineWidth', 1); 310 | plotmesh(curves.apl, 'b-', 'LineWidth', 1); 311 | plotmesh(curves.apr, 'b-', 'LineWidth', 1); 312 | 313 | plotmesh(landmarks.sm, 'ro', 'LineWidth', 2); 314 | plotmesh(landmarks.cm, 'go', 'LineWidth', 2); 315 | plotmesh(landmarks.aal, 'ko', 'LineWidth', 2); 316 | plotmesh(landmarks.aar, 'mo', 'LineWidth', 2); 317 | plotmesh(landmarks.apl, 'ko', 'LineWidth', 2); 318 | plotmesh(landmarks.apr, 'mo', 'LineWidth', 2); 319 | end 320 | 321 | %% Step 5: computing all anterior coronal cuts, moving away from the medial cut (cm) toward frontal 322 | 323 | idxcz = closestnode(landmarks.sm, landmarks.cz); 324 | 325 | skipcount = floor(10 / perc2); 326 | 327 | for i = 1:size(landmarks.aal, 1) - skipcount 328 | step = (perc2 * 25) * 0.1 * (1 + ((perc2 < 20 + perc2 < 10) && i == size(landmarks.aal, 1) - skipcount)); 329 | [landmarks.(sprintf('cal_%d', i)), leftpart, landmarks.(sprintf('car_%d', i)), rightpart] = slicesurf3(node, face, landmarks.aal(i, :), landmarks.sm(idxcz - i, :), landmarks.aar(i, :), step); 330 | if (showplot) 331 | plotmesh(leftpart, 'k-', 'LineWidth', 1); 332 | plotmesh(rightpart, 'k-', 'LineWidth', 1); 333 | 334 | plotmesh(landmarks.(sprintf('cal_%d', i)), 'yo', 'LineWidth', 2); 335 | plotmesh(landmarks.(sprintf('car_%d', i)), 'co', 'LineWidth', 2); 336 | end 337 | end 338 | 339 | %% Step 6: computing all posterior coronal cuts, moving away from the medial cut (cm) toward occipital 340 | 341 | for i = 1:size(landmarks.apl, 1) - skipcount 342 | step = (perc2 * 25) * 0.1 * (1 + ((perc2 < 20 + perc2 < 10) && i == size(landmarks.apl, 1) - skipcount)); 343 | [landmarks.(sprintf('cpl_%d', i)), leftpart, landmarks.(sprintf('cpr_%d', i)), rightpart] = slicesurf3(node, face, landmarks.apl(i, :), landmarks.sm(idxcz + i, :), landmarks.apr(i, :), step); 344 | if (showplot) 345 | plotmesh(leftpart, 'k-', 'LineWidth', 1); 346 | plotmesh(rightpart, 'k-', 'LineWidth', 1); 347 | 348 | plotmesh(landmarks.(sprintf('cpl_%d', i)), 'yo', 'LineWidth', 2); 349 | plotmesh(landmarks.(sprintf('cpr_%d', i)), 'co', 'LineWidth', 2); 350 | end 351 | end 352 | 353 | %% Step 7: create the axial cuts across priciple ref. points: left: nz, lpa, iz, right: nz, rpa, iz 354 | 355 | if (baseplane && perc2 <= 10) 356 | [landmarks.paal, curves.paal, landmarks.papl, curves.papl] = slicesurf3(node, face, landmarks.nz, landmarks.lpa, landmarks.iz, perc2 * 2); 357 | [landmarks.paar, curves.paar, landmarks.papr, curves.papr] = slicesurf3(node, face, landmarks.nz, landmarks.rpa, landmarks.iz, perc2 * 2); 358 | if (showplot) 359 | plotmesh(curves.paal, 'k-', 'LineWidth', 1); 360 | plotmesh(curves.paar, 'k-', 'LineWidth', 1); 361 | plotmesh(curves.papl, 'k-', 'LineWidth', 1); 362 | plotmesh(curves.papr, 'k-', 'LineWidth', 1); 363 | 364 | plotmesh(landmarks.paal, 'yo', 'LineWidth', 2); 365 | plotmesh(landmarks.papl, 'co', 'LineWidth', 2); 366 | plotmesh(landmarks.paar, 'yo', 'LineWidth', 2); 367 | plotmesh(landmarks.papr, 'co', 'LineWidth', 2); 368 | end 369 | end 370 | 371 | %% ------------------------------------------------------------------------------------- 372 | % helper functions 373 | % -------------------------------------------------------------------------------------- 374 | 375 | % the respond function when a data-cursor tip to popup 376 | 377 | function txt = myupdatefcn(empt, event_obj) 378 | pt = get(gcf, 'userdata'); 379 | pos = get(event_obj, 'Position'); 380 | % idx= get(event_obj,'DataIndex'); 381 | txt = {['x: ', num2str(pos(1))], ... 382 | ['y: ', num2str(pos(2))], ['z: ', num2str(pos(3))]}; 383 | if (~isempty(pt) && ismember(pos, pt, 'rows')) 384 | return 385 | end 386 | targetup = get(get(event_obj, 'Target'), 'parent'); 387 | idx = size(pt, 1) + 2; 388 | landmarkname = {'Nasion', 'Inion', 'Left-pre-auricular-point', 'Right-pre-auricular-point', 'Vertex/Cz', 'Done'}; 389 | title(sprintf('Rotate the mesh, select data cursor, click on P%d: %s', idx, landmarkname{idx})); 390 | set(targetup, 'userdata', struct('pos', pos)); 391 | pt = [pt; pos]; 392 | if (size(pt, 1) < 6) 393 | set(gcf, 'userdata', pt); 394 | end 395 | hpt = findobj(gcf, 'type', 'line'); 396 | if (~isempty(hpt)) 397 | set(hpt, 'xdata', pt(:, 1), 'ydata', pt(:, 2), 'zdata', pt(:, 3)); 398 | else 399 | hold on; 400 | plotmesh([pt; pos], 'gs', 'LineWidth', 4); 401 | end 402 | disp(['Adding landmark ' landmarkname{idx - 1} ':' txt]); 403 | datacursormode(gcf, 'off'); 404 | rotate3d('on'); 405 | -------------------------------------------------------------------------------- /brain2mesh.m: -------------------------------------------------------------------------------- 1 | function [brain_n, brain_el, brain_f] = brain2mesh(seg, varargin) 2 | % 3 | % Brain2mesh: a one-liner for human brain 3D mesh generation 4 | % 5 | % Author: Qianqian Fang 6 | % Other contributors: see AUTHORS.txt for details 7 | % Version: 0.8 8 | % URL: http://mcx.space/brain2mesh 9 | % License: GPL version 3 10 | % Reference: 11 | % Anh Phong Tran, Shijie Yan and Qianqian Fang, "Improving model-based 12 | % fNIRS analysis using mesh-based anatomical and light-transport models," 13 | % Neurophotonics, 7(1), 015008, URL: http://dx.doi.org/10.1117/1.NPh.7.1.015008 14 | % 15 | % == Format == 16 | % [node,elem,face] = brain2mesh(seg) 17 | % or 18 | % [node,elem,face] = brain2mesh(seg,cfg); 19 | % 20 | % == Input == 21 | % seg: pre-segmented brain volume (supporting both probalistic tissue 22 | % segmentation and labeled volume). Two formats are accepted 23 | % 1. a structure with subfields (wm,gm,csf,skull,scalp) 24 | % e.g.: seg.wm, seg.gm, seg.csf represents the white-matter, 25 | % gray-matter and csf segmentaions, respectively, 26 | % or 27 | % 2. a 4D array for with tissued sorted in outer-to-inner order 28 | % the 4th dimension of the array can 3-6, with the following assumptions 29 | % size(seg,4) == 6 assumes 1-Scalp, 2-Skull, 3-CSF, 4-GM, 5-WM, 6-air pockets 30 | % size(seg,4) == 5 assumes 1-Scalp, 2-Skull, 3-CSF, 4-GM, 5-WM 31 | % size(seg,4) == 4 assumes 1-Scalp, 2-CSF, 3-GM, 4-WM 32 | % size(seg,4) == 3 assumes 1-CSF, 2-GM, 3-WM 33 | % 34 | % cfg: a struct defining the options for the resulting tetrahedral mesh 35 | % default values are applied if field is not defined 36 | % cfg.radbound.{wm,gm,csf,skull,scalp} 37 | % Radius of the Delaunay sphere used in the sampling the surfaces. 38 | % Default values are 1.7, 1.7, 2, 2.5 and 3, respectively (reference values for 1x1x1mm^3) 39 | % Scale proportionally for denser volumes. Lower values correspond to denser, higher 40 | % fidelity surface extraction, but also results in denser meshes. 41 | % cfg.maxnode: [100000] - when the value cfg.sampling__ creates surfaces that are too 42 | % dense. This limits the maximum of number of nodes extracted for a given surface. 43 | % cfg.maxvol: [100] indicates the volumetric maximum size of elements 44 | % Lowering this value helps with obtaining a denser tetrahedral 45 | % mesh. For dense meshes, values close to 3-5 are recommended. 46 | % cfg.smooth: [0] - number of iterations to smooth each tissue surface 47 | % cfg.ratio: [1.414] radius-edge ratio. Lower values increase 48 | % the quality of tetrahedral elements, but results in denser meshes 49 | % cfg.dorelabel: [0] or 1 - This step removes most of the assumptions 50 | % created by the layered meshing workflow. Currently only works if all five tissue types are present. 51 | % When deactivated, a 1 voxel length gap is assumed between each of the tissue layers. 52 | % cfg.doairseg: 0 or [1]. Within the skull layer, additional segmentations can be found. 53 | % By default, these regions are merged to the skull because they can be ambiguously be 54 | % vessels or air. When the option 1 is chosen, these regions are labeled as air instead. 55 | % cfg.dotruncate: ['-z' or 1], or one of +x/-x/+y/-y/+z/-z, 0 to 56 | % disable. by default, brain2mesh assumes the neck is 57 | % positioned at the -z direction of the volume, the mesh is 58 | % truncated by cfg.marginsize voxels in the -z direction below 59 | % the lowest pixel containing CSF to focus on the brain areas. 60 | % A value of 0 gives a complete head mesh. 61 | % cfg.marginsize: [4]. when dotruncate is set, this flag 62 | % determines how many voxels below the CSF mesh to truncate 63 | % towards the neck. 64 | % cfg.imfill: ['fillholes3d'], 'imfill', 'mri_fillholes' etc, the function name 65 | % for 3D image hole-filling function, default is fillholes3d; using imfill 66 | % requires MATLAB image processing toolbox or octave-image 67 | % toolbox 68 | % 69 | % == Outputs == 70 | % node: node coordinates of the tetrahedral mesh 71 | % elem: element list of the tetrahedral mesh / the last column denotes the boundary ID 72 | % face: mesh surface element list of the tetrahedral mesh 73 | % 74 | % Tissue ID for the outputs are as follow: 75 | % 0-Air/background, 1-Scalp, 2-Skull, 3-CSF, 4-GM, 5-WM, 6-air pockets 76 | % 77 | % == Reference == 78 | % If you use Brain2Mesh in your publication, the authors of this toolbox 79 | % apprecitate if you can cite our Neurophotonics paper listed above. 80 | % 81 | % 82 | % -- this function is part of brain2mesh toolbox (http://mcx.space/brain2mesh) 83 | % License: GPL v3 or later, see LICENSE.txt for details 84 | % 85 | 86 | %% Handling the inputs 87 | if nargin == 0 88 | help brain2mesh; 89 | return 90 | end 91 | 92 | if (~exist('v2m', 'file')) 93 | error('Missing dependency. You must download and addpath to Iso2Mesh Toolbox, URL: https://github.com/fangq/iso2mesh'); 94 | end 95 | if (~exist('intriangulation', 'file')) 96 | error('Missing dependency. You must download and addpath to intriangulation.m, URL: https://www.mathworks.com/matlabcentral/fileexchange/43381-intriangulation-vertices-faces-testp-heavytest'); 97 | end 98 | 99 | density = struct('wm', 2, 'gm', 2, 'csf', 5, 'skull', 4, 'scalp', 8); 100 | adaptiveness = struct('wm', 1, 'gm', 1, 'csf', 1, 'skull', 1, 'scalp', 1); 101 | cfg = varargin2struct(varargin{:}); 102 | 103 | radbound = jsonopt('radbound', density, cfg); 104 | distbound = jsonopt('distbound', adaptiveness, cfg); 105 | qratio = jsonopt('ratio', 1.414, cfg); 106 | % sizefield=jsonopt('sizefield',100,cfg); 107 | maxvol = jsonopt('maxvol', 100, cfg); 108 | maxnode = jsonopt('maxnode', 100000, cfg); 109 | dotruncate = jsonopt('dotruncate', 1, cfg); 110 | dorelabel = jsonopt('dorelabel', 0, cfg); 111 | doairseg = jsonopt('doairseg', 1, cfg); 112 | threshold = jsonopt('threshold', 0.5, cfg); 113 | smooth = jsonopt('smooth', 0, cfg); 114 | surfonly = jsonopt('surfonly', 0, cfg); 115 | marginsize = jsonopt('marginsize', 4, cfg); 116 | 117 | segname = fieldnames(density); 118 | 119 | imfillstr = jsonopt('imfill', 'fillholes3d', cfg); 120 | imfillparam = 'holes'; 121 | imfill3d = str2func(imfillstr); 122 | 123 | if isstruct(seg) 124 | tpm = seg; 125 | elseif (ndims(seg) == 4) 126 | for i = 1:size(seg, 4) 127 | tpm.(segname{i}) = seg(:, :, :, i); 128 | end 129 | else 130 | fprintf('This seg input is currently not supported \n'); 131 | end 132 | 133 | % normalizing segmentation inputs to 0-1 134 | normalizer = @(x) double(x) * (~isinteger(x) + (isinteger(x)) * (1 / double(max(x(:))))); 135 | tpm = structfun(normalizer, tpm, 'UniformOutput', false); 136 | 137 | opt = struct; 138 | 139 | for i = 1:length(fieldnames(tpm)) 140 | opt(i).maxnode = maxnode; 141 | if (isfield(radbound, segname{i})) 142 | opt(i).radbound = radbound.(segname{i}); 143 | end 144 | opt(i).distbound = distbound.(segname{i}); 145 | end 146 | 147 | cube3 = true(3, 3, 3); 148 | 149 | %% Pre-processing steps to create separations between the tissues in the 150 | % volume space 151 | 152 | dim = size(tpm.wm); 153 | tpm.wm = imfill3d(tpm.wm > 0, imfillparam); 154 | p_wm = tpm.wm; 155 | p_pial = p_wm + tpm.gm; 156 | p_pial = max(p_pial, volgrow(p_wm, 1, cube3)); 157 | p_pial = imfill3d(p_pial > 0, imfillparam); 158 | expandedGM = p_pial - tpm.wm - tpm.gm; 159 | expandedGM = volgrow(expandedGM, 1, cube3); 160 | 161 | if (isfield(tpm, 'csf')) 162 | p_csf = p_pial + tpm.csf; 163 | p_csf(p_csf > 1) = 1; 164 | p_csf = max(p_csf, volgrow(p_pial, 1, cube3)); 165 | expandedCSF = p_csf - tpm.wm - tpm.gm - tpm.csf - expandedGM; 166 | expandedCSF = volgrow(expandedCSF, 1, cube3); 167 | end 168 | 169 | if isfield(tpm, 'skull') && isfield(tpm, 'scalp') && isfield(tpm, 'csf') 170 | p_bone = p_csf + tpm.skull; 171 | p_bone(p_bone > 1) = 1; 172 | p_bone = max(p_bone, volgrow(p_csf, 1, cube3)); 173 | p_skin = p_bone + tpm.scalp; 174 | p_skin(p_skin > 1) = 1; 175 | p_skin = max(p_skin, volgrow(p_bone, 1, cube3)); 176 | expandedSkull = p_bone - tpm.wm - tpm.gm - tpm.csf - tpm.skull - expandedCSF - expandedGM; 177 | expandedSkull = volgrow(expandedSkull, 1, cube3); 178 | elseif isfield(tpm, 'scalp') && ~isfield(tpm, 'skull') 179 | p_skin = p_csf + tpm.scalp; 180 | p_skin(p_skin > 1) = 1; 181 | p_skin = max(p_skin, volgrow(p_csf, 1, cube3)); 182 | elseif isfield(tpm, 'skull') && ~isfield(tpm, 'scalp') 183 | p_bone = p_csf + tpm.skull; 184 | p_bone(p_bone > 1) = 1; 185 | p_bone = max(p_bone, volgrow(p_csf, 1, cube3)); 186 | end 187 | 188 | %% Grayscale/Binary extractions of the surface meshes for the different 189 | % tissues 190 | 191 | thresh = 0.5; 192 | if (~isstruct(threshold)) 193 | thresh = threshold; 194 | end 195 | [wm_n, wm_f] = v2s(p_wm, jsonopt('wm', thresh, threshold), opt(1), 'cgalsurf'); 196 | [pial_n, pial_f] = v2s(p_pial, jsonopt('gm', thresh, threshold), opt(2), 'cgalsurf'); 197 | [wm_n, wm_f] = meshcheckrepair(wm_n, wm_f(:, 1:3), 'isolated'); 198 | [pial_n, pial_f] = meshcheckrepair(pial_n, pial_f(:, 1:3), 'isolated'); 199 | 200 | if (isfield(tpm, 'csf')) 201 | [csf_n, csf_f] = v2s(p_csf, jsonopt('csf', thresh, threshold), opt(3), 'cgalsurf'); 202 | [csf_n, csf_f] = meshcheckrepair(csf_n, csf_f(:, 1:3), 'isolated'); 203 | end 204 | 205 | if isfield(tpm, 'skull') 206 | optskull = struct('radbound', radbound.skull, 'maxnode', maxnode); 207 | [bone_n, bone_f] = v2s(p_bone, jsonopt('skull', thresh, threshold), optskull, 'cgalsurf'); 208 | 209 | [bone_node, el_bone] = s2m(bone_n, bone_f, 1.0, maxvol, 'tetgen1.5', [], [], '-A'); 210 | for i = 1:length(unique(el_bone(:, 5))) 211 | vol_bone(i) = sum(elemvolume(bone_node, el_bone(el_bone(:, 5) == i, 1:4))); 212 | end 213 | [maxval, I] = max(vol_bone); 214 | if (length(unique(el_bone(:, 5))) > 1) 215 | no_air2 = bone_node; 216 | el_air2 = el_bone(el_bone(:, 5) ~= I, :); 217 | [no_air2, el_air2] = removeisolatednode(no_air2, el_air2); 218 | f_air2 = volface(el_air2(:, 1:4)); 219 | end 220 | bone_n2 = bone_node; 221 | [bone_f2] = volface(el_bone(:, 1:4)); 222 | bone_f2 = removedupelem(bone_f2); 223 | [bone_n2, bone_f2] = removeisolatednode(bone_n2, bone_f2); 224 | if doairseg == 0 225 | bone_n = bone_n2; 226 | bone_f = bone_f2; 227 | end 228 | end 229 | if isfield(tpm, 'scalp') 230 | optscalp = struct('radbound', radbound.scalp, 'maxnode', maxnode); 231 | [skin_n, skin_f] = v2s(p_skin, jsonopt('scalp', thresh, threshold), optscalp, 'cgalsurf'); 232 | end 233 | if (isstruct(smooth) || smooth > 0) 234 | scount = 0; 235 | if (~isstruct(smooth)) 236 | scount = smooth; 237 | end 238 | if (jsonopt('wm', scount, smooth) > 0) 239 | wm_n = sms(wm_n, wm_f(:, 1:3), jsonopt('wm', scount, smooth), 0.5, 'lowpass'); 240 | [wm_n, wm_f] = meshcheckrepair(wm_n, wm_f(:, 1:3), 'meshfix'); 241 | end 242 | if (jsonopt('gm', scount, smooth) > 0) 243 | pial_n = sms(pial_n, pial_f(:, 1:3), jsonopt('gm', scount, smooth), 0.5, 'lowpass'); 244 | [pial_n, pial_f] = meshcheckrepair(pial_n, pial_f(:, 1:3), 'meshfix'); 245 | end 246 | if (isfield(tpm, 'csf') && jsonopt('csf', scount, smooth) > 0) 247 | csf_n = sms(csf_n, csf_f(:, 1:3), jsonopt('csf', scount, smooth), 0.5, 'lowpass'); 248 | [csf_n, csf_f] = meshcheckrepair(csf_n, csf_f(:, 1:3), 'meshfix'); 249 | end 250 | if (isfield(tpm, 'skull') && jsonopt('skull', scount, smooth) > 0) 251 | bone_n = sms(bone_n, bone_f(:, 1:3), jsonopt('skull', scount, smooth), 0.5, 'lowpass'); 252 | [bone_n, bone_f] = meshcheckrepair(bone_n, bone_f(:, 1:3), 'meshfix'); 253 | end 254 | if (isfield(tpm, 'scalp') && jsonopt('scalp', scount, smooth) > 0) 255 | skin_n = sms(skin_n, skin_f(:, 1:3), jsonopt('scalp', scount, smooth), 0.5, 'lowpass'); 256 | [skin_n, skin_f] = meshcheckrepair(skin_n, skin_f(:, 1:3), 'meshfix'); 257 | end 258 | end 259 | 260 | if (surfonly == 1) 261 | wm_f(:, 4) = 1; 262 | pial_f(:, 4) = 2; 263 | [brain_n, brain_el] = mergemesh(wm_n, wm_f, pial_n, pial_f); 264 | if (isfield(tpm, 'csf')) 265 | csf_f(:, 4) = 3; 266 | [brain_n, brain_el] = mergemesh(brain_n, brain_el, csf_n, csf_f); 267 | if (isfield(tpm, 'skull')) 268 | bone_f(:, 4) = 4; 269 | [brain_n, brain_el] = mergemesh(brain_n, brain_el, bone_n, bone_f); 270 | if (isfield(tpm, 'scalp')) 271 | skin_f(:, 4) = 5; 272 | [brain_n, brain_el] = mergemesh(brain_n, brain_el, skin_n, skin_f); 273 | end 274 | end 275 | end 276 | [labels, ia, ib] = unique(brain_el(:, 4)); 277 | labels = 5:-1:(5 - length(labels) + 1); 278 | brain_el(:, 4) = labels(ib); 279 | brain_f = []; 280 | return 281 | end 282 | 283 | %% Main loop for the meshing pipeline to combine the individual surface 284 | % meshes or each of the tissues and to generate the detailed 3D tetrahedral 285 | % mesh of the brain/head 286 | 287 | for loop = 1:2 288 | %% If the first pass fails, a second pass is called using the decoupled function 289 | % to eliminate intersections between surface meshes 290 | if (loop == 2) && (exist('label_elem', 'var')) 291 | continue 292 | end 293 | if (loop == 2) && (~exist('label_elem', 'var')) 294 | if (exist('bone_n', 'var') && exist('skin_n', 'var')) 295 | [bone_n, bone_f] = surfboolean(bone_n(:, 1:3), bone_f(:, 1:3), 'decouple', skin_n(:, 1:3), skin_f(:, 1:3)); 296 | end 297 | if (exist('bone_n', 'var') && exist('csf_n', 'var')) 298 | [csf_n, csf_f] = surfboolean(csf_n(:, 1:3), csf_f(:, 1:3), 'decouple', bone_n(:, 1:3), bone_f(:, 1:3)); 299 | end 300 | if (exist('pial_n', 'var') && exist('csf_n', 'var')) 301 | [pial_n, pial_f] = surfboolean(pial_n(:, 1:3), pial_f(:, 1:3), 'decouple', csf_n(:, 1:3), csf_f(:, 1:3)); 302 | end 303 | if (exist('pial_n', 'var') && exist('wm_n', 'var')) 304 | [wm_n, wm_f] = surfboolean(wm_n(:, 1:3), wm_f(:, 1:3), 'decouple', pial_n(:, 1:3), pial_f(:, 1:3)); 305 | end 306 | end 307 | if isfield(tpm, 'wm') && isfield(tpm, 'gm') 308 | [surf_n, surf_f] = surfboolean(wm_n(:, 1:3), wm_f(:, 1:3), 'resolve', pial_n, pial_f); 309 | end 310 | if isfield(tpm, 'csf') 311 | [surf_n, surf_f] = surfboolean(surf_n, surf_f, 'resolve', csf_n, csf_f); 312 | end 313 | if isfield(tpm, 'skull') 314 | [surf_n, surf_f] = surfboolean(surf_n, surf_f, 'resolve', bone_n, bone_f); 315 | end 316 | if isfield(tpm, 'scalp') 317 | [surf_n, surf_f] = surfboolean(surf_n, surf_f, 'resolve', skin_n, skin_f); 318 | end 319 | final_surf_n = surf_n; 320 | final_surf_f = surf_f; 321 | if (surfonly == 2) 322 | brain_n = final_surf_n; 323 | brain_el = final_surf_f; 324 | brain_f = []; 325 | return 326 | end 327 | %% If the whole head option is deactivated, the cut is made at the base of the brain using a box cutting 328 | if (dotruncate == 1 || ischar(dotruncate)) 329 | dim = max(surf_n); 330 | if isfield(tpm, 'csf') 331 | dim2 = min(csf_n); 332 | else 333 | dim2 = min(surf_n); 334 | end 335 | if (dotruncate == 1 || strcmp(dotruncate, '-z')) 336 | [nbox, fbox, ebox] = meshabox([-1 -1 dim2(3) + marginsize], [dim(1) + 1 dim(2) + 1 dim(3) + 1], 500); 337 | elseif (strcmp(dotruncate, '-y')) 338 | [nbox, fbox, ebox] = meshabox([-1 dim2(2) + marginsize -1], [dim(1) + 1 dim(2) + 1 dim(3) + 1], 500); 339 | elseif (strcmp(dotruncate, '-x')) 340 | [nbox, fbox, ebox] = meshabox([dim2(1) + marginsize -1 -1], [dim(1) + 1 dim(2) + 1 dim(3) + 1], 500); 341 | elseif (strcmp(dotruncate, '+z')) 342 | [nbox, fbox, ebox] = meshabox([-1 -1 -1], [dim(1) + 1 dim(2) + 1 dim2(3) - marginsize], 500); 343 | elseif (strcmp(dotruncate, '+y')) 344 | [nbox, fbox, ebox] = meshabox([-1 -1 -1], [dim(1) + 1 dim2(2) - marginsize dim(3) + 1], 500); 345 | elseif (strcmp(dotruncate, '+x')) 346 | [nbox, fbox, ebox] = meshabox([-1 -1 -1], [dim2(1) - marginsize dim(2) + 1 dim(3) + 1], 500); 347 | end 348 | fbox = volface(ebox); 349 | [nbox, fbox] = removeisolatednode(nbox, fbox); 350 | [final_surf_n, final_surf_f] = surfboolean(nbox, fbox, 'first', surf_n, surf_f); 351 | end 352 | if (surfonly == 3) 353 | brain_n = final_surf_n; 354 | brain_el = final_surf_f; 355 | brain_f = []; 356 | return 357 | end 358 | %% Generates a coarse tetrahedral mesh of the combined tissues 359 | try 360 | [final_n, final_e] = s2m(final_surf_n, final_surf_f, 1.0, maxvol, 'tetgen1.5', [], [], '-A'); 361 | catch 362 | fprintf('volumetric mesh generation failed, returning the intermediate surface model only'); 363 | brain_n = final_surf_n; 364 | brain_f = final_surf_f; 365 | brain_el = []; 366 | return 367 | end 368 | 369 | %% Removes the elements that are part of the box, but not the brain/head 370 | if (dotruncate == 1 || ischar(dotruncate)) 371 | [maxval, M] = max(final_n); 372 | k = find(final_e(:, 1:4) == M(3), 1); 373 | final_e = final_e(final_e(:, 5) ~= final_e(rem(k, length(final_e(:, 1))), 5), :); 374 | [final_n, final_e] = removeisolatednode(final_n, final_e); 375 | end 376 | 377 | %% Here the labels created through the coarse mesh generated through Tetgen are saved 378 | % with the centroid of one of the elements for intriangulation seg(:,:,:,1)testing later 379 | [label, label_elem] = unique(final_e(:, 5)); 380 | label_centroid = meshcentroid(final_n, final_e(label_elem, 1:4)); 381 | 382 | if isfield(tpm, 'scalp') 383 | [no_skin, el_skin] = s2m(skin_n, skin_f, 1.0, maxvol, 'tetgen1.5', [], [], '-A'); 384 | for i = 1:length(unique(el_skin(:, 5))) 385 | vol_skin(i) = sum(elemvolume(no_skin, el_skin(el_skin(:, 5) == i, 1:4))); 386 | end 387 | [maxval, I] = max(vol_skin); 388 | if (length(unique(el_skin(:, 5))) > 1) 389 | no_air = no_skin; 390 | el_air = el_skin(el_skin(:, 5) ~= I, :); 391 | [no_air, el_air] = removeisolatednode(no_air, el_air); 392 | f_air = volface(el_air(:, 1:4)); 393 | f_air = removedupelem(f_air); 394 | end 395 | el_skin = el_skin(el_skin(:, 5) == I, :); 396 | [no_skin, el_skin] = removeisolatednode(no_skin, el_skin); 397 | 398 | [f_skin] = volface(el_skin(:, 1:4)); 399 | f_skin = removedupelem(f_skin); 400 | end 401 | 402 | %% When the label_elem does not exist, it often indicates a failure at the generation of a coarse 403 | % tetrahedral mesh. The alternative meshing pathway using decoupling is then called to make a 404 | % second attempt at creating the combined tetrahedral mesh. 405 | if (~exist('label_elem')) && (loop == 1) 406 | fprintf('Initial meshing procedure failed. The option parameter might need to be adjusted. \n'); 407 | fprintf('Activating alternative meshing pathway... \n'); 408 | pause(2); 409 | continue 410 | end 411 | 412 | %% The labels are given to each of the tissues 413 | % WM(1) - GM(2) - CSF(3) - Bone(4) - Scalp(5) - Air(6) 414 | newlabel = zeros(length(label_elem), 1); 415 | if (exist('bone_n') && exist('no_air2')) 416 | newlabel = intriangulation(no_air2, f_air2(:, 1:3), label_centroid); 417 | end 418 | if (exist('no_skin') && exist('no_air')) 419 | newlabel = newlabel | intriangulation(no_air, f_air(:, 1:3), label_centroid); 420 | end 421 | newlabel = double(newlabel); 422 | idx = find(newlabel == 0); 423 | 424 | newtag = zeros(length(idx), 1); 425 | newtag = intriangulation(wm_n, wm_f(:, 1:3), label_centroid(idx, :)) * 6; 426 | newtag = max(newtag, intriangulation(pial_n, pial_f(:, 1:3), label_centroid(idx, :)) * 5); 427 | if (exist('csf_n', 'var')) 428 | newtag = max(newtag, intriangulation(csf_n, csf_f(:, 1:3), label_centroid(idx, :)) * 4); 429 | end 430 | if (exist('bone_n2', 'var')) 431 | newtag = max(newtag, intriangulation(bone_n2, bone_f2(:, 1:3), label_centroid(idx, :)) * 3); 432 | end 433 | if (exist('no_skin', 'var')) 434 | newtag = max(newtag, intriangulation(no_skin, f_skin(:, 1:3), label_centroid(idx, :)) * 2); 435 | end 436 | newlabel(idx) = newtag; 437 | newlabel = 7 - newlabel; 438 | final_e(:, 5) = newlabel(final_e(:, 5)); 439 | 440 | %% This step consolidates adjacent labels of the same tissue 441 | new_label = unique(final_e(:, 5)); 442 | face = []; 443 | for i = 1:length(new_label) 444 | face = [face; volface(final_e(final_e(:, 5) == new_label(i), 1:4))]; 445 | end 446 | face = sort(face, 2); 447 | face = unique(face, 'rows'); 448 | [node, face] = removeisolatednode(final_n, face); 449 | 450 | %% The final mesh is generated here with the desired properties 451 | cmdopt = sprintf('-A -pq%fa%f', qratio, maxvol); 452 | % node(:,4) = sizefield(:).*ones(length(node(:,1)),1); 453 | [brain_n, brain_el] = s2m(node, face, 1.0, maxvol, 'tetgen1.5', [], [], cmdopt); 454 | 455 | [label2, label_brain_el] = unique(brain_el(:, 5)); 456 | label_centroid2 = meshcentroid(brain_n, brain_el(label_brain_el, 1:4)); 457 | 458 | %% The labeling process is repeated for the final mesh 459 | % WM(1) - GM(2) - CSF(3) - Bone(4) - Scalp(5) - Air(6) 460 | newlabel = zeros(length(label_brain_el), 1); 461 | if (exist('bone_n') && exist('no_air2')) 462 | newlabel = intriangulation(no_air2, f_air2(:, 1:3), label_centroid2); 463 | end 464 | if (exist('no_skin') && exist('no_air')) 465 | newlabel = newlabel | intriangulation(no_air, f_air(:, 1:3), label_centroid2); 466 | end 467 | newlabel = double(newlabel); 468 | idx = find(newlabel == 0); 469 | 470 | newtag = zeros(length(idx), 1); 471 | newtag = intriangulation(wm_n, wm_f(:, 1:3), label_centroid2(idx, :)) * 6; 472 | newtag = max(newtag, intriangulation(pial_n, pial_f(:, 1:3), label_centroid2(idx, :)) * 5); 473 | if (exist('csf_n', 'var')) 474 | newtag = max(newtag, intriangulation(csf_n, csf_f(:, 1:3), label_centroid2(idx, :)) * 4); 475 | end 476 | if (exist('bone_n2', 'var')) 477 | newtag = max(newtag, intriangulation(bone_n2, bone_f2(:, 1:3), label_centroid2(idx, :)) * 3); 478 | end 479 | if (exist('no_skin', 'var')) 480 | newtag = max(newtag, intriangulation(no_skin, f_skin(:, 1:3), label_centroid2(idx, :)) * 2); 481 | end 482 | newlabel(idx) = newtag; 483 | newlabel = 7 - newlabel; 484 | brain_el(:, 5) = newlabel(brain_el(:, 5)); 485 | end 486 | 487 | %% Relabeling step to remove layered assumptions 488 | if dorelabel == 1 && (isfield(tpm, 'skull') && isfield(tpm, 'scalp')) 489 | centroid = meshcentroid(brain_n(:, 1:3), brain_el(:, 1:4)); 490 | centroid = ceil(centroid); 491 | tag = zeros(length(brain_el(:, 1)), 1); 492 | facenb = faceneighbors(brain_el(:, 1:4)); 493 | for i = 1:length(brain_el(:, 1)) 494 | if (expandedGM(centroid(i, 1), centroid(i, 2), centroid(i, 3)) > 0.5) && (brain_el(i, 5) == 2) 495 | if tpm.scalp(centroid(i, 1), centroid(i, 2), centroid(i, 3)) > 0.5 496 | brain_el(i, 5) = 5; 497 | elseif tpm.skull(centroid(i, 1), centroid(i, 2), centroid(i, 3)) > 0.5 498 | brain_el(i, 5) = 4; 499 | else 500 | brain_el(i, 5) = 3; 501 | end 502 | tag(i) = 1; 503 | for j = 1:4 504 | if facenb(i, j) > 0 505 | tag(facenb(i, j), 1) = 1; 506 | end 507 | end 508 | elseif (expandedCSF(centroid(i, 1), centroid(i, 2), centroid(i, 3)) > 0.5) && (brain_el(i, 5) == 3) 509 | if tpm.scalp(centroid(i, 1), centroid(i, 2), centroid(i, 3)) > 0.5 510 | brain_el(i, 5) = 5; 511 | else 512 | brain_el(i, 5) = 4; 513 | end 514 | brain_el(i, 5) = 4; 515 | tag(i) = 1; 516 | for j = 1:4 517 | if facenb(i, j) > 0 518 | tag(facenb(i, j), 1) = 1; 519 | end 520 | end 521 | elseif (expandedSkull(centroid(i, 1), centroid(i, 2), centroid(i, 3)) > 0.5) && (brain_el(i, 5) == 4) 522 | brain_el(i, 5) = 5; 523 | tag(i) = 1; 524 | for j = 1:4 525 | if facenb(i, j) > 0 526 | tag(facenb(i, j), 1) = 1; 527 | end 528 | end 529 | end 530 | end 531 | 532 | labels = zeros(length(brain_el(:, 1)), 4); 533 | labels2 = zeros(length(brain_el(:, 1)), 6); 534 | for i = 1:length(brain_el(:, 1)) 535 | for j = 1:4 536 | if facenb(i, j) > 0 537 | labels(i, j) = brain_el(facenb(i, j), 5); 538 | labels2(i, labels(i, j)) = labels2(i, labels(i, j)) + 1; 539 | else 540 | labels(i, j) = 0; 541 | end 542 | end 543 | end 544 | 545 | [labels(:, 5), labels(:, 6)] = max(labels2, [], 2); 546 | for i = 1:length(brain_el(:, 1)) 547 | if tag(i) == 1 548 | if (labels(i, 5) > 2) && (brain_el(i, 5) ~= labels(i, 6)) 549 | brain_el(i, 5) = labels(i, 6); 550 | end 551 | end 552 | end 553 | end 554 | 555 | brain_el(:, 5) = 6 - brain_el(:, 5); 556 | 557 | if (nargout > 2) 558 | brain_f = layersurf(brain_el); 559 | end 560 | -------------------------------------------------------------------------------- /closestnode.m: -------------------------------------------------------------------------------- 1 | function [idx, dist] = closestnode(node, p) 2 | % 3 | % [idx, dist]=closestnode(node,p) 4 | % 5 | % Find the closest point in a node list and return its index 6 | 7 | % author: Qianqian Fang (q.fang at neu.edu) 8 | % 9 | % input: 10 | % node: each row is an N-D node coordinate 11 | % p: a given position in the same space 12 | % 13 | % output: 14 | % idx: the index of the position in the node list that has the shortest 15 | % Euclidean distance to the position p 16 | % dist: the distances between p and each node 17 | % 18 | % 19 | % -- this function is part of brain2mesh toolbox (http://mcx.space/brain2mesh) 20 | % License: GPL v3 or later, see LICENSE.txt for details 21 | % 22 | 23 | dd = node - repmat(p, size(node, 1), 1); 24 | [dist, idx] = min(sum(dd .* dd, 2)); 25 | -------------------------------------------------------------------------------- /examples/SPM_example_brain.m: -------------------------------------------------------------------------------- 1 | clear; 2 | %% Dependencies: brain2mesh, iso2mesh, and zmat (https://github.com/NeuroJSON/zmat) 3 | 4 | %% Reading in data from c1-c5 SPM segmentations using a 4D array 5 | 6 | % the SPM segmentation files are stored in the text-based JNIfTI format 7 | % (.jnii) as defined in the JNIfTI file specification 8 | % https://github.com/NeuroJSON/jnifti/ 9 | 10 | names = {'gm', 'wm', 'csf', 'skull', 'scalp', 'air'}; 11 | for i = 1:5 12 | A = loadjnifti(sprintf('jnii/c%iANTS19-5Years_head.jnii', i)); 13 | dim = size(A.NIFTIData); 14 | seg.(names{i}) = A.NIFTIData; 15 | end 16 | 17 | %% call brain2mesh to create multi-layered brain mesh; cfg.smooth performs 10-iter. surface smoothing 18 | cfg.dotruncate = 1; 19 | % cfg.smooth=8; % apply smoothing to all surfaces, use a struct to smooth each 20 | % cfg.radbound=struct('scalp',10,'skull',10,'csf',10,'gm',5,'wm',5); % set mesh density per layer 21 | tic; 22 | [node, elem, face] = brain2mesh(seg, cfg); 23 | toc; 24 | 25 | %% call brain1020 to create the 10-5 landmarks on the scalp 26 | initpoints = [ 27 | 87.4123 188.5120 93.7087 28 | 87.4360 5.9213 116.0523 29 | 21.1737 93.9561 84.9446 30 | 159.6440 89.8472 86.3139 31 | 91.2203 104.7490 213.7747]; 32 | 33 | headsurf = volface(elem(:, 1:4)); 34 | tic; 35 | [landmarks, curves] = brain1020(node, headsurf, initpoints, 10, 5, 'cztol', 1e-8, 'minangle', 0.75 * pi); 36 | toc; 37 | view([-0.6 1.5 0.6]); 38 | 39 | %% Plotting of the result 40 | plotmesh(node, elem(elem(:, 5) == 5, :), 'FaceColor', [1 1 1]); %% wm 41 | hold on; 42 | plotmesh(node, elem(elem(:, 5) == 4, :), 'x>78 | y<110', 'FaceColor', [0.35 0.35 0.35]); %% pial 43 | plotmesh(node, elem(elem(:, 5) == 3, :), 'x>78 | z<135', 'FaceColor', [0.2 0.6 1]); %% csf 44 | plotmesh(node, elem(elem(:, 5) == 2, :), 'x>90 | z<130', 'FaceColor', [1 1 0.9]); %% bone 45 | plotmesh(node, elem(elem(:, 5) == 1, :), 'x>100 | z<125', 'FaceColor', [1 0.8 0.7]); %% scalp 46 | view([-0.6 1.5 0.6]); 47 | -------------------------------------------------------------------------------- /examples/SPM_example_wholehead.m: -------------------------------------------------------------------------------- 1 | clear; 2 | %% Dependencies: brain2mesh, iso2mesh, and zmat (https://github.com/NeuroJSON/zmat) 3 | 4 | %% Reading in data from c1-c5 SPM segmentations 5 | 6 | % the SPM segmentation files are stored in the text-based JNIfTI format 7 | % (.jnii) as defined in the JNIfTI file specification 8 | % https://github.com/NeuroJSON/jnifti/ 9 | 10 | names = {'gm', 'wm', 'csf', 'skull', 'scalp', 'air'}; 11 | for i = 1:5 12 | A = loadjnifti(sprintf('jnii/c%iANTS40-44Years_head.jnii', i)); 13 | dim = size(A.NIFTIData); 14 | seg.(names{i}) = A.NIFTIData; 15 | end 16 | 17 | %% call brain2mesh to create multi-layered brain mesh; dotruncate cuts the mesh below brain 18 | 19 | cfg.smooth = 10; 20 | tic; 21 | [node, elem, face] = brain2mesh(seg, cfg); 22 | toc; 23 | 24 | %% call brain1020 to create the 10-5 landmarks on the scalp 25 | initpoints = [ 26 | 86.4888 191.1470 100.8055 27 | 82.8704 1.2961 114.1993 28 | 15.1882 93.7385 71.0191 29 | 158.4230 90.3180 77.2665 30 | 83.7306 102.2434 207.2162]; 31 | 32 | headsurf = volface(elem(:, 1:4)); 33 | tic; 34 | [landmarks, curves] = brain1020(node, headsurf, initpoints, 10, 10, 'cztol', 1e-8); 35 | toc; 36 | view([-0.6 1.5 0.6]); 37 | 38 | %% Plotting of the result 39 | figure; 40 | plotmesh(node, elem(elem(:, 5) == 5, :), 'FaceColor', [1 1 1], 'EdgeAlpha', 0.6); %% wm 41 | hold on; 42 | plotmesh(node, elem(elem(:, 5) == 4, :), 'x>78 | y<110', 'FaceColor', [0.35 0.35 0.35], 'EdgeAlpha', 0.6); %% pial 43 | plotmesh(node, elem(elem(:, 5) == 3, :), 'x>78 | z<135', 'FaceColor', [0.2 0.6 1], 'EdgeAlpha', 0.6); %% csf 44 | plotmesh(node, elem(elem(:, 5) == 2, :), 'x>90 | z<130', 'FaceColor', [1 1 0.9], 'EdgeAlpha', 0.6); %% bone 45 | plotmesh(node, elem(elem(:, 5) == 1, :), 'x>100 | z<125', 'FaceColor', [1 0.8 0.7], 'EdgeAlpha', 0.6); %% scalp 46 | view([-0.6 1.5 0.6]); 47 | -------------------------------------------------------------------------------- /examples/check_brain2mesh_dependency.m: -------------------------------------------------------------------------------- 1 | if (~exist('brain2mesh', 'file')) 2 | error('Missing dependency. You must download and addpath to brain2mesh.m, URL: https://github.com/fangq/brain2mesh'); 3 | end 4 | 5 | if (~exist('zmat', 'file')) 6 | error('Missing dependency. You must download and addpath to zmat.m, URL: https://github.com/fangq/zmat'); 7 | end 8 | 9 | if (~exist('nii2jnii', 'file')) 10 | error('Missing dependency. You must download and addpath to iso2mesh toolbox, URL: https://github.com/fangq/iso2mesh'); 11 | end 12 | 13 | disp('all dependencies are present. brain2mesh is ready to run'); 14 | -------------------------------------------------------------------------------- /intriangulation.m: -------------------------------------------------------------------------------- 1 | function in = intriangulation(vertices, faces, testp, heavytest) 2 | % intriangulation: Test points in 3d wether inside or outside a (closed) triangulation 3 | % usage: in = intriangulation(vertices,faces,testp,heavytest) 4 | % 5 | % arguments: (input) 6 | % vertices - points in 3d as matrix with three columns 7 | % 8 | % faces - description of triangles as matrix with three columns. 9 | % Each row contains three indices into the matrix of vertices 10 | % which gives the three cornerpoints of the triangle. 11 | % 12 | % testp - points in 3d as matrix with three columns 13 | % 14 | % heavytest - int n >= 0. Perform n additional randomized rotation tests. 15 | % 16 | % IMPORTANT: the set of vertices and faces has to form a watertight surface! 17 | % 18 | % arguments: (output) 19 | % in - a vector of length size(testp,1), containing 0 and 1. 20 | % in(nr) = 0: testp(nr,:) is outside the triangulation 21 | % in(nr) = 1: testp(nr,:) is inside the triangulation 22 | % in(nr) = -1: unable to decide for testp(nr,:) 23 | % 24 | % Thanks to Adam A for providing the FEX submission voxelise. The 25 | % algorithms of voxelise form the algorithmic kernel of intriangulation. 26 | % 27 | % Thanks to Sven to discussions about speed and avoiding problems in 28 | % special cases. 29 | % 30 | % Example usage: 31 | % 32 | % n = 10; 33 | % vertices = rand(n, 3)-0.5; % Generate random points 34 | % tetra = delaunayn(vertices); % Generate delaunay triangulization 35 | % faces = freeBoundary(TriRep(tetra,vertices)); % use free boundary as triangulation 36 | % n = 1000; 37 | % testp = 2*rand(n,3)-1; % Generate random testpoints 38 | % in = intriangulation(vertices,faces,testp); 39 | % % Plot results 40 | % h = trisurf(faces,vertices(:,1),vertices(:,2),vertices(:,3)); 41 | % set(h,'FaceColor','black','FaceAlpha',1/3,'EdgeColor','none'); 42 | % hold on; 43 | % plot3(testp(:,1),testp(:,2),testp(:,3),'b.'); 44 | % plot3(testp(in==1,1),testp(in==1,2),testp(in==1,3),'ro'); 45 | % 46 | % See also: intetrahedron, tsearchn, inpolygon 47 | % 48 | % Author: Johannes Korsawe, heavily based on voxelise from Adam A. 49 | % E-mail: johannes.korsawe@volkswagen.de 50 | % Release: 1.3 51 | % Release date: 25/09/2013 52 | 53 | % check number of inputs 54 | if nargin < 3 55 | fprintf('??? Error using ==> intriangulation\nThree input matrices are needed.\n'); 56 | in = []; 57 | return 58 | end 59 | if nargin == 3 60 | heavytest = 0; 61 | end 62 | % check size of inputs 63 | if size(vertices, 2) ~= 3 || size(faces, 2) ~= 3 || size(testp, 2) ~= 3 64 | fprintf('??? Error using ==> intriagulation\nAll input matrices must have three columns.\n'); 65 | in = []; 66 | return 67 | end 68 | ipmax = max(faces(:)); 69 | zerofound = ~isempty(find(faces(:) == 0, 1)); 70 | if ipmax > size(vertices, 1) || zerofound 71 | fprintf('??? Error using ==> intriangulation\nThe triangulation data is defect. use trisurf(faces,vertices(:,1),vertices(:,2),vertices(:,3)) for test of deficiency.\n'); 72 | return 73 | end 74 | 75 | % loop for heavytest 76 | inreturn = zeros(size(testp, 1), 1); 77 | VER = vertices; 78 | TESTP = testp; 79 | 80 | for n = 1:heavytest + 1 81 | 82 | % Randomize 83 | if n > 1 84 | v = rand(1, 3); 85 | D = rotmatrix(v / norm(v), rand * 180 / pi); 86 | vertices = VER * D; 87 | testp = TESTP * D; 88 | else 89 | vertices = VER; 90 | end 91 | 92 | % Preprocessing data 93 | meshXYZ = zeros(size(faces, 1), 3, 3); 94 | for loop = 1:3 95 | meshXYZ(:, :, loop) = vertices(faces(:, loop), :); 96 | end 97 | 98 | % Basic idea (ingenious from FeX-submission voxelise): 99 | % If point is inside, it will cross the triangulation an uneven number of times in each direction (x, -x, y, -y, z, -z). 100 | 101 | % The function VOXELISEinternal is about 98% identical to its version inside voxelise.m. 102 | % This includes the elaborate comments. Thanks to Adam A! 103 | 104 | % z-direction: 105 | % intialization of results and correction list 106 | [in, cl] = VOXELISEinternal(testp(:, 1), testp(:, 2), testp(:, 3), meshXYZ); 107 | 108 | % x-direction: 109 | % has only to be done for those points, that were not determinable in the first step --> cl 110 | [in2, cl2] = VOXELISEinternal(testp(cl, 2), testp(cl, 3), testp(cl, 1), meshXYZ(:, [2, 3, 1], :)); 111 | % Use results of x-direction that determined "inside" 112 | in(cl(in2 == 1)) = 1; 113 | % remaining indices with unclear result 114 | cl = cl(cl2); 115 | 116 | % y-direction: 117 | % has only to be done for those points, that were not determinable in the first and second step --> cl 118 | [in3, cl3] = VOXELISEinternal(testp(cl, 3), testp(cl, 1), testp(cl, 2), meshXYZ(:, [3, 1, 2], :)); 119 | 120 | % Use results of y-direction that determined "inside" 121 | in(cl(in3 == 1)) = 1; 122 | % remaining indices with unclear result 123 | cl = cl(cl3); 124 | 125 | % mark those indices, where all three tests have failed 126 | in(cl) = -1; 127 | 128 | if n == 1 129 | inreturn = in; % Starting guess 130 | else 131 | % if ALWAYS inside, use as inside! 132 | % I = find(inreturn ~= in); 133 | % inreturn(I(in(I)==0)) = 0; 134 | 135 | % if AT LEAST ONCE inside, use as inside! 136 | I = find(inreturn ~= in); 137 | inreturn(I(in(I) == 1)) = 1; 138 | 139 | end 140 | 141 | end 142 | 143 | in = inreturn; 144 | 145 | end 146 | 147 | % ========================================================================== 148 | function [OUTPUT, correctionLIST] = VOXELISEinternal(testx, testy, testz, meshXYZ) 149 | 150 | % Prepare logical array to hold the logical data: 151 | OUTPUT = zeros(size(testx, 1), 1); 152 | 153 | % Identify the min and max x,y coordinates of the mesh: 154 | meshZmin = min(min(meshXYZ(:, 3, :))); 155 | meshZmax = max(max(meshXYZ(:, 3, :))); 156 | 157 | % Identify the min and max x,y,z coordinates of each facet: 158 | meshXYZmin = min(meshXYZ, [], 3); 159 | meshXYZmax = max(meshXYZ, [], 3); 160 | 161 | % ====================================================== 162 | % TURN OFF DIVIDE-BY-ZERO WARNINGS 163 | % ====================================================== 164 | % This prevents the Y1predicted, Y2predicted, Y3predicted and YRpredicted 165 | % calculations creating divide-by-zero warnings. Suppressing these warnings 166 | % doesn't affect the code, because only the sign of the result is important. 167 | % That is, 'Inf' and '-Inf' results are ok. 168 | % The warning will be returned to its original state at the end of the code. 169 | warningrestorestate = warning('query', 'MATLAB:divideByZero'); 170 | % warning off MATLAB:divideByZero 171 | 172 | % ====================================================== 173 | % START COMPUTATION 174 | % ====================================================== 175 | 176 | correctionLIST = []; % Prepare to record all rays that fail the voxelisation. This array is built on-the-fly, but since 177 | % it ought to be relatively small should not incur too much of a speed penalty. 178 | 179 | % Loop through each testpoint. 180 | % The testpoint-array will be tested by passing rays in the z-direction through 181 | % each x,y coordinate of the testpoints, and finding the locations where the rays cross the mesh. 182 | facetCROSSLIST = zeros(1, 1e3); % uses countindex: nf 183 | nm = size(meshXYZmin, 1); 184 | for loop = 1:length(OUTPUT) 185 | 186 | nf = 0; 187 | % % - 1a - Find which mesh facets could possibly be crossed by the ray: 188 | % possibleCROSSLISTy = find( meshXYZmin(:,2)<=testy(loop) & meshXYZmax(:,2)>=testy(loop) ); 189 | 190 | % % - 1b - Find which mesh facets could possibly be crossed by the ray: 191 | % possibleCROSSLIST = possibleCROSSLISTy( meshXYZmin(possibleCROSSLISTy,1)<=testx(loop) & meshXYZmax(possibleCROSSLISTy,1)>=testx(loop) ); 192 | 193 | % Do - 1a - and - 1b - faster 194 | possibleCROSSLISTy = find((testy(loop) - meshXYZmin(:, 2)) .* (meshXYZmax(:, 2) - testy(loop)) > 0); 195 | possibleCROSSLISTx = (testx(loop) - meshXYZmin(possibleCROSSLISTy, 1)) .* (meshXYZmax(possibleCROSSLISTy, 1) - testx(loop)) > 0; 196 | possibleCROSSLIST = possibleCROSSLISTy(possibleCROSSLISTx); 197 | 198 | if isempty(possibleCROSSLIST) == 0 % Only continue the analysis if some nearby facets were actually identified 199 | 200 | % - 2 - For each facet, check if the ray really does cross the facet rather than just passing it close-by: 201 | 202 | % GENERAL METHOD: 203 | % 1. Take each edge of the facet in turn. 204 | % 2. Find the position of the opposing vertex to that edge. 205 | % 3. Find the position of the ray relative to that edge. 206 | % 4. Check if ray is on the same side of the edge as the opposing vertex. 207 | % 5. If this is true for all three edges, then the ray definitely passes through the facet. 208 | % 209 | % NOTES: 210 | % 1. If the ray crosses exactly on an edge, this is counted as crossing the facet. 211 | % 2. If a ray crosses exactly on a vertex, this is also taken into account. 212 | 213 | for loopCHECKFACET = possibleCROSSLIST' 214 | 215 | % Check if ray crosses the facet. This method is much (>>10 times) faster than using the built-in function 'inpolygon'. 216 | % Taking each edge of the facet in turn, check if the ray is on the same side as the opposing vertex. If so, let testVn=1 217 | 218 | Y1predicted = meshXYZ(loopCHECKFACET, 2, 2) - ((meshXYZ(loopCHECKFACET, 2, 2) - meshXYZ(loopCHECKFACET, 2, 3)) * (meshXYZ(loopCHECKFACET, 1, 2) - meshXYZ(loopCHECKFACET, 1, 1)) / (meshXYZ(loopCHECKFACET, 1, 2) - meshXYZ(loopCHECKFACET, 1, 3))); 219 | YRpredicted = meshXYZ(loopCHECKFACET, 2, 2) - ((meshXYZ(loopCHECKFACET, 2, 2) - meshXYZ(loopCHECKFACET, 2, 3)) * (meshXYZ(loopCHECKFACET, 1, 2) - testx(loop)) / (meshXYZ(loopCHECKFACET, 1, 2) - meshXYZ(loopCHECKFACET, 1, 3))); 220 | 221 | if (Y1predicted > meshXYZ(loopCHECKFACET, 2, 1) && YRpredicted > testy(loop)) || (Y1predicted < meshXYZ(loopCHECKFACET, 2, 1) && YRpredicted < testy(loop)) || (meshXYZ(loopCHECKFACET, 2, 2) - meshXYZ(loopCHECKFACET, 2, 3)) * (meshXYZ(loopCHECKFACET, 1, 2) - testx(loop)) == 0 222 | % testV1 = 1; %The ray is on the same side of the 2-3 edge as the 1st vertex. 223 | else 224 | % testV1 = 0; %The ray is on the opposite side of the 2-3 edge to the 1st vertex. 225 | % As the check is for ALL three checks to be true, we can continue here, if only one check fails 226 | continue 227 | end % if 228 | 229 | Y2predicted = meshXYZ(loopCHECKFACET, 2, 3) - ((meshXYZ(loopCHECKFACET, 2, 3) - meshXYZ(loopCHECKFACET, 2, 1)) * (meshXYZ(loopCHECKFACET, 1, 3) - meshXYZ(loopCHECKFACET, 1, 2)) / (meshXYZ(loopCHECKFACET, 1, 3) - meshXYZ(loopCHECKFACET, 1, 1))); 230 | YRpredicted = meshXYZ(loopCHECKFACET, 2, 3) - ((meshXYZ(loopCHECKFACET, 2, 3) - meshXYZ(loopCHECKFACET, 2, 1)) * (meshXYZ(loopCHECKFACET, 1, 3) - testx(loop)) / (meshXYZ(loopCHECKFACET, 1, 3) - meshXYZ(loopCHECKFACET, 1, 1))); 231 | if (Y2predicted > meshXYZ(loopCHECKFACET, 2, 2) && YRpredicted > testy(loop)) || (Y2predicted < meshXYZ(loopCHECKFACET, 2, 2) && YRpredicted < testy(loop)) || (meshXYZ(loopCHECKFACET, 2, 3) - meshXYZ(loopCHECKFACET, 2, 1)) * (meshXYZ(loopCHECKFACET, 1, 3) - testx(loop)) == 0 232 | % testV2 = 1; %The ray is on the same side of the 3-1 edge as the 2nd vertex. 233 | else 234 | % testV2 = 0; %The ray is on the opposite side of the 3-1 edge to the 2nd vertex. 235 | % As the check is for ALL three checks to be true, we can continue here, if only one check fails 236 | continue 237 | end % if 238 | 239 | Y3predicted = meshXYZ(loopCHECKFACET, 2, 1) - ((meshXYZ(loopCHECKFACET, 2, 1) - meshXYZ(loopCHECKFACET, 2, 2)) * (meshXYZ(loopCHECKFACET, 1, 1) - meshXYZ(loopCHECKFACET, 1, 3)) / (meshXYZ(loopCHECKFACET, 1, 1) - meshXYZ(loopCHECKFACET, 1, 2))); 240 | YRpredicted = meshXYZ(loopCHECKFACET, 2, 1) - ((meshXYZ(loopCHECKFACET, 2, 1) - meshXYZ(loopCHECKFACET, 2, 2)) * (meshXYZ(loopCHECKFACET, 1, 1) - testx(loop)) / (meshXYZ(loopCHECKFACET, 1, 1) - meshXYZ(loopCHECKFACET, 1, 2))); 241 | if (Y3predicted > meshXYZ(loopCHECKFACET, 2, 3) && YRpredicted > testy(loop)) || (Y3predicted < meshXYZ(loopCHECKFACET, 2, 3) && YRpredicted < testy(loop)) || (meshXYZ(loopCHECKFACET, 2, 1) - meshXYZ(loopCHECKFACET, 2, 2)) * (meshXYZ(loopCHECKFACET, 1, 1) - testx(loop)) == 0 242 | % testV3 = 1; %The ray is on the same side of the 1-2 edge as the 3rd vertex. 243 | else 244 | % testV3 = 0; %The ray is on the opposite side of the 1-2 edge to the 3rd vertex. 245 | % As the check is for ALL three checks to be true, we can continue here, if only one check fails 246 | continue 247 | end % if 248 | 249 | nf = nf + 1; 250 | facetCROSSLIST(nf) = loopCHECKFACET; 251 | 252 | end % for 253 | 254 | % Use only values ~=0 255 | facetCROSSLIST = facetCROSSLIST(1:nf); 256 | 257 | % - 3 - Find the z coordinate of the locations where the ray crosses each facet: 258 | gridCOzCROSS = zeros(1, nf); 259 | for loopFINDZ = facetCROSSLIST 260 | 261 | % METHOD: 262 | % 1. Define the equation describing the plane of the facet. For a 263 | % more detailed outline of the maths, see: 264 | % http://local.wasp.uwa.edu.au/~pbourke/geometry/planeeq/ 265 | % Ax + By + Cz + D = 0 266 | % where A = y1 (z2 - z3) + y2 (z3 - z1) + y3 (z1 - z2) 267 | % B = z1 (x2 - x3) + z2 (x3 - x1) + z3 (x1 - x2) 268 | % C = x1 (y2 - y3) + x2 (y3 - y1) + x3 (y1 - y2) 269 | % D = - x1 (y2 z3 - y3 z2) - x2 (y3 z1 - y1 z3) - x3 (y1 z2 - y2 z1) 270 | % 2. For the x and y coordinates of the ray, solve these equations to find the z coordinate in this plane. 271 | 272 | planecoA = meshXYZ(loopFINDZ, 2, 1) * (meshXYZ(loopFINDZ, 3, 2) - meshXYZ(loopFINDZ, 3, 3)) + meshXYZ(loopFINDZ, 2, 2) * (meshXYZ(loopFINDZ, 3, 3) - meshXYZ(loopFINDZ, 3, 1)) + meshXYZ(loopFINDZ, 2, 3) * (meshXYZ(loopFINDZ, 3, 1) - meshXYZ(loopFINDZ, 3, 2)); 273 | planecoB = meshXYZ(loopFINDZ, 3, 1) * (meshXYZ(loopFINDZ, 1, 2) - meshXYZ(loopFINDZ, 1, 3)) + meshXYZ(loopFINDZ, 3, 2) * (meshXYZ(loopFINDZ, 1, 3) - meshXYZ(loopFINDZ, 1, 1)) + meshXYZ(loopFINDZ, 3, 3) * (meshXYZ(loopFINDZ, 1, 1) - meshXYZ(loopFINDZ, 1, 2)); 274 | planecoC = meshXYZ(loopFINDZ, 1, 1) * (meshXYZ(loopFINDZ, 2, 2) - meshXYZ(loopFINDZ, 2, 3)) + meshXYZ(loopFINDZ, 1, 2) * (meshXYZ(loopFINDZ, 2, 3) - meshXYZ(loopFINDZ, 2, 1)) + meshXYZ(loopFINDZ, 1, 3) * (meshXYZ(loopFINDZ, 2, 1) - meshXYZ(loopFINDZ, 2, 2)); 275 | planecoD = -meshXYZ(loopFINDZ, 1, 1) * (meshXYZ(loopFINDZ, 2, 2) * meshXYZ(loopFINDZ, 3, 3) - meshXYZ(loopFINDZ, 2, 3) * meshXYZ(loopFINDZ, 3, 2)) - meshXYZ(loopFINDZ, 1, 2) * (meshXYZ(loopFINDZ, 2, 3) * meshXYZ(loopFINDZ, 3, 1) - meshXYZ(loopFINDZ, 2, 1) * meshXYZ(loopFINDZ, 3, 3)) - meshXYZ(loopFINDZ, 1, 3) * (meshXYZ(loopFINDZ, 2, 1) * meshXYZ(loopFINDZ, 3, 2) - meshXYZ(loopFINDZ, 2, 2) * meshXYZ(loopFINDZ, 3, 1)); 276 | 277 | if abs(planecoC) < 1e-14 278 | planecoC = 0; 279 | end 280 | 281 | gridCOzCROSS(facetCROSSLIST == loopFINDZ) = (-planecoD - planecoA * testx(loop) - planecoB * testy(loop)) / planecoC; 282 | 283 | end % for 284 | 285 | if isempty(gridCOzCROSS) 286 | continue 287 | end 288 | 289 | % Remove values of gridCOzCROSS which are outside of the mesh limits (including a 1e-12 margin for error). 290 | gridCOzCROSS = gridCOzCROSS(gridCOzCROSS >= meshZmin - 1e-12 & gridCOzCROSS <= meshZmax + 1e-12); 291 | 292 | % Round gridCOzCROSS to remove any rounding errors, and take only the unique values: 293 | gridCOzCROSS = round(gridCOzCROSS * 1e10) / 1e10; 294 | 295 | % Replacement of the call to unique (gridCOzCROSS = unique(gridCOzCROSS);) by the following line: 296 | tmp = sort(gridCOzCROSS); 297 | I = [0, tmp(2:end) - tmp(1:end - 1)] ~= 0; 298 | gridCOzCROSS = [tmp(1), tmp(I)]; 299 | 300 | % - 4 - Label as being inside the mesh all the voxels that the ray passes through after crossing one facet before crossing another facet: 301 | 302 | if rem(numel(gridCOzCROSS), 2) == 0 % Only rays which cross an even number of facets are voxelised 303 | 304 | for loopASSIGN = 1:(numel(gridCOzCROSS) / 2) 305 | voxelsINSIDE = (testz(loop) > gridCOzCROSS(2 * loopASSIGN - 1) & testz(loop) < gridCOzCROSS(2 * loopASSIGN)); 306 | OUTPUT(loop) = voxelsINSIDE; 307 | if voxelsINSIDE 308 | break 309 | end 310 | end % for 311 | 312 | elseif numel(gridCOzCROSS) ~= 0 % Remaining rays which meet the mesh in some way are not voxelised, but are labelled for correction later. 313 | correctionLIST = [correctionLIST; loop]; 314 | end % if 315 | 316 | end % if 317 | 318 | end % for 319 | 320 | % ====================================================== 321 | % RESTORE DIVIDE-BY-ZERO WARNINGS TO THE ORIGINAL STATE 322 | % ====================================================== 323 | 324 | warning(warningrestorestate); 325 | 326 | % J.Korsawe: A correction is not possible as the testpoints need not to be 327 | % ordered in any way. 328 | % voxelise contains a correction algorithm which is appended here 329 | % without changes in syntax. 330 | return 331 | 332 | % ====================================================== 333 | % USE INTERPOLATION TO FILL IN THE RAYS WHICH COULD NOT BE VOXELISED 334 | % ====================================================== 335 | % For rays where the voxelisation did not give a clear result, the ray is 336 | % computed by interpolating from the surrounding rays. 337 | countCORRECTIONLIST = size(correctionLIST, 1); 338 | 339 | if countCORRECTIONLIST > 0 340 | 341 | % If necessary, add a one-pixel border around the x and y edges of the 342 | % array. This prevents an error if the code tries to interpolate a ray at 343 | % the edge of the x,y grid. 344 | if min(correctionLIST(:, 1)) == 1 || max(correctionLIST(:, 1)) == numel(gridCOx) || min(correctionLIST(:, 2)) == 1 || max(correctionLIST(:, 2)) == numel(gridCOy) 345 | gridOUTPUT = [zeros(1, voxcountY + 2, voxcountZ); zeros(voxcountX, 1, voxcountZ), gridOUTPUT, zeros(voxcountX, 1, voxcountZ); zeros(1, voxcountY + 2, voxcountZ)]; 346 | correctionLIST = correctionLIST + 1; 347 | end 348 | 349 | for loopC = 1:countCORRECTIONLIST 350 | voxelsforcorrection = squeeze(sum([gridOUTPUT(correctionLIST(loopC, 1) - 1, correctionLIST(loopC, 2) - 1, :), ... 351 | gridOUTPUT(correctionLIST(loopC, 1) - 1, correctionLIST(loopC, 2), :), ... 352 | gridOUTPUT(correctionLIST(loopC, 1) - 1, correctionLIST(loopC, 2) + 1, :), ... 353 | gridOUTPUT(correctionLIST(loopC, 1), correctionLIST(loopC, 2) - 1, :), ... 354 | gridOUTPUT(correctionLIST(loopC, 1), correctionLIST(loopC, 2) + 1, :), ... 355 | gridOUTPUT(correctionLIST(loopC, 1) + 1, correctionLIST(loopC, 2) - 1, :), ... 356 | gridOUTPUT(correctionLIST(loopC, 1) + 1, correctionLIST(loopC, 2), :), ... 357 | gridOUTPUT(correctionLIST(loopC, 1) + 1, correctionLIST(loopC, 2) + 1, :) ... 358 | ])); 359 | voxelsforcorrection = (voxelsforcorrection >= 4); 360 | gridOUTPUT(correctionLIST(loopC, 1), correctionLIST(loopC, 2), voxelsforcorrection) = 1; 361 | end % for 362 | 363 | % Remove the one-pixel border surrounding the array, if this was added 364 | % previously. 365 | if size(gridOUTPUT, 1) > numel(gridCOx) || size(gridOUTPUT, 2) > numel(gridCOy) 366 | gridOUTPUT = gridOUTPUT(2:end - 1, 2:end - 1, :); 367 | end 368 | 369 | end % if 370 | 371 | % disp([' Ray tracing result: ',num2str(countCORRECTIONLIST),' rays (',num2str(countCORRECTIONLIST/(voxcountX*voxcountY)*100,'%5.1f'),'% of all rays) exactly crossed a facet edge and had to be computed by interpolation.']) 372 | 373 | end % function 374 | 375 | % ========================================================================== 376 | 377 | function D = rotmatrix(v, deg) 378 | % calculate the rotation matrix about v by deg degrees 379 | 380 | deg = deg / 180 * pi; 381 | if deg ~= 0 382 | v = v / norm(v); 383 | v1 = v(1); 384 | v2 = v(2); 385 | v3 = v(3); 386 | ca = cos(deg); 387 | sa = sin(deg); 388 | D = [ca + v1 * v1 * (1 - ca), v1 * v2 * (1 - ca) - v3 * sa, v1 * v3 * (1 - ca) + v2 * sa 389 | v2 * v1 * (1 - ca) + v3 * sa, ca + v2 * v2 * (1 - ca), v2 * v3 * (1 - ca) - v1 * sa 390 | v3 * v1 * (1 - ca) - v2 * sa, v3 * v2 * (1 - ca) + v1 * sa, ca + v3 * v3 * (1 - ca)]; 391 | else 392 | D = eye(3, 3); 393 | end 394 | 395 | end 396 | -------------------------------------------------------------------------------- /label2tpm.m: -------------------------------------------------------------------------------- 1 | function seg = label2tpm(vol, names) 2 | % 3 | % seg=label2tpm(vol) 4 | % or 5 | % seg=label2tpm(vol, names) 6 | % 7 | % converting a multi-lable volume to binary tissue probablistic maps (TPMs) 8 | % 9 | % author: Qianqian Fang (q.fang at neu.edu) 10 | % 11 | % input: 12 | % vol: a 2-D or 3-D array of integer values. 13 | % names (optional): a cell array of strings defining the names of each label, 14 | % alternatively, a containers.Map object with label (integer) as 15 | % the key and name as the value. The names can be a subset of all 16 | % labels. 17 | % 18 | % output: 19 | % seg: a struct with subfields of 3D or 4D uint8 array; the subfield 20 | % names are defined via the optional names input; the unnamed 21 | % labels will be named as 'label_#'. 22 | % 23 | % -- this function is part of brain2mesh toolbox (http://mcx.space/brain2mesh) 24 | % License: GPL v3 or later, see LICENSE.txt for details 25 | % 26 | 27 | if (~isnumeric(vol)) 28 | error('input must be a numerical array'); 29 | end 30 | 31 | val = setdiff(sort(unique(vol(:))), 0); 32 | if (length(val) > numel(vol) * (0.1^ndims(vol))) 33 | error('please convert the input to labels first'); 34 | end 35 | 36 | seg = struct; 37 | for i = 1:length(val) 38 | nm = sprintf('label_%d', val(i)); 39 | if (nargin > 1) 40 | if (iscell(names)) 41 | if (i <= length(names)) 42 | nm = names{i}; 43 | end 44 | elseif (isa(names, 'containers.Map') && isKey(names, val(i))) 45 | nm = names(val(i)); 46 | end 47 | end 48 | seg.(nm) = uint8(vol == val(i)); 49 | end 50 | -------------------------------------------------------------------------------- /layersurf.m: -------------------------------------------------------------------------------- 1 | function [face, labels] = layersurf(elem, varargin) 2 | % 3 | % face=layersurf(elem, opt) 4 | % or 5 | % [face,labels]=layersurf(elem,'option1',value1,'option2',value2,...) 6 | % 7 | % process a multi-layered tetrahedral mesh, like a brain mesh, to extract 8 | % the layer surface meshes with one enclosed by another 9 | % 10 | % author: Qianqian Fang (q.fang at neu.edu) 11 | % 12 | % input: 13 | % elem: an Nx5 integer array, representing the tetrahedral mesh element 14 | % list. The first 4 columns represent the tetrahedral element node 15 | % indices; the last column represents tissue labels. 16 | % opt: (optional) a struct or pairs of names/values to provide 17 | % additional options; accepted options include 18 | % 'order': ['>='] or '=', '<=' . if set to '>=' (default), the 19 | % outmost layer has the lowest label count; if '<=', innermost 20 | % is lowest; if '=', surface of each label is extracted 21 | % individually - meaning that inner surfaces will have two 22 | % duplicates 23 | % 'innermost': [0] or an array of labels. The labels defined in 24 | % this list will be treated as the innermost regions, and its 25 | % boundary will be extracted using the '==' test (order='='). 26 | % by default; label 0 is assumed to be innermost (i.e. nothing 27 | % is enclosed inside). 28 | % 'unique': [0] or 1. if 1, remove duplicated triangles; if 0, keep 29 | % all triangles. 30 | % 'occurence': ['first'] or 'last'. if 'first', the unique operator 31 | % keeps a duplicated triangle with the lowest label number; 32 | % otherwise, a repeated triange keeps the highest label number. 33 | % 34 | % output: 35 | % vol: a 2-D or 3-D array of the same type/size of the input arrays. The 36 | % label for each voxel is determined by the index to the highest 37 | % value in TPM of the same voxel. If a voxel is a background voxel 38 | % - i.e. zeros for all TPMs, it stays 0 39 | % names: a cell array storing the names of the labels (if input is a 40 | % struct), the first string is the name for label 1, and so on 41 | % 42 | % -- this function is part of brain2mesh toolbox (http://mcx.space/brain2mesh) 43 | % License: GPL v3 or later, see LICENSE.txt for details 44 | % 45 | 46 | opt = varargin2struct(varargin{:}); 47 | outsideislower = jsonopt('order', '>=', opt); 48 | dounique = jsonopt('unique', false, opt); 49 | innermost = jsonopt('innermost', 0, opt); 50 | occurence = jsonopt('occurence', 'first', opt); 51 | 52 | labels = sort(unique(elem(:, 5))); 53 | face = []; 54 | for i = 1:length(labels) 55 | if (strcmp(outsideislower, '>=') && ~ismember(labels(i), innermost)) 56 | newface = volface(elem(elem(:, 5) >= labels(i), 1:4)); 57 | elseif (strcmp(outsideislower, '<=') && ~ismember(labels(i), innermost)) 58 | newface = volface(elem(elem(:, 5) <= labels(i), 1:4)); 59 | else 60 | newface = volface(elem(elem(:, 5) == labels(i), 1:4)); 61 | end 62 | newface(:, 4) = labels(i); 63 | face = [face; newface]; 64 | end 65 | 66 | if (dounique) 67 | face(:, 1:3) = sort(face(:, 1:3), 2); 68 | [uniqface, idx] = unique(face(:, 1:3), 'rows', occurence); 69 | face = [uniqface, face(idx, end)]; 70 | end 71 | -------------------------------------------------------------------------------- /polylineinterp.m: -------------------------------------------------------------------------------- 1 | function [idx, weight, newnodes] = polylineinterp(polylen, len, nodes) 2 | % 3 | % [idx, weight]=polylineinterp(polylen, len) 4 | % [idx, weight, newnodes]=polylineinterp(polylen, len, nodes) 5 | % 6 | % Find the polyline segment indices and interpolation weights for a 7 | % specified total length or a set of lengths 8 | % 9 | % author: Qianqian Fang (q.fang at neu.edu) 10 | % 11 | % input: 12 | % polylen: a 1D vector sequentially recording the length of each segment 13 | % of a polyline, the first number is the length of the 1st segment, 14 | % and so on 15 | % len: a single scalar, or a vector of scalars, specifying the total 16 | % length 17 | % nodes: if nodes is an array with a row-number equal to length(polylen)+1, 18 | % we assume each row defines a coordinate for the nodes along the 19 | % polyline 20 | % 21 | % output: 22 | % idx: the indices of the polyline segments, starting from 1, where each 23 | % length defined in len ends; if len> sum(polylen), nan is 24 | % returned; if len<0, the weight will be a negative value. 25 | % weight: the interpolation weight between 0-1 towards the end node 26 | % of the containing segment; the weight for the start-node is 1-weight 27 | % newnodes: the interpolated node positions at the end of the len 28 | % 29 | % example: 30 | % lineseg=[2,2,1,7,10]; 31 | % [idx, weight]=polylineinterp(lineseg, [3, 12, 7]) 32 | % 33 | % -- this function is part of brain2mesh toolbox (http://mcx.space/brain2mesh) 34 | % License: GPL v3 or later, see LICENSE.txt for details 35 | % 36 | 37 | cumlen = [0 cumsum(polylen(:)')]; 38 | idx = nan * ones(size(len)); 39 | weight = zeros(size(len)); 40 | 41 | if (nargin >= 3 && nargout >= 3) 42 | if (size(nodes, 1) == 1) 43 | nodes = nodes.'; 44 | end 45 | if (size(nodes, 1) ~= length(polylen) + 1) 46 | error('the row number of the nodes input must be 1 more than the length of polylen'); 47 | end 48 | newnodes = zeros(length(len), size(nodes, 2)); 49 | end 50 | 51 | for i = 1:length(len) 52 | pos = histc(len(i), cumlen); 53 | if (any(pos == 1)) 54 | idx(i) = find(pos); 55 | if (idx(i) == length(cumlen)) 56 | idx(i) = idx(i) - 1; 57 | weight(i) = 1; 58 | newnodes(i, :) = nodes(end, :); 59 | elseif (idx(i) <= length(polylen)) 60 | weight(i) = (len(i) - cumlen(idx(i))) / polylen(idx(i)); 61 | if (nargin >= 3 && nargout >= 3) 62 | newnodes(i, :) = (1 - weight(i)) * nodes(idx(i), :) + weight(i) * nodes(idx(i) + 1, :); 63 | end 64 | end 65 | end 66 | end 67 | 68 | idx(idx > length(polylen)) = nan; 69 | -------------------------------------------------------------------------------- /polylinelen.m: -------------------------------------------------------------------------------- 1 | function [len, node, inputreversed] = polylinelen(node, p0, p1, pmid) 2 | % 3 | % [len, node, inputreversed]=polylinelen(node, p0, p1, pmid) 4 | % 5 | % Calculate the polyline line segment length vector in sequential order 6 | % 7 | % author: Qianqian Fang (q.fang at neu.edu) 8 | % 9 | % input: 10 | % node: an N x 3 array defining each vertex of the polyline in 11 | % sequential order 12 | % p0:(optional) a given node to define the start of the polyline, if not 13 | % defined, start position is assumed to be 1st node 14 | % p1:(optional) a given node to define the end of the polyline, if not 15 | % defined, end position is assumed to be last node 16 | % pmid:(optional) a given node sits between p0 and p1, if not 17 | % defined, index of the middle (floored) node is used 18 | % 19 | % output: 20 | % len: the length of each segment between the start and the end points 21 | % node: the node list between the start and end points of the polyline 22 | % inputreversed: if 1, the input node is reversed from p0 to pmid to p1 23 | % 24 | % 25 | % -- this function is part of brain2mesh toolbox (http://mcx.space/brain2mesh) 26 | % License: GPL v3 or later, see LICENSE.txt for details 27 | % 28 | 29 | if (nargin < 3) 30 | p1 = size(node, 1); 31 | if (nargin < 2) 32 | p0 = 1; 33 | end 34 | end 35 | 36 | if (nargin < 4) 37 | pmid = floor((p0 + p1) * 0.5); 38 | end 39 | 40 | if (size(p0, 2) == 3) 41 | p0 = closestnode(node, p0); 42 | end 43 | if (size(p1, 2) == 3) 44 | p1 = closestnode(node, p1); 45 | end 46 | if (size(pmid, 2) == 3) 47 | pmid = closestnode(node, pmid); 48 | end 49 | 50 | if (p0 < pmid && pmid < p1) 51 | inputreversed = 0; 52 | node = node(p0:p1, :); 53 | elseif (p0 < pmid && p1 < pmid) 54 | inputreversed = (min(p0, p1) == p0); 55 | node = node([min(p0, p1):-1:1 end:-1:max(p0, p1)], :); 56 | if (~inputreversed) 57 | node = flipud(node); 58 | end 59 | elseif (p0 > pmid && pmid > p1) 60 | inputreversed = 1; 61 | node = node(p0:-1:p1, :); 62 | elseif (p0 > pmid && p1 > pmid) 63 | inputreversed = (max(p0, p1) == p1); 64 | node = node([max(p0, p1):end 1:min(p0, p1)], :); 65 | if (inputreversed) 66 | node = flipud(node); 67 | end 68 | end 69 | 70 | len = node(1:end - 1, :) - node(2:end, :); 71 | len = sqrt(sum(len .* len, 2)); 72 | -------------------------------------------------------------------------------- /polylinesimplify.m: -------------------------------------------------------------------------------- 1 | function [newnodes, len] = polylinesimplify(nodes, minangle) 2 | % 3 | % [newnodes, len]=polylinesimplify(nodes, minangle) 4 | % 5 | % Calculate a simplified polyline by removing nodes where two adjacent 6 | % segment have an angle less than a specified limit 7 | % 8 | % author: Qianqian Fang (q.fang at neu.edu) 9 | % 10 | % input: 11 | % node: an N x 3 array defining each vertex of the polyline in 12 | % sequential order 13 | % minangle:(optional) minimum segment angle in radian, if not given, use 14 | % 0.75*pi 15 | % 16 | % output: 17 | % newnodes: the updated node list; start/end will not be removed 18 | % len: the length of each segment between the start and the end points 19 | % 20 | % 21 | % -- this function is part of brain2mesh toolbox (http://mcx.space/brain2mesh) 22 | % License: GPL v3 or later, see LICENSE.txt for details 23 | % 24 | 25 | if (nargin < 2) 26 | minangle = 0.75 * pi; 27 | end 28 | 29 | v = segvec(nodes(1:end - 1, :), nodes(2:end, :)); 30 | ang = acos(max(min(sum(-v(1:end - 1, :) .* (v(2:end, :)), 2), 1), -1)); 31 | 32 | newnodes = nodes; 33 | newv = v; 34 | newang = ang; 35 | 36 | idx = find(newang < minangle); 37 | 38 | while (~isempty(idx)) 39 | newnodes(idx + 1, :) = []; 40 | newv(idx + 1, :) = []; 41 | newang(idx) = []; 42 | idx = unique(idx - (0:(length(idx) - 1))'); 43 | idx1 = idx(idx < size(newnodes, 1)); 44 | newv(idx1, :) = segvec(newnodes(idx1, :), newnodes(idx1 + 1, :)); 45 | idx1 = idx(idx < size(newv, 1)); 46 | newang(idx1) = acos(sum(-newv(idx1, :) .* (newv(idx1 + 1, :)), 2)); 47 | idx0 = idx(idx > 1); 48 | newang(idx0 - 1) = acos(sum(-newv(idx0 - 1, :) .* (newv(idx0, :)), 2)); 49 | idx = find(newang < minangle); 50 | end 51 | 52 | if (nargout > 1) 53 | len = newnodes(1:end - 1, :) - newnodes(2:end, :); 54 | len = sqrt(sum(len .* len, 2)); 55 | end 56 | 57 | function v = segvec(n1, n2) 58 | 59 | v = n2 - n1; 60 | normals = sqrt(sum(v .* v, 2)); 61 | v = v ./ repmat(normals, 1, size(v, 2)); 62 | -------------------------------------------------------------------------------- /ray2surf.m: -------------------------------------------------------------------------------- 1 | function [p, e0] = ray2surf(node, elem, p0, v0, e0) 2 | % 3 | % [p,e0]=ray2surf(node,elem,p0,v0,e0) 4 | % 5 | % Determine the entry position and element for a ray to intersect a mesh 6 | % 7 | % author: Qianqian Fang (q.fang neu.edu) 8 | % 9 | % input: 10 | % node: the mesh coordinate list 11 | % elem: the tetrahedral mesh element list, 4 columns 12 | % p0: origin of the ray 13 | % v0: direction vector of the ray 14 | % e0: search direction: '>' forward search, '<' backward search, '-' bidirectional 15 | % 16 | % output: 17 | % p: the intersection position 18 | % e0: if found, the index of the intersecting element ID 19 | % 20 | % this file is part of Mesh-based Monte Carlo (MMC) 21 | % 22 | % License: GPLv3, see http://mcx.sf.net/mmc/ for details 23 | % 24 | 25 | p = p0; 26 | if (size(elem, 2) == 3) 27 | face = elem; 28 | else 29 | face = volface(elem); 30 | end 31 | [t, u, v, idx] = raytrace(p0, v0, node, face); 32 | if (isempty(idx)) 33 | error('ray does not intersect with the mesh'); 34 | else 35 | t = t(idx); 36 | if (e0 == '>') 37 | % idx1=find(t>=0); 38 | idx1 = find(t >= 1e-10); 39 | elseif (e0 == '<') 40 | idx1 = find(t <= 0); 41 | elseif (isnan(e0) || e0 == '-') 42 | idx1 = 1:length(t); 43 | else 44 | error('ray direction specifier is not recognized'); 45 | end 46 | if (isempty(idx1)) 47 | error('no intersection is found along the ray direction'); 48 | end 49 | t0 = abs(t(idx1)); 50 | [tmin, loc] = min(t0); 51 | faceidx = idx(idx1(loc)); 52 | 53 | % update source position 54 | p = p0 + t(idx1(loc)) * v0; 55 | 56 | if (nargout < 2) 57 | return 58 | end 59 | 60 | % find initial element id 61 | if (size(elem, 2) == 3) 62 | e0 = faceidx; 63 | else 64 | felem = sort(face(faceidx, :)); 65 | f = elem; 66 | f = [f(:, [1, 2, 3]) 67 | f(:, [2, 1, 4]) 68 | f(:, [1, 3, 4]) 69 | f(:, [2, 4, 3])]; 70 | [tf, loc] = ismember(felem, sort(f, 2), 'rows'); 71 | loc = mod(loc, size(elem, 1)); 72 | if (loc == 0) 73 | loc = size(elem, 1); 74 | end 75 | e0 = loc; 76 | end 77 | end 78 | -------------------------------------------------------------------------------- /slicesurf.m: -------------------------------------------------------------------------------- 1 | function [bcutpos, bcutloop, bcutvalue] = slicesurf(node, face, varargin) 2 | % 3 | % [bcutpos, bcutloop]=slicesurf(node, face, varargin) 4 | % 5 | % Slice a closed surface by a plane and extract the intersection curve as a 6 | % polyline loop 7 | % 8 | % author: Qianqian Fang (q.fang at neu.edu) 9 | % 10 | % input: 11 | % node: an N x 3 array defining the 3-D positions of the mesh 12 | % face: an N x 3 interger array specifying the surface triangle indices; 13 | % 14 | % output: 15 | % bcutpos: the nodes on the intersection curve 16 | % bcutloop: the sequential order of the nodes to form a polyline loop; 17 | % the last node is assumed to be connected to the first node; an 18 | % nan indicates the end of a loop; the intersection may contain 19 | % multiple loops; if only bcutpos is returned, the nodes will be 20 | % made in sequential order. 21 | % 22 | % 23 | % -- this function is part of brain2mesh toolbox (http://mcx.space/brain2mesh) 24 | % License: GPL v3 or later, see LICENSE.txt for details 25 | % 26 | 27 | [bcutpos, bcutvalue, bcutedges] = qmeshcut(face(:, 1:3), node, node(:, 1), varargin{:}); 28 | [bcutpos, bcutedges] = removedupnodes(bcutpos, bcutedges); 29 | bcutloop = extractloops(bcutedges); 30 | if (nargout == 1) 31 | bcutloop(isnan(bcutloop)) = []; 32 | bcutpos = bcutpos(bcutloop, :); 33 | end 34 | -------------------------------------------------------------------------------- /slicesurf3.m: -------------------------------------------------------------------------------- 1 | function [leftpt, leftcurve, rightpt, rightcurve] = slicesurf3(node, elem, p1, p2, p3, step, minangle) 2 | % 3 | % [leftpt,leftcurve,rightpt,rightcurve]=slicesurf3(node,elem,p1,p2,p3,step,minangle) 4 | % 5 | % Slice a closed surface by a plane and extract the landmark nodes along 6 | % the intersection between p1 and p3, then output into 2 segments: between 7 | % p2 to p1 (left half), and p2 to p3 (right half) 8 | % 9 | % author: Qianqian Fang (q.fang at neu.edu) 10 | % 11 | % input: 12 | % node: an N x 3 array defining the 3-D positions of the mesh 13 | % elem: an N x 3 interger array specifying the surface triangle indices; 14 | % p1: 3D position of the start on the curve-of-interest 15 | % p2: 3D position of the middle on the curve-of-interest 16 | % p3: 3D position of the end on the curve-of-interest 17 | % step: (optional) a percentage (0-100) specifying the spacing of the 18 | % output landmark nodes; step=20 means the landmarks on the left 19 | % curve are spaced as 20% of the total lengths of the left-half, and 20 | % those on the right-curve are spaced at 20% of the right-half, 21 | % starting from p2. 22 | % minangle: (optional) a positive minangle will ask this function to 23 | % call polylinesimplify to remove sharp turns on the curve. 24 | % 25 | % output: 26 | % leftpt: the equal-spaced landmark nodes on the left-half (p2-p1) 27 | % intersection curve; spacing between these nodes are 28 | % (step% * length of the curve between p2-p1) 29 | % leftcurve: all nodes on the left-half (p2-p1) intersection curve 30 | % rightpt: the equal-spaced landmark nodes on the right-half (p2-p3) 31 | % intersection curve; spacing between these nodes are 32 | % (step% * length of the curve between p2-p3) 33 | % rightcurve: all nodes on the left-half (p2-p1) intersection curve 34 | % 35 | % -- this function is part of brain2mesh toolbox (http://mcx.space/brain2mesh) 36 | % License: GPL v3 or later, see LICENSE.txt for details 37 | % 38 | 39 | fullcurve = slicesurf(node, elem, [p1; p2; p3]); 40 | if (nargin >= 7 && minangle > 0) 41 | fullcurve = polylinesimplify(fullcurve, minangle); 42 | end 43 | 44 | [fulllen, fullcurve] = polylinelen(fullcurve, p1, p3, p2); 45 | 46 | [leftlen, leftcurve] = polylinelen(fullcurve, p2, p1); 47 | if (nargin >= 6) 48 | [idx, weight, leftpt] = polylineinterp(leftlen, sum(leftlen) * (step:step:(100 - step * 0.5)) * 0.01, leftcurve); 49 | else 50 | leftpt = leftcurve; 51 | end 52 | 53 | if (nargout > 2) 54 | [rightlen, rightcurve] = polylinelen(fullcurve, p2, p3); 55 | if (nargin >= 6) 56 | [idx, weight, rightpt] = polylineinterp(rightlen, sum(rightlen) * (step:step:(100 - step * 0.5)) * 0.01, rightcurve); 57 | else 58 | rightpt = rightcurve; 59 | end 60 | end 61 | -------------------------------------------------------------------------------- /tpm2label.m: -------------------------------------------------------------------------------- 1 | function [vol, names] = tpm2label(seg, segorder) 2 | % 3 | % vol=tpm2label(seg) 4 | % or 5 | % vol=tpm2label(seg, segorder) 6 | % [vol, names]=tpm2label(seg, segorder) 7 | % 8 | % converting tissue probablistic maps (TPMs) to a multi-lable volume 9 | % 10 | % author: Qianqian Fang (q.fang at neu.edu) 11 | % 12 | % input: 13 | % seg: a struct, a cell array, or a 3D or 4D array; if seg is a cell 14 | % or a struct, their subfields must be 2D/3D arrays of the same 15 | % sizes; 16 | % segorder: if seg is a struct, segorder allows one to assign output 17 | % labels using customized order instead of the creation order 18 | % 19 | % output: 20 | % vol: a 2-D or 3-D array of the same type/size of the input arrays. The 21 | % label for each voxel is determined by the index to the highest 22 | % value in TPM of the same voxel. If a voxel is a background voxel 23 | % - i.e. zeros for all TPMs, it stays 0 24 | % names: a cell array storing the names of the labels (if input is a 25 | % struct), the first string is the name for label 1, and so on 26 | % 27 | % -- this function is part of brain2mesh toolbox (http://mcx.space/brain2mesh) 28 | % License: GPL v3 or later, see LICENSE.txt for details 29 | % 30 | 31 | mask = seg; 32 | names = {}; 33 | if (isstruct(seg)) 34 | if (nargin > 1) 35 | seg = orderfields(seg, segorder); 36 | end 37 | names = fieldnames(seg); 38 | mask = cellfun(@(x) seg.(x), names, 'UniformOutput', false); 39 | end 40 | if (iscell(mask)) 41 | mask = cat(ndims(mask{1}) + 1, mask{:}); 42 | end 43 | if (~isnumeric(mask)) 44 | error('input must be a cell/struct array with numeric elements of matching dimensions'); 45 | end 46 | 47 | [newmask, vol] = max(mask, [], ndims(mask)); 48 | vol = vol .* (sum(mask, ndims(mask)) > 0); 49 | --------------------------------------------------------------------------------