Skip to content

Commit

Permalink
Initial Commit
Browse files Browse the repository at this point in the history
  • Loading branch information
Alexandre Boulch committed Apr 3, 2019
0 parents commit deabcc4
Show file tree
Hide file tree
Showing 9 changed files with 831 additions and 0 deletions.
26 changes: 26 additions & 0 deletions LICENSE.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@

# License information

Code is released under dual license depending on applications, research or commercial.

---

## COMMERCIAL PURPOSES

Please contact the ONERA [www.onera.fr/en/contact-us](www.onera.fr/en/contact-us) for additional information or directly the author Alexandre Boulch.

---

## RESEARCH AND NON COMMERCIAL PURPOSES

For research and non commercial purposes, all the code and documentation of github.com/aboulch/ConvPoint is released under the GPLv3 license:

ConvPoint: UGeneralizing discrete convolutions for unstructured point clouds
Copyright (C) 2019 ONERA, Alexandre Boulch
This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation; either version 3 of the License, or any later version.
This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA

PLEASE ACKNOWLEDGE THE AUTHORS AND PUBLICATION ACCORDING TO THE
REPOSITORY github.com/aboulch/ConvPoint OR IF NOT AVAILABLE:
"Generalizing discrete convolutions for unstructured point clouds", A.Boulch,
Eurographics Workshop on 3D Object Retrieval 2019
59 changes: 59 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
# ConvPoint: Generalizing discrete convolutions for unstructured point clouds

## Introduction

## License

Code is released under dual license depending on applications, research or commercial. Reseach license is GPLv3.

## Citation

If you use this code in your research, please consider citing:
(citation will be updated as soon as 3DOR proceedings will be released)

```
@inproceedings{boulch2019,
title={Generalizing discrete convolutions for unstructured point clouds},
author={Boulch, Alexandre},
booktitle={Eurographics Workshop on 3D Object Retrieval},
year={2019}
}
```

## Dependencies

- Pytorch
- Scikit-learn for confusion matrix computation, and efficient neighbors search
- Trimesh (for Modelnet40) for loading triangular meshes and sampling points
- TQDM for progress bars

All these dependencies can be install via conda in an Anaconda environment or via pip.

## The library

## Usage

We propose scripts for training on several point cloud datasets:
- ModelNet40 (meshes can be found [here](http://modelnet.cs.princeton.edu/)). The meshes are sampled in the code using Trimesh.
- ShapeNet *(code to be added)*
- S3DIS *(code to be added)*
- Semantic8 *(code to added)*

### ModelNet40

#### Training
```
python modelnet_classif.py --rootdir path_to_modelnet40_data
```

#### Testing

For testing with one tree per shape:
```
python modelnet_classif.py --rootdir path_to_modelnet40_data --savedir path_to_statedict_directory --test
```
For testing with more than one tree per shape: *(this code is not optimized at all and is very slow)*
```
python modelnet_classif.py --rootdir path_to_modelnet40_data --savedir path_to_statedict_directory --test --ntree 2
```

1 change: 1 addition & 0 deletions layers/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
from .conv import PtConv
82 changes: 82 additions & 0 deletions layers/conv.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
import torch
import torch.nn as nn
import torch.nn.functional as F
import numpy as np
import math

class PtConv(nn.Module):
def __init__(self, input_features, output_features, n_centers, dim, use_bias=True):
super(PtConv, self).__init__()

# Weight
self.weight = nn.Parameter(
torch.Tensor(input_features, n_centers, output_features), requires_grad=True)
bound = math.sqrt(3.0) * math.sqrt(2.0 / (input_features + output_features))
self.weight.data.uniform_(-bound, bound)

# bias
self.use_bias = use_bias
if use_bias:
self.bias = nn.Parameter(torch.Tensor(output_features), requires_grad=True)
self.bias.data.uniform_(0,0)

# centers
center_data = np.zeros((dim, n_centers))
for i in range(n_centers):
coord = np.random.rand(dim)*2 - 1
while (coord**2).sum() > 1:
coord = np.random.rand(dim)*2 - 1
center_data[:,i] = coord
self.centers = nn.Parameter(torch.from_numpy(center_data).float(),
requires_grad=True)

# MLP
self.l1 = nn.Linear(dim*n_centers, 2*n_centers)
self.l2 = nn.Linear(2*n_centers, n_centers)
self.l3 = nn.Linear(n_centers, n_centers)


def forward(self, input, points, indices, next_points=None, normalize=True):

batch_size = input.size(0)
n_pts = input.size(1)

# compute indices for indexing points
add_indices = torch.arange(batch_size).type(indices.type()) * n_pts
indices = indices + add_indices.view(-1,1,1)

# get the features and point cooridnates associated with the indices
features = input.view(-1, input.size(2))[indices]
pts = points.view(-1, points.size(2))[indices]

# if the projecting points is provided, use it, otherwise, it is gravity center
if next_points is not None:
pts = pts - next_points.unsqueeze(2)
else:
pts = pts - pts.sum(2, keepdim=True) / indices.size(2)

# normalize to unit ball, or not
if normalize:
maxi = torch.sqrt((pts**2).sum(3).max(2)[0])
maxi[maxi==0] = 1
pts = pts / maxi.view(maxi.size()+(1,1,))

# compute the distances
dists = pts.view(pts.size()+(1,)) - self.centers
dists = dists.view(dists.size(0), dists.size(1), dists.size(2), -1)
dists = F.relu(self.l1(dists))
dists = F.relu(self.l2(dists))
dists = F.relu(self.l3(dists))
dists = dists.unsqueeze(3)

# compute features
features = features.view(features.size()+(1,)) * dists
features = features.mean(2)
features = features.view(features.size()+(1,)) * self.weight
features = features.sum([2,3])

# add a bias
if self.use_bias:
features = features + self.bias

return features
76 changes: 76 additions & 0 deletions metrics.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import numpy as np

#==============================================
#==============================================
# STATS
#==============================================
#==============================================


def stats_overall_accuracy(cm):
"""Compute the overall accuracy.
"""
return np.trace(cm)/cm.sum()


def stats_pfa_per_class(cm):
"""Compute the probability of false alarms.
"""
sums = np.sum(cm, axis=0)
mask = (sums>0)
sums[sums==0] = 1
pfa_per_class = (cm.sum(axis=0)-np.diag(cm)) / sums
pfa_per_class[np.logical_not(mask)] = -1
average_pfa = pfa_per_class[mask].mean()
return average_pfa, pfa_per_class


def stats_accuracy_per_class(cm):
"""Compute the accuracy per class and average
puts -1 for invalid values (division per 0)
returns average accuracy, accuracy per class
"""
# equvalent to for class i to
# number or true positive of class i (data[target==i]==i).sum()/ number of elements of i (target==i).sum()
sums = np.sum(cm, axis=1)
mask = (sums>0)
sums[sums==0] = 1
accuracy_per_class = np.diag(cm) / sums #sum over lines
accuracy_per_class[np.logical_not(mask)] = -1
average_accuracy = accuracy_per_class[mask].mean()
return average_accuracy, accuracy_per_class


def stats_iou_per_class(cm, ignore_missing_classes=True):
"""Compute the iou per class and average iou
Puts -1 for invalid values
returns average iou, iou per class
"""

sums = (np.sum(cm, axis=1) + np.sum(cm, axis=0) - np.diag(cm))
mask = (sums>0)
sums[sums==0] = 1
iou_per_class = np.diag(cm) / sums
iou_per_class[np.logical_not(mask)] = -1

if mask.sum()>0:
average_iou = iou_per_class[mask].mean()
else:
average_iou = 0

return average_iou, iou_per_class


def stats_f1score_per_class(cm):
"""Compute f1 scores per class and mean f1.
puts -1 for invalid classes
returns average f1 score, f1 score per class
"""
# defined as 2 * recall * prec / recall + prec
sums = (np.sum(cm, axis=1) + np.sum(cm, axis=0))
mask = (sums>0)
sums[sums==0] = 1
f1score_per_class = 2 * np.diag(cm) / sums
f1score_per_class[np.logical_not(mask)] = -1
average_f1_score = f1score_per_class[mask].mean()
return average_f1_score, f1score_per_class
Loading

0 comments on commit deabcc4

Please sign in to comment.