角速度与旋转矩阵的转换关系及思考

  在机器人的控制和轨迹规划等领域,算法结果常常能够得到机器人执行器的速度,如移动机器人的角速度和线速度。在有些仿真情况下,或是其它需要实时更新机器人的运动状态时,我们常常需要根据这些计算出的速度参数(控制率)对机器人的位姿姿态进行更新。这就涉及到了角速度与旋转矩阵之间的对应转换关系。本文将简要给出一种它们之间的转换关系,并在后半部分着重介绍转换出的姿态矩阵在不同情况下的左乘、右乘的不同意义,这一问题也曾经让作者混淆了很久。

一. 控制率与位姿变换矩阵( 4×4 )的转换

  这里主要以Peter corke的视觉伺服工具箱中给出的转换方法为例,事实上,更为详尽的转换方式可以去了解一下指数映射关系,可以参见白老师的文章《 lie group and computer vision : 李群、李代数在计算机视觉中的应用》。
  我们知道,一个变量的导数等于它本身再乘以一个系数。对于三维空间中的旋转矩阵来说,有如下表达式:

R˙(t)=S(ω)R(t)

其中, S(ω) 为关于角速度 ω 的反对称矩阵:
S(ω)=0ωzωyωz0ωxωyωx0

  考虑在 R(t) 处进行微分:
R˙R(t+t)R(t)ζt

  经过整理后可得:
R(t+t)ζtS(ω)R(t)+R(t)(ζtS(ω)+I3×3))R(t)

  因此,矩阵 R=ζtS(ω)+I3×3 即为角速度 ω 对应的旋转矩阵。
  对于线速度 v 来说,在一定时间t内,所导致的坐标系的变化只有平移没有旋转。因此,速度参数(控制率) v ω所对应的位姿变换矩阵 T 为:
T=[ζtS(ω)01×3ζtv0]+I4×4

  当需要把位姿变换矩阵 T 转换为速度参数 v ω时,按照上面的步骤反着来就行了。

二. 那么问题来了:

1. 问题描述

  上述控制率与位姿变换矩阵的转换关系在视觉伺服工具箱中对应于 delta2tr( v )tr2delta(T0, T1 )这两个函数。书中给出了一个具体的例子:

这里写图片描述 这里写图片描述
这里写图片描述

  这个示例我们记为例1。从上面我们可以看出,经过转换出的位姿变换矩阵 T=delta2trd 最终被 T0 左乘能够得到 T1 的值。 但是在具体的例程代码中,如下面工具箱中的视觉伺服仿真的代码:
这里写图片描述

  我们看到,位姿变换矩阵 T=delta2trd 是被摄像机位姿 Tcam 右乘来实现位姿更新的。
  除了这个视觉伺服工具箱外,在VISP开源库中,摄像机的姿态也是通过 右乘来实现的:
这里写图片描述

  函数 direct(v_sat,delta_t)为利用指数映射将速度参数 v_sat 转换为位姿变换矩阵 T
  这两个例子我们记为例2。那么为什么有些情况下是左乘,有些情况下是右乘,这两者之间又有什么区分呢?

2. 左乘与右乘的不同情况及意义

  首先来说例1中的左乘。对于左乘的情况,需要弄清楚的一点是,例1中 T0 T1 表示的是目标姿态在相同的参考坐标系(默认为世界坐标系)下的表示,因此,在这个例子中 tr2delta(d)求出的速度参数 v 也是表示在这个坐标系下的。
  所以,速度参数v对应的姿态转换矩阵 T=delta2trd 也是相对于这个世界坐标系来表示的。因此,当从状态 T0 转换到状态 T1 时,需要被 T0 左乘(如果不理解为什么在世界坐标系下的速度对应的矩阵需要左乘,可以自己假想一下下面的场景):
  
  位姿 T0 和位姿 T1 都是在世界坐标系下表示的,在 T0 位姿时,使其以速度 v 进行运动,其对应的姿态转换矩阵为T。考虑一个虚拟的与世界坐标系 Cworld 重合的坐标系 B ,假定T0在坐标系 B 中固定不动,那么姿态变化矩阵T相当于坐标系 B 在世界坐标系Cworld下的表示。即:

worldTB=T

  因此,通过连杆矩阵相乘,可以得到:
T1=worldTBBT0=TT0

  其次再说右乘。例2中算法所求出的摄像机运动速度 v 均是相对于当时摄像机自身坐标系来说的。因此,根据连杆矩阵变换,可以得到:

T1=T0T
  此外,右乘这种情况也可以这样理解:
  摄像机的运行速度对应的位姿转换矩阵 T 可以理解成目标新的位姿 T1 在当前位姿 T0 所在的坐标系下的表示。比如,假定目标当前处于原点(0,0,0)的位置(自身坐标系),线速度为(5,0,0),那么当一定时间过去后,目标(新的位姿)是不是位于(5,0,0)处(原自身坐标系)?

  所以,同样的对于速度参数转换得到的位姿变换矩阵,要注意分清楚这个速度参数v是在哪个坐标系下表示的,才能决定我们是左乘/右乘这个矩阵

对于例1 中,如果要求取相对于 T0 位姿自身坐标系 Ccam 的速度 v1 ,那么该如何求?
  1. 计算 T1 T0 自身坐标系 Ccam 下的表示 T1
  2. 计算 T1 与单位矩阵 I4×4 T0 在自身坐标系下的位姿是单位阵 I4×4 )的所对应的 v
  利用视觉伺服工具箱简单验证一下:

>> T0 = transl(1,2,3)*trotx(1)*troty(1)*trotz(1);
>> T1 = T0*transl(0.01,0.02,0.03)*trotx(0.001)*troty(0.002)*trotz(0.003)

T1 =

    0.2889   -0.4547    0.8425    1.0191
    0.8372   -0.3069   -0.4527    1.9887
    0.4644    0.8361    0.2920    3.0301
         0         0         0    1.0000

>> T1_in_0=inv(T0)*T1;
>> I=eye(4);
>> v=tr2delta(I,T1_in_0);
>> v'

ans =
    0.0100    0.0200    0.0300    0.0010    0.0020    0.0030

三. Tips:

  1. 对于位姿变换矩阵左乘和右乘,不同的人有不同的理解。比如有些情况下,目标从位姿运动到位姿时,可以把速度首先取负号,表达从状态运动到状态的控制率,这种情况下右乘的 还需要先对其求逆才能相乘。
  2. 理解来源于研一机器人学课件中“右乘联体左乘基”这句话,所以说学过的基础知识一定要学扎实,不然之后就有可能被一些不起眼的细节知识绊住☺。




个人理解,如有错误请指出

       

(转载请注明作者和出处: http://blog.youkuaiyun.com/gh234505 未经允许请勿用于商业用途)

<think>好的,我现在需要帮助用户写一段C++ OpenCV的程序,实现图片以中心为轴按照角速度sin曲线旋转,并且导出为MP4视频。用户还给出了一些参数和背景要求。我得一步步思考怎么解决这个问题。 首先,用户的需求很明确:图片在黑色背景(1920x1080)的中心旋转角速度是spd = a*sin(ω*t) + b,其中b=2.090 -a。图片不会超出范围,所以不用考虑边缘处理。导出MP4视频。 首先,我需要确定如何实现旋转。OpenCV中的旋转通常涉及仿射变换,使用warpAffine函数。每次旋转都需要计算当前的旋转角度,这需要对时间积分角速度。因为角速度是随时间变化的,所以每一步的旋转角度需要累积。 接下来,参数设置部分。用户提到a和ω是自定义参数,比如用户可能会设置a=1.0,ω=2π,或者其他值。但程序中需要用户输入这些参数,可能需要通过命令行参数或者直接在代码中设置。这里可能需要假设用户会在代码里修改a和omega的值,或者在运行时输入。 然后,关于时间。视频的每一帧对应一个时间点t,比如帧率为30fps,那么每帧间隔是1/30秒。角速度spd是rad/s,所以每个时间步长Δt=1/fps,需要计算这段时间内的角度变化。但这里可能需要注意,角速度是随时间连续变化的,所以积分可能需要使用累积的方式,比如每个时间步t=i*Δt,然后计算从t=0到当前t的累积角度。 不过,用户给出的角速度公式是spd = a*sin(ω*t) + b,所以角度θ(t)应该是积分从0到t的spd(t')dt'。积分结果应该是θ(t) = (a/ω)*(1 - cos(ω*t)) + b*t。因为积分sin(ωt)的积分是- (1/ω)cos(ωt) + C,加上b*t的部分。因此,角度计算应该是这个表达式。 这可能会比逐步累加更精确,因为如果使用欧拉方法逐步累加的话,可能会有累积误差。但需要确认是否正确。例如,在离散的时间点t_i = i*Δt,计算θ(t_i) = ∫₀^t_i spd(t) dt = (a/ω)(1 - cos(ω t_i)) + b*t_i。这样可以避免误差累积,特别是当Δt较大时。 所以,正确的方式应该是在每一帧计算当前时间t,然后计算积分后的θ(t),而不是通过spd*Δt累加。这样角度会更准确,尤其是在高频ω的情况下。 接下来,关于视频的生成。需要设置视频编解码器,比如使用MP4V编解码器,设置帧率,尺寸为1920x1080。然后,每一帧都要将原图旋转当前角度,然后放置到黑色背景的中心。 原图的中心需要背景中心重合。所以,在旋转图像时,需要围绕图像的中心点进行旋转。OpenCV的getRotationMatrix2D函数可以指定旋转中心。旋转后的图像可能会有不同的尺寸,但因为用户保证图片不会超出范围,所以不需要剪裁,直接将旋转后的图像叠加到背景的中心位置。 具体步骤可能是: 1. 读取原始图像,假设为srcImage。 2. 创建黑色背景图像bg(1920x1080,CV_8UC3,Scalar::all(0))。 3. 将srcImage放在bg的中心,可能需要先调整srcImage的大小,或者在旋转时处理。但用户说图片不会超出范围,可能原图已经合适,或者旋转后的图像不会超出背景。但需要确保旋转后的图像在叠加到背景时,其位置正确。 4. 计算每一帧的时间t,根据帧率和帧数计算。总时长可能需要用户指定,或者循环一定的时间。 5. 对每一帧,计算当前θ(t) = (a/ω)*(1 - cos(ω*t)) + b*t。 6. 使用getRotationMatrix2D获取旋转矩阵,中心为srcImage的中心。 7. 应用仿射变换,得到旋转后的图像rotated。 8. 将rotated图像放置在背景bg的中心位置。这里可能需要计算旋转后的图像的位置,因为旋转后的图像可能会有不同的尺寸,所以需要找到旋转后的图像的左上角坐标,使得其中心bg的中心重合。或者,可以使用warpAffine直接处理,并在旋转时保持原始图像的大小,这样旋转后的图像可能会有黑边,但用户可能希望旋转后的图像不被裁剪,所以可能需要将旋转后的图像放在背景上,覆盖中心部分。 这部分可能需要更仔细的处理。例如,旋转后的图像可能比原图大,但由于用户保证不会超出范围,所以旋转后的图像在叠加到背景时,其中心点对准背景中心,并且周围不会有超出部分。因此,需要将旋转后的图像正确放置在背景的中心。 例如,假设原图的尺寸是w x h,旋转后的图像可能会有不同的尺寸,但用户保证旋转后的图像不会超出背景。因此,在旋转时,可能需要使用cv::BORDER_CONSTANT边框模式,填充黑色,但之后将旋转后的图像复制到背景的中心位置。或者,旋转后的图像的大小可能原图相同,但需要确保其中心对准。 或者,可能更简单的是,每次旋转原图,然后将旋转后的图像的中心点放置在背景的中心。例如,原图中心是(src.cols/2, src.rows/2),旋转后的图像的中心点不变,但放置到背景的(960, 540)位置。这可能需要计算旋转后的图像在背景中的位置。 或者,另一种方法是,旋转后的图像的大小可能改变,因此需要计算其左上角的位置,使得旋转后的图像的中心等于背景的中心。例如,旋转后的图像宽高为rw和rh,则其在背景中的左上角坐标为(960 - rw/2, 540 - rh/2)。然后将旋转后的图像复制到背景的这个区域。 这可能需要以下步骤: - 旋转后的图像rotated,可能比原图大或者小,但用户保证不会超出背景,所以可以直接计算其在背景中的位置。 但具体实现时,如何处理?例如,在仿射变换后的图像,可能因为旋转而改变尺寸,导致roi区域可能超出背景。但用户已知图片不会超出范围,所以可以假设旋转后的图像放置到背景中心时不会超出。或者,可以在旋转时使用cv::RotateFlags,保持原图尺寸?或者,旋转后的图像可能有不同的尺寸,因此需要调整。 可能正确的做法是,在旋转时,使用getRotationMatrix2D,并设置scale=1.0,这样旋转后的图像尺寸可能改变。然后,将旋转后的图像的中心对准背景的中心,即将其放置在背景的中心位置。例如,计算旋转后的图像的左上角位置:bg的宽度是1920,旋转后的图像宽度是rw,则x坐标为 (1920 - rw)/2;同样高度方向。然后将旋转后的图像复制到背景的该区域。 在代码中,如何实现这一点? 假设旋转后的图像是rotated,其尺寸可能不同。那么,在背景图像bg中,找到ROI区域:Rect( (bg.cols - rotated.cols)/2, (bg.rows - rotated.rows)/2, rotated.cols, rotated.rows )。然后,将rotated复制到bg的这个ROI区域。 这样处理的话,可以保证旋转后的图像的中心背景的中心重合。这应该是正确的做法。 接下来,关于视频的生成。需要创建VideoWriter对象,设置正确的编码格式、帧率、尺寸。OpenCV中,MP4的编码通常使用MP4V,即fourcc('m','p','4','v')。帧率比如设为30,尺寸1920x1080。 参数方面,用户需要设定a和omega的值,同时b=2.090 -a。例如,假设a=1.0,则b=1.090。程序中可能需要用户输入a和omega的值,或者在代码中设置。 关于时间t的计算。假设视频的总时长是T秒,例如10秒。然后,总帧数为T * fps。每一帧的时间t = frame_index / fps。例如,第i帧的时间是i * (1/fps)。 累积的角度计算:θ(t) = (a/ω) * (1 - cos(ω*t)) + b*t。因为积分的结果是这样的。 需要注意的是,当ω非常小时,可能会出现除以零的情况,所以需要确保omega不为零。或者,在程序中处理omega为零的情况,但用户可能应该保证omega不为零。 现在,将这些步骤转化为代码: 首先,读取源图像。假设源图像已经被正确加载,并且放置在背景中心。然后,在每一帧中: 1. 计算当前时间t = frame_count / fps。 2. 计算θ(t) = (a/ω)*(1 - cos(ω*t)) + b*t。注意单位是弧度。 3. 创建旋转矩阵,中心为源图像的中心,角度为θ(t)(转换为度数,因为OpenCV的旋转角度是度数)。 4. 应用仿射变换,得到旋转后的图像。 5. 将旋转后的图像复制到背景图像的中心位置。 6. 将背景图像写入视频帧。 现在,代码的大致结构: #include <opencv2/opencv.hpp> #include <cmath> using namespace cv; int main() { // 参数设置 double a = 1.0; // 用户自定义 double omega = 2.0 * CV_PI; // 用户自定义,例如2π,周期为1秒 double b = 2.090 - a; int fps = 30; double duration = 10.0; // 视频时长,假设10秒 int total_frames = static_cast<int>(duration * fps); // 读取源图像 Mat src = imread("input.jpg"); if (src.empty()) { std::cerr << "无法加载图片!" << std::endl; return -1; } // 创建黑色背景 Mat bg(1080, 1920, CV_8UC3, Scalar(0, 0, 0)); // 视频写入器 VideoWriter writer("output.mp4", VideoWriter::fourcc('m','p','4','v'), fps, bg.size()); if (!writer.isOpened()) { std::cerr << "无法创建视频文件!" << std::endl; return -1; } // 源图像中心坐标 Point2f src_center(src.cols / 2.0f, src.rows / 2.0f); for (int i = 0; i < total_frames; ++i) { double t = i / static_cast<double>(fps); // 当前时间 double theta_rad = (a / omega) * (1 - cos(omega * t)) + b * t; double theta_deg = theta_rad * 180.0 / CV_PI; // 计算旋转矩阵 Mat rot_mat = getRotationMatrix2D(src_center, theta_deg, 1.0); Mat rotated; warpAffine(src, rotated, rot_mat, src.size(), INTER_LINEAR, BORDER_CONSTANT, Scalar(0,0,0)); // 将旋转后的图像放置在背景中心 Mat frame = bg.clone(); int x = (frame.cols - rotated.cols) / 2; int y = (frame.rows - rotated.rows) / 2; Rect roi(x, y, rotated.cols, rotated.rows); rotated.copyTo(frame(roi)); writer.write(frame); } writer.release(); return 0; } 但这里可能存在一个问题:旋转后的图像尺寸可能和原图一样?因为我们在warpAffine时指定了输出尺寸为src.size()。这样,旋转后的图像会保持原图相同的尺寸,但可能会有部分区域被裁剪。但用户已知图片不会超出范围,所以可能没有问题。这样,在将旋转后的图像放置在背景中心时,只需要计算x和y为背景尺寸减去原图尺寸的一半,可能更简单。 例如,如果原图是800x600,则旋转后的图像还是800x600,因为warpAffine的dsize参数设置为src.size()。这时候,每次旋转后的图像尺寸不变,所以roi的位置是固定的,只需要将旋转后的图像复制到背景的中心。 这可能更有效率,并且用户已经保证图片不会超出背景范围,所以旋转后的图像不会因为旋转而被裁剪导致显示不全。 因此,在代码中,warpAffine的dsize参数设置为src.size(),这样得到的旋转图像尺寸原图相同。这样,在复制到背景时,x和y的计算可以提前计算,而不是每一帧都计算: int x_offset = (bg.cols - src.cols) / 2; int y_offset = (bg.rows - src.rows) / 2; 然后在循环中,只需将旋转后的图像复制到bg的Rect(x_offset, y_offset, src.cols, src.rows)区域。 这样更高效,因为x_offset和y_offset只需要计算一次。 修改后的循环部分: // 计算原图在背景中的位置 int x_offset = (bg.cols - src.cols) / 2; int y_offset = (bg.rows - src.rows) / 2; Rect roi(x_offset, y_offset, src.cols, src.rows); for (int i = 0; i < total_frames; ++i) { // 计算theta... // 旋转图像... // 创建frame为bg的拷贝 Mat frame = bg.clone(); rotated.copyTo(frame(roi)); writer.write(frame); } 这样可以节省每帧计算x和y的时间。 另外,需要注意,当原图的尺寸是奇数时,比如bg的尺寸是1920x1080,如果原图的宽度是奇数,则x_offset可能不是整数,但因为是整数坐标,所以会自动取整。这可能引起图像位置偏移一个像素,但用户可能可以接受,或者需要确保原图的尺寸是偶数。 综上,整个程序的流程大致正确。现在需要验证是否正确计算了角度,以及视频生成是否正常。 可能的测试案例:当a=0时,spd = b = 2.090。这时候,角度θ(t) = b*t,即匀速旋转。这种情况下,视频应该以恒定速度旋转。 另一个测试案例:当omega非常小时,比如omega趋近于0,这时候积分中的cos(omega*t)可以用泰勒展开近似,可能会出现数值问题,但用户需要保证omega不为零。 另外,关于旋转中心是否正确。因为旋转矩阵的中心是原图的中心,旋转后的图像原图尺寸相同,所以当将其放置在背景中心时,旋转的中心点应该是正确的。 最后,导出视频的编码可能需要根据系统环境调整。例如,在某些系统上,MP4V可能无法正确编码,可以尝试其他fourcc码,如'avc1'等,但用户需要根据实际情况调整。 综上,这段代码应该能满足用户的需求。</think>以下是实现该功能的C++ OpenCV代码,包含详细注释和实现逻辑: ```cpp #include <opencv2/opencv.hpp> #include <cmath> using namespace cv; int main() { // ========== 参数配置 ========== double a = 1.0; // 振幅参数(用户自定义) double omega = 2 * CV_PI; // 角频率(用户自定义) double b = 2.090 - a; // 偏移量 int fps = 30; // 视频帧率 double duration = 5.0; // 视频时长(秒) std::string output_path = "rotation_output.mp4"; // ========== 初始化资源 ========== // 读取源图像(确保图片路径正确) Mat src = imread("input.jpg"); if (src.empty()) { std::cerr << "错误:无法加载源图像" << std::endl; return -1; } // 创建黑色背景(1920x1080) Mat background(1080, 1920, CV_8UC3, Scalar(0, 0, 0)); // 计算源图像在背景中的位置 Point start_pos( (background.cols - src.cols) / 2, (background.rows - src.rows) / 2 ); Rect roi(start_pos.x, start_pos.y, src.cols, src.rows); // ========== 视频写入配置 ========== VideoWriter writer( output_path, VideoWriter::fourcc('m','p','4','v'), fps, background.size() ); if (!writer.isOpened()) { std::cerr << "错误:无法创建视频文件" << std::endl; return -1; } // ========== 旋转动画生成 ========== Point2f center(src.cols/2.0f, src.rows/2.0f); // 旋转中心 int total_frames = duration * fps; for (int i = 0; i < total_frames; ++i) { // 计算当前时间和旋转角度 double t = i / static_cast<double>(fps); double angle_rad = (a/omega)*(1 - cos(omega*t)) + b*t; double angle_deg = angle_rad * 180.0 / CV_PI; // 执行图像旋转 Mat rot_mat = getRotationMatrix2D(center, angle_deg, 1.0); Mat rotated; warpAffine(src, rotated, rot_mat, src.size(), INTER_LINEAR, BORDER_CONSTANT, Scalar(0,0,0)); // 合成到背景并写入视频 Mat frame = background.clone(); rotated.copyTo(frame(roi)); writer.write(frame); } writer.release(); std::cout << "视频生成完成:" << output_path << std::endl; return 0; } ``` ### 关键实现原理说明: 1. **运动方程计算**: - 角速度公式:$$ \omega(t) = a \cdot \sin(\omega t) + b $$ - 角度积分结果:$$ \theta(t) = \frac{a}{\omega}(1 - \cos(\omega t)) + b t $$ - 通过离散时间采样(按帧率)计算累积旋转角度 2. **图像旋转处理**: - 使用`getRotationMatrix2D`生成旋转矩阵 - `warpAffine`执行实际的图像旋转操作 - `BORDER_CONSTANT`用黑色填充旋转产生的空白区域 3. **视频合成**: - 始终保持源图像中心背景中心对齐 - 每帧生成新的背景副本避免画面残留 ### 使用注意事项: 1. 根据需要调整以下参数: - `a`:控制正弦波的振幅 - `omega`:控制旋转速度变化频率 - `duration`:视频总时长 - `fps`:帧率(影响流畅度和文件大小) 2. 输入图片应满足: - 任意尺寸但不超过1920x1080 - 保证旋转后不会超出背景范围(如问题描述要求) 3. 输出视频: - 使用MP4V编码器 - 固定分辨率1920x1080 - 黑色背景的MP4格式文件 ### 数学公式验证: 角速度积分过程: $$ \theta(t) = \int_0^t [a \sin(\omega \tau) + b] d\tau = \frac{a}{\omega}(1 - \cos(\omega t)) + b t $$ 该实现通过精确的积分计算避免了欧拉积分法的累计误差,确保运动轨迹的数学准确性。
评论 10
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值