C++中重载operator()的意义


C++中,操作符operator(),也就是小括号,与其他操作符一样,都可以被重载,并且operator()可以接收任意个参数。由于它的调用语法与函数调用完全一致,也与operator[]极为相似,因此operator()有两种常见的重载用法,Callable索引

Callable

Callable,也就是可调用对象,包括了函数指针、重载operator()的对象以及可隐式转化为前两者的对象。重载operator()的对象,也称Functor,中文翻译有时候叫做函子。在谈论为什么使用Functor之前,我们先来看看函子是什么,以及怎么用。

比如,我们这里有一个函数,叫做for_each,是std::for_each的简化版,它对于C数组中的每一个元素都进行一个处理。在函数中,Func类型定义了一个Callable的参数。

template <typename T, typename Func>  void for_each(T* begin, T* end, const Func& f)  {      while (begin != end) f(*begin++);  }  

现在我们定义一个函数print,它的功能是打印一个变量。我们把它作为函数for_each的第三个参数,以打印一个int数组。

template <typename T>  void print(const T& x)  {      std::cout << x << " ";  }    int main()  {      int arr[5] = { 1, 2, 3, 4, 5 };      // 这里的print<int>自动decay为decltype(&print<int>)      for_each(arr, arr + 5, print<int>);      return 0;  }  

我们可以再写一个Functor的版本。

template <typename T>  struct Print  {      void operator()(const T& x) const      {          std::cout << x << " ";      }  };    for_each(arr, arr + 5, Print<int>{});  

当然,你也可以定义一个lambda,这个lambda函数本质上也是一个匿名Functor,C++标准中称之为闭包类型Closure。它与Functor没有什么区别,本质上是一个语法糖。

for_each(arr, arr + 5, [](auto&& x) { std::cout << x << " "; });  

当然,如果你想要通过写一份难以理解但能用的代码来阻止其他人维护(可能为了报复不让你午休的CEO),你也可以定义一个能够转化为函数指针的类型,这就是Callable的第三种形态。应该注意到,这里本质上还是一个函数指针。

struct PrintForYuTangCEO  {      typedef void(*Func_Type)(const int&);      operator Func_Type() const { return &print<int>; }  };    for_each(arr, arr + 5, PrintForYuTangCEO{});  

现在,你可能突然有了一个奇怪的需求,想要对打印的对象进行计数,你当然可以在函数print中加入一个static变量,但是这破坏了面向对象,也不利于使用(报复CEO除外)。

static int count = 0;    template <typename T>  void print(const T& x)  {      std::cout << count << " : " << x << std::endl;      count++;  }    count = 0;  for_each(arr, arr + 5, print<int>);  

如果使用Functor来定义,则会方便得多。

template <typename T>  struct Print  {      mutable int count = 0;        void operator()(const T& x) const      {          std::cout << count << " : " << x << std::endl;          count++;      }  };    for_each(arr, arr + 5, Print<int>{});  

当然,你也可以不重载operator(),而是采用一个普通的成员函数来print。但是这会让你的代码写的比较exciting!当然,这种用法在某些框架中随处可见,主要用于传入回调函数。

template <typename T, typename Func, typename... Args>  void for_each_Ex(T* begin, T* end, const Func& f, const Args&... args)  {      while (begin != end) std::invoke(f, args..., *begin++);  }    template <typename T>  struct Print_Exciting  {      mutable int count = 0;        void print(const T& x) const      {          std::cout << count << " : " << x << std::endl;          count++;      }  };    for_each_Ex(arr, arr + 5, &Print_Exciting<int>::print, Print_Exciting<int>{});  

这里有人会问,std::invoke是C++17才出现的函数,哪怕是变参模板也是C++11的语法,那么此前将类中普通成员函数作为参数该怎么写呢?这里就要涉及一个more exciting的写法了,成员函数指针及其调用。

// 这里C是Func函数所属的类别,这里的函数f是类C中除类限定外类型为Func的成员函数.  // 具体于本例,T = int, Func = void(const int&) const, C = Print_Exciting<int>  template <typename T, typename Func, typename C>  void for_each_More_Ex(T* begin, T* end, Func C::* f, const C& obj)  {      while (begin != end) (obj.*(f))(*begin++);  }    // 应该注意到,for_each函数的调用方式没有任何变化  for_each_More_Ex(arr, arr + 5, &Print_Exciting<int>::print, Print_Exciting<int>{});  

值得一提的是,由于我们只能够在定义形参时指定函数类型,而非带有函数名的签名,因此这两种普通成员函数指针做参数的写法,并不能够提供充足的信息以供编译器优化,因为你完全可以在同一个类中提供两个甚至更多具有相同类型,但名字不同函数,这使得函数的地址是运行时的,而非编译时的。这对于普通的函数指针同样成立。

而operator()在给定签名后,是可以在编译期唯一确定的,因此编译器可以对operator()的调用做出优化,例如将其内联。

当然,你也可以将函数指针作为模板的非类型模板参数而不是函数的形参,这使得函数指针成为了编译时常量,因此可允许编译器优化。但是由于非类型模板参数中的类型名只能引用模板形参列表中前面出现过的类型名,因此会你的函数写法极其exciting,需要手动指定模板实参以实例化(专用化),使用时极不方便,在语法上是一个灾难。例子如下。

template <typename Func, typename C, Func C::* f, typename T>  void for_each_More_More_Ex(T* begin, T* end, const C& obj)  {      while (begin != end) (obj.*(f))(*begin++);  }    // 务必注意第一个函数类型中不可出现(*),如果出现会导致模板具体化时类型错误,  // 即f会成为一个诡异的类型,如果第一个实参为void (*)(const int&),则f为  // void (* Print_Exciting<int>::* )(const int&) const,无法实例化该模板  for_each_More_More_Ex<void(const int&) const, Print_Exciting<int>, &Print_Exciting<int>::print>      (arr, arr + 5, Print_Exciting<int>{});  

此外,Functor还有一个比较有趣的用法,是Functor重载。比如,你可以用同一个Functor来同时实现Hash和Equal,这会让你的类变得好用一点(也许吧)。

struct A  {      int x;  };    struct A_Hash  {      std::size_t operator() (const A& a) const      { return std::hash<int>{}(a.x); }  };    struct A_Equal  {      std::size_t operator() (const A& lhs, const A& rhs) const       { return lhs.x == rhs.x; }  };    template <typename... Ops>  struct AllOps : Ops...  {      using Ops::operator()...;  };    using A_Hasher = AllOps<A_Hash, A_Equal>;    std::unordered_set<A, A_Hasher, A_Hasher> a_hashset;  

举了这些例子之后,你现在可能已经理解了一些Functor的好处。

第一,Functor是C++风格的Callable,C++标准对Functor的支持要更为完善,提供了丰富的接口以及编译器优化,比如Functor会被编译器优化为内联函数。

第二,Functor可以方便的进行有状态操作。

第三,Functor是面向对象的,让你的代码更为抽象。

第四,你可以同时将多个函数聚集在同一个Functor上,实现重载。

索引

如果你用过Python,你一定会嫉妒numpy中的index,你可以用方括号的方式来索引多维数组。比如

import numpy as np  x = np.random.randn(10, 10)  x[5, 5] # correct

不幸的是,C++的operator[]只允许你声明一个参数,这就导致这个运算符对于多维数组而言无比鸡肋,然而这种需求对于矩阵等类型而言又是客观存在的。所以,你可能会见到一些库使用operator()实现这个操作。这里,我们就举一个矩阵的例子。

template <typename T, std::size_t ROWS, std::size_t COLS>  struct Matrix  {      T data[ROWS][COLS];        T operator() (int x, int y) const      {          return data[x][y];      }        T& operator() (int x, int y)      {          return data[x][y];      }        template <typename... Args>      auto get(Args&&... args) const      { return this->operator()(std::forward<Args>(args)...); };  };    Matrix<int, 10, 20> m;    m(5, 5) = 10;  std::cout << m.get(5, 5) << std::endl;  

总结

C++中operator ()的重载主要用于实现Functor和索引,前者是对函数指针的面向对象化,具有诸多优点,后者则是为了弥补operator[]不能使用两个参数。



 

复制构造和移动构造

复制构造是这样的:
在对象被复制后临时对象和复制构造的对象各自占有不同的同样大小的堆内存,就是一个副本。

移动构造是这样的:
就是让这个临时对象它原本控制的内存的空间转移给构造出来的对象,这样就相当于把它移动过去了。

复制构造和移动构造的差别:
这种情况下,我们觉得这个临时对象完成了复制构造后,就不需要它了,我们就没有必要去首先产生一个副本,然后析构这个临时对象,这样费两遍事,又占用内存空间,干脆将临时对象原本的堆内存直接转给构造的对象就行了。 当临时对象在被复制后,就不再被利用了。我们完全可以把临时对象的资源直接移动,这样就避免了多余的复制构造。

什么时候该触发移动构造呢?
如果临时对象即将消亡,并且它里面的资源是需要被再利用的,这个时候我们就可以触发移动构造。

std::move
std::move函数可以以非常简单的方式将左值转换为右值引用。
通过std::move,可以避免不必要的拷贝操作。
std::move是为性能而生。

int main()
{
string str = "Hello";//这里假设我们只需要将str的内容放到vector中,完成以后永远都不需要再用到str
vector<string> v;
//调用常规的拷贝构造函数,新建字符数组,拷贝数据
v.push_back(str);
cout << "After copy, str is :" << str << endl;
//先把str转为右值引用,然后调用移动构造函数转交所有权
v.push_back(move(str));
cout << "After move, str is:" << str << endl;
cout << "The contents of the vector are:{" << v[0]
<< "," << v[1] << "}"<<endl;

system("pause");
return 0;
}


string类的数据成员简单的来说其实最关键的就是一个char*(至于其他的东西不重要),指向动态内存分配空间的首地址。这里可以看我的文章:string类的简单实现



再看一个例子:

int main()
{
string s1 = "apple";
string s2 = "banana";

s1 = move(s2); 
// s1=="banana"
cout << s2 << endl;

vector<int> v1;
vector<int> v2 = { 1, 2, 3, 4 };

v1 = move(v2);  //从v2转移到v1
//v1=={1, 2, 3, 4}
system("pause");
return 0;

}


可以看到A的构造函数调用一次,拷贝构造函数调用了一次,构造函数和拷贝构造函数是消耗比较大的,这里是否可以避免拷贝构造?C++11做到了这一点。(注意,上面这个例子中,虽然getA()是一个右值,但是由于没有自定义move constructor,所以调用了默认的copy constructor。如果对象中有堆内存管理,必须定义move constructor。)

2.自定义A的移动构造函数,代码如下:

class A
{
public:
A() { cout << "Constructor" << endl; }
A(const A&) { cout << "Copy Constructor" << endl; }
A(const A&&) { cout << "Move Constructor" << endl; }
~A() {}
};

A getA()
{
A a;
return a;
}

int main()
{
A a = getA();// getA()是一个右值
system("pause");
return 0;
}
调用拷贝构造函数,而是调用移动构造。这里并没有看到移动构造的优点。

3.修改代码,给A类添加一个成员变量如下:

class B
{
public:
B() {}
B(const B&) { cout << "B Copy Constructor" << endl; }
};

class A
{
public:
A() : pb(new B()) { cout << "A Constructor" << endl; }
A(const A& src) :pb(new B(*(src.pb)))//深拷贝
{
cout << "A Copy Constructor" << endl;
}
A(A&& src) :pb(src.pb)
{
src.pb = nullptr;//这里是关键,这样以后,当src.pb被delete时,由于其为空指针,并不会释放原来的堆内存
cout << "A Move Constructor" << endl;
}
~A() { delete pb; }

private:
B* pb;
};

static A getA()
{
A a;
cout << "================================================" << endl;
return a;
}

int main()
{
A a = getA();
cout << "================================================" << endl;
A a1(a);
system("pause");
return 0;
}


A a = getA();调用的是A的移动构造,A a1(a); 调用的是A的拷贝构造。A的拷贝构造需要对成员变量B进行深拷贝,而A的移动构造不需要,很明显,A的移动构造效率高。

4.std::move语句可以将左值变为右值而避免拷贝构造,修改代码如下:

class B
{
public:
B() {}
B(const B&) { cout << "B Copy Constructor" << endl; }
};

class A
{
public:
A() : pb(new B()) { cout << "A Constructor" << endl; }
A(const A& src) :pb(new B(*(src.pb)))//深拷贝
{
cout << "A Copy Constructor" << endl;
}
A(A&& src) :pb(src.pb)
{
src.pb = nullptr;
cout << "A Move Constructor" << endl;
}
~A() { delete pb; }

private:
B* pb;
};

static A getA()
{
A a;
cout << "================================================" << endl;
return a;
}

int main()
{
A a = getA();
cout << "================================================" << endl;
A a1(a);
cout << "================================================" << endl;
A a2(move(a));
system("pause");
return 0;
}


A a2(std::move(a));将a转换为右值,因此a2调用的是移动构造而不是拷贝构造。

5.赋值操作符也可以是移动赋值。

class B
{
public:
B() {}
B(const B&) { cout << "B Copy Constructor" << endl; }
};

class A
{
public:
A() : pb(new B()) { cout << "A Constructor" << endl; }
A(const A& src) :pb(new B(*(src.pb)))//深拷贝
{
cout << "A Copy Constructor" << endl;
}
A(A&& src) :pb(src.pb)
{
src.pb = nullptr;
cout << "A Move Constructor" << endl;
}


A& operator=(const A& src) noexcept//深拷贝
{
if (this == &src)
return *this;

delete pb;
pb = new B(*(src.pb));
cout << "operator=(const A& src)" << endl;
return *this;
}
A& operator=(A&& src) noexcept
{
if (this == &src)
return *this;

delete pb;
pb = src.pb;
src.pb = nullptr;
cout << "operator=(const A&& src)" << endl;
return *this;
}
~A() { delete pb; }

private:
B* pb;
};

static A getA()
{
A a;
cout << "================================================" << endl;
return a;
}

int main()
{
A a = getA();//移动构造
cout << "================================================" << endl;
A a1(a);//拷贝构造
cout << "================================================" << endl;
A a2(move(a));//移动构造
cout << "================================================" << endl;
a2 = getA();//移动赋值(因为getA()是右值)
cout << "================================================" << endl;
a2 = a1;//拷贝赋值(因为a1是左值)
system("pause");
return 0;
}

总之尽量给类添加移动构造和移动赋值函数,而减少拷贝构造和拷贝赋值的消耗。 移动构造,移动赋值要加上noexcept,用于通知标准库不抛出异常。

使用move函数转交unique_ptr的所有权
#include <iostream>
#include <memory>
using namespace std;

int main(int argc, char *argv[])
{
int*p1 = new int(56);
unique_ptr<int> up_int1(p1);
unique_ptr<int> up_int2= move(up_int1);//转交所有权(法1)
//unique_ptr<int> p2(p1.release()); //转交所有权(法2)

string* p2 = new string[2];//对象数组
p2[0] = "apple";
p2[1] = "banana";
unique_ptr<string[]> up_str1(p2);
unique_ptr<string[]> up_str2;
up_str2 = move(up_str1);

system("pause");
return 0;
}

如何在 C++ 中从函数中返回一个字符串

使用 std::string func() 从 C++ 中的函数中返回字符串
使用 std::string &func() 记法从函数中返回字符串
使用 char *func() 记法从函数中返回字符串
本文介绍了几种在 C++ 中如何从函数中返回字符串的方法。

使用 std::string func() 从 C++ 中的函数中返回字符串


按值返回是从函数返回字符串对象的首选方法。因为 std::string 类有 move 构造函数,所以即使是长字符串,通过值返回也是高效的。如果一个对象有一个 move 构造函数,那么它就被称为具有移动语义的特性。移动语义意味着在函数返回时,对象不会被复制到不同的位置,因此,提供更快的函数执行时间。


#include <iostream>
#include <algorithm>
#include <iterator>

using std::cout; using std::endl;
using std::string; using std::reverse;

string ReverseString(string &s){
    string rev(s.rbegin(), s.rend());
    return rev;
}

int main() {
    string str = "This string shall be reversed";

    cout << str << endl;
    cout << ReverseString(str) << endl;

    return EXIT_SUCCESS;
}
输出:

This string shall be reversed
desrever eb llahs gnirts sihT
使用 std::string &func() 记法从函数中返回字符串
这个方法使用了通过引用返回的符号,这可以作为解决这个问题的另一种方法。尽管通过引用返回是返回大型结构或类的最有效的方法,但在这种情况下,与前一种方法相比,它不会带来额外的开销。注意,你不应该用引用来替换函数中声明的局部变量,这会导致悬空引用。

#include <iostream>
#include <algorithm>
#include <iterator>

using std::cout; using std::endl;
using std::string; using std::reverse;

string &ReverseString(string &s) {
    reverse(s.begin(), s.end());
    return s;
}

int main() {
    string str = "Let this string be reversed";

    cout << str << endl;
    cout << ReverseString(str) << endl;

    return EXIT_SUCCESS;
}
输出:

Let this string be reversed
desrever eb gnirts siht teL
使用 char *func() 记法从函数中返回字符串
另外,我们也可以使用 char *从函数中返回一个字符串对象。请记住,std::string 类以连续数组的形式存储字符。因此,我们可以通过调用内置的 data() 方法返回一个指向该数组中第一个 char 元素的指针。然而,当返回 std::string 对象的空端字符数组时,请确保不要使用类似的 c_str() 方法,因为它取代了指向第一个 char 元素的 const 指针。

#include <iostream>
#include <algorithm>
#include <iterator>

using std::cout; using std::endl;
using std::string; using std::reverse;

char *ReverseString(string &s) {
    reverse(s.begin(), s.end());
    return s.data();
}

int main() {
    string str = "This string must be reversed";

    cout << str << endl;
    cout << ReverseString(str) << endl;

    return EXIT_SUCCESS;
}
输出:

This string must be reversed
desrever eb tsum gnirts sihT

C++对象内存布局测试总结

C++对象内存布局测试总结

 

  http://hi.baidu.com/%D6%F2%C7%EF/blog/item/826d38ff13c32e3a5d6008e8.html

  上文是半年前对虚函数、虚拟继承的理解。可能有一些错漏。而且只是理解了比较简单的部分,表达也不够清晰,这次决定花的时间再做一次总结。

  对于普通的C++对象内存布局,简单得不得了,就不做总结了。这里只总结涉及到虚拟继承的情况。

    因为不同编译器对虚拟继承的实现采用不同的方式,所以要完整的分析是不可能的。这里只考虑VS2005/2008,还有简单涉及GCC编译器。

1、 单个虚拟继承

只是为了分析而已,实际中并没有太大的作用。跟虚拟继承相关的派生类对象的内存布局跟具体的编译器相关。

   (1)VS编译器:无论有无虚函数,必然含有虚基类表指针。虚基类表中的内容为本类实例的偏移和基类实例的相对偏移值。如果有虚函数,那么基类的虚函数表跟派生类的虚函数表是分开的。

在内存布局上,地址从低到高,顺序如下:派生类的虚函数表指针+虚基类表指针+派生类的成员变量+"间隔"(4个字节)+基类的虚函数表指针+基类的成员变量。派生类跟基类实例的位置关系跟普通继承正好相反。

    说明:"间隔"产生的原因是派生类重写了基类的虚函数。如果没重写,则这一项没有。"本类地址"指的是包含有虚基类的对象(或部分对象),也就是继承链上的直接子类对象的地址,本例比较简单,就是派生类对象地址。"本类地址跟虚基类表指针地址只差",这个值经常是-4、0,-4表明"本类"还有一个虚函数表指针;0则表明"本类"的第一个4字节保存的就是虚基类表指针,没有虚函数表指针。

    

图 1 VS编译器—单个虚拟继承

   (2)GNU的GCC编译器:跟VS的编译器类似,有不同的地方是,虚基类表跟派生类的虚函数表合并。另外通过虚基类表指针往正负两个方向寻址,可以获得不同偏移值,也就是说有两个功能一样的虚函数表。不过在实际应用的时候,不知道虚基类表是否真的有用,测试了简单的情况发现编译器做了优化,根本就没有用虚基类表来寻址虚基类实例。

 

图 2 GCC编译器—单个虚拟继承

2、 虚拟继承多个基类

虚基类表要增加内容,有N个虚基类就有N项基类实例偏移值,再加上1项本类实例的偏移值,也就是N+1。

假设C虚拟继承了A类和B类,考虑最复杂的情况(都有虚函数),那么C类对象的内存布局如下

 

(VS编译器):

C类虚函数表指针+虚基类表指针+C类成员变量+A类间隔(4个字节) + A类虚函数表指针+ A类成员变量+ B类间隔(4个字节)+B类虚函数表指针+ B类成员变量。

说明:当派生类重写了该基类的虚函数,才会有"间隔"。"间隔"属于虚函数被重新实现了的虚基类,可能是一个标志,也有可能是在函数调用的时候用上。不是很清楚。

 

图 3 VS编译器—虚拟继承多个基类

(GCC编译器):

C类虚函数表指针(包含虚基类表) + C类成员变量 + A类虚函数表指针 +  A类成员变量 + B类虚函数表指针 + B类成员变量。

相比较执行,使用GCC编译器,派生类对象小一些。(图略)

3、 虚拟继承之菱形继承

这里的菱形继承指的是:B、C虚拟继承A,然后D普通继承B、C。

D类的对象的内存布局如下

(VS编译器)

B类虚函数表指针(该虚函数表包含D类独有的虚函数的地址)+B类虚基类表指针+B类成员变量+C类虚函数表指针+C类虚基类表指针+C类成员变量+D类成员变量+"间隔"+A类虚函数表指针+A类成员变量。

说明:如果A类的虚函数没有被重写,那么就没有"间隔"。

 

图 4 VS编译器—菱形继承

(GCC编译器)

把B、C类的虚函数表跟虚基类表合并就是了。(图略)

4、VS编译器,"间隔"的疑问

 "间隔"的问题,在没有虚函数的情况下,重写是没有"间隔"的,所以觉得可能跟虚函数有关,也就是说是为了实现多态,具体是用在哪个地方,做了简单的反汇编调试(父类指针指向子类对象,调用被子类重写了的虚函数),并没有发现哪里用到了"间隔",可能要在复杂的调用才会用上吧,目前搞不清楚。

5、虚基类表的问题

通过反汇编调试发现在使用多态的时候,VS编译器会去使用虚基类表,用于寻址虚基类地址。而GCC编译器则没有这么做,测试了比较简单的情况,发现它做了优化,并没有利用虚基类表,而是直接在派生类对象地址上加上一个常数,获得虚基类实例的地址。

 

 

学习参考:

http://blog.csdn.net/haoel/archive/2008/10/15/3081328.aspx

http://blog.csdn.net/BlueDog/archive/2009/10/22/4711169.aspx

《深度探索C++对象模型》也是非常好的资料,只是该书较旧,而类对象布局又是编译器的个性属性,手上的编译器有可能不是按照书里的来实现(譬如书中强调虚函数表指针放在类对象的结尾,以便兼容C,而实际上编译器没有这么做,还有,数据成员的地址也没有像书中所说的那样),所以动手实践才有真理。

 

2010.8.19

cs_wuyg@126.com

Python 实现三维姿态估计遮挡匹配预测

Python 实现三维姿态估计遮挡匹配预测

作者 | 李秋键
出品 | AI科技大本营(ID:rgznai100)

引言:随着计算机技术的飞速发展以及人们对智能化设备需求的提高,人体行为识别已经成为计算机视觉领域热门研究方向之一,其广泛应用于公共安防、人机交互、虚拟现实、体育运动和医疗健康等领域,具有极高的理论研究价值。早期的方法主要针对于 RGB 视频图像,由于易受复杂背景、光照强度的影响,很难达到理想效果。但随着深度传感器技术的发展,高精度获取三维骨架关节点信息变得方便可行。对比传统 RGB 视频图像数据,骨架姿势信息对行为的描述有其内在优势,它不仅能够更准确地描述人体姿态和运动状态而且不受背景复杂度及光照强度等因素的影响,同时骨架信息也可以被广泛应用于行为识别。

三维人体姿态估计的主要任务是预测出人体关节点的三维坐标位置和角度等信息。其对于人体姿态的表述比2D更为精准,因此其应用范围和参考价值都要高于2D人体姿态估计,但是3D姿态估计的难度也更高,存在着遮挡,单视角2D到3D的映射中固有的深度模糊性、不适定性,缺少大型的室外数据集等挑战。

故今天我们尝试在获取三维姿态估计骨骼数据的基础上,对其缺少的数据进行匹配性的预测,参考GCN等构建空间结构算法可能更优,但这里只作为一种方法的思路,其他方法后续补充,效果如下,代码放置文末,大家可以优化: 

图片


图片

方法基本介绍

1.1 余弦相似度

余弦相似度,又称为余弦相似性,是通过计算两个向量的夹角余弦值来评估他们的相似度。余弦相似度将向量根据坐标值,绘制到向量空间中,如最常见的二维空间。余弦值越接近1,就表明夹角越接近0度,也就是两个向量越相似。余弦相似度计算源于向量余弦计算的思想,通过测量两个向量内积空间夹角的余弦值来度量它们之间的相似性,常用于机器学习中对文本的处理过程。计算两段文本的相似度首先需要将文本分词,清除标点符号以及停用词,对文本中涉及的词进行统一数字编码,根据编码将文本中的词频向量化,再用余弦定理计算两个向量的余弦值,即可得到两段文本的相似度。

图片

图片

1.2 皮尔逊系数

皮尔逊相关系数是一种广泛用于测量两个变量之间线性相关性的方法。它基于数据的协方差矩阵来评估两个向量之间关系的强度。通常,两个向量之间的皮尔逊相关系数为:

图片

皮尔逊相关系数可以应用于样本或总体。样本和总体的皮尔逊相关系数的绝对值均小于或等于 1。在样本相关的情况下,相关系数等于 1 或-1 对应于恰好位于一条线上的数据点;在总体相关的情况下,这对应于一条完全支持双变量分布的线。

1.3 匹配方法介绍

本文所采用的方法和图像模板匹配基本类似,这里做基本介绍。

图像模板匹配方法可以分成四种类型:基于图像灰度的图像匹配算法、基于变换域的图像匹配算法、基于模板的图像匹配算法和给予特征的图像匹配算法。基于特征的匹配算法通过提取图像中对形变、光照等具有不变性的信息,对这些信息进行描述构造描述符,之后对这些特征进行匹配,计算出图像之间几何变换的参数。基于特征的匹配算法比起基于灰度的匹配算法是用更少的信息进行匹配,从而大大提高运算速度。

基于特征的匹配算法主要包括特征提取、特征匹配、生成几何变换这几个步骤。特征提取是指提取出图像中具有代表性的信息,例如:图像中的角点、拐点等,提取出来的信息必须满足对尺度、旋转、光照、视角和噪声干扰等影响因素具有一定程度的鲁棒性。除此之外所提取的特征还必须具有独特性,以防止将相似的特征被误认为是相同的事物,从而造成特征的误配


图片
项目搭建


这里首先介绍下基本思路,首先需要构建匹配数据集,然后提取骨骼保存为模型文件,然后使用匹配方法找到匹配点预测补充即可。故可分为以下几个步骤进行:

2.1 匹配模型构建

首先要准备数据集如下图可见,可以自行添加更多数据:

图片

然后构建提取模型,保存为npy文件。部分代码如下:

if np.max(joint_heat)>0.1:        [x, y, z] = np.where(joint_heat == np.max(joint_heat))        x = int(x[-1])        y = int(y[-1])        z = int(z[-1])        pos_x = offset3D[j * 28 + x, y, z] + x        pos_y = offset3D[24 * 28 + j * 28 + x, y, z] + y        pos_z = offset3D[24 * 28 * 2 + j * 28 + x, y, z] + z        kps[j, 0] = pos_x        kps[j, 1] = pos_y        kps[j, 2] = pos_z        positions.append([pos_x, pos_y, pos_z])    else:        [x, y, z] = np.where(joint_heat == np.max(joint_heat))        x = int(x[-1])        y = int(y[-1])        z = int(z[-1])        pos_x = offset3D[j * 28 + x, y, z] + x        pos_y = offset3D[24 * 28 + j * 28 + x, y, z] + y        pos_z = offset3D[24 * 28 * 2 + j * 28 + x, y, z] + z        kps[j, 0] = pos_x        kps[j, 1] = pos_y        kps[j, 2] = pos_z        positions.append([pos_x, pos_y, pos_z])positions=np.array(positions)data = np.vstack((data, positions))np.save('data.npy', data)

2.2 遮挡匹配估计

这里主要测试了余弦相似度匹配和皮尔逊系数进行匹配,核心代码如下:

if np.max(joint_heat)>0.1:    [x, y, z] = np.where(joint_heat == np.max(joint_heat))    x = int(x[-1])    y = int(y[-1])    z = int(z[-1])    pos_x = offset3D[j * 28 + x, y, z] + x    pos_y = offset3D[24 * 28 + j * 28 + x, y, z] + y    pos_z = offset3D[24 * 28 * 2 + j * 28 + x, y, z] + z    kps[j, 0] = pos_x    kps[j, 1] = pos_y    kps[j, 2] = pos_z    positions.append([pos_x, pos_y, pos_z])else:    tmp = sum(positions, [])    lens = len(tmp)    idx=0    max_similar = float('-inf')    min_similar=float('inf')    for i in range(1,len(res)):        arr = list(np.concatenate(res[i].reshape((-1, 1), order="F")))[:lens]        '''1 余弦相似度'''        s = cosine_similarity([arr], [tmp])        print(s)        if s >= max_similar:            max_similar = s            idx = i        '''2、皮尔逊系数'''        # s = pearsonrSim(arr, tmp)        # print(s)        # if s < min_similar:        #     min_similar = s        #     idx = i    print((res[idx][lens//3][1]-res[idx][lens//3-1][1]))    pos_x = positions[-1][0]+(res[idx][lens//3][0]-res[idx][lens//3-1][0])    pos_y = positions[-1][1]+(res[idx][lens//3][1]-res[idx][lens//3-1][1])    pos_z = positions[-1][2]+(res[idx][lens//3][2]-res[idx][lens//3-1][2])    kps[j, 0] = pos_x    kps[j, 1] = pos_y    kps[j, 2] = pos_z    print(idx)    positions.append([pos_x, pos_y, pos_z])

图片图片

完整代码:

链接:https://pan.baidu.com/s/1DmGasSN219rrFORdy300Tw

提取码:jjy1