Ray Casting Visuelle Suche (RCVS). Einfacher und schneller Algorithmus zum Auffinden von 3D-Modellen mit ähnlicher Geometrie



Für mich sind diese beiden Modelle sehr ähnlich, aber sie haben keine offensichtlichen Eigenschaften, an denen ihre Ähnlichkeit gemessen werden könnte. Diese Modelle haben eine unterschiedliche Anzahl von Eckpunkten, Kanten und Polygonen, sie haben unterschiedliche Größen, außerdem werden sie im Raum unterschiedlich gedreht und beide haben die gleichen Transformationen (Position = [0,0,0], Drehung im Bogenmaß = [0,0, 0], Skala = [1,1,1]). Wie kann man ihre Ähnlichkeit feststellen?

Wenn diese Modelle räumlich gleichermaßen ausgerichtet wären, könnten sie auf die Gesamtgröße des Begrenzungsrahmens reduziert werden, den Ursprung lokaler Koordinaten in die Mitte verschieben, zu einer Voxelansicht führen und die Voxellisten vergleichen.


Affen und ihre Voxel.

Wenn diese Modelle willkürlich entlang der Koordinatenachsen ausgerichtet wären, könnten sie auch auf die Gesamtgröße des Begrenzungsrahmens reduziert werden, den Ursprung lokaler Koordinaten in die Mitte verschieben, sie dann auf sechs Seiten rendern und Bildsätze untereinander vergleichen.


Renderer der Oberflächenkrümmung.

Um eine Rotationsinvarianz zu erreichen, entschied ich mich für ein sphärisches Koordinatensystem.

r=x2+y2+z2

Zuerst habe ich die Entfernungen zu den Voxeln vom Ursprung gezählt, die Entfernungen in geordneten Listen gesammelt und diese Listen verglichen. Dies führte zu guten Ergebnissen, aber es dauerte lange, bis die Voxelform in eine ausreichende Auflösung konvertiert und große Listen verglichen wurden.

Kurzbeschreibung von RCVS


Anstelle von Voxelisierung und Rendering verwendet meine Methode Ray Casting. Das Modell wird innerhalb des ikosaedrischen kugelförmigen Polyeders platziert, von dessen Polygonenstrahlen geworfen werden und dessen Länge (Pfad) zur Modelloberfläche aufgezeichnet wird. Listen dieser Pfade werden sortiert und miteinander verglichen. Durch das Sortieren werden die Windungen eliminiert, da bei gleichen oder engen Geometriemodellen der Strahlengang innerhalb des Fehlers zusammenfällt, sich jedoch in der Reihenfolge unterscheidet.


80 bzw. 1280 Strahlen.

Die Ergebnisse des Vergleichs von Suzannes Affenmodellen auf unterschiedliche Weise.


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

Detaillierte Beschreibung von RCVS


Schritte Abbildungen
















Vor dem Vergleich von 3D-Modellen (im Folgenden als Objekte bezeichnet) durchläuft jedes Modell die Vorbereitungsphase (ich habe diesen Teil in Python in Blender implementiert):

  1. Der Ursprung der lokalen Koordinaten der Komponenten des Objekts wird im Massenmittelpunkt des Volumens festgelegt.
  2. .
  3. .
  4. ( ) , . , : 80, 320, 1280, 5120, 22480. 1280.
  5. , .
  6. .
  7. Von jedem Polygon der Ikosphäre werden erneut Strahlen gesendet, und ihr Pfad wird gespeichert, bis sie auf die Oberfläche des Objekts treffen, deren Normalen innerhalb des Objekts gerichtet sind. (Dies korrigiert die innere Geometrie und / oder füllt die toten Winkel früherer Strahlen)
  8. Die von entgegengesetzten Polygonen gesendeten Strahlenpfade sind in Listen angeordnet, wobei die ersten beiden Pfade aus Schritt 5 und die zweiten beiden aus Schritt 7 sortiert sind. Jede Liste ist nach dem kleinsten Pfadwert aus Schritt 5 sortiert, so dass die Pfade aus den Schritten 5 und 7 paarweise miteinander übereinstimmen.
  9. Die Liste der Listen aller Pfade wird nach dem ersten Wert sortiert. Diese Liste enthält 4 Spalten und die Anzahl der Zeilen entspricht der Hälfte der Anzahl der Polygone in der Ikosphäre.
  10. Die Liste der Strahlengänge wird in eine Datei geschrieben.

Die Liste der Strahlengänge sieht ungefähr so ​​aus:

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

Wenn alle Objekte Listen mit Strahlengängen fertig haben, können sie verglichen werden (ich habe diesen Teil in Rust geschrieben):

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

, RCVS


Der Algorithmus eignet sich hervorragend für die Suche nach Objekten mit enger Geometrie, z. B. hoch- und niedrigpolygonalen Varianten desselben Modells, und manchmal kommt er gut mit ähnlichen Objekten zurecht, z. B. mit Teekannen oder Gitarren unterschiedlicher Form. Da jedoch die Anzahl der Modelle, die der gleichen Liste von Strahlengängen entsprechen, in ungefähr geschätzt werden kannn!(n2)!Es besteht eine signifikante Wahrscheinlichkeit, dass Objekte, die sich in der Geometrie visuell unterscheiden, eine Übereinstimmung im Bereich von 0,5 bis 0,9 aufweisen. Während der Tests passierte es mit Bananen, die um 0,7 mit Flugzeugen und Menschen in verschiedenen Posen zusammenfielen, was wiederum mit „beeindruckenden“ 0,85 mit Tigern und Hunden zusammenfiel.

Berücksichtigen Sie für eine genauere Suche die Größe und / oder Drehung von Objekten.

Quellen.


Python-Code zum Erstellen eines Wörterbuchs gegensätzlicher Ikosphären-Polygone in 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----")


Python-Code zum Vorbereiten von Objekten und Erstellen einer Liste von Strahlengängen in 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)


Rostcode zum Vergleichen von Listen.
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
        );
    }
}



Link zur Quelle und zum Blender-Projekt mit einer Testszene.

Link zu GitHub.


All Articles