Ray Casting Visual Search (RCVS). Algoritma sederhana dan cepat untuk menemukan model 3D yang serupa dalam geometri



Bagi saya, kedua model ini sangat mirip, tetapi mereka tidak memiliki karakteristik yang jelas dengan mana kesamaan mereka dapat diukur. Model-model ini memiliki jumlah yang berbeda dari simpul, tepi dan poligon, mereka memiliki ukuran yang berbeda, selain mereka diputar berbeda dalam ruang, dan keduanya memiliki transformasi yang sama (Posisi = [0,0,0], Rotasi dalam radian = [0,0, 0], Skala = [1,1,1]). Bagaimana cara menentukan kesamaan mereka?

Jika model-model ini sama-sama berorientasi di ruang angkasa, mereka dapat direduksi menjadi ukuran keseluruhan Kotak Bounding, memindahkan asal koordinat lokal ke pusatnya, mengarah ke tampilan voxel dan membandingkan daftar voxel.


Monyet dan voxelnya.

Jika model ini berorientasi sewenang-wenang di sepanjang sumbu koordinat, mereka juga dapat dikurangi menjadi ukuran keseluruhan Kotak Bounding, memindahkan asal koordinat lokal ke pusatnya, dan kemudian merendernya di enam sisi dan membandingkan set gambar di antara mereka sendiri.


Memberikan kelengkungan permukaan.

Untuk mencapai invarian rotasi, saya memutuskan untuk beralih ke sistem koordinat bola.

r=x2+y2+z2

Pada awalnya saya menghitung jarak ke voxel dari asal, mengumpulkan jarak dalam daftar terurut dan membandingkan daftar ini. Ini menghasilkan hasil yang baik, tetapi butuh waktu lama untuk mengubah bentuk voxel ke resolusi yang cukup dan untuk membandingkan daftar besar.

Deskripsi singkat tentang RCVS


Alih-alih voxelization dan rendering, metode saya menggunakan Ray Casting. Model ditempatkan di dalam polyhedron bola icosahedral, dari mana poligon dipancarkan sinar dan panjangnya (jalur) ke permukaan model dicatat. Daftar jalur ini diurutkan dan dibandingkan satu sama lain. Penyortiran menghilangkan belokan, karena untuk model geometri yang sama atau dekat dari lintasan sinar akan bertepatan dalam kesalahan, tetapi berbeda dalam urutan.


80 dan 1280 masing-masing sinar.

Hasil membandingkan model monyet Suzanne dengan cara yang berbeda.


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

Penjelasan rinci tentang RCVS


Ilustrasi Langkah
















Sebelum membandingkan model 3D (selanjutnya disebut sebagai objek), masing-masing melewati tahap persiapan (saya mengimplementasikan bagian ini dengan Python di Blender):

  1. Asal usul koordinat lokal dari komponen objek diatur pada pusat massa volume.
  2. .
  3. .
  4. ( ) , . , : 80, 320, 1280, 5120, 22480. 1280.
  5. , .
  6. .
  7. Sinar dikirim lagi dari setiap poligon icosphere dan jalurnya disimpan sampai bertemu dengan permukaan objek, yang normalnya diarahkan ke dalam objek. (Ini memperbaiki geometri internal dan / atau mengisi titik-titik buta dari sinar sebelumnya)
  8. Jalur sinar yang dikirim dari poligon berlawanan diatur dalam daftar, di mana dua jalur pertama dari langkah 5 dan kedua dari langkah 7. Setiap daftar diurutkan berdasarkan nilai jalur terkecil dari langkah 5 sehingga jalur dari langkah 5 dan 7 saling cocok berpasangan.
  9. Daftar daftar semua jalur diurutkan berdasarkan nilai pertama. Daftar ini memiliki 4 kolom dan jumlah baris sama dengan setengah jumlah poligon di icosphere.
  10. Daftar jalur ray ditulis ke file.

Daftar jalur ray terlihat seperti ini:

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

Ketika semua objek menyiapkan daftar jalur ray, mereka dapat dibandingkan (saya menulis bagian ini di Rust):

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

, RCVS


Algoritma ini bekerja sangat baik untuk mencari objek yang dekat dalam geometri, misalnya, varian poligonal tinggi dan rendah dari model yang sama, dan kadang-kadang cocok dengan benda yang sama, misalnya, dengan teko atau gitar dengan bentuk yang berbeda. Namun, karena jumlah model yang akan sesuai dengan daftar jalur ray yang sama dapat diperkirakan dalamn!(n2)!, ada kemungkinan yang signifikan bahwa objek yang secara visual berbeda dalam geometri akan memiliki akun kebetulan di kisaran 0,5 - 0,9. Selama tes, itu terjadi dengan pisang, yang sebesar 0,7 bertepatan dengan pesawat dan orang-orang dalam pose yang berbeda, yang pada gilirannya oleh "mengesankan" 0,85 bertepatan dengan harimau dan anjing.

Untuk pencarian yang lebih akurat, pertimbangkan ukuran dan / atau rotasi objek.

Sumber.


Kode python untuk membuat kamus poligon icosphere yang berlawanan di 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----")


Kode python untuk mempersiapkan objek dan membuat daftar jalur ray di 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)


Kode karat untuk membandingkan daftar.
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
        );
    }
}



Tautkan ke sumber dan proyek Blender dengan adegan pengujian.

Tautan ke GitHub.


All Articles