-
Notifications
You must be signed in to change notification settings - Fork 27
Expand file tree
/
Copy pathhxsurf.R
More file actions
810 lines (759 loc) · 30.3 KB
/
hxsurf.R
File metadata and controls
810 lines (759 loc) · 30.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
#' Read Amira surface (aka HxSurface or HyperSurface) files into hxsurf object
#'
#' @details Note that when \code{RegionChoice="both"} or
#' \code{RegionChoice=c("Inner", "Outer")} both polygons in inner and outer
#' regions will be added to named regions. To understand the significance of
#' this, consider two adjacent regions, A and B, with a shared surface. For
#' the polygons in both A and B, Amira will have a patch with (say)
#' InnerRegion A and OuterRegion B. This avoids duplication in the file.
#' However, it might be convenient to add these polygons to both regions when
#' we read them into R, so that regions A and B in our R object are both
#' closed surfaces. To achieve this when \code{RegionChoice="both"},
#' \code{read.hxsurf} adds these polygons to region B (as well as region A)
#' but swaps the order of the vertices defining the polygon to ensure that the
#' surface directionality is correct.
#'
#' As a rule of thumb, stick with \code{RegionChoice="both"}. If you get more
#' regions than you wanted, then try switching to \code{RegionChoice="Inner"}
#' or \code{RegionChoice="Outer"}.
#'
#' Note that the support for reading Amira's binary mesh format (HxSurface
#' binary) is less mature and in particular only a few multi region mesh files
#' have been tested. Finally there is no support to read meshes from the newer
#' "Amira Binary Surface format" although such files can be read into a list
#' using the \code{read.amiramesh} function.
#'
#' @param filename Character vector defining path to file
#' @param RegionNames Character vector specifying which regions should be read
#' from file. Default value of \code{NULL} => all regions.
#' @param RegionChoice Whether the \emph{Inner} or \emph{Outer} material, or
#' \emph{both} (default), should define the material of the patch. See
#' details.
#' @param FallbackRegionCol Colour to set regions when no colour is defined
#' @param Verbose Print status messages during parsing when \code{TRUE}
#' @return A list with S3 class hxsurf with elements \describe{
#'
#' \item{Vertices}{ A data.frame with columns \code{X, Y, Z, PointNo}}
#'
#' \item{Regions}{ A list with 3 column data.frames specifying triplets of
#' vertices for each region (with reference to \code{PointNo} column in
#' \code{Vertices} element)}
#'
#' \item{RegionList}{ Character vector of region names (should match names of
#' \code{Regions} element)}
#'
#' \item{RegionColourList}{ Character vector specifying default colour to plot
#' each region in R's \code{\link{rgb}} format}
#'
#' }
#' @export
#' @seealso \code{\link{plot3d.hxsurf}, \link{rgb}}
#' @aliases hxsurf
#' @family amira
#' @family hxsurf
#' @examples
#' \dontrun{
#' read.hxsurf("my.surf", RegionChoice="both")
#' }
read.hxsurf<-function(filename,RegionNames=NULL,RegionChoice="both",
FallbackRegionCol="grey",Verbose=FALSE){
# Check for header confirming file type
firstLine=readLines(filename,n=1)
if(!any(grep("#\\s+hypersurface\\s+[0-9.]+\\s+ascii",firstLine,ignore.case=T,perl=T))){
if(!any(grep("#\\s+hypersurface\\s+[0-9.]+\\s+binary",firstLine,ignore.case=T,perl=T))){
stop(filename," does not appear to be an Amira HyperSurface file!")
}
res = tryCatch(
read.hxsurf.bin(
filename = filename,
FallbackRegionCol = FallbackRegionCol,
Verbose = Verbose
),
error = function(e)
stop(
"Support for reading binary Amira HyperSurface is still limited.\n",
"See https://github.com/natverse/nat/issues/429. Detailed error message",
as.character(e)
)
)
return(res)
}
initialcaps<-function(x) {substr(x,1,1)=toupper(substr(x,1,1)); x}
RegionChoice=match.arg(initialcaps(RegionChoice), c("Inner", "Outer", "Both"),
several.ok = TRUE)
if(RegionChoice[1]=="Both") RegionChoice=c("Inner", "Outer")
t=readLines(filename)
nLines=length(t)
if(Verbose) cat(nLines,"lines of text to parse\n")
# Find the start of the Vertices
dataStart=grep("^\\s*Vertices\\s*",t)[1]
if(Verbose) cat("Data start line =",dataStart,"\n")
headerLines=t[seq(dataStart-1)]
getfield=function(fName,textLines=headerLines,pos=2) unlist(strsplit(trimws(textLines[grep(fName,textLines)]),"\\s+",perl=TRUE))[pos]
nVertices=as.numeric(getfield("Vertices",t[dataStart],2))
if(Verbose) cat("nVertices =",nVertices,"\n")
d=list()
d$Vertices=read.table(filename,skip=dataStart,nrows=nVertices,col.names=c("X","Y","Z"),colClasses=rep("numeric",3))
d$Regions <- list()
d$Vertices$PointNo=seq(nrow(d$Vertices))
if(Verbose) cat("Finished processing Vertices\n")
# Now read in Triangles that define patches:
linesSkipped=dataStart+nVertices-1
remainingLines=t[(dataStart+nVertices):nLines]
PatchDefLine=grep("^\\s*Patches\\s*",remainingLines,perl=TRUE)
if(Verbose) cat("PatchDefLine =",PatchDefLine,"\n")
nPatches=as.numeric(getfield("Patches",remainingLines[PatchDefLine],2))
if(Verbose) cat("nPatches =",nPatches,"\n")
PatchStarts=grep("^\\s*{",remainingLines[PatchDefLine:length(remainingLines)],perl=TRUE)+PatchDefLine-1
if(length(PatchStarts)>nPatches) PatchStarts=PatchStarts[1:nPatches]
PatchEnds=grep("^\\s*}",remainingLines[PatchDefLine:length(remainingLines)],perl=TRUE)+PatchDefLine-1
if(length(PatchEnds)>nPatches) PatchEnds=PatchEnds[1:nPatches]
TriangleDeflines<-grep("Triangles",remainingLines)
if(length(TriangleDeflines)!=nPatches)
stop("Incorrect number of Triangle definition lines in",filename,"\n")
for(i in 1:nPatches){
if(Verbose) cat("TriangleDefline =",TriangleDeflines[i],"\n")
PatchHeader<-remainingLines[PatchStarts[i]:TriangleDeflines[i]]
# remove any opening braces - these would cause a problem if on same line
PatchHeader=sub("^\\s*\\{\\s*","",PatchHeader)
# convert all whitespace to single spaces
PatchHeader=gsub("\\s+"," ",PatchHeader)
if(Verbose) cat("PatchHeader is",length(PatchHeader),"lines long\n")
# note use of RegionChoice to switch naming between inner and outer
for(RegChoice in RegionChoice) {
RegionName=getfield(paste(RegChoice,"Region",sep=""),PatchHeader,2)
nTriangles=as.numeric(getfield("Triangles",PatchHeader,2))
if(nTriangles<0 || nTriangles>100000) stop("Bad triangle number: ", nTriangles)
if(Verbose) cat("nTriangles =",nTriangles,"for patch =",i,"\n")
# Check if we want to load in this region
if( is.null(RegionNames) || RegionName%in%RegionNames ){
# Ensure we do not try to add no triangles, or the exterior region
if(nTriangles == 0 || RegionName == "Exterior") next
thispatch=read.table(filename,skip=linesSkipped+TriangleDeflines[i],nrows=nTriangles,
quote='',colClasses='integer',blank.lines.skip=FALSE,
fill=FALSE,comment.char="",
col.names=c("V1","V2","V3"))
if(getfield(paste(RegChoice,"Region",sep=""),PatchHeader,1) == "OuterRegion") {
thispatch <- thispatch[, c(1,3,2)]
if(Verbose) message("Permuting vertices for ", RegionName, "...")
colnames(thispatch) <- c("V1","V2","V3")
}
# scan no quicker in these circs, problem is repeated file access
# specifying text directly also does not help dues to very slow textConnection
# check if we have already loaded a patch in this name
if(RegionName%in%names(d$Regions)){
# add to the old patch
if(Verbose) cat("Adding to patch name",RegionName,"\n")
d[['Regions']][[RegionName]]=rbind(d[['Regions']][[RegionName]],thispatch)
} else {
# new patch
if(Verbose) cat("Making new patch name",RegionName,"\n")
d[['Regions']][[RegionName]]=thispatch
}
}
}
}
d$RegionList=names(d$Regions)
# Handle colours for regions
d$RegionColourList <- vector(length=length(d$RegionList))
closeBraces <- grep("}", headerLines)
for(regionName in d$RegionList) {
# Find section in headerLines corresponding to this region
headerSecStart <- grep(paste0("^\\s*", regionName, "(\\s+ \\{){0,1}"), headerLines)[1]
headerSecEnd <- closeBraces[closeBraces > headerSecStart][1]
# Extract colour information
colorLine <- grep("Color", headerLines[headerSecStart:headerSecEnd], value=T)
if(length(colorLine) > 0) {
rgbValues <- strsplit(regmatches(colorLine, gregexpr("[0-9]$|[0-9][^\\.]|[0-9]\\.[0-9]+", colorLine, perl=T))[[1]], " ")
# clean up any trailing commas
rgbValues <- gsub("[,}]","", rgbValues)
color <- rgb(rgbValues[[1]], rgbValues[[2]], rgbValues[[3]])
} else {
color <- FallbackRegionCol
}
d$RegionColourList[which(d$RegionList == regionName)] <- color
}
class(d) <- c('hxsurf',class(d))
return(d)
}
read.hxsurf.bin <- function(filename, return.raw=FALSE, FallbackRegionCol='grey', Verbose=FALSE) {
con=file(filename, open='rb')
on.exit(close(con))
vertex_regex='^Vertices \\d+$'
# read header
h <- character()
line <- readLines(con, n=1)
while(!isTRUE(grepl(vertex_regex, line))) {
h=c(h, line)
line <- readLines(con, n=1)
}
params=.ParseAmirameshParameters(h)
materials=names(params$Parameters$Materials)
# read data blocks
data_regex='^\\s*(\\w+)\\s+(\\d+)$'
parse_data_line <- function(line) {
tryCatch({
res=stringr::str_match(line, data_regex)
n=suppressWarnings(as.integer(res[,3]))
checkmate::assert_int(n)
label=checkmate::assert_character(res[,2])
names(n)=label
n
}, error=function(e) {NA_integer_})
}
data <- list(header=h, params=params)
curpatch=NA_integer_
while(TRUE) {
if(length(line)<1) break
if(is.finite(curpatch)) {
if(length(data[['PatchInfo']])<curpatch)
data[['PatchInfo']][[curpatch]]=line
else
data[['PatchInfo']][[curpatch]]=c(data[['PatchInfo']][[curpatch]], line)
} else {
data[['header']]=c(data[["header"]], line)
}
# is this a closing bracket at the end of a section
firstchar=substr(trimws(line), 1, 1)
if(isTRUE(firstchar=='}') && is.finite(curpatch)) curpatch=curpatch+1
n=parse_data_line(line)
if(is.na(n) || n==0) {
line <- readLines(con, 1)
next
}
label=names(n)
if(label=='Vertices') {
chunk=readBin(con, what='numeric', n=n*3, size=4, endian = 'big')
data[['Vertices']]=matrix(chunk, ncol=3, byrow = T)
} else if (label=='Triangles') {
npatches=length(data[['Patches']])
chunk=readBin(con, what='integer', n=n*3, size=4, endian = 'big')
data[['Patches']][[npatches+1]]=matrix(chunk, ncol=3, byrow = T)
} else if(label=='Patches') {
curpatch=1
if(is.null(data[['Patches']])) data[['Patches']]=list()
if(is.null(data[['PatchInfo']])) data[['PatchInfo']]=list()
} else {
stop("Error parsing binary hxsurf file!")
}
line <- readLines(con, 1)
}
if(return.raw)
data
else parse.hxsurf.bin(data, FallbackRegionCol=FallbackRegionCol, Verbose=Verbose)
}
# FIXME: Ideally this would be harmonised with the code for read.hxsurf to
# avoid duplication
parse.hxsurf.bin <- function(data, FallbackRegionCol, Verbose) {
materials=data$params$Parameters$Materials
d=list()
d[['Vertices']]=as.data.frame(xyzmatrix(data$Vertices))
d[['Vertices']][,'PointNo']=seq_len(nrow(d[['Vertices']]))
d$Regions=list()
for(p in seq_along(data$Patches)) {
thispatch=as.data.frame(data$Patches[[p]])
pi=.ParseAmirameshParameters(data$PatchInfo[[p]])
RegionName <- pi$InnerRegion
if(RegionName%in%names(d$Regions)){
# add to the old patch
if(Verbose) cat("Adding to patch name",RegionName,"\n")
d[['Regions']][[RegionName]]=rbind(d[['Regions']][[RegionName]],thispatch)
} else {
# new patch
if(Verbose) cat("Making new patch name",RegionName,"\n")
d[['Regions']][[RegionName]]=thispatch
}
}
d$RegionList=names(d$Regions)
d$RegionColourList <- vector(length=length(d$RegionList))
for(regionName in d$RegionList) {
rgbValues <- materials[[regionName]][['Color']]
if(isTRUE(length(rgbValues)==3)) {
color <- rgb(rgbValues[[1]], rgbValues[[2]], rgbValues[[3]])
} else {
color <- FallbackRegionCol
}
d$RegionColourList[which(d$RegionList == regionName)] <- color
}
class(d) <- c('hxsurf',class(d))
return(d)
}
#' Write Amira surface (aka HxSurface or HyperSurface) into .surf file.
#'
#' @param surf hxsurf object to write to file.
#' @param filename character vector defining path to file.
#' @return \code{NULL} or integer status from \code{\link{close}}.
#' @export
#' @seealso \code{\link{plot3d.hxsurf}},\code{\link{read.hxsurf}}, \code{\link{rgb}}
#' @family amira
#' @family hxsurf
write.hxsurf <- function(surf, filename) {
fc <- file(filename, open="at")
cat("# HyperSurface 0.1 ASCII\n\n", file=fc)
cat("Parameters {\n", file=fc)
cat(" Materials {\n", file=fc)
cat(" Exterior {\n Id 1\n }\n", file=fc)
regionData <- cbind(surf$RegionList, surf$RegionColourList)
for (i in 1:nrow(regionData)) {
cat(" ", regionData[i, 1], " {\n", sep="", file=fc)
cat(" Id ", i+1, ",\n", sep="", file=fc)
cat(" Color ", paste(zapsmall(col2rgb(regionData[i, 2])/255), collapse=" "), "\n", sep="", file=fc)
cat(" }\n", file=fc)
}
cat(" }\n", file=fc)
cat(" BoundaryIds {\n Name \"BoundaryConditions\"\n }\n", file=fc)
cat("}\n\n", file=fc)
cat("Vertices ", nrow(surf$Vertices), "\n", sep="", file=fc)
apply(surf$Vertices[, 1:3], 1, function(x) cat(" ", sprintf(x[1], fmt="%.6f"), " ", sprintf(x[2], fmt="%.6f"), " ", sprintf(x[3], fmt="%.6f"), "\n", sep="", file=fc))
cat("NBranchingPoints 0\nNVerticesOnCurves 0\nBoundaryCurves 0\n", file=fc)
cat("Patches ", length(surf$Regions), "\n", sep="", file=fc)
for(i in 1:length(surf$Regions)) {
region <- surf$Regions[[i]]
cat("{\n", file=fc)
cat("InnerRegion ", names(surf$Regions[i]), "\n", sep="", file=fc)
cat("OuterRegion Exterior\n", file=fc)
cat("BoundaryId 0\n", file=fc)
cat("BranchingPoints 0\n\n", file=fc)
cat("Triangles ", nrow(region), "\n", sep="", file=fc)
apply(region, 1, function(x) cat(" ", paste(x, collapse=" "), "\n", sep="", file=fc))
cat("}\n", file=fc)
}
close(fc)
}
#' Plot amira surface objects in 3D using rgl
#'
#' @param x An hxsurf surface object
#' @param materials Character vector or \code{\link[base]{regex}} naming
#' materials to plot (defaults to all materials in x). See
#' \code{\link{subset.hxsurf}}.
#' @param col Character vector specifying colors for the materials, or a
#' function that will be called with the number of materials to plot. When
#' \code{NULL} (default) will use material colours defined in Amira (if
#' available), or \code{rainbow} otherwise.
#' @param ... Additional arguments passed to \code{triangles3d}
#' @inheritParams plot3d.neuronlist
#' @export
#' @seealso \code{\link{read.hxsurf}}
#' @family hxsurf
#' @examples
#' plot3d(kcs20)
#' plot3d(MBL.surf)
#'
#' \donttest{
#' # plot only vertical lobe
#' nclear3d()
#' plot3d(MBL.surf, materials="VL", alpha=0.3)
#'
#' # everything except vertical lobe
#' nclear3d()
#' plot3d(MBL.surf, alpha=0.3,
#' materials=grep("VL", MBL.surf$RegionList, value = TRUE, invert = TRUE))
#' }
plot3d.hxsurf<-function(x, materials=NULL, col=NULL, gridlines = FALSE, ...,
plotengine = getOption('nat.plotengine')){
plotengine <- check_plotengine(plotengine)
if (plotengine == 'rgl'){
# skip so that the scene is updated only once per hxsurf object
skip <- par3d(skipRedraw = TRUE)
on.exit(par3d(skip))
}
if (plotengine == 'plotly') {
psh <- openplotlyscene()$plotlyscenehandle
params=list(...)
opacity <- if("alpha" %in% names(params)) params$alpha else 1
}
materials=subset(x, subset = materials, rval='names')
if(is.null(col)) {
if(length(x$RegionColourList)){
col=x$RegionColourList[match(materials,x$RegionList)]
} else col=rainbow
}
if(is.function(col)) col=col(length(materials))
if(is.factor(col)) col=rainbow(nlevels(col))[as.integer(col)]
if(length(col)==1 && length(materials)>1) col=rep(col,length(materials))
names(col)=materials
rlist=list()
for(mat in materials) {
# get order triangle vertices
tri=as.integer(t(x$Regions[[mat]]))
if (plotengine == 'rgl'){
rlist[[mat]]=triangles3d(x[['Vertices']]$X[tri],x[['Vertices']]$Y[tri],
x[['Vertices']]$Z[tri],col=col[mat], ...)
} else {
tmpx <- as.mesh3d.hxsurf(x, Regions = mat)
psh <- psh %>%
plotly::add_trace(x = tmpx$vb[1,],
y = tmpx$vb[2,],
z = tmpx$vb[3,],
i = tmpx$it[1,]-1,
j = tmpx$it[2,]-1,
k = tmpx$it[3,]-1,
type = "mesh3d", opacity = opacity,
hovertext=mat,
hoverinfo="x+y+z+text",
facecolor = rep(col[mat],
length(tmpx$it[1,])))
}
}
if (plotengine == 'rgl'){
invisible(rlist)
} else {
psh <- psh %>% plotly::layout(showlegend = FALSE, scene=list(camera=.plotly3d$camera))
if(gridlines == FALSE){
psh <- psh %>% plotly::layout(scene = list(xaxis=.plotly3d$xaxis,
yaxis=.plotly3d$yaxis,
zaxis=.plotly3d$zaxis))
}
assign("plotlyscenehandle", psh, envir=.plotly3d)
psh
}
}
#' Convert an object to an rgl mesh3d
#'
#' Note that this provides a link to the Rvcg package
#' @param x Object to convert to mesh3d
#' @param ... Additional arguments for methods
#' @param Regions Character vector or regions to select from \code{hxsurf}
#' object
#' @param material rgl materials such as \code{color}
#' @param drop Whether to drop unused vertices (default TRUE)
#' @export
#' @rdname as.mesh3d
#' @seealso \code{\link[rgl]{as.mesh3d}}, \code{\link[rgl]{tmesh3d}},
#' \code{\link{as.hxsurf}}, \code{\link{read.hxsurf}}
#' @family hxsurf
as.mesh3d.hxsurf<-function(x, Regions=NULL, material=NULL, drop=TRUE, ...){
if(is.null(Regions)) {
Regions=x$RegionList
}
x=subset(x, Regions, drop=drop)
if(length(Regions)==1 && is.null(material)){
# find colour
material=list(color=x$RegionColourList[match(Regions,x$RegionList)])
}
verts=t(data.matrix(x$Vertices[,1:3]))
inds=t(data.matrix(do.call(rbind, x$Regions)))
tmesh3d(vertices=verts, indices=inds, homogeneous = FALSE, material = material, ...)
}
#' @description \code{as.mesh3d.boundingbox} converts a nat
#' \code{\link{boundingbox}} object into an rgl compatible \code{mesh3d}
#' object.
#' @rdname as.mesh3d
#' @export
#' @examples
#' bb=boundingbox(kcs20)
#' mbb=as.mesh3d(bb)
#' \donttest{
#' plot3d(kcs20)
#' # simple plot
#' plot3d(bb)
#' shade3d(mbb, col='red', alpha=0.3)
#'
#' }
as.mesh3d.boundingbox <- function(x, ...) {
centroid=colMeans(x)
size=diff(x)/2
mat=scaleMatrix(size[1], size[2], size[3])%*%translationMatrix(centroid[1], centroid[2], centroid[3])
cube3d(mat)
}
#' Convert an object to a nat hxsurf object
#'
#' @details \code{hxsurf} objects are based on the format of Amira's surface
#' objects (see \code{\link{read.hxsurf}}). They have the ability to include
#' multiple distinct regions. However, at the moment the only method that we
#' provide converts \code{mesh3d} objects, which can only include one region.
#' @param x A surface object
#' @param ... Additional arguments passed to methods
#'
#' @return A new surface object of class \code{hxsurf} (see
#' \code{\link{read.hxsurf}}) for details.
#' @export
#' @family hxsurf
#' @seealso \code{\link{as.mesh3d}}
#' @examples
#' tet=tetrahedron3d(col='red')
#' teth=as.hxsurf(tet)
#' \donttest{
#' plot3d(teth)
#' }
as.hxsurf <- function(x, ...) UseMethod('as.hxsurf')
#' @param region The default name for the surface region
#' @param col The surface colour (default value of NULL implies the colour
#' specified in mesh3d object or \code{grey} when the \code{mesh3d} object has
#' no colour.)
#' @export
#' @rdname as.hxsurf
as.hxsurf.mesh3d <- function(x, region="Interior", col=NULL, ...) {
if (is.null(x$it))
stop("This method only works for triangular mesh3d objects!")
h=list()
h$Vertices=data.frame(xyzmatrix(x))
colnames(h$Vertices)=c("X","Y","Z")
h$Vertices$PointNo=1:nrow(h$Vertices)
h$Regions[[region]]=data.frame(t(x$it))
colnames(h$Regions[[region]])=c("V1","V2","V3")
h$RegionList=names(h$Regions)
if(is.null(col)) col=x$material$col
h$RegionColourList <- if(!is.null(col)) col else 'grey'
class(h)=c("hxsurf","list")
h
}
#' Subset hxsurf object to specified regions
#'
#' @param x A dotprops object
#' @param subset Character vector specifying regions to keep. Interpreted as
#' \code{\link[base]{regex}} if of length 1 and no fixed match.
#' @param drop Whether to drop unused vertices after subsetting (default:
#' \code{TRUE})
#' @param rval Whether to return a new \code{hxsurf} object or just the names of
#' the matching regions
#' @param ... Additional parameters (currently ignored)
#' @return subsetted hxsurf object
#' @export
#' @family hxsurf
#' @examples
#' # plot only vertical lobe
#' vertical_lobe=subset(MBL.surf, "VL")
#' \donttest{
#' plot3d(vertical_lobe, alpha=0.3)
#' plot3d(kcs20)
#'
#' # there is also a shortcut for this
#' nclear3d()
#' plot3d(MBL.surf, subset = "VL", alpha=0.3)
#' }
subset.hxsurf<-function(x, subset=NULL, drop=TRUE, rval=c("hxsurf","names"), ...){
rval=match.arg(rval)
if(!is.null(subset)){
tokeep=integer(0)
if(is.character(subset)){
tokeep=match(subset,x$RegionList)
if(is.na(tokeep[1]) && length(subset)==1){
# try as regex
tokeep=grep(subset,x$RegionList)
}
}
if(!length(tokeep) || any(is.na(tokeep)))
stop("Invalid subset! See ?subset.hxsurf")
if(rval=='names') return(x$RegionList[tokeep])
x$Regions=x$Regions[tokeep]
x$RegionList=x$RegionList[tokeep]
x$RegionColourList=x$RegionColourList[tokeep]
} else if(rval=='names') return(x$RegionList)
if(drop){
# see if we need to drop any vertices
vertstokeep=sort(unique(unlist(x$Regions)))
# a vector where each position is the old vertex id and the value is the
# new one i.e. newid=vert_table[oldid]
vert_table=match(seq_len(nrow(x$Vertices)), vertstokeep)
# convert all vertex ids from old to new sequence
for(r in x$RegionList){
for(i in seq_len(ncol(x$Regions[[r]]))){
x$Regions[[r]][[i]]=vert_table[x$Regions[[r]][[i]]]
}
}
# drop unused vertices
x$Vertices=x$Vertices[vertstokeep, ]
}
x
}
#' Subset methods for different nat objects
#'
#' These methods enable subsets of some nat objects including neurons and
#' neuronlists to be obtained. See the help for each individual method for
#' details.
#'
#' @name subset
#' @seealso \code{\link{subset.neuron}}, \code{\link{subset.dotprops}},
#' \code{\link{subset.hxsurf}}, \code{\link{subset.neuronlist}}
NULL
#' Find which points of an object are inside a surface
#'
#' @details Note that \code{hxsurf} surface objects will be converted to
#' \code{mesh3d} before being passed to \code{Rvcg::vcgClostKD}, so if you are
#' testing repeatedly against the same surface, it may make sense to
#' pre-convert.
#'
#' \code{pointsinside} depends on the face normals for each face pointing out
#' of the object (see example). The face normals are defined by the order of
#' the three vertices making up a triangular face. You can flip the face
#' normal for a face by permuting the vertices (i.e. 1,2,3 -> 1,3,2). If you
#' find for a given surface that points are outside when you expect them to be
#' inside then the face normals are probably all the wrong way round. You can
#' invert them yourself or use the \code{Morpho::invertFaces} function to fix
#' this.
#'
#' The \code{rval} argument determines the return value. These options should
#' be fairly clear, but the difference between \code{logical} and
#' \code{consistent_logical} needs some explanation. The \code{logical} method
#' now does a pre-test to remove any points that are not in the 3D bounding
#' box (cuboid) enclosing the surf object. This often results in a significant
#' speed-up by rejecting distant points and has the additional benefit of
#' rejecting distant points that sometimes are erroneously declared inside the
#' mesh (see below). Regrettably it is not yet possible to extend this
#' approach when distances are being returned, which means there will be a
#' discrepancy between the results of \code{rval="logical"} and looking for
#' points with distance >=0. If you want to ensure consistency between these
#' approaches, use \code{rval="consistent_logical"}.
#'
#' If you find that some points but not all points are not behaving as you
#' would expect, then it may be that some faces are not coherently oriented.
#' The \code{Rvcg::\link[Rvcg]{vcgClean}} function can sometimes be used to
#' correct the orientation of the faces. Fixing more problematic cases may be
#' possible by generating a new surface using
#' \code{alphashape3d::\link[alphashape3d]{ashape3d}} (see examples).
#'
#' @param x an object with 3D points.
#' @param surf The reference surface - either a \code{mesh3d} object or any
#' object that can be converted using \code{as.mesh3d} including \code{hxsurf}
#' and \code{ashape3d} objects.
#' @param ... additional arguments for methods, eventually passed to
#' \code{\link{as.mesh3d}}.
#' @export
#' @examples
#' # check if the vertices in these neurons are inside the mushroom body calyx
#' # surface object
#' inout=pointsinside(kcs20, surf=subset(MBL.surf, "MB_CA_L"))
#' table(inout)
#' # you can also check if points are inside a bounding box
#' mbcalbb=boundingbox(subset(MBL.surf, "MB_CA_L"))
#' inout2=pointsinside(kcs20, mbcalbb)
#' # compare those two
#' table(inout, inout2)
#' pts=xyzmatrix(kcs20)
#' # nb that colour expressions maps combinations of two logicals onto 1:4
#' plot(pts[,1:2], col=1+inout+inout2*2)
#' # the colours are defined by
#' palette()[1:4]
#'
#' # be a bit more lenient and include points less than 5 microns from surface
#' MBCAL=subset(MBL.surf, "MB_CA_L")
#' inout5=pointsinside(kcs20, surf=MBCAL, rval='distance') > -5
#' table(inout5)
#' \donttest{
#' # show which points are in or out
#' # Hmm seems like there are a few red points in the vertical lobe
#' # that are well outside the calyx
#' points3d(xyzmatrix(kcs20), col=ifelse(inout5, 'red', 'black'))
#' plot3d(MBL.surf, alpha=.3)
#'
#' # Let's try to make an alphashape for the mesh to clean it up
#' library(alphashape3d)
#' MBCAL.as=ashape3d(xyzmatrix(MBCAL), alpha = 10)
#' # Plotting the points, we can see that is much better behaved
#' points3d(xyzmatrix(kcs20),
#' col=ifelse(pointsinside(kcs20, MBCAL.as), 'red', 'black'))
#' }
#'
#' \dontrun{
#' # Show the face normals for a surface
#' if(require('Morpho')) {
#' # convert to a mesh3d object used by rgl and Morpho packge
#' MBCAL.mesh=as.mesh3d(subset(MBL.surf, "MB_CA_L"))
#' fn=facenormals(MBCAL.mesh)
#' wire3d(MBCAL.mesh)
#' # show that the normals point out of the object
#' plotNormals(fn, long=5, col='red')
#'
#' # invert the faces of the mesh and show that normals point in
#' MBCAL.inv=invertFaces(MBCAL.mesh)
#' plotNormals(facenormals(MBCAL.inv), long=5, col='cyan')
#' }
#' }
pointsinside<-function(x, surf, ...) UseMethod('pointsinside')
#' @export
#' @param rval what to return.
#' @return A vector of logical values or distances (positive inside, negative
#' outside) equal to the number of points in x or the \code{mesh3d} object
#' returned by \code{Rvcg::vcgClostKD}.
#' @rdname pointsinside
pointsinside.default<-function(x, surf, ..., rval=c('logical','distance',
'mesh3d', 'consistent_logical')) {
rval=match.arg(rval)
if(rval=='logical') {
# use optimised contains_points approach
return(contains_points(surf, x, ...))
}
if(inherits(surf, "boundingbox")) {
stop("Only logical return values are currently possible ",
"with boundingbox objects!")
}
if(!requireNamespace('Rvcg', quietly = TRUE))
stop("Please install suggested library Rvcg to use pointsinside")
if(!inherits(surf,'mesh3d')) {
surf=as.mesh3d(surf, ...)
}
pts=xyzmatrix(x)
rmesh=Rvcg::vcgClostKD(pts, surf, sign = TRUE)
switch(rval,
consistent_logical = rmesh$quality >= 0,
distance = rmesh$quality,
mesh3d = rmesh
)
}
contains_points <- function(obj, points, ...) UseMethod("contains_points")
#' @export
contains_points.boundingbox <- function(obj, points, ...) {
xyz=xyzmatrix(points)
xyz[,1] >= obj[1,1] & xyz[,2] >= obj[1,2] & xyz[,3] >= obj[1,3] &
xyz[,1] <= obj[2,1] & xyz[,2] <= obj[2,2] & xyz[,3] <= obj[2,3]
}
#' @export
contains_points.mesh3d <- function(obj, points, ...) {
xyz=xyzmatrix(points)
inbb=contains_points(boundingbox(obj), xyz, ...)
# nb must call the original logical method to avoid infinite recursion
iosurf=pointsinside(xyz[inbb,,drop=F], surf = obj, ..., rval='consistent_logical')
res=inbb
res[inbb]=iosurf
res
}
#' @export
contains_points.hxsurf<-contains_points.mesh3d
#' @export
contains_points.ashape3d<-function(obj, points, ...) {
alphashape3d::inashape3d(obj, points=xyzmatrix(points), ...)
}
# Concatenate two HyperSurface objects
#
# @param x hxsurf
# @param y another hxsurf
# @return new hxsurf
# @examples
# h1 = as.hxsurf(icosahedron3d(), 'a')
# h2 = as.hxsurf(tetrahedron3d()+1, 'b')
# h3=c(h1, h2)
concat_hxsurfs <- function(x, y) {
nx <- x
nx$Vertices <- rbind(x$Vertices, y$Vertices)
nx$Vertices[,4] <- 1:length(nx$Vertices[,4])
for (reg in y$RegionList) {
nx$Regions[[reg]] <- nrow(x$Vertices) + y$Regions[[reg]]
}
nx$RegionList <- c(x$RegionList, y$RegionList)
nx$RegionColourList <- c(x$RegionColourList, y$RegionColourList)
nx
}
#' Concatenate HyperSurface objects
#'
#' @param ... multiple hxsurf objects
#' @return new hxsurf
#' @export
#' @rdname hxsurf-ops
#' @examples
#' h1 = as.hxsurf(icosahedron3d(), 'a')
#' h2 = as.hxsurf(tetrahedron3d()+1, 'b')
#' h3 = as.hxsurf(icosahedron3d()+3, 'c')
#' hc = c(h1, h2, h3)
`c.hxsurf` <- function(...) {
items <- list(...)
if (length(items) == 1) {
return(items[[1]])
} else {
hxs <- items[[1]]
for (i in 2:length(items)) {
hxs <- concat_hxsurfs(hxs, items[[i]])
}
hxs
}
}