The basic idea of doing instance segmentation leveraging 3D shape contexts is that we iteratively find and remove the best matching shape of each query so as to retrieve the individual instance shapes. This enables a simple form of instance segmentation. For example, using this method, we can retrieve all the instances of straight leg chairs from a room as input, as shown in Figure 1.
In Figure 2 we can take a close look at the retrieved chair instances.
However, this method is pretty timeconsuming, especially when retrieving instances from large input scenes.
]]>The most regular method is to assign weights to each class. Considering a dataset named data_training
with the target variable target
as a binary variable and with Y
being positive and N
being negative, the class weight assignment can be written as follows.


The simplest data resampling methods are downsampling and upsampling. Both of them are covered by the caret package.


We only need to specify the resampling method in the control object for training. For downsampling:


And for upsampling:


There are also a few hybrid methods, such as random oversampling examples (ROSE) and synthetic minority oversampling technique (SMOTE), which downsample the majority class and synthesize new data points in the minority class. To use ROSE, we need to load the ROSE package.


And we create a wrapper around the ROSE
function.


We specify the resampling method in the control object.


Similarly, to use SMOTE, we first load the DMwR package.


Then we create a wrapper around the SMOTE
function.


Finally, we specify the resampling method in the control object.


We travel just to travel.
The world looks different when you bike. A car window is more or less a divide between the traveler and the outside world, a motorcycle engine could be too noisy for a person who loves a peaceful trip. Biking, however, makes you taste the true beauty of nature: suffering an exhausting uphill ride and then enjoying the kisses on the wind in a downhill.
I, as well as my roommate, Zhitong, had decided to take a bicycle trip to celebrate our graduation. We traveled from Shanghai to Xiamen, biking around 1200 kilometers in total. The bike is not a fast vehicle that could only travel about 100 kilometers per day, so we were able to fully enjoy the beautiful scenery and experience people’s lives along the way.
We set off on June 3rd, 2017.
It was a sunny and cloudless morning. By noon, we reached the province boundary of Shanghai and Zhejiang.
We went across the Beijing–Hangzhou Grand Canal in the afternoon, which is the oldest and longest artificial river in the world, built over 1400 years ago and running over 1700 kilometers. Today, it is still heavily used by shipping.
Hangzhou, the capital city of Zhejiang Province, is one of the most visited cities in China, renowned for its historic relics as well as natural beauty. We arrived in Hangzhou on the previous night. Although both of us have been to this city before, we still took one rest day here and visited a few interesting places. Being an amateur in Chinese calligraphy, the Xiling Seal Art Society, though little known by tourists, is one of my favorite spots in Hangzhou. It was founded over 100 years ago and is located in a garden on an island in the north of West Lake, where we could find a lot of art pieces of Chinese calligraphy and seal carving. The picture below was taken inside the Xiling Seal Art Society. Written on the rock are two Chinese characters “闲泉” (read xiánquán) in seal script, meaning free spring water.
For sure we also walked by the West Lake, the most tourist attractive place in Hangzhou.
It was raining. Before setting out early in the morning, I took a photo of the lovely hostel that we stayed at in Hangzhou.
It kept raining. This made me feel that I was boating rather than biking.
Heading south all the way, we were gradually getting away from the Yangtze River Delta, which is dominated by modern cities, and coming into the country, where we could find traditional things such as this arch.
Leaving the city, mountains were ahead of us.
We arrived in Yiwu the previous night. However, I was struck by an IT band pain and couldn’t ride a long distance the next day. So on day 4, we decided to make a short detour and ride to Hengdian Town only, which is about 30 kilometers southeast from Yiwu and where the largest film studio in China is located (so it is called Chinawood).
Every year, tens of thousands of amateur actors/actresses come to this small town, playing as extras in movies or TV series, dreaming of becoming movie stars someday, though the chance is very little. These amateur actors/actresses almost take up half of the population in Hengdian and you come across them almost everywhere in the town. As you see, in the picture, an amateur actor was practicing his script.
Being passing travelers, we were not really interested in the film making things. We climbed up a hill and enjoyed the fascinating scenery of a Chinese mountain town in the sunset.
And we walked through the pedestrian street of Hengdian in the evening.
We left Hengdian early in the morning, as the destination of this day was Lishui, which is about 120 kilometers away. At the time of departure, I saw this sign, which is the title of the drama movie I Am Somebody, a movie about extras working in Hengdian.
We passed by a small village, Baita Village, in the morning, where villagers were making straw mats in a traditional manner. An old lady was using a big fan to blow away those defective straws.
The straws need to be dried in the sun.
South Zhejiang is a mountainous region where a lot of rivers flow through. People here build houses either by the river,
or by the road. And usually, the houses are on the mountain, nestling against the slope.
We biked through Jinyun County in the afternoon, which is famous for its pancake. I tried one, taking me only 5 yuan. It is filled up with meat, very juicy and tasty.
The national road in Zhejiang is really in nice condition. Pretty enjoyable ride!
We arrived in Lishui in the evening. Lishui is a small city and a bit undeveloped. This makes some old things better preserved, for example, a busy night market, where you could always make good bargains, but now hard to find in big cities.
Leaving Lishui, immediately we rode into the mountains. The weather was good in the morning when we were riding on a riverside road.
At around 3:00 PM, thunder rumbled from a distance, cool winds blew from the back. When turning around, I saw dark clouds gathering, indicating that a thunderstorm was coming, also indicating that we need some more speed.
Apparently, nobody can ride faster than winds and clouds. I took shelter in a roadside village when it started to rain, finding this Shigandang tablet at the entrance of the village.
After the rain, it was pleasant to bike in the mistshrouded mountains.
In the evening, we came to Jingning County, which is an autonomous county for the She people. We tried some unique dishes, for example, the green Tofu, which is actually a kind of Tofulike food made from Premna leaves.
Besides, we found this interesting phenomenon: the smog rising on the light was actually caused by the raindrop falling on it.
Jingning lies in a valley surrounded by mountains. This indicates a long uphill ride in front of us.
There are wild animals in the mountains. The sign in the picture warns people against feeding the monkeys passing by.
There are springs in the mountains as well. Although it is unsafe to drink spring water directly, we still tried a bit. The water tastes cold and sweet.
We reached the mountaintop in the afternoon, where there is a tunnel, and the altitude is over 1000m, the highest place we arrived during the trip. The sign by the tunnel entrance says that the continuous steep downhill ahead has caused 25 deaths.
During the downhill ride, we experienced the best mountain views throughout the trip:
as well as lovely wildflowers in the sunset.
However, Zhitong’s bike got punctured. It took us some time to fix the tube.
Due to the issue of Zhitong’s bike, we couldn’t ride further and had to take a rest in Siqian Town, a small town in Taishun County, lying in the foothills of the mountains. Fortunately, we met a man in the town, Zhangqiang, who used to travel around China by bike in 2010, raised funds while traveling, and donated the money for needy students in the mountains. We could even find a news report about him (in Chinese). He helped us repair the bike and treated us to dinner. We greatly appreciated his help!
We stayed in the small town for one night and set off the next morning, it was a sunny day.
A Double waterfall by the road. Local people call the falls Hongyan Double Falls or the Couple Falls.
Zhitong’s bike was still not in good condition, so we found a bike repair shop in Taishun, and had the bike fixed.
The boundary line between Zhejiang and Fujian lies only about 5 kilometers south to Taishun. We officially came into Fujian Province in the afternoon. I have a photo of me standing by the crossboundary bridge at the boundary line.
It seems like that some Chinese customs and traditions are better preserved in Fujian, for example, an ancestral shrine, where people perform ancestor worship.
And there is also an interesting old house.
An old peasant.
A town in the valley, which was very quiet and almost empty. We conjectured that young people had basically left such remote mountain towns for cities to either pursue their study or work.
Look back down the valley and the town that we had passed by.
A small brick kiln. The red clay here in the mountains is very suitable for brick making.
We stayed the previous night in Nanyang Town, a small mountain town. In the morning, we started down the road by a mountain stream. It was downhill all the way. People built a small dam on the stream.
The dam does not let too much water through.
We came to another mountain town, Wuqu Town. Written on the door was a piece of military conscription, in pretty good handwriting.
The sun was shining fiercely, so we took a short rest in the town, and tried some Bian Rou, a kind of Fujianese style wonton, over a roadside snack stall.
Due to the hot weather, we couldn’t ride for a very long distance this day and finally arrived in Saiqi, a town of Fu’an. People in Fujian have their own unique religious belief, the worship of Mazu. We saw this Mazu temple on our way.
It was a cloudy day, we still had to ride in the mountains before we reached Ningde, the northernmost seaside city of Fujian. There is a converting station in the mountains.
The advertisement on the wall says that this guy sells guns. Considering the extremely strict gun control policy in China, such advertisements are probably deceptive, which means you won’t get anything after paying him money.
A small waterfall flowing between mountains.
Crossing the bridge, we came into Ningde. On the bridge it writes a verse of Chairman Mao: “虎距龙盘今胜昔, 天翻地覆慨而慷”, translated as The city, a tiger crouching, a dragon curling, outshines its ancient glories; In heroic triumph heaven and earth have been overturned.
We planned to get to Fuzhou this day. According to the map, the sea was only a few kilometers away from us. However, there were still quite a few mountains on our way.
From the mountain road, we could see a factory by the sea.
And finally, we reached a coastal road, although the bay by the road was not beautiful.
Fuzhou, the capital city of Fujian Province, famous for the hundreds of big Banyan trees planted in the city, has a poetic nickname Rongcheng, meaning the city of Banyan.
I was a bit surprised when I saw such a large mosque in Fuzhou, as I had thought everybody here is Mazuist. Actually, this Fuzhou Mosque was built almost 800 years ago, in Yuan dynasty, and is one of the most ancient mosques in China.
In the evening, I went to the old alleys of Fuzhou, to experience some traditional atmosphere of the city.
Still having an exam to take, Zhitong had to go back to Shanghai from Fuzhou. I carried on with my trip alone. Xiamen was only 234 kilometers ahead of me.
I reached Putian by afternoon. It was raining for the whole day.
The road was under construction outside Putian, where the road condition was made even worse by the rain.
Very interestingly, I came across a Mongolian village. I had thought Mongolians only live in North Asia.
I arrived in Quanzhou at the end of the day, a city used to be a major port for foreign traders and hometown of overseas Chinese, and stayed there for the night. The buildings over there are in a style mixing east with west.
Xiamen is only about 90 kilometers from Quanzhou, so this was the last day of my bike tour. There was a big traffic jam on the way to Xiamen.
The boundary marker of Xiamen, carrying a lot of names and comments from previous travelers.
Due to the rain, the crosssea bridge to Xiamen Island was temporarily closed to pedestrians and cyclists. So I had to take a 5minute shuttle bus ride to get into the city.
I met up with this interesting guy on arrival in Xiamen when we were both sheltering from the rain under an overpass. He talked with me about some local customs and traditional worships here. Actually, apart from Mazu, Xiamenese people worship Nezha as well. He took me to a temple of Nezha later and showed me around the temple.
The Nezha temple.
I found a youth hostel on Gulangyu, a small and artsy island off the coast of Xiamen, renowned for its varied architecture and multicultural history.
The next day, I went to the east beach, from where the Kinmen Islands, which are still governed by the Republic of China, were within my sight.
There is a big propaganda sign on the coast of Xiamen facing the Kinmen Islands, telling the hope of Chinese reunification.
I also visited Xiamen University and walked through the Furong Tunnel, which is filled with graffiti designed by university students.
I stayed in this lovely coastal city (though the rainy weather was a bit frustrating) for 3 days, also tried as much local seafood as possible. On the last day of my stay, when taking a ferry from Gulangyu to Xiamen, I took this skyline picture, drawing the modern face of the city. It was the last picture of our trip.
]]>There are some existing plane detection and point cloud deep learning approaches proposed by recent researches.
The 3D Hough transform is one possible approach for doing plane detection. As well as line detection in 2D space, planes can be parameterized into a 3D Hough space. The RAPter is another method that can be used for plane detection. It finds out the planes in a scene according to the predefined interplane relations, so RAPter is efficient for manmade scenes with significant interplane relations, but not adequate for some more general cases.
People have designed many deep learning approaches for different representations of 3D data. For instance, the volumetric CNN consumes volumetric data as input, and apply 3D convolutional neural networks to voxelized shapes. The multiview CNN projects the 3D shape into 2D images, and then apply 2D convolutional neural networks to classify them. The featurebased DNN focuses on generating a shape vector of the object according to its traditional shape features, and then use a fully connected net to classify the shape. The PointNet proposed a new design of neural network based on symmetric functions that can take unordered input of point clouds.
PointNet uses symmetric functions that can effectively capture the global features of a point cloud. Inspired by this, this experiment uses a symmetric network that concatenates global features and local features. Besides, for comparison, I also used a traditional network that simply generates a high dimensional local feature space by multilayer perceptions.
According to the universal approximation of symmetric function proposed by PointNet, a symmetric function $f$ can be arbitrarily approximated by a composition of a set of singlevariable functions and a max pooling function, as described in Theorem 1.
Theorem 1: Suppose $f\colon\chi\to\mathbb{R}$ is a continuous set function w.r.t. Hausdorff distance $d_H$. $\forall\epsilon>0$, $\exists$ a continuous function $h$ and a symmetric function $g(\mathbf{x}_1,\mathbf{x}_2,\ldots,\mathbf{x}_n)=\gamma\circ\max$, such that for any $S\in\chi$,$$\begin{equation}f(S)\gamma(\max_{\mathbf{x}_i\in S}\{h(\mathbf{x}_i)\})<\epsilon,\tag{1}\label{eq:1}\end{equation}$$where $\mathbf{x}_1,\mathbf{x}_2,\ldots,\mathbf{x}_n$ is the full list of elements in $S$ ordered arbitrarily, $\gamma$ is a continuous function, and $\max$ is a vector max operator that takes $n$ vectors as input and returns a new vector of the elementwise maximum.
Thus, according to Theorem 1, the symmetric network can be designed as a multilayer perceptron network connected with a max pooling function, which is shown in Figure 1.
In order to achieve the invariance towards geometric transformation, an input alignment ($\mathbf{T}_1$ in Figure 1) and a feature alignment ($\mathbf{T}_2$ in Figure 1) are respectively applied to the input space and feature space. The points are first mapped to a 64dimensional feature space and then mapped to a 1024dimensional feature space. A max pooling function is applied to the 1024dimensional feature space to generate a 1024length global feature vector. The global vector is then concatenated to the 64dimensional feature space which generates a 1088dimensional space. Lastly, a 2dimensional vector for each point, which represents the score for the planar part and nonplanar part, is updated from the 1088dimensional space.
For doing a comparison, a traditional asymmetric network is also introduced in this experiment. It is basically modified from the symmetric network by detaching the max pooling function from the multilayer perceptron network. The architecture of the asymmetric network is shown in Figure 2.
Instead of concatenating the global feature vector to the 64dimensional feature space, the asymmetric network simply concatenates the 64dimensional feature space and the 1024dimensional feature space, and generates the 2dimensional scores from that.
The experiment is conducted in the following pattern: first, prepare the data for training and testing; then feed the training data respectively to the symmetric network and asymmetric network, and find the besttrained model according to the minimum total loss; lastly, compare the plane detection results of symmetric network and asymmetric network.
This experiment uses data from the ShapeNetPart dataset. I chose 64 tables, which have a significant planar surface, from the table repository for training, and 8 for testing and evaluation.
Each the point cloud was previously undersampled to a size of 2048 points, using a random sample. The point clouds for training were written into an HDF5 file, which contains 2 views, points
and pid
, respectively recording the point coordinate and the planar information associated with each point.
The planar parts were marked out manually on the original data. Figure 3 shows a few examples of table objects for training.
Basically, a point cloud contains more points in the nonplanar part than points in the planar part. In order to handle this unbalanced data, I used a weighted crossentropy function to calculate the mean loss for each epoch. The planar part is assigned with a weight of 0.7 and the nonplanar part is assigned with a weight of 0.3.
Both networks are trained for 150 epochs. The plot of mean loss for training the symmetric network is shown in Figure 4.
According to Figure 4, the total mean loss is stuck at a very low value after the 100th epoch, so I chose the trained model from the 130th epoch for testing.
Besides, Figure 5 is the plot of mean loss for training the asymmetric network.
The loss value stays low after the 100th epoch, and there is a fluctuation around the 130th epoch. Such fluctuation may be caused by overfitting, so I chose the trained model from the 110th epoch for testing.
The test set has a size of 8 objects. The test result for the model of the symmetric network shows an accuracy of 83.4534% and an Intersection over Union (IoU) of 71.1421%. Figure 6 illustrates the plane detection result in the test set using the model generated by the symmetric network.
The result shows a fairly good performance of neural networks doing plane detection for objects from a single category. The accuracy could even possibly rise if a larger training set is prepared. The model seems to favor a table object with a more normal shape, i.e., a table with a square tabletop and four straight legs. For tables without a regular shape, the classification accuracy is relatively lower, and the model tends to misclassify the points in the middle of the tabletop.
The asymmetric network shows a similar test result, which comes out with an accuracy of 85.7117% and an IoU of 75.0279%. The plane detection results of the asymmetric network are shown in Figure 7 below.
Similar to the results of the symmetric network, the model based on the asymmetric network shows a good performance on objects with a more regular shape. It may also fail to classify the points in the middle of the tabletop. Furthermore, the asymmetric network could also misclassify a few points on the legs of a table, which is shown in the 1st, 2nd, and 8th objects, and such a pattern is not observed in the results of the symmetric network.
In this experiment, although the asymmetric network shows a slightly higher classification accuracy, we cannot conclude that the asymmetric network has a better performance for doing the plane detection work. Since only one category of object is included in the experiment, the global vector generated in the symmetric function actually does not make an effort. In further experiments, we can introduce more categories of objects and see how the networks will work on more complicated shapes.
This experiment shows the potential of neural networks for doing plane detection. According to the experiment results, misclassification is resulting in holes on the detected planes. This is a not a very severe problem for plane detection tasks, as we can apply a 3D Hough transform afterwards, which is robust to missing and contaminated data, to the points of detected planar parts to generate the plane information.
All the code and data of this experiment can be found over my GitHub repository IsaacGuan/PointNetPlaneDetection.
]]>We can download raw data from a certain 3D data repositories, for instance, the ShapeNetPart dataset. The data directly derived from those repositories is basically in the PTS file format, which is a set of unordered point coordinates with no headers or trailers. This actually makes things easier, as we can directly read the PTS file line by line and store the point cloud into an array lines
. For example, before generating HDF5 datasets, we want that each point cloud has the same length. Thus, a simple subsampler can be applied to the PTS files. The following code snippet shows a random sampler subsampling the point cloud to 2048 points.


PLY is a very famous file format that stores 3D data. It has headers to specify the variation and elements of the PLY file. Thus it could be a bit more complicated to deal with such data than PTS data. Luckily, we can find some readymade tools to read PLY files, e.g., the plyfile, which is able to read the numerical data from the PLY file as a NumPy structured array. The installation of this tool is pretty easy, we can get it directly via pip.


For sure, prior to this, we should also have the NumPy installed.


The deserialization and serialization of PLY file data are done through PlyData
and PlyElement
instances, so we have to first import them. Besides, the NumPy module also needs to be loaded.


Then we can start to read a PLY file. Concretely, the code is as follows.


We use the h5py package as the interface to the HDF5 data format.


We first import this package.


For creating an HDF5 file, we use the h5py.File()
function to initialize it, which takes two arguments. The first argument provides the filename and location, the second the mode. We’re writing the file, so we provide a w
for write access.


Then we need to define the shape and type of the data to write to the HDF5 file.


After filling data
with the point clouds information read from the PTS or PLY files, we can write it to the HDF5 file f
, using the create_dataset
function associated with it, where we provide a name for the dataset, and the NumPy array.


I have a very concrete example of providing data for PointNet in my GitHub repository IsaacGuan/PointNetPlaneDetection. We can take the script here as a reference.
]]>The first thing is to create a workspace
directory in the user’s home directory and a 3rdparty
subdirectory under workspace
. Thus we can define the workspace path WORKSPACE_DIR
as $ENV{HOME}/workspace
and the third party dependencies path THIRD_PARTY_DIR
as ${WORKSPACE_DIR}/3rdparty
. In the following steps, we will store all the implementations of RAPter tools and experimental data in workspace
and download all the dependencies in 3rdparty
.
RAPter requires several dependencies, including OpenCV, PointCloudLibrary, CoinBonmin, and libfbi. Prior to downloading and installing all these dependences, we need to first get prepared with a few tools, including Git, SVN, and CMake via the following commands.


To install the latest version of OpenCV, we can use the installation script here. We first download this script in the 3rdparty
directory, and then execute:


Type the sudo
password and OpenCV will be installed.
There is no readymade script for PointCloudLibrary, so the installation of it could be a bit tedious.
The PointCloudLibrary also needs a bunch of prerequisite tools. We use the commands below to have them set up.


In the 3rdparty
directory, PointCloudLibrary is obtained by:


Now we get into pcl
, create a release
directory in it, and then follow the cmake
build process.


Once the build is finished, we install it by:


As usual, we need to install the prerequisites:


Then we download CoinBonmin in 3rdparty
:


CoinBonmin also requires a third party solver. To get it, we have to first go to the directory CoinBonminstable/ThirdParty/Mumps
and run:


To compile and install CoinBonmin, we have to go back to the root directory of it, which is CoinBonminstable
, and create a build
directory in it, then run the cmake
process.


We only have to download libfbi in 3rdparty
, no prerequisites or installation is required.


Finally, all the dependencies are satisfied. We first get the RAPter in the workspace
directory:


Then get into the root of RAPter, which is RAPter/RAPter
, and build it.


Besides, we may also need to change the default directories of dependencies in CMakelist.txt
. For example, following this article, the directory of PointCloudLibrary PCL_DIR
should be ${THIRD_PARTY_DIR}/pcl/
instead of ${THIRD_PARTY_DIR}/pcltrunk2/install/share/pcl1.8/
.
In order to implement a Tutte embedding, we have to use two vectors $\mathbf{u}$ and $\mathbf{v}$ to store the parameterized coordinates, and another two vectors $\bar{\mathbf{u}}$ and $\bar{\mathbf{v}}$ to store the boundary vertices. For the interior vertex $i$, we can directly apply the following equation to it to build the barycentric mapping:$$\begin{equation}\sum_{}a_{i,j}\begin{pmatrix}u_j\\v_j\end{pmatrix}a_{i,i}\begin{pmatrix}u_i\\v_i\end{pmatrix}=0,\tag{1}\label{eq:1}\end{equation}$$where $j$ denotes the adjacent vertices of $i$.
For those vertices on the boundary, we use the equation below and set $a_{i,i}=1$, to have them fixed on a convex shape:$$\begin{equation}\sum_{}a_{i,i}\begin{pmatrix}u_i\\v_i\end{pmatrix}=\begin{pmatrix}\bar{u}_i\\\bar{v}_i\end{pmatrix}.\tag{2}\label{eq:2}\end{equation}$$
Thus, we can build the two linear systems below:$$\begin{equation}\begin{cases}\mathbf{A}\mathbf{u}=\bar{\mathbf{u}},\\\mathbf{A}\mathbf{v}=\bar{\mathbf{v}},\end{cases}\tag{3}\label{eq:3}\end{equation}$$where $\mathbf{A}=(a_{i,j})$ is a sparse matrix storing all the weights $a_{i,j}$. And we can find the parameterized coordinates by solving for the two linear systems.
I chose a disk as the convex shape for fixing the boundary vertices and used the geometryprocessingjs library for the data structure of meshes.
I tried three different weight sets, namely the uniform Laplacian weights, the LaplaceBeltrami weights, and the mean value weights.
The uniform Laplacian weight set is easiest to build, but it does not take the geometric feature of the mesh into consideration. We can simply set the entries of the weight matrix in the following manner:$$\begin{equation}\begin{cases}a_{i,j}=1,\\a_{i,i}=\sum_{}a_{i,j}=D_i,\end{cases}\tag{4}\label{eq:4}\end{equation}$$where $D_i$ is the degree of vertex $i$, i.e., the number of edges connecting $i$.
We can also derive weights from the LaplaceBeltrami operator:$$\begin{equation}\begin{cases}a_{i,j}=\frac{1}{2A_i}(\cot\alpha_{i,j}+\cot\beta_{i,j}),\\a_{i,i}=\sum_{}a_{i,j},\end{cases}\tag{5}\label{eq:5}\end{equation}$$where $A_i$ denotes the area of the Voronoi region of the vertex $i$, $\alpha_{i,j}$ and $\beta_{i,j}$ denote respectively the two corners opposite to the edge $(i,j)$, as shown in Figure 1.
For calculating the value of $A_i$, we can use:$$\begin{equation}A_i=\frac{1}{8}\sum_{}(\cot\alpha_{i,j}+\cot\beta_{i,j})\\mathbf{x}_i\mathbf{x}_j\^2,\tag{6}\label{eq:6}\end{equation}$$where $\\mathbf{x}_i\mathbf{x}_j\$ is the length of edge $(i,j)$.
As far as one triangle from the onering neighborhood of a vertex is concerned, which is shown in Figure 2, the Voronoi region inside that triangle can be calculated as:$$\begin{equation}W=\frac{1}{8}\sum_{}(\\mathbf{x}_i\mathbf{x}_j\^2\cot\alpha+\\mathbf{x}_i\mathbf{x}_j\^2\cot\beta).\tag{7}\label{eq:7}\end{equation}$$
As the data structure of mesh is based on halfedge, we can simply find the neighbor of a certain halfedge by retrieving its previous halfedge, e.g., in Figure 2, halfedge $\langle j_2,i\rangle$ is previous to halfedge $\langle i,j_1\rangle$. Thus, by traversing outgoing halfedges associated on a vertex, we can easily calculate the Voronoi region of it using the following code snippet:


The LaplaceBeltrami weight takes the geometry of a mesh into account, but it could be negative with obtuse triangles. Based on the mean value theorem, we can derive the mean value weights, which are always positive and generate onetoone mappings:$$\begin{equation}\begin{cases}a_{i,j}=\frac{1}{\\mathbf{x}_i\mathbf{x}_j\}(\tan\frac{\delta_{i,j}}{2}+\tan\frac{\gamma_{i,j}}{2}),\\a_{i,i}=\sum_{}a_{i,j},\end{cases}\tag{8}\label{eq:8}\end{equation}$$where $\\mathbf{x}_i\mathbf{x}_j\$ is the length of edge $(i,j)$, $\delta_{i,j}$ and $\gamma_{i,j}$ are two neighboring corners on both sides of edge $(i,j)$, as shown in Figure 3.
In the real implementation, like what we did in calculating the Voronoi region, we can still take advantage of the sequence of halfedges to find the corresponding corners:


The results of Tutte embedding are shown in Figure 4, applied to different objects with different weight sets.
We can conclude from the results that the LaplaceBeltrami weight set and mean value weight set can better preserve the shape of triangles in the original mesh. This leads to the interior holes of a mesh to enlarge, which is clearly shown in the beetle. For better illustrating this property, we can take a close look at the mesh (Figure 5).
Figure 5 takes the same 200×200 pixels parts from the screenshots of the parametrizations of the cow object. It shows that the uniform Laplacian weight can cause the triangles on the parameterization to become more regular, but the triangles on the parameterized mesh of the LaplaceBeltrami weight and the mean value weight are more similar to those on the original mesh.
Besides, the LaplaceBeltrami weight and the mean value weight can even better preserve the patterns on the mesh. As shown in Figure 6, the features on the face object (i.e., the mouth and the eyes) are more distinguishable if we use the LaplaceBeltrami weight or the mean value weight.
I have also uploaded the project to this blog: /projects/tutteembedding, we can go there to see how the algorithms work.
]]>Similar to a line in 2D space, a plane in 3D can be described using a slope–intercept equation, where $k_x$ is the slope in xdirection, $k_y$ is the slope in ydirection, and $b$ is the intercept on zaxis:$$\begin{equation}z=k_xx+k_yy+b.\tag{1}\label{eq:1}\end{equation}$$With $\eqref{eq:1}$ we can simply parameterize a plane as $(k_x,k_y,b)$.
However, values of $k_x$, $k_y$, and $b$ are unbounded, which would pose a problem when we try to parameterize a vertical plane. Thus, for computational reasons, we can write the plane equation in the Hesse normal form, where $\theta$ is the angle between the normal vector of this plane and the xaxis, $\phi$ is the angle between the normal vector and zaxis, and $\rho$ is the distance from the plane to the origin:$$\begin{equation}x\cos\theta\sin\phi+y\sin\phi\sin\theta+z\cos\phi=\rho.\tag{2}\label{eq:2}\end{equation}$$As shown in Figure 1, the plane can be parameterized as $(\theta,\phi,\rho)$.
To find planes in a 3D point cloud, we have to calculate the Hough transform for each point, which is to say that we parameterize every possible plane that goes through every point in the $(\theta,\phi,\rho)$ Hough space. For instance, Figure 2 shows the parameterization of three points $(0,0,1)$, $(0,1,0)$, and $(1,0,0)$. Each point is marked as a 3D sinusoid curve in Hough space and the intersection which is marked in black denotes the plane defined by the three points.
Basically, the algorithm for Hough transform can be described as a voting method, where we discretize the Hough space with a bunch of $(\theta,\phi,\rho)$ cells. A data structure called accumulator then is needed to store all these cells with a scoring parameter for every cell. Incrementing a cell means increasing the score of it by +1. Each point votes for all cells of $(\theta,\phi,\rho)$ that define a plane on which it may lie.
A most basic and naïve Hough transform algorithm for plane detection is outlined as follows:
Begin
Step 1: Traverse all the points in the point cloud $P$;
Step 2: For each point $\mathbf{p}_i$ in $P$, vote for all the $A(\theta,\phi,\rho)$ cells in the accumulator $A$ defined by this point;
Step 3: After the whole iteration, search for the most prominent cells in the accumulator $A$, that define the detected planes in $P$.
End
The standard Hough transform is performed in two stages: incrementing the cells, which needs $O(P\cdot N_\phi\cdot N_\theta)$ operations, and searching for the most prominent cells, which takes $O(N_\rho\cdot N_\phi\cdot N_\theta)$ time, where $P$ is the size of the point cloud, $N_\phi$ is the number of cells in direction of $\phi$, $N_\theta$ in direction of $\theta$, and $N_\rho$ in direction of $\rho$.
In the standard Hough transform, the size of the point cloud $P$ is usually much larger than the number $N_\rho \cdot N_\phi \cdot N_\theta$ of cells in the accumulator array. We can simply reduce the number of points to improve the computational expense. Thus, the standard Hough transform can be adapted to the probabilistic Hough transform, which is shown as follows:
Begin
Step 1: Determine the size value $m$ and the threshold value $t$;
Step 2: randomly select $m$ points to create $P^m\subset P$;
Step 3: Do the standard Hough transform on the point set $P^m$;
Step 4: Delete the cells from the accumulator $A$ with a value that does not reach $t$, and search for the most prominent cells in $A$, that define the detected planes in $P$.
End
The $m$ points (with $m<P$) are randomly selected from the point cloud $P$, so the dominant part of the runtime is proportional to $O(m\cdot N_\phi\cdot N_\theta)$. However, the optimal choice of $m$ and the threshold $t$ depends on the actual problem, e.g., the number of planes, or the noise in the point cloud.
In order to find the optimal subsample of the point cloud, we can use an adaptive method to determine the reasonable number of selected points. The adaptive probabilistic Hough transform monitors the accumulator. The structure of the accumulator changes dynamically during the voting phase. As soon as stable structures emerge and turn into significant peaks, voting is terminated.
Begin
Step 1: Check the stability order $m_k$ of the list of $k$ peaks $S_k$ in the accumulator, if it reaches the threshold value $t_k$ then finish;
Step 2: Randomly select a small subset $P^m\subset P$;
Step 3: Do the standard Hough transform on the point set $P^m$;
Step 4: Merge the active list of peaks $S_k$ with the previous one, determine the stability order $m_k$, goto Step 1;
End
In this algorithm, The stability is described by a set $S_k$ of $k$ peaks in the list, if the set contains all largest peaks before and after one update phase. The number $m_k$ of consecutive lists in which $S_k$ is stable is called the stability order of $S_k$.
The progressive probabilistic Hough transform calculates stopping time for random selection of points. The algorithm stops whenever a cell count exceeds a threshold.
Begin
Step 1: Check the input point cloud $P$, if it is empty then finish;
Step 2: Update the accumulator with a single point $\mathbf{p}_i$ randomly selected from $P$;
Step 3: Remove $\mathbf{p}_i$ from $P$ and add it to $P_\text{voted}$;
Step 4: Check if the highest peak in the accumulator that was modified by the new point is higher than the threshold $t$, if not then goto Step 1;
Step 5: Select all points from $P$ and $P_\text{voted}$ that are close to the plane defined by the highest peak and add them to $P_\text{plane}$;
Step 6: Search for the largest connected region $P_\text{region}$ in $P_\text{plane}$ and remove from $P$ all points in $P_\text{region}$;
Step 7: Reset the accumulator by unvoting all the points in $P_\text{region}$;
Step 8: If the area covered by $P_\text{region}$ is larger than a threshold, add it to the output list, goto Step 1;
End
In this algorithm, $P_\text{voted}$ is the point set of all the voted points before a plane is detected, $P_\text{plane}$ is the set of points in the detected planes, and $P_\text{region}$ denotes the largest connected region in $P_\text{plane}$. For determining the stopping time, the threshold $t$ is predicated on the percentage of votes for one cell from all points that have voted.
As we know the fact that a plane is defined by three points. For detecting planes, three points from the input space are mapped onto one point in the Hough space. When a plane is represented by a large number of points, it is more likely that three points from this plane are randomly selected. Eventually, the cells corresponding to actual planes receive more votes and are distinguishable from the other cells. Inspired by this idea, we can come up with an algorithm described as follows:
Begin
Step 1: Check the input point cloud $P$, if it is empty then finish;
Step 2: Randomly pick three points $\mathbf{p}_1$, $\mathbf{p}_2$, and $\mathbf{p}_3$ from $P$;
Step 3: If $\mathbf{p}_1$, $\mathbf{p}_2$, and $\mathbf{p}_3$ fulfill the distance criterion, calculate plane $(\theta,\phi,\rho)$ spanned by $\mathbf{p}_1$, $\mathbf{p}_2$, and $\mathbf{p}_3$, and increment cell $A(\theta,\phi,\rho)$ in the accumulator space;
Step 4: If the count of $A(\theta,\phi,\rho)$ reaches the threshold $t$, $(\theta,\phi,\rho)$ parameterize the detected plane, delete all points close to $(\theta,\phi,\rho)$ from $P$, and reset the accumulator $A$;
Step 5: Goto Step 1;
End
This algorithm simply decreases the number of cells touched by exploiting the fact that a curve with $n$ parameters is defined by $n$ points. And also note that, if points are very far apart, they most likely do not belong to one plane. To take care of this and to diminish errors from sensor noise a distance criterion is introduced: $\mathrm{dist}(\mathbf{p}_1,\mathbf{p}_2,\mathbf{p}_3)\leq\mathrm{dist}_\text{max}$, i.e., the maximum pointtopoint distance between $\mathbf{p}_1$, $\mathbf{p}_2$, and $\mathbf{p}_3$ is below a fixed threshold.
An inappropriate accumulator may lead to detection failures of some specific planes and difficulties in finding local maxima, displays low accuracy, large storage space, and low speed. A tradeoff has to be found between a coarse discretization that accurately detects planes and a small number of cells in the accumulator to decrease the time needed for the Hough transform.
For the standard implementation of the 2D Hough transform, the Hough space is divided into $N_\rho\times N_\theta$ rectangular cells. The size of the cells is variable and is chosen problemdependent. Using the same subdivision for the 3D Hough space by dividing it into cuboid cells results in the patches seen in Figure 3. The cells closer to the poles are smaller and comprise less normal vectors. This means voting favors the larger equatorial cells.
We can also project the unit sphere $S^2$ onto the smallest cube that contains the sphere using the diffeomorphism:$$\begin{equation}\phi:S^2\to\mathrm{cube},s\mapsto\s\_\infty.\tag{3}\label{eq:3}\end{equation}$$
Each face of the cube is divided into a regular grid, which is shown in Figure 4. With this design of the accumulator, the difference of size between the patches on the unit sphere is negligible.
The commonly used design, the accumulator array, which is shown in Figure 3, causes the irregularity between the patches on the unit sphere. To solve this issue, we can simply customize the resolution in polar coordinates depending on the position of the sphere. The resolution of the longitude $\phi$ is kept as for the accumulator array, which is defined as $\phi^\prime$. In the $\theta$ direction, the largest latitude circle is the equator located at $\phi=0$. For the unit sphere it has the $l_\text{max}=2\pi$. The length of the latitude circle in the middle of the segment located above $\phi_i$ is given by $l_i=2\pi(\phi+\phi^\prime)$. The step width in $\theta$ direction for each slice is now computed as $\theta_{\phi_i}=\frac{360^\circ\cdot l_\text{max}}{l_i\cdot N_\theta}$, where $N_\theta$ is a constant that can be customized. The resulting design is illustrated in Figure 5.
There are a lot of mesh libraries that give us a data structure such that we can use to build a mesh for a 3D object. What I am using is the geometryprocessingjs library. It is a library written in JavaScript which constructs a mesh based on halfedge. It also gives plenty of predefined functions regarding the components of a mesh e.g. halfedge, vertex, edge, face, etc. Besides, it also includes a linear algebra library which makes it handy for users to do matrixvector operations.
I implemented two algorithms for mesh smoothing, a mean filtering algorithm, and a weighted mean filtering algorithm.
A most naïve mean filtering algorithm goes as follows:
Begin
Step 1: Create a hash map $\mathrm{positions}$ mapping each vertex with its position to be updated;
Step 2: For each vertex $\mathbf{v}$ in the mesh, calculate $\mathrm{positions}[\mathbf{v}]$ by the average position of the neighbor vertices in the onering neighborhood of $\mathbf{v}$;
Step 3: After the whole iteration, update the positions of vertices in the mesh by the hash map $\mathrm{positions}$.
End
However, if the object has some holes in it, this algorithm could cause the holes to enlarge after times of iteration, which is shown in Figure 1.
Sometimes, we don’t like such distortion, so it is necessary to preserve the boundary of holes on the mesh. We can add a step to the algorithm to constrain the vertices of boundary edges:
Begin
Step 1: Create a hash map $\mathrm{positions}$ mapping each vertex with its position to be updated;
Step 2: For each vertex $\mathbf{v}$ in the mesh, calculate $\mathrm{positions}[\mathbf{v}]$ by the average position of the neighbor vertices in the onering neighborhood of $\mathbf{v}$;
Step 3: To preserve the boundary of holes on the mesh, if vertex $\mathbf{v}$ is on the boundary, update $\mathrm{positions}[\mathbf{v}]$ to the original position of vertex $\mathbf{v}$ in the mesh;
Step 4: After the whole iteration, update the positions of vertices in the mesh by the hash map $\mathrm{positions}$.
End
The weighting method simply weights each neighboring vertex by the area of the two faces incident to it. The weighted mean filtering algorithm goes like this:
Begin
Step 1: Create a hash map $\mathrm{positions}$ that maps each vertex to its position to be updated;
Step 2: For each vertex $\mathbf{v}$ in the mesh, traverse its neighborhood vertices. And for each neighbor vertex $\mathbf{v}^\ast$ calculate the weight value $w^\ast$ on each neighborhood vertex by adding the area of the two incident faces together;
Step 3: After the traversal of neighborhood vertices, calculate the total weight $w$ on the vertex $\mathbf{v}$ by accumulating every $w^\ast$, and calculate $\mathrm{positions}[\mathbf{v}]=\frac{\sum_{}w^\ast\cdot\mathbf{v}^\ast}{w}$;
Step 4: To preserve the boundary of holes on the mesh, if vertex $\mathbf{v}$ is on the boundary, update $\mathrm{positions}[\mathbf{v}]$ to the original position of vertex $\mathbf{v}$ in the mesh;
Step 5: After the whole iteration, update the positions of vertices in the mesh by the hash map $\mathrm{positions}$.
End
However, in the implementation, I found this could cause the faces of the mesh overlapping one another after several times of iterations, as the position of vertex could fall outside its onering neighborhood. As shown in Figure 2, overlapping occurs after 5 times of iteration.
In order to prevent such thing from happening, I tried to find those vertices which could fall outside its neighborhood during an iteration. The onering neighborhood of one vertex is actually a starshaped polygon so that we can use the leftturn/rightturn check to decide whether the vertex is inside it or not.
To find out the starshaped neighborhood of a certain vertex $\mathbf{v}$, we should first project all the neighbors onto the plane which is defined by vertex $\mathbf{v}$ and its normal vector $\mathbf{n}$. We can find the projection $\mathbf{q}=(x^\prime,y^\prime,z^\prime)$ of vertex $\mathbf{p}=(x,y,z)$ on the plane that defined by the normal $\mathbf{n}=(a,b,c)$ and vertex $\mathbf{v}=(x_0,y_0,z_0)$ in the way below.
There are following implicit geometric relationships between $\mathbf{p}$, $\mathbf{q}$, $\mathbf{n}$, and $\mathbf{v}$: $(\mathbf{q}\mathbf{p})\parallel\mathbf{n}$ and $(\mathbf{q}\mathbf{v})\perp\mathbf{n}$.
The following equation can be derived from $(\mathbf{q}\mathbf{p})\parallel\mathbf{n}$:$$\begin{equation}\frac{x^\primex}{a}=\frac{y^\primey}{b}=\frac{z^\primez}{c}=t.\tag{1}\label{eq:1}\end{equation}$$
And from $(\mathbf{q}\mathbf{v})\perp\mathbf{n}$, we can derive:$$\begin{equation}a(x^\primex_0)+b(y^\primey_0)+c(z^\primez_0)=0.\tag{2}\label{eq:2}\end{equation}$$
Combining $\eqref{eq:1}$ and $\eqref{eq:2}$, we can solve for $t$:$$\begin{equation}t=\frac{ax_0+by_0+cz_0(ax+by+cz)}{a^2+b^2+c^2}.\tag{3}\label{eq:3}\end{equation}$$
Then we can solve for the projection $\mathbf{q}=(x^\prime,y^\prime,z^\prime)$ by combining $\eqref{eq:1}$ and $\eqref{eq:3}$.
As the data structure of mesh is based on halfedge, and all the faces are defined by halfedges in counterclockwise order. Thus, for each vertex $\mathbf{v}$, we can simply get the boundary edges of a starshaped neighborhood in counterclockwise order, project them along with the position to be updated $\mathrm{positions}[\mathbf{v}]$ onto the plane that is defined by vertex $\mathbf{v}$ and its normal vector $\mathbf{n}$. Then we can do the leftturn/rightturn check on that plane to see whether the new position for vertex $\mathbf{v}$ $\mathrm{positions}[\mathbf{v}]$ is outside its neighborhood polygon or not.
So the modified algorithm goes as follows:
Begin
Step 1: Create a hash map $\mathrm{positions}$ that maps each vertex to its position to be updated;
Step 2: For each vertex $\mathbf{v}$ in the mesh, traverse its neighborhood vertices. And for each neighbor vertex $\mathbf{v}^\ast$ calculate the weight value $w^\ast$ on each neighborhood vertex by adding the area of the two incident faces together;
Step 3: After the traversal of neighborhood vertices, calculate the total weight $w$ on the vertex $\mathbf{v}$ by accumulating every $w^\ast$, and calculate $\mathrm{positions}[\mathbf{v}]=\frac{\sum_{}w^\ast\cdot\mathbf{v}^\ast}{w}$;
Step 4: Check whether $\mathrm{positions}[\mathbf{v}]$ falls outside its onering neighborhood. If so, use the mean filtering algorithm to update the new position;
Step 5: To preserve the boundary of holes on the mesh, if vertex $\mathbf{v}$ is on the boundary, update $\mathrm{positions}[\mathbf{v}]$ to the original position of vertex $\mathbf{v}$ in the mesh;
Step 6: After the whole iteration, update the positions of vertices in the mesh by the hash map $\mathrm{positions}$.
End
As for the mean filtering algorithm, Figure 3 shows the results after applying it to different objects with different step numbers.
As for the weighted mean filtering algorithm, Figure 4 shows the results after applying it to different objects with different step numbers.
We can conclude that the results of both algorithms basically look alike, and both of them could cause the volume of the object to shrink if we apply too many times.
I have also uploaded the project to this blog: /projects/meshsmoothing, we can go there to see how exactly the algorithms work.
]]>The Voronoi diagram of a set of points, also known as Thiessen polygons, is a partitioning of a plane into regions by a set of continuous polygons consisting of perpendicular bisectors of the connecting lines of two adjacent points. These regions are called Voronoi cells. And for each point in the set, there is a corresponding Voronoi cell consists of all points closer to that point than to any other.
The Delaunay triangulation of a set of points is dual to its Voronoi diagram. It is a collection of connected but nonoverlapping triangles, and the outer circumcircle of these triangles does not contain any other points in this set.
There are a lot of ways to generate a Voronoi diagram from a set of points in the plane. In this implementation, I obtained the Voronoi diagram from generating its dual, the Delaunay triangulation. Generally speaking, for the set of $n$ points $P=\{\mathbf{p}_1,\mathbf{p}_2,\ldots,\mathbf{p}_n\}$ in $\mathbb{R}^2$, the algorithm goes in this way: the Delaunay triangulation of the set of points is firstly generated, then we calculate the center of the circumcircle of each triangle, and finally, we connect these centers with straight lines and form the polygon mesh generated from the vertices of the triangles.
I implemented this algorithm in the objectoriented language, so the design of the data structure is in the form of classes.
Point:


Voronoi edge:


Delaunay edge:


Triangle:


On top of that, I defined a point list and a triangle list under the Collections class to store all the points in the point set $P$ and all the triangles during the procedure of triangulation as global variables:


Thus, in the Point
class, DelaunayEdge
class, and Triangle
class, I can use an integer to represent the index of a certain point or triangle from the global lists and retrieve it directly.
The algorithm of building up a Vonoroi diagram goes in this way:
Begin
Step 1: Obtain the Delaunay triangulation by generating the list of Delaunay edges.
Step 2: Traverse all the Delaunay edges.
Step 3: For each Delaunay edge, traverse the two triangle lists stored with the starting point and the endpoint, and find the two same triangles in the two lists which are the adjacent triangles of this Delaunay edge.
Step 4: Construct a Voronoi edge by connecting the two centers of the circumcircles of the two adjacent triangles and add it to the Voronoi edge list.
End
I planned to apply the Bowyer–Watson algorithm for computing the Delaunay triangulation. A most naïve Bowyer–Watson algorithm goes like this:
Begin
Step 1: Construct a “super” triangle that covers all the points from the point set, add it to the Delaunay triangle list.
Step 2: Insert points from the point set $P=\{\mathbf{p}_1,\mathbf{p}_2,\ldots,\mathbf{p}_n\}$ to the “super” triangle one by one.
Step 3: For each point $\mathbf{p}_i$ inserted, traverse the Delaunay triangle list to find all the triangles whose circumcircles cover this point $\mathbf{p}_i$ as invalid triangles, delete these triangles from the Delaunay triangle list and delete all the common edges of these triangles, and leave a starshaped polygonal hole.
Step 4: Connect the point $\mathbf{p}_i$ to all the vertices of this starshaped polygon, and add the newly formed triangles to the Delaunay triangle list.
Step 5: After all the points are inserted, obtain the Delaunay edge list from the Delaunay triangle list, and delete the edges from the Delaunay edge list that contains a vertex of the original “super” triangle.
End
The following pictures can better illustrate the key steps of this algorithm. As shown in Figure 1, when a new point is inserted, all the triangles whose circumcircles contain this point will be found. The common edges of these triangles, which are highlighted in yellow, will be deleted, leaving the starshaped boundary in red.
And then, the inserted point will be connected to all the vertices of the starshaped polygon, as shown in Figure 2, the new Delaunay triangles will be formed.
However, the aforementioned naïve manner of Delaunay triangulation is clearly an $O(n^2)$ time algorithm that is incapable of handling a massive amount of points.
For improving its efficiency, we can first sort the set of points by xcoordinates and then use an open list and a closed list to store all the Delaunay triangles. In each time a point is inserted, all the triangles with circumcircles to the left of the inserting point are put into the closed list and removed from the open list. All the new triangles generated in an insertion are put into the open list. So in each time of insertion, we just have to traverse the open list to find the invalid triangles, the length of the sequential search of triangles is much reduced.
Besides, in order to derive the Voronoi diagram, when a new point is inserted, all the newly formed triangles that are incident on the point are put into the triangle list that is stored with the point.
Thus, the optimized algorithm goes as follows:
Begin
Step 1: Sort the points in the point set $P=\{\mathbf{p}_1,\mathbf{p}_2,\ldots,\mathbf{p}_n\}$ by xcoordinate.
Step 2: Construct a “super” triangle that covers all the points from the point set, add it to the open list.
Step 3: Insert points from $P$ to the “super” triangle one by one in ascending order of xcoordinates.
Step 4: For each point $\mathbf{p}_i$ inserted, traverse the open list to: 1) find all the triangles with circumcircles lying to the left of the point $\mathbf{p}_i$, delete these triangles from the open list and add them to the closed list; 2) find all the triangles with circumcircles covering this point $\mathbf{p}_i$ as invalid triangles, delete these triangles from the open list and the triangle list stored with $\mathbf{p}_i$, delete all the common edges of these triangles, leaving a starshaped polygonal hole.
Step 5: Connect the point $\mathbf{p}_i$ to all the vertices of this starshaped polygon, and add the newly formed triangles to the open list and the triangle list stored with $\mathbf{p}_i$.
Step 6: After all the points are inserted, merge the open list and the closed list into the Delaunay triangle list, obtain the Delaunay edge list from the Delaunay triangle list, and delete the edges from the Delaunay edge list that contains a vertex of the original “super” triangle.
End
The optimized algorithm for Delaunay triangulation takes $O(n\log n)$ time. As Delaunay triangulation is a planar graph, the number of triangles incident on one point is constant, so the procedure of finding adjacent triangles takes constant time and the time of generating the Voronoi diagram is $O(n)$. Therefore, the total running time of this algorithm is $O(n\log n)$.
This algorithm is implemented in C#. Figure 3 shows the result of the implementation of this algorithm, it is capable of handling massive input of 10000 points.
The GitHub repository of this implementation is IsaacGuan/VoronoiDelaunay.
]]>Let $S$ be a set of $n$ points in $\mathbb{R}^2$. If $l$ and $l^\prime$ are two parallel lines, then the region between them is called a slab. The width of $S$ is defined to be the minimum distance between the bounding lines of any slab that contains all points of $S$.
Before presenting the widthfinding algorithm, I’d like first to characterize the width of a set of points, introduce some terminology, and prove several theorems.
When we say that $A$ is contained in $B$, it means that every point of $A$ also lies in $B$. The bounding lines of a slab could contain a vertex or an edge of the convex hull $H(S)$ of the set of points $S$.
Theorem 1: Let $l$ and $l^\prime$ be two parallel lines that define the width of $S$, both $l$ and $l^\prime$ contain a vertex of the convex hull $H(S)$ of $S$.
Proof. Assume otherwise. The width is determined by parallel lines $l$ and $l^\prime$, and $l$ contains a vertex of $H(S)$, $l^\prime$ does not, as shown in Figure 1. Then there must exist a line $l^{\prime\prime}$ closer to the set of points $S$ and produces a smaller distance. Q.E.D.
Theorem 2: Let $l$ and $l^\prime$ be two parallel lines that define the width of $S$, at least one of $l$ and $l^\prime$ contain an edge of the convex hull $H(S)$ of $S$.
Proof. Assume otherwise. The width is determined by parallel lines $l$ and $l^\prime$ which both pass a vertex but neither of them passes an edge. We can rotate the parallel lines in preferred direction of rotation which means we rotate $l$ and $l^\prime$ about the two vertices they pass to form new parallel lines $l_1$ and ${l_1}^\prime$, and the distance between $l_1$ and ${l_1}^\prime$ is smaller. This contradicts the assumption. Q.E.D.
We can divide the convex hull into 2 parts: the upper hull and the lower hull, so that when one of the parallel lines of support of $S$ meets the convex hull on the upper hull, the other is on the lower hull.
According to the duality transformation, a nonvertical line $y=kx+b$ can be transformed into the point $(k,b)$ which is formed by the slope and intercept of the line. And a point $(a,b)$ can be transformed into the line $y=k(xa)+b$ which means the set of lines that pass through it.
Hence, we define the transform of an edge of the convex hull $H(S)$ of $S$ as the slope of the line containing that edge and the transform of a vertex of $H(S)$ as the set of slopes of lines that pass through it. I.e., the line containing an edge $y=kx+b$ can be mapped to the one dimensional point $(k)$ and the vertex that lies between two edges $y=k_1x+b_1$ and $y=k_2x+b_2$ can be mapped to the closed interval $[k_1,k_2]$. As shown in Figure 2, the convex hull is transformed into a line that consists of an upper part and a lower part.
As the slopes of the edges of the convex hull are already sorted, when we transform the convex hull to a line, the upper and lower sets of points and intervals on the line are in sorted order.
As known from Theorem 1, the parallel lines of support pass through an edge and a vertex of the convex hull $H(S)$, which means the corresponding point and interval pair contained in the parallel lines must intersect on the upper and lower lines, e.g., $\mathbf{p}_5$ and $l_8$, $\mathbf{p}_6$ and $l_2$ in Figure 2.
Thus, finding the width of a set of points $S$ can be reduced to scanning the intersections of point and interval pairs on the upper and lower hulls of its convex hull $H(S)$.
To summarize, the width of a set of points $S$ can be computed in the following manner.
Begin
Step 1: Construct the convex hull $H(S)$ of $S$.
Step 2: Apply the transform to obtain two ordered sets of points and intervals.
Step 3: Scan the sets for intersections between the intervals of one set and the points of the other, generating the corresponding vertex and edge pair. For each such pair, compute the distance between the vertex and the extended edge, and note the smallest such distance. When the scan is complete, that distance is the width.
End
According to the gift wrapping algorithm and the Graham scan algorithm, the running time of finding the convex hull of a set of points can be minimized to $O(n\log h)$, where $h$ is the number of points on the convex hull. And obtaining the two ordered sets and scanning the sets both takes $O(h)$ time. Therefore, finding the width of a set of points $S$ in $\mathbb{R}^2$ takes $O(n\log h)$ time.
]]>Existence precedes essence.
During the high school years, I once kept a diary, sort of artsy lifestyle possessed by the petty bourgeoisie. And this experience told me the truth that writing is really timeconsuming, especially when you are making “sentimental twaddle” on trivial things in daily life.
Time flies. I haven’t been keeping that habit for a long time and I will be 23 this month. Like every young man in his 20s, I feel at a loss from time to time. Looking back on the past few years, seemingly I have gone through a lot of things and met up with a lot of people, but the future still remains uncertain.
Some people of this era are distressed and tend to explore the meaning of their existence. That is why I am quoting the words of JeanPaul Sartre at the beginning of this post. It tells that life could be meaningless, according to my understanding, unless you create yourself a meaning.
But I cannot say that I am an existentialist. An existentialist should be fearless, like Friedrich Nietzsche, who asserted that God is dead. I am just following the guidance of these great philosophers to add something that I believe is meaningful to my life.
Thus, I decided to build this place and record something of myself, which generally includes:
It is lucky for me to live in this digital age so that I can easily build up this small website on my own, using Github Pages as the blogging platform and Hexo as the framework. I also would like to express my thanks to fi3ework who designed this beautiful theme archer that I am using.
Anyhow, this is the very first post of my blog. Wish me happy blogging :)
]]>