Install dependencies 1/2

!pip install wget gdown
import os
from os.path import exists, join, basename, splitext

Download the dataset with initial structure

YOUR_PATH = '/content/'
!cd $YOUR_PATH
!pwd

#input_filename = 'init_structure'
dir_name = 'structure_approx'

# download dataset 
# https://drive.google.com/uc?id=1_R4cFk8gbQE_fUSQ5Bcsfcfh2KLLS15w
dataset_url = 'https://drive.google.com/uc?id=12hOunzCbMB3fvcxD_W-165jxCaj3LxZn' 
!gdown $dataset_url -O $dir_name'.tar.gz'

# unpack
if not exists(YOUR_PATH+dir_name):
  !mkdir $YOUR_PATH$dir_name
!tar -xf $dir_name'.tar.gz' -C $YOUR_PATH$dir_name
%cd $YOUR_PATH$dir_name
/content
Downloading...
From: https://drive.google.com/uc?id=12hOunzCbMB3fvcxD_W-165jxCaj3LxZn
To: /content/structure_approx.tar.gz
18.0MB [00:00, 67.7MB/s]
/content/structure_approx

Install dependencies 2/2

import time
import argparse 
import numpy as np 
from numpy import linalg as la 
from scipy import special as sp
from utils import mm3d_utils
from mpl_toolkits import mplot3d
%matplotlib inline 
import matplotlib.pyplot as plt
import pandas as pd
import plotly.express as px

Class that computes the covariance matrix over a pointset

class Ellipse3D():
    """ Class that calculates & stores an ellipsoid """
    def __init__(self):
        self.CoG = [0,0,0]
        self.Sxx = 0.0
        self.Syy = 0.0
        self.Szz = 0.0
        self.Sxy = 0.0
        self.Sxz = 0.0
        self.Syz = 0.0
        self.Pds = 0
        self.Norm = False

    def Normalise(self):
        if self.Norm == False:

            self.Norm = True
            Pds_ = self.Pds
            self.CoG /= Pds_

            CoG_ = self.CoG
            self.Sxx = self.Sxx / Pds_ - CoG_[0]*CoG_[0]
            self.Syy = self.Syy / Pds_ - CoG_[1]*CoG_[1]
            self.Szz = self.Szz / Pds_ - CoG_[2]*CoG_[2]
            self.Sxy = self.Sxy / Pds_ - CoG_[0]*CoG_[1]
            self.Sxz = self.Sxz / Pds_ - CoG_[0]*CoG_[2]
            self.Syz = self.Syz / Pds_ - CoG_[1]*CoG_[2]

    def Add(self,Pt,Pds):
        if self.Norm == False:
            self.CoG += Pds*Pt
            self.Sxx += Pds*Pt[0]*Pt[0]
            self.Syy += Pds*Pt[1]*Pt[1]
            self.Szz += Pds*Pt[2]*Pt[2]
            self.Sxy += Pds*Pt[0]*Pt[1]
            self.Sxz += Pds*Pt[0]*Pt[2]
            self.Syz += Pds*Pt[1]*Pt[2]
            self.Pds += Pds

    def Show(self):
        print('==== Ellipsoid Params ===== ')
        print('Is normalised?:', self.Norm)
        print('CoG:',self.CoG)
        print('Coviariance matrix transposed:')
        print(self.Sxx,"-","-")
        print(self.Sxy,self.Syy,"-")
        print(self.Sxz,self.Syz,self.Szz)

Class that generates virtual points

class GenPoints():
    """" Class that generates the visrtual points """
    def __init__(self,anEl3D):
        self.CoG = anEl3D.CoG

        # update covariance matrix
        CovMat = np.zeros([3,3],dtype=float) 
        CovMat[0,0] = anEl3D.Sxx
        CovMat[1,1] = anEl3D.Syy
        CovMat[2,2] = anEl3D.Szz
        CovMat[0,1] = CovMat[1,0] = anEl3D.Sxy
        CovMat[0,2] = CovMat[2,0] = anEl3D.Sxz
        CovMat[1,2] = CovMat[2,1] = anEl3D.Syz 
  
        self.EigVal,self.EigVec = la.eig(CovMat)
        self.EigVal = np.sqrt(self.EigVal)
        self.EigVec = self.EigVec.transpose()

    """
    _ X
    / Y
    | Z

        _____
    P1 *     /|
      /     / |
    /___ P2*  |
    | P4x   |  *P3   x - pt in the middle
    |       | /      * - pts in the corners
    P5*_____|/
    
    """
    def Generate5Pts(self):
        CorrFactor = np.sqrt(2.0)/(1-0.3333/(1.0+0.5))
        #print("CorrFactor",CorrFactor)
      
        e1E1 = self.EigVec[0,:] * (CorrFactor*self.EigVal[0])
        e2E2 = self.EigVec[1,:] * (CorrFactor*self.EigVal[1])
        e3E3 = self.EigVec[2,:] * (CorrFactor*self.EigVal[2])
 

        h1 = [-1.0, 1.0,  1.0]
        h2 = [1.0, -1.0,  1.0]
        h3 = [1.0,  1.0, -1.0]
        h4 = [0.0,  0.0,  0.0]
        h5 = [-1.0,-1.0, -1.0]

        # erfinv is a function of x in <-1,1> and it returns inf at the limits
        # to avoid the 'inf', we make sure not to go too close to -1 or 1
        
        Pt1 = self.CoG \
        + e1E1 * sp.erfinv(h1[0]*2.0/(2.0+1.0)) \
        + e2E2 * sp.erfinv(h1[1]*2.0/(2.0+1.0)) \
        + e3E3 * sp.erfinv(h1[2]*2.0/(2.0+1.0))
        
        Pt2 = self.CoG \
        + e1E1 * sp.erfinv(h2[0]*2.0/(2.0+1.0)) \
        + e2E2 * sp.erfinv(h2[1]*2.0/(2.0+1.0)) \
        + e3E3 * sp.erfinv(h2[2]*2.0/(2.0+1.0))

        Pt3 = self.CoG \
        + e1E1 * sp.erfinv(h3[0]*2.0/(2.0+1.0)) \
        + e2E2 * sp.erfinv(h3[1]*2.0/(2.0+1.0)) \
        + e3E3 * sp.erfinv(h3[2]*2.0/(2.0+1.0))
        
        Pt4 = self.CoG \
        + e1E1 * sp.erfinv(h4[0]*2.0/(2.0+1.0)) \
        + e2E2 * sp.erfinv(h4[1]*2.0/(2.0+1.0)) \
        + e3E3 * sp.erfinv(h4[2]*2.0/(2.0+1.0))

        Pt5 = self.CoG \
        + e1E1 * sp.erfinv(h5[0]*2.0/(2.0+1.0)) \
        + e2E2 * sp.erfinv(h5[1]*2.0/(2.0+1.0)) \
        + e3E3 * sp.erfinv(h5[2]*2.0/(2.0+1.0))

        V5Pts = [Pt1,Pt2,Pt3,Pt4,Pt5]
        return  np.array(V5Pts,dtype=float)

    def Show(self):
        print("Eigenvalues:",self.EigVal)
        print("Eigenvectors:\n",self.EigVec[0,:])
        print(self.EigVec[1,:])
        print(self.EigVec[2:])

Do 3D structure approximation

  1. Read the initial structure (init_structure.txt)
  2. Calculate the ellipsoid/covariance matrix over the pointset (Ellipse3D class)
  3. Generate a set of virtual points approximating the initial structure (Generate5Pts)
input_filename = 'init_structure.txt'
assert( os.path.exists(input_filename) == True )

# Read the 3D points (i.e., the structure)
Pts3DVec = mm3d_utils.ReadPts(input_filename) 
print("Nb of input 3D points: ",np.shape(Pts3DVec)[0])

# Initialise the ellipsoid class with the structure
El3D = Ellipse3D()
for i in Pts3DVec : 
    El3D.Add(i,1.0)
El3D.Normalise()
El3D.Show()

# Generate virtual points and save to file
GenVPts = GenPoints(El3D)
GenVPts.Show()

V5Pts = GenVPts.Generate5Pts() 

print("Nb of virtual 3D points: ",np.shape(V5Pts)[0])
Nb of input 3D points:  8921
==== Ellipsoid Params ===== 
Is normalised?: True
CoG: [-0.27392994  1.95253133 -3.0309648 ]
Coviariance matrix transposed:
0.18852879264800654 - -
0.04578957649572768 0.11249391721704471 -
0.022997731420398515 -0.011128840134303708 0.012079671892042398
Eigenvalues: [0.45972464 0.30917506 0.07852656]
Eigenvectors:
 [0.907821   0.41130209 0.08180232]
[ 0.38515357 -0.89492108  0.22532864]
[[-0.16588476  0.17305161  0.97084262]]
Nb of virtual 3D points:  5

Plot the initial and approximated structure

In blue is the initial struacture and in red is the generated structure.

Pts3DVecCol = np.column_stack((Pts3DVec,np.ones(len(Pts3DVec))*len(Pts3DVec))) 
V5PtsCol = np.column_stack((V5Pts,np.ones(len(V5Pts))*5))
# concatenate intial structure with the virtual structure
Pts3DV5PtsCol = np.vstack((Pts3DVecCol,V5PtsCol))
 
# transform to dataframe
df_Pts3D = pd.DataFrame(Pts3DV5PtsCol, columns = ['X','Y','Z','RGB'])
# convert the RGB to string for discrete colors
df_Pts3D["RGB"] = df_Pts3D["RGB"].astype(str) 
# plot
fig = px.scatter_3d(df_Pts3D,x='X', y='Y', z='Z',color="RGB") 
fig.show()