C#实现Google S2算法
S2其实是来自几何数学中的一个数学符号 S²,它表示的是单位球。S2 这个库其实是被设计用来解决球面上各种几何问题的。值得提的一点是,除去 golang 官方 repo 里面的 geo/s2 完成度目前只有40%,其他语言,Java,C++,Python 的 S2 实现都完成100%了。看看怎么用 S2 来解决多维空间点索引的问题。通常地球上的点我们会用经纬度来表示,将经纬度坐标转换为希尔伯特曲线 Cell ID包含如下步骤:
- 把经纬度转换成弧度。
- 经纬弧度转换成坐标系上的一个点 S(lat,lng) -> f(x,y,z)
- 球面变平面 f(x,y,z) -> g(face,u,v)
- 球面矩形投影修正:g(face,u,v) -> h(face,s,t)
- 点与坐标轴点相互转换:h(face,s,t) -> H(face,i,j)
- 坐标轴点与希尔伯特曲线 Cell ID 相互转换:H(face,i,j) -> CellID
1. 球面坐标转换 S(lat,lng) -> f(x,y,z)
按照之前我们处理多维空间的思路,先考虑如何降维,再考虑如何分形。众所周知,地球是近似一个球体。球体是一个三维的,如何把三维降成一维呢?球面上的一个点,在直角坐标系中,可以这样表示:
x = r * sin θ * cos φ
y = r * sin θ * sin φ
z = r * cos θ- 通常地球上的点我们会用经纬度来表示。
- 再进一步,我们可以和球面上的经纬度联系起来。不过这里需要注意的是,纬度的角度 α 和直角坐标系下的球面坐标 θ 加起来等于 90°。所以三角函数要注意转换。于是地球上任意的一个经纬度的点,就可以转换成 f(x,y,z)。
- 在 S2 中,地球半径被当成单位 1 了。所以半径不用考虑。x,y,z的值域都被限定在了[-1,1]这个区间之内了。
- 示例:坐标 (36.683, 117.1412)
- 第一步:把经纬度转换成弧度。由于经纬度是角度,弧度转角度乘以 π / 180°
- 第二步:经纬弧度转换成坐标系上的一个点 S(lat,lng) -> f(x,y,z)
2. 球面变平面 f(x,y,z) -> g(face,u,v)
接下来一步 S2 把球面碾成平面。首先在地球外面套了一个外切的正方体,如下图。
- 从球心向外切正方体6个面分别投影。S2 是把球面上所有的点都投影到外切正方体的6个面上。
- 这里简单的画了一个投影图,上图左边的是投影到正方体一个面的示意图,实际上影响到的球面是右边那张图。
- 从侧面看,其中一个球面投影到正方体其中一个面上,边缘与圆心的连线相互之间的夹角为90°,但是和x,y,z轴的角度是45°。我们可以在球的6个方向上,把45°的辅助圆画出来,见下图左边。
上图左边的图画了6个辅助线,蓝线是前后一对,红线是左右一对,绿线是上下一对。分别都是45°的地方和圆心连线与球面相交的点的轨迹。这样我们就可以把投影到外切正方体6个面上的球面画出来,见上图右边。投影到正方体以后,我们就可以把这个正方体展开了。
- 一个正方体的展开方式有很多种。不管怎么展开,最小单元都是一个正方形。 以上就是 S2 的投影方案。
- 接下来讲讲六边形的投影方案。按照六边形来投影可能是目前最好的方式,不过也可能是最复杂的方式。
- 六边形的菱角比较少,六个边也能相互衔接其他的六边形。看上图最后边的图可以看出来,六边形足够多以后,非常近似球体。
六边形展开以后就是上面这样。当然这里只有12个六边形。六边形个数越多越好,粒度越细,就越贴近球体。Uber 在一个公开分享上提到了他们用的是六边形的网格,把城市划分为很多六边形。这块应该是他们自己开发的。也许滴滴也是划分六边形,也许滴滴有更好的划分方案也说不定。
回到 S2 上面来,S2是用的正方形。这样第一步的球面坐标进一步的被转换成 f(x,y,z) -> g(face,u,v),face是正方形的六个面,u,v对应的是六个面中的一个面上的x,y坐标。
3. 球面矩形投影修正:g(face,u,v) -> h(face,s,t)
上一步我们把球面上的球面矩形投影到正方形的某个面上,形成的形状类似于矩形,但是由于球面上角度的不同,最终会导致即使是投影到同一个面上,每个矩形的面积也不大相同。
- 上图就表示出了球面上个一个球面矩形投影到正方形一个面上的情况。
经过实际计算发现,最大的面积和最小的面积相差5.2倍。见上图左边。相同的弧度区间,在不同的纬度上投影到正方形上的面积不同。现在就需要修正各个投影出来形状的面积。如何选取合适的映射修正函数就成了关键。目标是能达到上图右边的样子,让各个矩形的面积尽量相同。
线性变换是最快的变换,但是变换比最小。
tan() 变换可以使每个投影以后的矩形的面积更加一致,最大和最小的矩形比例仅仅只差1.414。可以说非常接近了。但是 tan() 函数的调用时间非常长。如果把所有点都按照这种方式计算的话,性能将会降低3倍。
最后谷歌选择的是二次变换,这是一个近似切线的投影曲线。它的计算速度远远快于 tan() ,大概是 tan() 计算的3倍速度。生成的投影以后的矩形大小也类似。不过最大的矩形和最小的矩形相比依旧有2.082的比率。
上表中,ToPoint 和 FromPoint 分别是把单位向量转换到 Cell ID 所需要的毫秒数、把 Cell ID 转换回单位向量所需的毫秒数。
(Cell ID 就是投影到正方体六个面,某个面上矩形的 ID,矩形称为 Cell,它对应的 ID 称为 Cell ID)。ToPointRaw 是某种目的下,把 Cell ID 转换为非单位向量所需的毫秒数。在 S2 中默认的转换是二次转换。
- 投影之后的修正函数三种变换应该如下:
- 注意上面虽然变换公式只写了u,不代表只变换u。实际使用过程中,u,v都分别当做入参,都会进行变换。
- 经过修正变换以后,u,v都变换成了s,t。值域也发生了变化。u,v的值域是[-1,1],变换以后,是s,t的值域是[0,1]。
- S2 可以优化的点有两处,一是投影的形状能否换成六边形?二是修正的变换函数能否找到一个效果和 tan() 类似的函数,但是计算速度远远高于 tan(),以致于不会影响计算性能?
4. 点与坐标轴点相互转换:h(face,s,t) -> H(face,i,j)
在 S2 算法中,默认划分 Cell 的等级是30,也就是说把一个正方形划分为 2^30 * 2^30个小的正方形。那么上一步的s,t映射到这个正方形上面来,对应该如何转换呢?
- s,t的值域是[0,1],现在值域要扩大到[0,2^30^-1]。
5. 坐标轴点与希尔伯特曲线 Cell ID 相互转换:H(face,i,j) -> CellID
最后一步,把 i,j 和希尔伯特曲线上的点关联起来,如下画一个局部的图,i,j从0-7变化
- 推理过程略:参见官方PPT: https://docs.google.com/presentation/d/1Hl4KapfAENAOf4gv-pSngKwvS_jwNVHRPZTTDzXXn6Q/view#slide=id.i22
6. S2 Cell ID 数据结构
S2 Cell ID 数据结构,这个数据结构直接关系到不同 Level 对应精度的问题。如下图:
- 在 S2 中,每个 CellID 是由64位的组成的。可以用一个 uint64 存储。开头的3位表示正方体6个面中的一个,取值范围[0,5]。3位可以表示0-7,但是6,7是无效值。
- 64位的最后一位是1,这一位是特意留出来的。用来快速查找中间有多少位。从末尾最后一位向前查找,找到第一个不为0的位置,即找到第一个1。这一位的前一位到开头的第4位(因为前3位被占用)都是可用数字。
- 绿色格子有多少个就能表示划分多少格。上图左图,绿色的有60个格子,于是可以表示[0,2^30^ -1] [0,2^30^ -1]这么多个格子。上图右图中,绿色格子只有36个,那么就只能表示[0,2^18^ -1][0,2^18^ -1]这么多个格子。
- level 0 就是正方体的六个面之一。地球表面积约等于510,100,000 km^2^。level 0 的面积就是地球表面积的六分之一。level 30 能表示的最小的面积0.48cm^2^,最大也就0.93cm^2^ 。
7. C#示例 代码
private static void GetCellDetail(double lat,double lng)
{
Console.WriteLine(string.Format("-------------坐标: 36.683,117.1412---------------- ")); /*第一步:把经纬度转换成弧度。由于经纬度是角度,弧度转角度乘以 π / 180° */
S2LatLng ll = S2LatLng.FromDegrees(36.683, 117.1412);
Console.WriteLine(string.Format("-------------第一步:把经纬度转换成弧度--------------"));
Console.WriteLine(string.Format("latr:{0},lngr{1}", ll.LatRadians, ll.LngRadians)); //第二步:经纬弧度转换成坐标系上的一个点 S(lat,lng) -> f(x,y,z) */
S2Point point = ll.ToPoint();
Console.WriteLine(string.Format("-------------第二步:经纬弧度转换成坐标系上的一个点 S(lat,lng) -> f(x,y,z) --------------"));
Console.WriteLine(string.Format("f(x,y,z): x:{0},y:{1},z:{2}", point.X, point.Y, point.Z)); //第三步:进行投影 f(x,y,z) -> g(face,u,v)
int face = S2Projections.XyzToFace(point);
R2Vector vector = S2Projections.ValidFaceXyzToUv(face, point);
Console.WriteLine(string.Format("-------------进行投影 f(x,y,z) -> g(face,u,v) --------------"));
Console.WriteLine(string.Format("g(face,u,v): {0},{1},{2}", face, vector.X, vector.Y)); //第四步: 投影面积修正 g(face,u,v) -> h(face,s,t)
double s = S2Projections.UvToSt(vector.X);
double t = S2Projections.UvToSt(vector.Y);
Console.WriteLine(string.Format("-------------第四步: 投影面积修正 g(face,u,v) -> h(face,s,t) --------------"));
Console.WriteLine(string.Format("h(face,s,t): {0},{1},{2}", face, s, t)); //第五步:点与坐标轴点相互转换 h(face,s,t) -> H(face,i,j)
int i = S2CellId.StToIj(s);
int j = S2CellId.StToIj(t);
Console.WriteLine(string.Format("-------------第五步:点与坐标轴点相互转换 h(face,s,t) -> H(face,i,j) --------------"));
Console.WriteLine(string.Format("H(face, i, j): {0},{1},{2}", face, i, j)); //第六步:坐标轴点与希尔伯特曲线 Cell ID 相互转换
S2CellId cellid = S2CellId.FromFaceIj(face, i, j);
Console.WriteLine(string.Format("-------------第六步:坐标轴点与希尔伯特曲线 Cell ID 相互转换 --------------"));
Console.WriteLine(string.Format("CellID.id: {0},level:{1}", cellid.Id, cellid.Level)); //验证转坐标
S2LatLng lan = cellid.ToLatLng();
Console.WriteLine(string.Format("lat: {0},lng:{1}", lan.LatDegrees, lan.LngDegrees));
}
8、S2 与 Geohash 对比
- Geohash 有12级,从5000km 到 3.7cm。中间每一级的变化比较大。有时候可能选择上一级会大很多,选择下一级又会小一些。比如选择字符串长度为4,它对应的 cell 宽度是39.1km,需求可能是50km,那么选择字符串长度为5,对应的 cell 宽度就变成了156km,瞬间又大了3倍了。这种情况选择多长的 Geohash 字符串就比较难选。选择不好,每次判断可能就还需要取出周围的8个格子再次进行判断。Geohash 需要 12 bytes 存储。
- S2 有30级,从 0.7cm² 到 85,000,000km² 。中间每一级的变化都比较平缓,接近于4次方的曲线。所以选择精度不会出现 Geohash 选择困难的问题。S2 的存储只需要一个 uint64 即可存下。
- S2 库里面不仅仅有地理编码,还有其他很多几何计算相关的库。地理编码只是其中的一小部分。本文没有介绍到的 S2 的实现还有很多很多,各种向量计算,面积计算,多边形覆盖,距离问题,球面球体上的问题,它都有实现。
参考资料
C#实现Google S2算法的更多相关文章
- 高效的多维空间点索引算法 — Geohash 和 Google S2
原文地址:https://www.jianshu.com/p/7332dcb978b2 引子 每天我们晚上加班回家,可能都会用到滴滴或者共享单车.打开 app 会看到如下的界面: app ...
- S2算法应用
需求:计算不同区域范围,X公里半径内实体店或场站覆盖率. 实现思路: 为了便于理解,将地球看成一个基于经纬度线的坐标系.将经度和纬度看成二维坐标系中的两个纬度,横轴表示经度[-180o, 0o),(0 ...
- 空间数据库系列二:空间索引S2与Z3分析对比
S2与Z3对比分析 1. S2 2. Geohash 3. Geomesa Z3 4. S2对比geohash 4.1. geohash存在的问题 4.2. S2优势 4.3. 实际对比例子 5. 测 ...
- 相似文档查找算法之 simHash及其 java 实现
传统的 hash 算法只负责将原始内容尽量均匀随机地映射为一个签名值,原理上相当于伪随机数产生算法.产生的两个签名,如果相等,说明原始内容在一定概 率 下是相等的:如果不相等,除了说明原始内容不相等外 ...
- 面试体验:Google 篇(转)
http://www.cnblogs.com/cathsfz/archive/2012/08/08/google-interview-experience.html 尝试在自己的博客上搜索点东西,结果 ...
- Google, FaceBook, Amazon 加州求职记 (转)
http://blog.csdn.net/ithomer/article/details/8774006 http://www.myvisajobs.com 一年多前,出于显而易见的原因,下定决心肉身 ...
- GOOGLE RANKBRAIN 完整指南
[译]GOOGLE RANKBRAIN 完整指南 ( 2018 最新版 ) 2018.01.29 来源 http://www.zhidaow.com/post/google-rankbrain ...
- 通过AI自学习,Google让Pixel 3的人像模式更优秀
通过AI自学习,Google让Pixel 3的人像模式更优秀 Link: https://news.cnblogs.com/n/613720/ 虽然双摄手机已经在市场上普及,其所带来的人像模式.多倍变 ...
- 《ArcGIS Runtime SDK for Android开发笔记》——(13)、图层扩展方式加载Google地图
1.前言 http://mt2.google.cn/vt/lyrs=m@225000000&hl=zh-CN&gl=cn&x=420&y=193&z=9& ...
随机推荐
- WPA3在2018年为无线安全添砖加瓦
Wi-Fi Alliance Announces WPA3, the Successor to Wi-Fi's WPA2 Security Protocol The Wi-Fi Alliance -- ...
- python3:logging模块 输出日志到文件
python自动化测试脚本运行后,想要将日志保存到某个特定文件,使用python的logging模块实现 参考代码: import logging def initLogging(logFilenam ...
- 对于src路径问题,深层理解的实践。且对于输出流write()两个方法的源码阅读。
根据昨天的总结,可深层理解图片中src的路径.所以今天实现了一个想法.就是路径写入的是Controller,然后自动去本地找. 其实就是将电脑的本地图片 显示出来.通过输出流的方式. 代码如下: @R ...
- Web服务器使用基于纯文本表单的身份验证——.net(未完待续)
asp.net 表单验证方式 Asp.net的身份验证有有三种,分别是"Windows | Forms| Passport",其中又以Forms验证用的最多,也最灵活. 根据实际需 ...
- Android Native Hook技术(一)
原理分析 ADBI是一个著名的安卓平台hook框架,基于 动态库注入 与 inline hook 技术实现.该框架主要由2个模块构成:1)hijack负责将so注入到目标进程空间,2)libbase是 ...
- 设置 SSH 通过密钥登录
我们一般使用 PuTTY 等 SSH 客户端来远程管理 Linux 服务器.但是,一般的密码方式登录,容易有密码被暴力破解的问题.所以,一般我们会将 SSH 的端口设置为默认的 22 以外的端口,或者 ...
- Arcgis API for JS——打印控件乱码
在通过Arcgis API for JS编写打印控件进行地图下载时,总发现地图字体乱码,如下图: 解决方法: 在装有ArcGIS Server,要调用服务的电脑或服务器上找到下图文件夹
- RNN-LSTM-GRU-BIRNN
https://blog.csdn.net/wangyangzhizhou/article/details/76651116 共三篇 RNN的模型展开后多个时刻隐层互相连接,而所有循环神经网络都有一个 ...
- Count the Sheep 思维题
Altough Skipping the class is happy, the new term still can drive luras anxious which is of course b ...
- MySQL中INFORMATION_SCHEMA
select database(); 获取当前连接的数据库name 来源:http://www.cnblogs.com/drake-guo/p/6099436.html select auto_in ...