Recherche visuelle Ray Casting (RCVS). Algorithme simple et rapide pour trouver des modèles 3D similaires en géométrie



Pour moi, ces deux modèles sont très similaires, mais ils n'ont pas de caractéristiques évidentes permettant de mesurer leur similitude. Ces modèles ont différents nombres de sommets, d'arêtes et de polygones, ils sont de tailles différentes, en plus ils tournent différemment dans l'espace, et les deux ont les mêmes transformations (Position = [0,0,0], Rotation en radians = [0,0, 0], échelle = [1,1,1]). Comment déterminer leur similitude?

Si ces modèles étaient également orientés dans l'espace, ils pourraient être réduits à la taille globale de la boîte englobante, déplacer l'origine des coordonnées locales vers son centre, conduire à une vue de voxels et comparer les listes de voxels.


Les singes et leurs voxels.

Si ces modèles étaient arbitrairement orientés le long des axes de coordonnées, ils pourraient également être réduits à la taille globale de la boîte englobante, déplacer l'origine des coordonnées locales vers son centre, puis les rendre sur six côtés et comparer des ensembles d'images entre eux.


Rendu de courbure de surface.

Afin d'obtenir l'invariance de rotation, j'ai décidé de me tourner vers un système de coordonnées sphériques.

r=x2+y2+z2

Au début, j'ai compté les distances aux voxels depuis l'origine, rassemblé les distances dans des listes ordonnées et comparé ces listes. Cela a donné de bons résultats, mais il a fallu beaucoup de temps pour convertir la forme voxel à une résolution suffisante et pour comparer de grandes listes.

Brève description de RCVS


Au lieu de la voxélisation et du rendu, ma méthode utilise Ray Casting. Le modèle est placé à l'intérieur du polyèdre sphérique icosaédrique, à partir duquel des polygones sont émis des rayons et leur longueur (chemin) à la surface du modèle est enregistrée. Les listes de ces chemins sont triées et comparées les unes aux autres. Le tri élimine les spires, car pour une géométrie identique ou proche, les modèles de trajectoire des rayons coïncideront au sein de l'erreur, mais diffèrent dans l'ordre.


80 et 1280 rayons respectivement.

Les résultats de la comparaison des modèles de singe de Suzanne de différentes manières.


Suzanne_lo
                          Suzanne_lo | 1             
                 Suzanne_lo_rot_rand | 0.9878376875939173
                   Suzanne_lo_rot_90 | 0.9800766750109137
                 Suzanne_hi_rot_rand | 0.9761078629585936
                          Suzanne_hi | 0.9730722945028376
                   Suzanne_hi_rot_90 | 0.9680325422930756
                               Skull | 0.7697333228143514
             Ceramic_Teapot_rot_rand | 0.6634783235048608
                      Ceramic_Teapot | 0.6496529954470333
                         Grenade_MK2 | 0.5275616759076022

Description détaillée de RCVS


Illustrations des étapes
















Avant de comparer les modèles 3D (ci-après dénommés objets), chacun passe par la phase de préparation (j'ai implémenté cette partie en Python dans Blender):

  1. L'origine des coordonnées locales des composants de l'objet est définie au centre de masse du volume.
  2. .
  3. .
  4. ( ) , . , : 80, 320, 1280, 5120, 22480. 1280.
  5. , .
  6. .
  7. Des rayons sont à nouveau envoyés depuis chaque polygone de l'icosphère et leur chemin est enregistré jusqu'à ce qu'ils rencontrent la surface de l'objet, dont les normales sont dirigées à l'intérieur de l'objet. (Cela corrige la géométrie interne et / ou remplit les angles morts des rayons précédents)
  8. Les trajets de rayons envoyés à partir de polygones opposés sont organisés en listes, où les deux premiers trajets de l'étape 5 et les deux seconds de l'étape 7. Chaque liste est triée par la plus petite valeur de traçage de l'étape 5 de sorte que les trajets des étapes 5 et 7 se correspondent par paires.
  9. La liste des listes de tous les chemins est triée selon la première valeur. Cette liste comprend 4 colonnes et le nombre de lignes est égal à la moitié du nombre de polygones dans l'icosphère.
  10. La liste des chemins de rayons est écrite dans un fichier.

La liste des chemins de rayons ressemble à ceci:

0.00271218840585662  0.2112752957162676 0.00271218840585662  0.2112752957162676
0.004740002405006037 0.210980921765861  0.004740002405006037 0.2109809188911709
0.00660892842734402  0.2066613370130234 0.00660892842734402  0.2066613370130234
0.2692299271137439   0.2725085782639611 0.2692299271137439   0.2725085782639611
0.2705489991382905   0.2707842753523402 0.270548999138295    0.2707843056356914
0.2709498404192674   0.271036677664217  0.2709498404192674   0.271036642944409

Lorsque tous les objets ont préparé des listes de chemins de rayons, ils peuvent être comparés (j'ai écrit cette partie dans Rust):

  1. . 0.01, 1%.
  2. .
  3. , , . , . , .
  4. , . .
  5. ( ), .

, RCVS


L'algorithme fonctionne très bien pour rechercher des objets qui sont proches en géométrie, par exemple, des variantes polygonales hautes et basses du même modèle, et parfois il fait bien avec des objets similaires, par exemple, avec des théières ou des guitares de formes différentes. Cependant, comme le nombre de modèles qui correspondront à la même liste de trajets de rayons peut être approximativement estimén!(n2)!, il existe une probabilité significative que des objets visuellement différents en géométrie aient une coïncidence comprise entre 0,5 et 0,9. Pendant les tests, cela s'est produit avec des bananes, qui par 0,7 coïncident avec des avions et des personnes dans des poses différentes, qui à leur tour par "impressionnant" 0,85 coïncident avec des tigres et des chiens.

Pour une recherche plus précise, tenez compte de la taille et / ou de la rotation des objets.

Sources.


Code Python pour créer un dictionnaire de polygones d'icosphères opposés dans Blender
import bpy
import os

name = bpy.context.active_object.data.name

polys = bpy.data.meshes[name].polygons
print("-----")
length = len(polys)

x = 0
p = 0
dict = []
for i in range(0, length):
    print(i, "/", length)
    for l in range(i, length):
        norm1 = polys[i].normal
        norm2 = polys[l].normal
        if norm1 == -norm2:
            dict.append(str(i)+", "+str(l)) 
            p += 1  


print("Polys", p)
basedir = os.path.dirname(bpy.data.filepath) + "/ico_dicts/"  
if not os.path.exists(basedir):
    os.makedirs(basedir)
file = open(basedir + "/" + str(length) + ".csv","w")
for poly in dict:      
    file.write(poly + "\n")
file.close()
print("----ok----")


Code Python pour préparer des objets et créer une liste de chemins de rayons dans Blender.
import bpy
import os
from math import sqrt
import csv


#Icosphere properties
#icosubd:
#5 -> 5120 X 2 rays
#4 -> 1280 X 2 rays
#3 ->  320 X 2 rays
#2 ->   80 X 2 rays
icosubd = 4
size = 1280 #name of directory
radius = 1




def icoRC(icosubd, rad, raydist, normals, target):
    icorad = rad
    bpy.ops.mesh.primitive_ico_sphere_add(radius=icorad, subdivisions=icosubd, enter_editmode=True, location=(0, 0, 0))
    bpy.ops.mesh.normals_make_consistent(inside=normals)
    bpy.ops.object.editmode_toggle()
    

    ico = bpy.context.active_object
    mesh = bpy.data.meshes[ico.data.name]
    rays = []
    size = len(mesh.polygons)
    for poly in mesh.polygons:
        normal = poly.normal
        start = poly.center
        
        ray = target.ray_cast(start, normal, distance=raydist)
      
        if ray[0] == True:
            length = sqrt((start[0]-ray[1][0]) ** 2 + (start[1]-ray[1][1]) ** 2 + (start[2]-ray[1][2]) ** 2)
            rays.append(length / (radius *2))
        else:
            rays.append(-1)
            
        
    result = []
    #Combine opposite rays using CSV dicts
    dictdir = os.path.dirname(bpy.data.filepath) + "/ico_dicts/"
    with open(dictdir+str(size)+'.csv', newline='') as csvfile:
        ico_dict_file = csv.reader(csvfile, delimiter=',')
        for row in ico_dict_file:
            result.append([rays[int(row[0])], rays[int(row[1])]])
            
            
    bpy.ops.object.select_all(action='DESELECT')
    bpy.data.objects[ico.name].select_set(True) 
    bpy.ops.object.delete() 
    
    return result
   
   
    
    
#Batch preparation of objects by scale and origin 
def batch_prepare():   
    for obj in bpy.context.selected_objects:
        bpy.ops.object.origin_set(type='ORIGIN_CENTER_OF_VOLUME', center='MEDIAN')
        max_dist = 0
        bb = bpy.data.objects[obj.name].bound_box
        
        for vert in obj.data.vertices:
            dist = vert.co[0]**2 + vert.co[1]**2 + vert.co[2]**2
            if dist > max_dist:
                max_dist = dist
        
        obj.scale *= (1 / sqrt(max_dist))



def write_rays(dir):  
    for obj in bpy.context.selected_objects:
        bpy.ops.object.transform_apply(location=False, rotation=False, scale=True)
        
        #Cast rays on object with normals turned inside   
        bpy.context.view_layer.objects.active  = obj
        bpy.ops.object.editmode_toggle()
        bpy.ops.mesh.normals_make_consistent(inside=True)
        bpy.ops.object.editmode_toggle()
        result1 = icoRC(icosubd, radius, radius*2, True, obj)
        
        #Cast rays on object with normals turned outside 
        bpy.context.view_layer.objects.active  = obj
        bpy.ops.object.editmode_toggle()
        bpy.ops.mesh.normals_make_consistent(inside=False)
        bpy.ops.object.editmode_toggle()
        result2 = icoRC(icosubd, radius, radius*2, True, obj)
        
        final = []
        #Sort sub-lists and append them to main list
        for i in range(0, len(result1)):
            if result2[i][0] < result2[i][1]:
                final.append([result2[i][0], result2[i][1], result1[i][0], result1[i][1]])
            else:
                final.append([result2[i][1], result2[i][0], result1[i][1], result1[i][0]])
        
        
        
     
        basedir = os.path.dirname(bpy.data.filepath) + "/rays/" + str(size) + "/" 
        if not os.path.exists(basedir):
            os.makedirs(basedir)
        file = open(basedir + "/" + obj.name,"w")
        
        final.sort(key=lambda x: x[0]) #Sort list by first values in sub-lists
     
        #Writing to file   
        for ray in final:
            ray_str = ""
            for dist in ray:   
                ray_str += str(dist) + " "
            file.write(ray_str + "\n")

        file.close()
        
    
    
batch_prepare()
write_rays(size)


Code de rouille pour comparer les listes.
use rayon::prelude::*;
use std::collections::HashSet;
use std::fs;
use std::io::{BufRead, BufReader, Error, Read};

//Creates Vector from file
fn read<R: Read>(io: R) -> Result<Vec<Vec<f64>>, Error> {
    let br = BufReader::new(io);
    br.lines()
        .map(|line| {
            line.and_then(|v| {
                Ok(v.split_whitespace()
                    .map(|x| match x.trim().parse::<f64>() {
                        Ok(value) => value,
                        Err(_) => -1.0,
                    })
                    .collect::<Vec<f64>>())
            })
        })
        .collect()
}

//Compares two objects
fn compare(one: &Vec<Vec<f64>>, two: &Vec<Vec<f64>>, error: f64) -> f64 {
    let length = one.len();
    let mut count: f64 = 0.0;
    let mut seen: HashSet<usize> = HashSet::new();

    for orig in one {
        let size = orig.len();

        for i in 0..length {
            if seen.contains(&i) {
                continue;
            } else {
                let targ = &two[i];

                if (0..size).map(|x| (orig[x] - targ[x]).abs()).sum::<f64>() > error * size as f64 {
                    continue;
                }

                let err_sum = (0..size)
                    .map(|x| 1.0 - (orig[x] - targ[x]).abs())
                    .sum::<f64>();

                count += err_sum / size as f64;
                seen.insert(i);
                break;
            }
        }
    }
    count / length as f64
}

//Compares one object with others on different threads
fn one_with_others(
    obj: &(String, Vec<Vec<f64>>),
    dict: &Vec<(String, Vec<Vec<f64>>)>,
) -> Vec<(usize, f64)> {
    let length = dict.len();

    (0..length)
        .into_par_iter()
        .map(|x| {
            println!("Puny human is instructed to wait.. {:}/{:}", x + 1, length);
            (x, compare(&obj.1, &dict[x].1, 0.01))
        })
        .collect()
}

fn main() {
    let ray = 1280; //name of directory
    let obj_cur = "Suzanne_lo"; //name of object

    let mut objects = Vec::<(String, Vec<Vec<f64>>)>::new();
    let path_rays = format!("./rays/{:}/", ray);
    let paths = fs::read_dir(path_rays).unwrap();

    let obj_path = format!("./rays/{:}/{:}", ray, obj_cur);
    let obj = (
        obj_cur.to_string(),
        read(fs::File::open(obj_path).unwrap()).unwrap(),
    );

    for path in paths {
        let dir = &path.unwrap().path();
        let name = dir.to_str().unwrap().split("/").last().unwrap();

        objects.push((
            name.to_string(),
            read(fs::File::open(dir).unwrap()).unwrap(),
        ));
    }

    let mut result = one_with_others(&obj, &objects);
    result.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap());
    print!("{}\n", "_".repeat(150));
    println!("{:}", obj.0);
    for line in result {
        println!(
            "{:>36} |{:<100}| {:<14}",
            objects[line.0].0,
            "▮".repeat((line.1 * 100.0).round() as usize),
            line.1
        );
    }
}



Lien vers la source et le projet Blender avec une scène de test.

Lien vers GitHub.


All Articles