Pesquisa visual de projeção de raios (RCVS). Algoritmo simples e rápido para encontrar modelos 3D semelhantes em geometria



Para mim, esses dois modelos são muito semelhantes, mas não possuem características óbvias para medir sua similaridade. Esses modelos têm diferentes números de vértices, arestas e polígonos, são de tamanhos diferentes, além de serem rotacionados de forma diferente no espaço, e ambos têm as mesmas transformações (Posição = [0,0,0], Rotação em radianos = [0,0, 0], Escala = [1,1,1]). Como determinar sua semelhança?

Se esses modelos fossem igualmente orientados no espaço, eles poderiam ser reduzidos ao tamanho geral da caixa delimitadora, mover a origem das coordenadas locais para o centro, levar a uma exibição de voxel e comparar as listas de voxels.


Macacos e seus voxels.

Se esses modelos fossem arbitrariamente orientados ao longo dos eixos de coordenadas, eles também poderiam ser reduzidos ao tamanho geral da Caixa delimitadora, mover a origem das coordenadas locais para o centro e depois renderizá-las em seis lados e comparar conjuntos de imagens entre si.


Renderiza a curvatura da superfície.

Para obter a invariância da rotação, decidi recorrer a um sistema de coordenadas esféricas.

r=x2+y2+z2

Inicialmente, contei as distâncias dos voxels da origem, coletei as distâncias em listas ordenadas e comparei essas listas. Isso resultou em bons resultados, mas levou muito tempo para que a pesquisa do voxel chegasse a uma resolução suficiente e comparasse listas grandes.

Breve descrição do RCVS


Em vez de voxelização e renderização, meu método usa Ray Casting. O modelo é colocado dentro do poliedro esférico icosaédrico, de cujos raios de polígonos são lançados e seu comprimento (caminho) até a superfície do modelo é registrado. As listas desses caminhos são classificadas e comparadas entre si. A classificação elimina as curvas, pois para o mesmo ou para o próximo modelo de geometria do caminho dos raios coincidirá com o erro, mas diferirá em ordem.


80 e 1280 raios, respectivamente.

Os resultados da comparação dos modelos de macacos de Suzanne de diferentes maneiras.


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

Descrição detalhada do RCVS


Passos Ilustrações
















Antes de comparar modelos 3D (a seguir denominados objetos), cada um passa pela fase de preparação (implementei esta parte no Python no Blender):

  1. A origem das coordenadas locais dos componentes do objeto é definida no centro de massa do volume.
  2. .
  3. .
  4. ( ) , . , : 80, 320, 1280, 5120, 22480. 1280.
  5. , .
  6. .
  7. Os raios são enviados de cada polígono da icosfera mais uma vez e seu caminho é salvo até encontrar a superfície do objeto, cujas normais são direcionadas para dentro do objeto. (Isso corrige a geometria interna e / ou preenche os pontos cegos dos raios anteriores)
  8. Os caminhos dos raios enviados de polígonos opostos são organizados em listas, onde os dois primeiros caminhos da etapa 5 e os dois segundos da etapa 7. Cada lista é classificada pelo menor valor do caminho da etapa 5, para que os caminhos das etapas 5 e 7 se combinem em pares.
  9. A lista de listas de todos os caminhos é classificada pelo primeiro valor. Esta lista possui 4 colunas e o número de linhas igual à metade do número de polígonos na icosfera.
  10. A lista de caminhos de raios é gravada em um arquivo.

A lista de caminhos de raios é mais ou menos assim:

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

Quando todos os objetos preparam listas de caminhos de raios, eles podem ser comparados (escrevi esta parte em Rust):

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

, RCVS


O algoritmo funciona muito bem para pesquisar objetos que são próximos na geometria, por exemplo, variantes alta e baixa poligonais do mesmo modelo e, às vezes, lida bem com objetos semelhantes, como bules ou violões de formas diferentes. Entretanto, como o número de modelos que corresponderá à mesma lista de caminhos de raios pode ser estimado aproximadamente emn!(n2)!, há uma probabilidade significativa de que objetos visualmente diferentes em geometria tenham uma conta de coincidência no intervalo de 0,5 a 0,9. Durante os testes, aconteceu com bananas, que em 0,7 coincidem com aviões e pessoas em poses diferentes, que por sua vez, por "impressionante" 0,85, coincidem com tigres e cães.

Para uma pesquisa mais precisa, considere o tamanho e / ou a rotação dos objetos.

Fontes.


Código Python para criar um dicionário de polígonos opostos da icosfera no 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----")


Código Python para preparar objetos e criar uma lista de caminhos de raios no 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)


Código de ferrugem para comparar listas.
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 para a fonte e o projeto Blender com uma cena de teste.

Link para o GitHub.


All Articles