这篇文章发表于 1590 天前,可能其部分内容已经发生变化,如有疑问可询问作者。

空间自相关主要是用于研究在某块区域内各部分与之相邻部分的相关性关系。可能有点小绕,看接下来的图就能够明白了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
library(rgdal) #矢量数据栅格数据的读取所需要的包
library(spdep)

##读入数据 打开爱尔兰的空间数据文件
eire <- readOGR(system.file("shapes/eire.shp", package="spData")[1])
##计算邻居单元
eire.nb <- poly2nb(eire) #创建一个地区的所有邻居列表
data(eire) #将数据eire加载到R中,似乎这步略过也可

##画图
brks <- round(fivenum(eire$A), digits=2) #fivenum返回A的5个统计参数(最小值、下四分位数、中位数、上四分位数、最大值),round四舍五入,小数点后保留两位
cols <- rev(heat.colors(4)) #heat.colors颜色调色板,这一系列函数有5个;rev向量或矩阵的翻转
plot(eire, col=cols[findInterval(eire$A, brks, all.inside=TRUE)])
title(main="Percentage with blood group A in Eire")#写了一个标题,findInterval(x,v)v按照非降序排列,找到x对应的再v中的排序号
plot(eire.nb, coordinates(eire), add=TRUE, col='blue') #依据坐标,add=TRUE是在原来的基础上继续画图,反之会仅留下最新画的图。这张图形象地表现出每块地方与周围所有地方的连通。

捕获

可见这里的蓝线是其中的每个部分与周围区域相连的线条。每部分的颜色代表了相关部分的数据大小情况。从图上能够看出来这块区域里面的数值分布具有一定的聚集性,但是这样看图肯定是不行的,必须要有一个统计量来确切地描述才行。

Moran‘s I 是衡量区域 (Global)尺度上某空间随机变量整体自相关的程度的统计量。

理论基础

这里不多说,效果绝对没有直接看书好。放几张图片意思一下。

图片3

GlobalMoran’s I

这个是狭义上的莫兰指数。Moran’s I取值范围:[-1, 0, +1]Moran’s I > 0表示空间正相关性,其值越大,空间相关性越明显,Moran’s I < 0表示空间负相关性,其值越小,空间差异越大,否则,Moran’s I = 0,空间呈随机性。严格来说,显著的正Z(+1.65, 单侧0.05)表明变量空间分布比期望的随机分布更加聚集(正相关) ,显著的负Z( -1.65, 单侧0.05)表明变量空间分布比期望的随机分布更加分散(负相关)。

这里再提醒一点——单尾检验和双尾检验的Z值:比如双侧0.10的Z值(确切的说应该是Z值绝对值)就等于单侧0.05的Z值。ArcGIS里面的功能主要提供的是双尾检验的结果(其实单尾检验的意义更加明确点,但我估计是插件开发者节约成本将最后提供的是双尾检验的图及其对应的标准),而我接下来提供的方法主要是单尾检验。如果还是对单尾检验和双尾检验还是不了解的话,这里附上一个传送门

对于moran.test,其原假设H0是变量X在空间N个单元上随机分布,也就意味着这时的Moran’s I趋向于0(当N趋向于无穷大时)。

接下来的代码建议使用R-3.6.3。安装好maptoolsrgdalspdep三个包之后,我们来算这个统计量,并且画出一张Moran’s I Scatterplot散点图

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
library(maptools)
library(rgdal) #矢量数据栅格数据的读取所需要的包
library(spdep)

##读入数据 打开爱尔兰的空间数据文件
eire <- readOGR(system.file("shapes/eire.shp", package="spData")[1])
##计算邻居单元
eire.nb <- poly2nb(eire) #创建一个地区的所有邻居列表
data(eire) #将数据eire加载到R中

##全局空间自相关Moran'I测试
moran.test(eire$A, nb2listw(eire.nb)) #nb2listw是把nb对象(neighbours list object)转为listw对象,listw是在空间回归命令中可以直接使用的一种空间矩阵形式
#在结果中z-score=standard deviate。如果alternative hypothesis: greater,也就是大于随机期望,这就意味着聚集

##Moran'I散点图
moran.plot(as.vector(scale(eire$A)), nb2listw(eire.nb), labels=eire$names, pch=20, cex=1.6, col='blue')# 第一分量为变量,第二分量为空间权重矩阵

Moran's I Scatterplot散点图的x轴是给定单元上变量值的标准化值(z-scores),y轴是该单元邻居单元上变量均值的z-scores,为lagged value

1捕获

现在已经画好了那个散点图,接下来画spatial correlograms,也就是空间相关图。要画这张图,就相当于将这样一整块区域中所有部分的多边形转化成一个个点,再依据每个点之间的距离来生成correlograms。这个图里面的x轴是距离classes(阈值),y轴对应的是Moran's I

1
2
3
4
5
6
7
8
9
10
11
12
13
14
library(maptools)
library(pgirmess) #有些库需要先install
library(spdep)
library(rgdal)

eire <- readOGR(system.file("shapes/eire.shp", package="spData")[1])
coords<-coordinates(eire)
plot(coords[,1], coords[,2], type='p', asp=1, pch=20, cex=1.8, col='blue', ylim=c(5800, 6100))
text(coords[,1]+5, coords[,2]+10, eire$names, cex=0.5)

coords<-coordinates(eire)
colnames(coords)<-c("X", "Y") #固定格式的coords的数组
res<-correlog(coords,eire$A, method="Moran", nbclass=18) #correlog是pgirmess包里的函数,nbclass是用来控制尺度的。
plot(res)

运行之后即可得到相应的结果。

2捕获

很显然,随着测量尺度减小(右‐>左),局部观测异质性影响增高,分布‐聚集模式比较清晰;随着测量尺度增大(左‐>右),局部异质性的差异被忽略,空间分布模式的模糊性增高。

R简单实现—AnselinLocal Moran’s I

很明显之前的GlobalMoran’s I虽然能够让人从全局的角度来看到相关情况,但是并不能很好地观测到局地的空间自相关情况。另外全局的版本遭遇非均质的情况会出问题,所以在后来,local版的Moran’s I横空出世。

由于R里面貌似暂时没有能够直接作出local moran’s I的函数,于是我参照了一位老哥的代码,改了改,他的项目地址在https://github.com/gisUTM/spatialplots。

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
require(spdep)

plot.local.moran <- function(x, variable.name, local.moran, weights, sig = 0.05, plot.only.significant = TRUE, legend.location = "bottomleft", zero.policy = NULL){
if(!inherits(local.moran, "localmoran"))
stop("local.moran not an object of class localmoran")
if(!inherits(weights, "listw"))
stop("weight not a listw")
if (!inherits(x, "SpatialPointsDataFrame") & !inherits(x, "SpatialPolygonsDataFrame"))
stop("MUST be sp SpatialPointsDataFrame OR SpatialPolygonsDataFrame CLASS OBJECT")

# Check if local.moran subsetted missing data
#x <- na.action(local.moran)
na.act <- na.action(local.moran)

# Rows to drop in weight matrix (weights)
subVec <- !(1:length(weights$neighbours) %in% na.act)

# Subset weights
weights <- subset(weights, subVec, zero.policy = zero.policy)

# Subset localmoran
local.moran <- local.moran[subVec,]

# Subset Polygons
origPoly <- x
x <- subset(x, subVec)


# Get length of x
n <- length(x)

vec <- c(1:n)
vec <- ifelse(local.moran[,5] < sig, 1,0)

# Create the lagged variable
lagvar <- lag.listw(weights, x[[variable.name]])

# get the mean of each
m.myvar <- mean(x[[variable.name]])
m.lagvar <- mean(lagvar)

myvar <- x[[variable.name]]

# Derive quadrants
q <- c(1:n)

for (i in 1:n){
if (myvar[[i]]>=m.myvar & lagvar[[i]]>=m.lagvar)
q[i] <- 1
if (myvar[[i]]<m.myvar & lagvar[[i]]<m.lagvar)
q[i] <- 2
if (myvar[[i]]<m.myvar & lagvar[[i]]>=m.lagvar)
q[i] <- 3
if (myvar[[i]]>=m.myvar & lagvar[[i]]<m.lagvar)
q[i] <- 4
}

# set coloring scheme
q.all <- q
colors <- c(1:n)
for (i in 1:n) {
if (q.all[i]==1)
colors[i] <- "red"
if (q.all[i]==2)
colors[i] <- "blue"
if (q.all[i]==3)
colors[i] <- "lightblue"
if (q.all[i]==4)
colors[i] <- "pink"
if (q.all[i]==0)
colors[i] <- "white"
if (q.all[i]>4)
colors[i] <- "white"
}

# Mark all non-significant regions white
locm.dt <- q*vec
colors1 <- colors
for (i in 1:n){
if ( !(is.na (locm.dt[i])) ) {

if (locm.dt[i]==0) colors1[i] <- "grey78"

}
}

colors2 <- colors
colors2 <- paste(colors2,vec)
pos = list()
for (i in 1:n) {
pos[[i]] <- c(which(myvar==colors2["blue 0"]))
}

blue0 <- which(colors2=="blue 0")
red0 <- which(colors2=="red 0")
lightblue0 <- which(colors2=="lightblue 0")
pink0 <- which(colors2=="pink 0")
lb <- 6
labels=c("High-High", "High-Low", "Low-High", "Low-Low", "Not Significant", "Missing Data")
# plot the map
# Plot out the full set of polygons (missing data will not be overlaid)
plot(origPoly, col = "black")
if (plot.only.significant == TRUE){
plot(x, col=colors1,border=T, lwd=0.2, add = TRUE)
}else{
plot(x, col=colors,border=T, add = TRUE)
}
legend(legend.location, legend = labels, fill = c("red", "pink", "lightblue", "blue", "grey78", "black"), bty = "n")
}

使用方法同https://github.com/gisUTM/spatialplots/blob/master/README.md

在此基础上,在之前的global版本上继续操作。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
library(maptools)
library(pgirmess)
library(spdep)
library(rgdal)

eire <- readOGR(system.file("shapes/eire.shp", package="spData")[1])
eire.nb<-poly2nb(eire)
eire.wt<-nb2listw(eire.nb)
eire.lmi<-localmoran(eire@data$A,eire.wt,na.action=na.exclude)
if (FALSE){ #如果不是特别清楚,建议看一下这里的数据是咋样的
eire@data
attributes(eire.nb)
eire.lmi
}

plot.local.moran(eire,variable.name='A',weight=eire.wt,local.moran=eire.lmi,legend.location='topright')

3捕获

感觉效果有点寒碜。。

利用python简单实现

这部分数据和上面R中用的不一样,数据可在此处下载,是本人某门课作业时从网上爬下来再整合而成的数据,没错就是旅游人次==。由于这部分数据在R上导入总是报错,所以趁机用python上的jupyter notebook尝试了一遍,具体环境在Windows上搭建比较困难,我已经在后面部分给出了docker的快速环境搭建——熟练的话可以在10分钟内搭建起全新的环境,并且画出图。

这里介绍两种方法,它们的算法之间存在少量区别,这二种方法在计算的时候存在着小区别,呈现出来的效果也就未必相同,个人感觉第二种的效果比较好看……

方法一

这里的数据暂不提供,仅提供脚本,见谅。首先是导入各种需要的模块。

1
2
3
4
5
6
7
8
9
10
%matplotlib inline

import matplotlib.pyplot as plt
from libpysal.weights.contiguity import Queen
from libpysal import examples
import numpy as np
import pandas as pd
import geopandas as gpd
import os
import splot

然后进行整体Moran’s I的计算:

1
2
3
4
5
6
7
8
9
10
11
12
gdf = gpd.read_file("output.json")
gdf = gdf[0:31]
gdf = gdf.drop([20])
#y = (gdf['2017'].values-gdf['2016'])/gdf['Sall'].values
y = gdf['2017'].values
w = Queen.from_dataframe(gdf)
w.transform = 'r'

from esda.moran import Moran
w = Queen.from_dataframe(gdf)
moran = Moran(y, w)
moran.I

最后计算局部Moran’s I并进行画图:

1
2
3
4
5
from esda.moran import Moran_Local
moran_loc = Moran_Local(y, w)
from splot.esda import lisa_cluster
lisa_cluster(moran_loc, gdf, p=0.05, figsize = (9,9))
plt.show()

参见https://pysal.org/notebooks/viz/splot/esda_morans_viz.html。

方法二

这里先是对各个省份的旅游人次按大小分一下类并且表现在地图上。

1
2
3
4
5
6
7
8
9
10
import geopandas as gpd
#data=gpd.read_file('province.shp',encoding='utf-8')
data=gpd.read_file('tourism.json',encoding='utf-8')
data.head()
#type(data['2014'])
data=data[0:31] #这里除去没有数据的港澳台地区
data['new_2014']=data['2014'].apply(float)
import pysal as ps
cl=ps.viz.mapclassify.Quantiles(data['new_2014'],k=5)
data.assign(cl=cl.yb).plot('cl', legend=True, categorical=True)

这里就作出了这样的一幅画:

4捕获

很丑,但这个不是重点……接下来,对Local Moran’s I分四类探索,这个其实可以跳过。

1
2
3
4
5
6
7
8
9
wq=pysal.lib.weights.Queen.from_dataframe(data)
y=data['new_2014'].values
from pysal.viz.splot.esda import moran_scatterplot
#moran_scatterplot(y,wq)
mi=pysal.explore.esda.Moran(y,wq)
mi.I
li=pysal.explore.esda.Moran_Local(y, wq)
li.q
data.assign(cl=li.q).plot('cl', legend=True, cmap='GnBu', categorical=True)

5捕获

依然很丑,但因为这个分类有点默认的感觉,所以这个依然不是重点。。。

啊哈,接下来才是重点。

接下来的数据换了一下,因变量采用了各省域的GDP(来自国家统计局),前期数据整理的步骤略过了。

1
2
3
4
5
6
7
8
9
10
11
sig = li.p_sim < 0.05
hotspot = 1 * (sig * li.q==1) #高高聚集,正自相关程度高
coldspot = 3 * (sig * li.q==3) #低低聚集
doughnut = 2 * (sig * li.q==2) #LH,此区域低,周围区域高
diamond = 4 * (sig * li.q==4) #HL,此区域高,周围区域低
spots = hotspot + coldspot + doughnut + diamond

spot_labels = [ '0 ns', '1 hot spot', '2 doughnut', '3 cold spot', '4 diamond'] #ns是No Significant
labels = [spot_labels[i] for i in spots]
data.assign(cl=labels).plot('cl', legend=True, cmap='GnBu', categorical=True)
data.assign(cl=labels).legend(loc = 'bottom')

于是画出了下面这样的图,也就是我们真正想看到的漂亮的Local Moran’s I的图:

3-1捕获

不过发现中国的东北被挡住了,进行改进。

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
sig = li.p_sim < 0.05
hotspot = 1 * (sig * li.q==1)
coldspot = 3 * (sig * li.q==3)
doughnut = 2 * (sig * li.q==2)
diamond = 4 * (sig * li.q==4)
spots = hotspot + coldspot + doughnut + diamond

spot_labels = [ '0 ns', '1 hot spot', '2 doughnut', '3 cold spot', '4 diamond']
labels = [spot_labels[i] for i in spots]

#这里先设置好cmap,使得之后颜色能够很好地对应
import matplotlib.cm as cm
import matplotlib.colors as col
colors=['#bbe0c6','#74c190','#00a15c','#17693e','#101e15']
cmap2 = col.LinearSegmentedColormap.from_list('own2',colors)
# extra arguments are N=256, gamma=1.0
cm.register_cmap(cmap=cmap2)

import matplotlib.pyplot as plt
import matplotlib.patches as mpatches

#先画个没有图例的图片
pic=data.assign(cl=labels).plot('cl', cmap=cmap2, legend=False, categorical=True)

#这里绘制图例,不挡住地图的那种
patches=[]
for i in range(len(spot_labels)):
patch = mpatches.Patch(color=colors[i],label=spot_labels[i])
patches.append(patch)
plt.legend(handles=patches,loc='lower right')
plt.show()

4-1捕获

这个效果很不错了。

可作参考的资料:

https://pdf.hanspub.org/SA20150300000_71814845.pdf

https://rspatial.org/raster/analysis/3-spauto.html

https://www.insee.fr/en/statistiques/fichier/3635545/imet131-g-chapitre-3.pdf

https://cran.r-project.org/web/packages/ape/vignettes/MoranI.pdf

https://r-spatial.github.io/spdep/reference/localmoran.html

https://cran.r-project.org/web/packages/spdep/spdep.pdf

https://ithelp.ithome.com.tw/articles/10209784

https://www.osgeo.cn/pysal/api.html

https://www.osgeo.cn/pysal/generated/pysal.viz.splot.esda.moran_scatterplot.html#pysal.viz.splot.esda.moran_scatterplot

https://www.geog-daily.org/geogforum/14

https://methods.sagepub.com/base/download/DatasetStudentGuide/local-morans-i-berlin-districts-2018-python

数据处理的小彩蛋

这里谈论的小操作和上面部分的关联较强,故放在这里作为彩蛋。。。

这部分写的比较乱,见谅。

json数据获取

首先声明一下,我拿到的原始数据是全国的省域shp和shx文件,首先转化一下。

1
2
3
import geopandas as gpd
data = gpd.read_file('data.shp')
data.to_file("data.json", driver='GeoJSON', encoding="utf-8")

不管怎样,反正最终的json在这里

OK,下面这个程序是大杀器(特别是处理国家统计数据的时候),但需要Excel上面的数据遵从那个顺序,就是我国官方数据省域的统计顺序。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# -* - coding:utf-8
import json
import re
with open("C:/Users/user/Desktop/R/Chinaprovince/land.json",'r') as f:
land_dict=json.load(f)
provinces=["北京","天津","河北","山西","内蒙古","辽宁","吉林","黑龙江","上海","江苏","浙江","安徽","福建","江西","山东","河南","湖北","湖南","广东","广西","海南","重庆","四川","贵州","云南","西藏","陕西","甘肃","青海","宁夏","新疆","台湾","香港","澳门"]

#Excel转json的网站,感谢这个轮子https://www.bejson.com/json/col2json/
data=[
{"地区":"北京","FAC_1":"-0.74386","FAC_2":"3.26951","FAC_3":"0.38552","FAC":"0.59"},
{"地区":"天津","FAC_1":"-0.44824","FAC_2":"1.29721","FAC_3":"0.36253","FAC":"0.17"}
#########这里将Excel文件转化后的结果像所示这样的直接复制进来即可##########
]
for i in range(len(data)):
dict=data[i]
dict['id']=provinces[i]
land_dict['features'][i]['properties']=dict

with open("C:/Users/user/Desktop/output.json","w") as f:
json.dump(land_dict,f)

最后输出的output.json结合keplergl或是以上的手段,足以打出一套干净漂亮的组合拳了。

制作数据分析专用的dockerfile

这部分大可不必看,主要是为了在docker里面搞一手python36+R+数据分析常用包+jupyter+fq的可视化Linux环境。。。

附上一个写得不专业的Dockerfile+部分的src,

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
FROM dorowu/ubuntu-desktop-lxde-vnc:bionic

#proxy
COPY ./src/sources.list /etc/apt/
RUN apt update
RUN apt-get -y install unzip
RUN mkdir /root/v2ray
COPY ./src/v2ray-linux-64.zip /root/v2ray
RUN unzip -d /root/v2ray /root/v2ray/v2ray-linux-64.zip
#RUN apt-get -y install coreutils
RUN apt-get -y install proxychains
COPY ./src/config.json /root/v2ray/
COPY ./src/proxychains.conf /etc/
#RUN nohup ./root/v2ray/v2ray >/dev/null 2>&1 &

COPY ./src/r-tuna.list /etc/apt/sources.list.d/
COPY ./src/requirements.txt /root/

RUN apt-get -y install dirmngr
RUN apt-key adv --keyserver keyserver.ubuntu.com --recv-keys E298A3A825C0D65DFD57CBB651716619E084DAB9
RUN apt-get update
RUN apt-get -y install python3-pip
#RUN apt-get -y install make
#RUN apt-get -y install cmake
RUN apt-get -y install libspatialindex-dev

RUN pip3 install setuptools==47.1.1
RUN pip3 install -r /root/requirements.txt -i https://pypi.tuna.tsinghua.edu.cn/simple some-package
RUN pip3 install -i https://pypi.tuna.tsinghua.edu.cn/simple pip -U
RUN pip3 config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple

RUN apt-get -y install r-base

其中src文件夹文件如下,

proxychains.conf::

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
# (dead proxies are skipped)
# otherwise EINTR is returned to the app
#
strict_chain
#
# Strict - Each connection will be done via chained proxies
# all proxies chained in the order as they appear in the list
# all proxies must be online to play in chain
# otherwise EINTR is returned to the app
#
#random_chain
#
# Random - Each connection will be done via random proxy
# (or proxy chain, see chain_len) from the list.
# this option is good to test your IDS :)

# Make sense only if random_chain
#chain_len = 2

# Quiet mode (no output from library)
#quiet_mode

# Proxy DNS requests - no leak for DNS data
proxy_dns

# Some timeouts in milliseconds
tcp_read_time_out 15000
tcp_connect_time_out 8000

# ProxyList format
# type host port [user pass]
# (values separated by 'tab' or 'blank')
#
#
# Examples:
#
# socks5 192.168.67.78 1080 lamer secret
# http 192.168.89.3 8080 justu hidden
# socks4 192.168.1.49 1080
# http 192.168.39.93 8080
#
#
# proxy types: http, socks4, socks5
# ( auth types supported: "basic"-http "user/pass"-socks )
#
[ProxyList]
# add proxy here ...
# meanwile
# defaults set to "tor"
socks5 127.0.0.1 1080

requirements.txt::

1
2
3
pysal==2.1.0
jupyter==1.0.0
keplergl==0.2.0

sources.list::

1
2
3
4
5
6
7
8
9
10
deb http://mirrors.aliyun.com/ubuntu/ bionic main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ bionic-security main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ bionic-updates main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ bionic-proposed main restricted universe multiverse
deb http://mirrors.aliyun.com/ubuntu/ bionic-backports main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ bionic main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ bionic-security main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ bionic-updates main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ bionic-proposed main restricted universe multiverse
deb-src http://mirrors.aliyun.com/ubuntu/ bionic-backports main restricted universe multiverse

r-tuna.list::

1
deb https://mirrors.tuna.tsinghua.edu.cn/CRAN/bin/linux/ubuntu bionic-cran40/

还有config.jsonv2ray-linux-64.zip两个文件,但我不予提供。

然后只要运行两条命令就能创建一个全新的环境——

1
2
3
4
docker build -t data:1.0 .

docker run -d --cap-add=NET_ADMIN --device=/dev/net/tun -p 36001:80 -p 36000:5900 -p 49000:49000 -e VNC_PASSWORD=password data:1.0
#只要通过vnc连接这个36000端口即可完成可视化,具体参见dorowu/ubuntu-desktop-lxde-vnc:bionic这个项目