Ray Casting Visual Search (RCVS). خوارزمية بسيطة وسريعة للعثور على نماذج ثلاثية الأبعاد متشابهة في الهندسة



بالنسبة لي ، هذان النموذجان متشابهان جدًا ، لكنهما لا يملكان خصائص واضحة يمكن من خلالها قياس التشابه بينهما. تحتوي هذه النماذج على أعداد مختلفة من القمم والحواف والمضلعات ، وهي بأحجام مختلفة ، إلى جانب أنها تدور بشكل مختلف في الفضاء ، وكلاهما لهما نفس التحولات (Position = [0،0،0] ، الدوران بوحدات الراديان = [0،0 ، 0] ، مقياس = [1،1،1]). كيفية تحديد تشابهها؟

إذا كانت هذه النماذج موجهة بشكل متساوٍ في الفضاء ، فيمكن تقليلها إلى الحجم الكلي للصندوق المحيط ، ونقل أصل الإحداثيات المحلية إلى مركزها ، مما يؤدي إلى عرض فوكسل وقارن قوائم فوكسلز.


القرود و ثعلباتهم.

إذا كانت هذه النماذج موجهة بشكل تعسفي على طول محاور الإحداثيات ، فيمكن أيضًا اختزالها إلى الحجم الكلي للصندوق المحيط ، ونقل أصل الإحداثيات المحلية إلى مركزها ، ثم تقديمها على ستة جوانب ومقارنة مجموعات الصور فيما بينها.


يجعل انحناء السطح.

من أجل تحقيق ثبات الدوران ، قررت التحول إلى نظام إحداثيات كروية.

r=x2+y2+z2

في البداية أحصيت المسافات إلى فوكسلز من الأصل ، جمعت المسافات في القوائم المرتبة وقارنت هذه القوائم. أدى هذا إلى نتائج جيدة ، ولكن الأمر استغرق وقتًا طويلاً لتحويل شكل voxel إلى دقة كافية ولمقارنة القوائم الكبيرة.

وصف موجز لـ RCVS


بدلاً من voxelization والعرض ، تستخدم طريقي Ray Casting. يتم وضع النموذج داخل متعدد السطوح الكروية الأيكوزاهيدرالية ، الذي يتم من خلاله إلقاء أشعة المضلعات وتسجيل طولها (مسار) إلى سطح النموذج. يتم فرز قوائم هذه المسارات ومقارنتها ببعضها البعض. يؤدي التصنيف إلى التخلص من المنعطفات ، لأنه بالنسبة لنفس النماذج أو إغلاقها في النماذج الهندسية لمسار الأشعة سيتزامن في الخطأ ، ولكنه يختلف في الترتيب.


80 و 1280 شعاعًا على التوالي.

نتائج مقارنة نماذج قرد سوزان بطرق مختلفة.


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

وصف مفصل ل RCVS


خطوات الرسوم التوضيحية
















قبل مقارنة النماذج ثلاثية الأبعاد (المشار إليها فيما يلي باسم الكائنات) ، يمر كل منها بمرحلة التحضير (قمت بتطبيق هذا الجزء في Python in Blender):

  1. يتم تعيين أصل الإحداثيات المحلية لمكونات الكائن في مركز كتلة الحجم.
  2. .
  3. .
  4. ( ) , . , : 80, 320, 1280, 5120, 22480. 1280.
  5. , .
  6. .
  7. يتم إرسال الأشعة مرة أخرى من كل مضلع من الغلاف الجليدي ويتم حفظ مسارها حتى تلتقي بسطح الجسم ، وتوجه أعرافه داخل الجسم. (يعمل هذا على إصلاح الهندسة الداخلية و / أو ملء البقع العمياء للأشعة السابقة)
  8. يتم ترتيب مسارات الأشعة المرسلة من المضلعات المقابلة في قوائم ، حيث يتم ترتيب المسارين الأولين من الخطوة 5 والثاني من الخطوة 7. ويتم تصنيف كل قائمة حسب أصغر قيمة مسار من الخطوة 5 بحيث تتطابق المسارات من الخطوتين 5 و 7 مع بعضها البعض في أزواج.
  9. يتم فرز قائمة قوائم جميع المسارات حسب القيمة الأولى. تحتوي هذه القائمة على 4 أعمدة وعدد الصفوف يساوي نصف عدد المضلعات في الغلاف الجوي.
  10. تتم كتابة قائمة مسارات الأشعة إلى ملف.

تبدو قائمة مسارات الأشعة على النحو التالي:

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

عندما حصلت جميع الكائنات على قوائم جاهزة لمسارات الأشعة ، يمكن مقارنتها (كتبت هذا الجزء في الصدأ):

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

, RCVS


تعمل الخوارزمية بشكل رائع للبحث عن الكائنات القريبة في الهندسة ، على سبيل المثال ، المتغيرات عالية ومنخفضة المضلعات من نفس النموذج ، وأحيانًا تتوافق بشكل جيد مع كائنات مماثلة ، على سبيل المثال ، مع أباريق الشاي أو القيثارات من الأشكال المختلفة. ومع ذلك ، نظرًا لأن عدد النماذج التي تتوافق مع نفس قائمة مسارات الأشعة يمكن تقديرها تقريبًاn!(n2)!، هناك احتمال كبير أن الأجسام غير المتشابهة بصريًا في الهندسة سيكون لها حساب مصادفة في النطاق 0.5 - 0.9. خلال الاختبارات ، حدث ذلك مع الموز ، الذي يتزامن مع 0.7 مع الطائرات والأشخاص في أوضاع مختلفة ، والذي بدوره يتزامن مع 0.85 "المثير للإعجاب" مع النمور والكلاب.

من أجل بحث أكثر دقة ، ضع في اعتبارك حجم و / أو دوران الكائنات.

المصادر.


كود Python لإنشاء قاموس لمضلعات الأيسوسفير المتعارضة في 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 لإعداد الكائنات وإنشاء قائمة بمسارات الأشعة في 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)


كود الصدأ لمقارنة القوائم.
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
        );
    }
}



اربط بالمصدر ومشروع الخلاط بمشهد اختبار.

رابط إلى جيثب.


All Articles