How to create heatmap illustraing mesh differences controlling the position of center color for divergence color palette?

好久不见. 提交于 2019-12-11 06:06:11

问题


I have two 3D meshes of human faces and I wish to use heatmap to illustrate differences. I want to use red-blue divergent color scale.

My data can be found here. In my data, "vb1.xlsx" and "vb2.xlsx" contain 3D coordinates of the two meshes. "it.xlsx" is the face information. The "dat_col.xlsx" contains pointwise distances between the two meshes based on which heatmap could be produced. I used the following code to generate the two meshes based on vertex and face information. I then used the meshDist function in Morpho package to calculate distances between each pair of vertex on the two meshes.

library(Morpho)
library(xlsx)
library(rgl)
library(RColorBrewer)
library(tidyverse)


mshape1 <- read.xlsx("...\\vb1.xlsx", sheetIndex = 1, header = F)
mshape2 <- read.xlsx("...\\vb2.xlsx", sheetIndex = 1, header = F)

it <- read.xlsx("...\\it.xlsx", sheetIndex = 1, header = F)

# Preparation for use in tmesh3d
vb_mat_mshape1 <- t(mshape1)
vb_mat_mshape1 <- rbind(vb_mat_mshape1, 1)
rownames(vb_mat_mshape1) <- c("xpts", "ypts", "zpts", "")

vb_mat_mshape2 <- t(mshape2)
vb_mat_mshape2 <- rbind(vb_mat_mshape2, 1)
rownames(vb_mat_mshape2) <- c("xpts", "ypts", "zpts", "")

it_mat <- t(as.matrix(it))
rownames(it_mat) <- NULL

vertices1 <- c(vb_mat_mshape1)
vertices2 <- c(vb_mat_mshape2)

indices <- c(it_mat)

mesh1 <- tmesh3d(vertices = vertices1, indices = indices, homogeneous = TRUE, 
               material = NULL, normals = NULL, texcoords = NULL)
mesh2 <- tmesh3d(vertices = vertices2, indices = indices, homogeneous = TRUE, 
               material = NULL, normals = NULL, texcoords = NULL)

mesh1smooth <- addNormals(mesh1)
mesh2smooth <- addNormals(mesh2)

# Calculate mesh distance using meshDist function in Morpho package
mD <- meshDist(mesh1smooth, mesh2smooth)
pd <- mD$dists

The pd, containing information on pointwise distances between the two meshes, can be found in the first column of the "dat_col.xlsx" file.

A heatmap is generated from the meshDist function as follows:

I wish to have better control of the heatmap by using red-blue divergent color scale. More specifically, I want positive/negative values to be colored blue/red using 100 colors from the RdBu color pallete in the RColorBrewer package. To do so, I first cut the range of pd values into 99 intervals of equal lengths. I then determined which of the 99 intervals does each pd value lie in. The code is as below:

nlevel <- 99

breaks <- NULL
for (i in 1:(nlevel - 1)) {
    breaks[i] <- min(pd) + ((max(pd) - min(pd))/99) * i
}

breaks <- c(min(pd), breaks, max(pd))

pd_cut <- cut(pd, breaks = breaks, include.lowest = TRUE)

dat_col <- data.frame(pd = pd, pd_cut = pd_cut, group = as.numeric(pd_cut))

The pd_cut is the inteval corresponding to each pd and group is the interval membership of each pd. Color is then assgined to each pd according to the value in group with the following code:

dat_col <- dat_col %>%
           mutate(color = colorRampPalette(
                            brewer.pal(n = 9, name = "RdBu"))(99)[dat_col$group])

The final heatmap is as follows:

open3d()    
shade3d(mesh1smooth, col=dat_col$color, specular = "#202020", polygon_offset = 1)

Since I have 99 intervals, the middle interval is the 50th, (-3.53e-05,-1.34e-05]. However, it is the 51th interval, (-1.34e-05,8.47e-06], that contains the 0 point.

Following my way of color assignment (colorRampPalette(brewer.pal(n = 9, name = "RdBu"))(99)[dat_col$group]), the center color (the 50th color imputed from colorRampPalette) is given to pds belonging to the 50th interval. However, I want pds that belong to the 51th interval, the interval that harbors 0, to be assgned the center color.

I understand that in my case, my issue won't affect the appearance of heatmap too much. But I believe this is not a trivial issue and can significantly affect the heatmap when the interval that contains 0 is far from the middle interval. This could happen when the two meshes under comparison is very different. It makes more sense to me to assign center color to the interval that contains 0 rather than the one(s) that lie in the middle of all intervals.

Of course I can manually replace hex code of the 50th imputed color to the desired center color as follows:

color <- colorRampPalette(brewer.pal(n = 9, name = "RdBu"))(99)
color2 <- color
color2[50] <- "#ffffff" #assume white is the intended center color

But the above approach affected the smoothness of color gradient since the color that was originally imputed by some smooth function is replaced by some arbitrary color. But how could I assign center color to pds that lie in the interval that transgresses 0 while at the same time not affecting the smoothness of the imputed color?


回答1:


There are a couple of things to fix to get what you want.

First, the colours. You base the colours on this code:

color <- colorRampPalette(brewer.pal(n = 9, name = "RdBu"))(99)

You can look at the result of that calculation, and you'll see that there is no white in it. The middle color is color[50] which evaluates to "#F7F6F6", i.e. a slightly reddish light gray colour. If you look at the original RdBu palette, the middle colour was "#F7F7F7", so this change was done by colorRampPalette(). To me it looks like a minor bug in that function: it truncates the colour values instead of rounding them, so the values

[50,] 247.00000 247.00000 247.00000

convert to "#F7F6F6", i.e. red 247, green 246, blue 246. You can avoid this by choosing some other number of colours in your palette. I see "F7F7F7" as the middle colour with both 97 and 101 colours. But being off by one probably doesn't matter much, so I wouldn't worry about this.

The second problem is your discretization of the range of the pd values. You want zero in the middle bin. If you want the bins all to be of equal size, then it needs to be symmetric: so instead of running from min(pd) to max(pd), you could use this calculation:

limit <- max(abs(pd))
breaks <- -limit + (0:nlevel)*2*limit/nlevel

This will put zero exactly in the middle of the middle bin, but some of the bins at one end or the other might not be used. If you don't care if the bins are of equal size, you could get just as many negatives as positives by dividing them up separately. I like the above solution better.

Edited to add: For the first problem, a better solution is to use

color <- hcl.colors(99, "RdBu")

with the new function in R 3.6.0. This does give a light gray as the middle color.



来源:https://stackoverflow.com/questions/57196381/how-to-create-heatmap-illustraing-mesh-differences-controlling-the-position-of-c

标签
易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!