修正螺杆的手眼转换; 工件孔的更新算法

This commit is contained in:
杰仔 2026-05-22 11:03:02 +08:00
parent fde0fb3eee
commit c745c36743
22 changed files with 637 additions and 476 deletions

View File

@ -0,0 +1,132 @@
#pragma once
#include "CoordinateTypes.h"
#include <array>
struct HandEyeExtrinsic;
struct RobotPose6D;
// 螺杆 / 工具盘姿态计算工具集。
//
// 解决两类问题:
// 1) 当主轴接近世界 Z 时,原 Y = Z_world × X 构造的副轴对 X 的微小抖动敏感(叉乘量级
// 退化到 sin(夹角)),会被放大成 ~10° 的方向摆动。BuildAnchoredFrame 用「参考方向
// 投影到 X 垂直平面」的 Gram-Schmidt 方式锚定 Y 轴,对主轴抖动鲁棒。
// 2) 「Eye 三轴 → Eye 补偿 → Eye→Robot 变换 → Robot 补偿 → 提欧拉 → 万向锁消歧」
// 的流水线在螺杆和工具盘场景重复出现,封装为 ComputeRobotPoseAngles 单次调用。
namespace PoseAxesBuilder
{
// ============ 基础数值工具 ============
CTVec3D NormalizeVector(const CTVec3D& v);
bool IsValidVector(const CTVec3D& v);
// ============ 手眼标定相关 ============
// 从 16 元素行优先数组构建手眼标定矩阵。
// 约定:clibMatrix 表达 T_flange_camera(即 Eye 系下的点经其变换得到 Flange 系下的点),
// 与 CCoordinateTransform::sixAxisEyeInHandBuildTransform 入参一致。
CTHomogeneousMatrix BuildHandEyeMatrix(const double clibMatrix[16]);
// 把法兰系下的局部方向(例如 (0,1,0) = 法兰 Y 轴)转换为 Eye 系下的方向向量。
// 用于得到稳定的 Y 锚点:法兰姿态固定时其轴在 Eye 系下的方向也稳定。
// 内部走 handEyeMatrix.rotation^T,仅旋转、不平移。
CTVec3D FlangeAxisToEye(const CTHomogeneousMatrix& handEyeMatrix,
const CTVec3D& flangeLocalAxis);
// 按 poseOutputOrder 把 RobotPose6D.a/b/c 重排成真实绕轴角度 rx/ry/rz(度)。
void ResolveRobotPoseAnglesDegrees(const RobotPose6D& robotPose,
int poseOutputOrder,
double& rxDeg,
double& ryDeg,
double& rzDeg);
// ============ 锚点式正交三元组构建 ============
// 给定主轴 primary 和参考方向 referenceY,构建正交三元组:
// X = primary(归一化)
// Y = (referenceY - (referenceY·X) X) 归一化 (Gram-Schmidt 投影)
// Z = X × Y(归一化)
//
// 典型用法:
// - 螺杆:primary = 螺杆轴向, referenceY = FlangeAxisToEye(handEye, (0,1,0))
// - 工具盘:primary = 算法 X 轴, referenceY = 算法 Y 轴(已正交时退化为恒等)
//
// 退化判定:投影后 |Y_proj| < minPerpendicularity(即 primary 与 referenceY 近共线),
// 此时副轴无法稳定确定,返回 false;调用方应换用另一参考方向或跳过该目标。
bool BuildAnchoredFrame(const CTVec3D& primary,
const CTVec3D& referenceY,
std::array<CTVec3D, 3>& axes,
double minPerpendicularity = 0.1);
// ============ 三元组运算 ============
// 在三轴自身坐标系内施加 R = Rx(rotX)·Ry(rotY)·Rz(rotZ)(角度)。
// 物理含义:把"算法工具系"二次旋转到"机器人期望 TCP 系"或远离万向锁的位姿。
void ApplyAxesRotation(std::array<CTVec3D, 3>& axes,
double rotXDeg, double rotYDeg, double rotZDeg);
// 把 srcAxes 经齐次变换的旋转部分搬到 dstAxes(不平移),并归一化。
// 任一轴归一化失败返回 false。
bool TransformAxes(const std::array<CTVec3D, 3>& srcAxes,
const CTHomogeneousMatrix& transform,
std::array<CTVec3D, 3>& dstAxes);
// 把三轴当列向量组装成 3x3 旋转矩阵。
CTRotationMatrix BuildRotationMatrix(const std::array<CTVec3D, 3>& axes);
// ============ 欧拉提取与消歧 ============
// 旋转矩阵 → 欧拉角(按 order),自动把 pitch 归一化到 [-90°, 90°]。
void RotationMatrixToConfiguredEulerDegrees(const CTRotationMatrix& rotation,
CTEulerOrder order,
double& rollDeg,
double& pitchDeg,
double& yawDeg);
// 万向锁附近(|pitch| 接近 90°)把 yaw/roll 锚定到 (refYaw/refRoll) 周围,让多帧输出
// 稳定落在机器人当前法兰姿态附近。|pitch| < 80° 时不做任何修改。
void ResolveGimbalAmbiguity(double& rollDeg,
double pitchDeg,
double& yawDeg,
double refRollDeg,
double refYawDeg);
// ============ 完整流水线(DetectScrew / DetectToolDisk 共用)============
struct PoseAngles
{
double rollDeg = 0.0;
double pitchDeg = 0.0;
double yawDeg = 0.0;
};
// 中间状态调试信息;不需要可传 nullptr。
struct PoseDebugInfo
{
PoseAngles eyeEulerBefore; // ApplyAxesRotation(rotX/Y/Z) 之前的 Eye 系欧拉角
PoseAngles eyeEulerAfter; // 同上之后的 Eye 系欧拉角
PoseAngles robotEulerBeforeOutRot; // Eye→Robot 变换后、ApplyAxesRotation(outRotX/Y/Z) 之前的 Robot 系欧拉角
// —— 标定 outRot 时把它粘进 CloudView「姿态补偿」的"当前"栏
CTRotationMatrix eyeRotationAfter; // Eye 补偿后的旋转矩阵
CTRotationMatrix robotRotation; // 最终 Robot 系旋转矩阵
};
// Eye 系三轴 → Robot 系欧拉角完整流水线:
// 1) Eye 系内补偿:ApplyAxesRotation(extrinsic.rotX/Y/Z)
// 2) Eye → Robot 变换:TransformAxes(eyeInHandTransform)
// 3) Robot 系内补偿:ApplyAxesRotation(extrinsic.outRotX/Y/Z)
// 4) 提欧拉角 + 万向锁消歧(refRollDeg / refYawDeg 通常取机器人当前法兰姿态)
//
// 返回 false:变换链中任一步退化(向量长度为零等)。
bool ComputeRobotPoseAngles(const std::array<CTVec3D, 3>& eyeAxes,
const CTHomogeneousMatrix& eyeInHandTransform,
const HandEyeExtrinsic& extrinsic,
CTEulerOrder eulerOrder,
double refRollDeg,
double refYawDeg,
PoseAngles& outAngles,
PoseDebugInfo* outDebugInfo = nullptr);
} // namespace PoseAxesBuilder

View File

@ -1,21 +1,14 @@
#include "DetectPresenter.h"
#include "AlgorithmParamConverter.h"
#include "CoordinateTransform.h"
#include "PoseAxesBuilder.h"
#include "ScrewPositionTCPProtocol.h"
#include "rodAndBarDetection_Export.h"
#include <QColor>
#include <array>
#include <cmath>
#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif
namespace {
constexpr double kVectorEpsilon = 1e-8;
constexpr double kRotationEpsilon = 1e-9;
QImage BuildScrewPointCloudImage(const std::vector<std::vector<SVzNL3DPosition>>& xyzData,
const std::vector<SSX_rodPoseInfo>& screwInfo,
const std::vector<std::pair<double, double>>* approachXY = nullptr)
@ -67,7 +60,6 @@ QImage BuildToolDiskPointCloudImage(const std::vector<std::vector<SVzNL3DPositio
const QColor normalColor(0, 180, 255); // 蓝色:法向 Z
const QColor xAxisColor(255, 0, 0); // 红色:X 轴
const QColor yAxisColor(0, 255, 0); // 绿色:Y 轴
const QColor textColor(255, 255, 0);
const QColor approachColor(255, 128, 0); // 橙色:接近点
// 绘制定位盘中心点
@ -114,389 +106,11 @@ void SaveDebugImageIfNeeded(int cameraIndex,
image.save(QString::fromStdString(fileName));
}
CTHomogeneousMatrix BuildHandEyeMatrix(const double clibMatrix[16])
{
CTHomogeneousMatrix handEyeMatrix;
for (int row = 0; row < 4; ++row) {
for (int col = 0; col < 4; ++col) {
handEyeMatrix.at(row, col) = clibMatrix[row * 4 + col];
}
}
return handEyeMatrix;
}
void ResolveRobotPoseAnglesDegrees(const RobotPose6D& robotPose,
int poseOutputOrder,
double& rxDeg,
double& ryDeg,
double& rzDeg)
{
switch (poseOutputOrder) {
case 1:
rxDeg = robotPose.a;
ryDeg = robotPose.b;
rzDeg = robotPose.c;
break;
case 2:
rxDeg = robotPose.b;
ryDeg = robotPose.a;
rzDeg = robotPose.c;
break;
case 3:
rxDeg = robotPose.c;
ryDeg = robotPose.a;
rzDeg = robotPose.b;
break;
case 4:
rxDeg = robotPose.b;
ryDeg = robotPose.c;
rzDeg = robotPose.a;
break;
case 5:
rxDeg = robotPose.c;
ryDeg = robotPose.b;
rzDeg = robotPose.a;
break;
case 0:
default:
rxDeg = robotPose.a;
ryDeg = robotPose.b;
rzDeg = robotPose.c;
break;
}
}
// 将角度归一化到 (-180°, 180°]
double WrapDegreesTo180(double deg)
{
while (deg > 180.0) {
deg -= 360.0;
}
while (deg <= -180.0) {
deg += 360.0;
}
return deg;
}
// 对 Tait-Bryan 欧拉角做等价归一化,使 pitch ∈ [-90°, 90°]
// 依据:(α, β, γ) ≡ (α±180°, 180°-β, γ±180°) 以及 (α±180°, -180°-β, γ±180°)
// 适用于 CTEulerOrder 中所有 12 种 Tait-Bryan 顺序(内旋/外旋一致)
void NormalizePitchRange(double& rollDeg, double& pitchDeg, double& yawDeg)
{
pitchDeg = WrapDegreesTo180(pitchDeg);
if (pitchDeg > 90.0) {
pitchDeg = 180.0 - pitchDeg;
rollDeg += 180.0;
yawDeg += 180.0;
} else if (pitchDeg < -90.0) {
pitchDeg = -180.0 - pitchDeg;
rollDeg += 180.0;
yawDeg += 180.0;
}
rollDeg = WrapDegreesTo180(rollDeg);
yawDeg = WrapDegreesTo180(yawDeg);
}
// 统一走 CCoordinateTransform::rotationMatrixToEuler(与 eulerToRotationMatrix 成对互逆),
// 再把 pitch 压入 [-90°, 90°],保证对机器人侧输出的姿态语义稳定。
void RotationMatrixToConfiguredEulerDegrees(const CTRotationMatrix& rotation,
CTEulerOrder order,
double& rollDeg,
double& pitchDeg,
double& yawDeg)
{
const CTEulerAngles euler = CCoordinateTransform::rotationMatrixToEuler(rotation, order);
euler.toDegrees(rollDeg, pitchDeg, yawDeg);
NormalizePitchRange(rollDeg, pitchDeg, yawDeg);
}
// 万向锁消歧:当 |pitch| 接近 90° 时,Rz(yaw)·Ry(pitch)·Rx(roll) 的分解退化成一个自由度。
// 严格展开可得:
// pitch = +90° → R 只依赖 (roll - yaw);任意满足 roll - yaw = const 的组合都表示同一 R
// pitch = -90° → R 只依赖 (roll + yaw);任意满足 roll + yaw = const 的组合都表示同一 R
// 本函数把 yaw 锚定到 refYaw 附近,把剩余自由度全部放到 roll,
// 让不同帧的输出稳定落在参考(通常是机器人法兰的 rx、rz)附近。
//
// 【强消歧配置】:阈值覆盖 80°-85°,优先保证数值稳定性,代价是 pitch∈[80°,85°]
// 的过渡带会改变真实旋转矩阵(X 轴可能偏 3°-5°)。如需精确定姿请收窄阈值或换欧拉顺序。
// |pitch| < kSoftThreshold:完全不改(保留真实分解)
// |pitch| ≥ kHardThreshold:完全锁到参考(对 R 影响最小)
// kSoftThreshold ≤ |pitch| < kHardThreshold:线性插值过渡
void ResolveGimbalAmbiguity(double& rollDeg, double pitchDeg, double& yawDeg,
double refRollDeg, double refYawDeg)
{
constexpr double kSoftThresholdDeg = 80.0;
constexpr double kHardThresholdDeg = 85.0;
const double absPitch = std::abs(pitchDeg);
if (absPitch < kSoftThresholdDeg) {
return;
}
// 不变量方向与 pitch 符号的关系(由 Rz·Ry·Rx 展开式导出):
// pitch > 0 → inv = roll - yaw
// pitch < 0 → inv = roll + yaw
const bool positivePitch = (pitchDeg >= 0.0);
const double sum = positivePitch ? (rollDeg - yawDeg) : (rollDeg + yawDeg);
const double refSum = positivePitch ? (refRollDeg - refYawDeg) : (refRollDeg + refYawDeg);
double adjustedSum = sum;
while (adjustedSum - refSum > 180.0) { adjustedSum -= 360.0; }
while (adjustedSum - refSum < -180.0) { adjustedSum += 360.0; }
const double fullYaw = WrapDegreesTo180(refYawDeg);
// 反解 roll:positive 时 roll = sum + yaw;negative 时 roll = sum - yaw(与 sum 方向匹配)
const double fullRoll = WrapDegreesTo180(positivePitch ? (adjustedSum + fullYaw)
: (adjustedSum - fullYaw));
if (absPitch >= kHardThresholdDeg) {
rollDeg = fullRoll;
yawDeg = fullYaw;
return;
}
// 过渡带:alpha 从 0(kSoftThreshold)线性增到 1(kHardThreshold)
const double alpha = (absPitch - kSoftThresholdDeg) /
(kHardThresholdDeg - kSoftThresholdDeg);
auto lerpAngle = [alpha](double from, double to) {
const double diff = WrapDegreesTo180(to - from);
return WrapDegreesTo180(from + alpha * diff);
};
rollDeg = lerpAngle(rollDeg, fullRoll);
yawDeg = lerpAngle(yawDeg, fullYaw);
}
CTVec3D NormalizeVector(const CTVec3D& vector)
{
const double length = vector.norm();
if (length < kVectorEpsilon) {
return CTVec3D();
}
return vector * (1.0 / length);
}
double DotProduct(const CTVec3D& a, const CTVec3D& b)
{
return a.x * b.x + a.y * b.y + a.z * b.z;
}
CTVec3D CrossProduct(const CTVec3D& a, const CTVec3D& b)
{
return CTVec3D(a.y * b.z - a.z * b.y,
a.z * b.x - a.x * b.z,
a.x * b.y - a.y * b.x);
}
bool IsValidVector(const CTVec3D& vector)
{
return vector.norm() >= kVectorEpsilon;
}
CTVec3D ToCTVec3D(const SVzNL3DPoint& point)
{
return CTVec3D(point.x, point.y, point.z);
}
bool TryProjectedAxis(const CTVec3D& reference, const CTVec3D& primary, CTVec3D& axis)
{
const CTVec3D candidate = NormalizeVector(reference - primary * DotProduct(reference, primary));
if (!IsValidVector(candidate)) {
return false;
}
axis = candidate;
return true;
}
bool BuildFrameFromXAxis(const CTVec3D& primary, std::array<CTVec3D, 3>& axes)
{
const CTVec3D xAxis = NormalizeVector(primary);
if (!IsValidVector(xAxis)) {
return false;
}
CTVec3D yAxis = NormalizeVector(CrossProduct(CTVec3D(0.0, 0.0, 1.0), xAxis));
if (!IsValidVector(yAxis) &&
!TryProjectedAxis(CTVec3D(0.0, 1.0, 0.0), xAxis, yAxis) &&
!TryProjectedAxis(CTVec3D(1.0, 0.0, 0.0), xAxis, yAxis)) {
return false;
}
const CTVec3D zAxis = NormalizeVector(CrossProduct(xAxis, yAxis));
if (!IsValidVector(zAxis)) {
return false;
}
axes = {xAxis, yAxis, zAxis};
return true;
}
void ApplyAxesRotation(std::array<CTVec3D, 3>& axes,
double rotXDeg, double rotYDeg, double rotZDeg)
{
if (std::fabs(rotXDeg) < kRotationEpsilon &&
std::fabs(rotYDeg) < kRotationEpsilon &&
std::fabs(rotZDeg) < kRotationEpsilon) {
return;
}
const double rx = rotXDeg * M_PI / 180.0;
const double ry = rotYDeg * M_PI / 180.0;
const double rz = rotZDeg * M_PI / 180.0;
const double cx = std::cos(rx), sx = std::sin(rx);
const double cy = std::cos(ry), sy = std::sin(ry);
const double cz = std::cos(rz), sz = std::sin(rz);
// R = Rx(rx) * Ry(ry) * Rz(rz);对 Eye 坐标系下的工具轴组合(列向量)右乘 R,
// 等价于在工具坐标系内按内旋 XYZ 顺序施加补偿旋转。
double R[3][3];
R[0][0] = cy * cz;
R[0][1] = -cy * sz;
R[0][2] = sy;
R[1][0] = sx * sy * cz + cx * sz;
R[1][1] = -sx * sy * sz + cx * cz;
R[1][2] = -sx * cy;
R[2][0] = -cx * sy * cz + sx * sz;
R[2][1] = cx * sy * sz + sx * cz;
R[2][2] = cx * cy;
const CTVec3D oldX = axes[0];
const CTVec3D oldY = axes[1];
const CTVec3D oldZ = axes[2];
axes[0] = oldX * R[0][0] + oldY * R[1][0] + oldZ * R[2][0];
axes[1] = oldX * R[0][1] + oldY * R[1][1] + oldZ * R[2][1];
axes[2] = oldX * R[0][2] + oldY * R[1][2] + oldZ * R[2][2];
}
bool TransformAxes(const std::array<CTVec3D, 3>& eyeAxes,
const CTHomogeneousMatrix& transform,
std::array<CTVec3D, 3>& robotAxes)
{
for (size_t i = 0; i < eyeAxes.size(); ++i) {
robotAxes[i] = NormalizeVector(transform.transformVector(eyeAxes[i]));
if (!IsValidVector(robotAxes[i])) {
return false;
}
}
return true;
}
CTRotationMatrix BuildRotationMatrix(const std::array<CTVec3D, 3>& axes)
{
CTRotationMatrix rotation;
rotation.at(0, 0) = axes[0].x;
rotation.at(0, 1) = axes[1].x;
rotation.at(0, 2) = axes[2].x;
rotation.at(1, 0) = axes[0].y;
rotation.at(1, 1) = axes[1].y;
rotation.at(1, 2) = axes[2].y;
rotation.at(2, 0) = axes[0].z;
rotation.at(2, 1) = axes[1].z;
rotation.at(2, 2) = axes[2].z;
return rotation;
}
// ============ ZXY 欧拉角分解/重建(绕开 ZYX 奇异区) ============
// ZXY 分解:R = Rz(α)·Rx(β)·Ry(γ)
// ZXY 奇异点在 β=±90°(R(2,1)=±1),与 ZYX 奇异点 pitch≈±90° 不重叠。
static void DecomposeRotationZXY(const CTRotationMatrix& R,
double& alpha, double& beta, double& gamma)
{
const double sb = std::max(-1.0, std::min(1.0, R.at(2, 1)));
const double betaRad = std::asin(sb);
const double cb = std::cos(betaRad);
if (std::fabs(cb) > 1e-7) {
alpha = std::atan2(-R.at(0, 1), R.at(1, 1)) * 180.0 / M_PI;
gamma = std::atan2(-R.at(2, 0), R.at(2, 2)) * 180.0 / M_PI;
} else {
alpha = std::atan2(R.at(1, 0), R.at(0, 0)) * 180.0 / M_PI;
gamma = 0.0;
}
beta = betaRad * 180.0 / M_PI;
}
static CTRotationMatrix RebuildRotationZXY(double alphaDeg, double betaDeg, double gammaDeg)
{
const double a = alphaDeg * M_PI / 180.0;
const double b = betaDeg * M_PI / 180.0;
const double g = gammaDeg * M_PI / 180.0;
const double ca = std::cos(a), sa = std::sin(a);
const double cb = std::cos(b), sb = std::sin(b);
const double cg = std::cos(g), sg = std::sin(g);
CTRotationMatrix R;
R.at(0,0) = ca*cg + sa*sb*sg; R.at(0,1) = -sa*cb; R.at(0,2) = ca*sg + sa*sb*cg;
R.at(1,0) = sa*cg - ca*sb*sg; R.at(1,1) = ca*cb; R.at(1,2) = sa*sg - ca*sb*cg;
R.at(2,0) = -cb*sg; R.at(2,1) = sb; R.at(2,2) = cb*cg;
return R;
}
// 直接 ZYX 分解:R = Rz(yaw)·Ry(pitch)·Rx(roll)
static void DecomposeRotationZYX(const CTRotationMatrix& R,
double& roll, double& pitch, double& yaw)
{
const double sp = std::max(-1.0, std::min(1.0, -R.at(2, 0)));
const double pitchRad = std::asin(sp);
const double cp = std::cos(pitchRad);
if (std::fabs(cp) > 1e-7) {
roll = std::atan2(R.at(2, 1), R.at(2, 2)) * 180.0 / M_PI;
yaw = std::atan2(R.at(1, 0), R.at(0, 0)) * 180.0 / M_PI;
} else {
yaw = std::atan2(-R.at(0, 1), R.at(1, 1)) * 180.0 / M_PI;
roll = 0.0;
}
pitch = pitchRad * 180.0 / M_PI;
}
// 稳定 ZYX 欧拉角提取:
// 1) |R(2,0)| ≤ threshold → 直接 ZYX 分解
// 2) |R(2,0)| > threshold → ZXY 分解(奇异点不重叠)→ 重建 R' → ZYX 分解
// 3) 用拍照法兰姿态做连续性修正(±360° 拉回参考附近)
static void ExtractStableZYXEulerDegrees(const CTRotationMatrix& R,
double& roll, double& pitch, double& yaw,
double refRoll, double refYaw)
{
constexpr double kSingularityThreshold = 0.9998;
const double absSinPitch = std::fabs(R.at(2, 0));
if (absSinPitch > kSingularityThreshold) {
LOG_INFO("[Algo Thread] ZYX singularity: |R(2,0)|=%.6f > %.4f\n",
absSinPitch, kSingularityThreshold);
// ZXY 分解
double alpha, beta, gamma;
DecomposeRotationZXY(R, alpha, beta, gamma);
LOG_INFO("[Algo Thread] ZXY: Z=%.3f, X=%.3f, Y=%.3f\n", alpha, beta, gamma);
// 重建 R' 并验证
CTRotationMatrix Rp = RebuildRotationZXY(alpha, beta, gamma);
double err = 0;
for (int i = 0; i < 3; ++i)
for (int j = 0; j < 3; ++j)
err += (Rp.at(i, j) - R.at(i, j)) * (Rp.at(i, j) - R.at(i, j));
err = std::sqrt(err);
LOG_INFO("[Algo Thread] R' rebuild error: %.10f\n", err);
// 对 R' 做 ZYX 分解
DecomposeRotationZYX(Rp, roll, pitch, yaw);
LOG_INFO("[Algo Thread] ZYX from R': Roll=%.3f, Pitch=%.3f, Yaw=%.3f\n", roll, pitch, yaw);
} else {
DecomposeRotationZYX(R, roll, pitch, yaw);
}
// 连续性修正:拉到参考姿态附近
double yawDiff = yaw - refYaw;
while (yawDiff > 180.0) { yaw -= 360.0; yawDiff -= 360.0; }
while (yawDiff < -180.0) { yaw += 360.0; yawDiff += 360.0; }
double rollDiff = roll - refRoll;
while (rollDiff > 180.0) { roll -= 360.0; rollDiff -= 360.0; }
while (rollDiff < -180.0) { roll += 360.0; rollDiff += 360.0; }
}
}
DetectPresenter::DetectPresenter(/* args */)
@ -588,31 +202,39 @@ int DetectPresenter::DetectScrew(
detectionResult.success = true;
detectionResult.errorCode = 0;
detectionResult.message = QStringLiteral("螺杆检测成功");
const CTHomogeneousMatrix handEyeMatrix = BuildHandEyeMatrix(clibMatrix);
const CTHomogeneousMatrix handEyeMatrix = PoseAxesBuilder::BuildHandEyeMatrix(clibMatrix);
const CTEulerOrder order = static_cast<CTEulerOrder>(extrinsic.eulerOrder);
double rx, ry, rz;
ResolveRobotPoseAnglesDegrees(robotPose, poseOutputOrder, rx, ry, rz);
PoseAxesBuilder::ResolveRobotPoseAnglesDegrees(robotPose, poseOutputOrder, rx, ry, rz);
const CTRobotPose flangePose = CTRobotPose::fromDegrees(robotPose.x, robotPose.y, robotPose.z, rx, ry, rz);
// flangePose.rx/ry/rz 已经是真实轴角,可直接交给 sixAxisEyeInHandBuildTransform 按 eulerOrder 组装。
const CTHomogeneousMatrix eyeInHandTransform = CCoordinateTransform::sixAxisEyeInHandBuildTransform(
flangePose, order, handEyeMatrix);
// 锚点:法兰 Y 轴在 Eye 系下的方向。法兰姿态在一次检测内固定,所以 flangeYInEye 也固定,
// 用它作 BuildAnchoredFrame 的参考方向可以消除"螺杆轴向接近 Z_world 时副轴方向被放大抖动"。
const CTVec3D flangeYInEye = PoseAxesBuilder::FlangeAxisToEye(handEyeMatrix, CTVec3D(0.0, 1.0, 0.0));
if (debugParam.enableDebug && debugParam.printDetailLog) {
LOG_INFO("[Algo Thread] Robot flange pose fields: X=%.3f, Y=%.3f, Z=%.3f, RX=%.3f, RY=%.3f, RZ=%.3f (eulerOrder=%d)\n",
robotPose.x, robotPose.y, robotPose.z, rx, ry, rz, static_cast<int>(order));
LOG_INFO("[Algo Thread] Flange Y axis in Eye frame (anchor): X=%.6f, Y=%.6f, Z=%.6f\n",
flangeYInEye.x, flangeYInEye.y, flangeYInEye.z);
}
std::vector<std::pair<double, double>> approachEyeXY;
approachEyeXY.reserve(screwInfo.size());
const bool needDebug = debugParam.enableDebug && debugParam.printDetailLog;
for (size_t i = 0; i < screwInfo.size(); ++i) {
const auto& screw = screwInfo[i];
const CTVec3D eyeCenter = ToCTVec3D(screw.center);
const CTVec3D eyeAxialDir = NormalizeVector(ToCTVec3D(screw.axialDir));
const CTVec3D eyeAxialDir = PoseAxesBuilder::NormalizeVector(ToCTVec3D(screw.axialDir));
const CTVec3D robotCenter = eyeInHandTransform.transformPoint(eyeCenter);
const CTVec3D robotAxialDir = NormalizeVector(eyeInHandTransform.transformVector(eyeAxialDir));
const CTVec3D robotAxialDir = PoseAxesBuilder::NormalizeVector(eyeInHandTransform.transformVector(eyeAxialDir));
// 接近点:Eye 系沿螺杆轴偏移 offset,再经 T 变换到 Robot 系;姿态与目标点共用。
const double offset = -extrinsic.approachOffset;
@ -622,40 +244,22 @@ int DetectPresenter::DetectScrew(
const CTVec3D robotApproach = eyeInHandTransform.transformPoint(eyeApproach);
approachEyeXY.emplace_back(eyeApproach.x, eyeApproach.y);
// 姿态:以螺杆轴为 X 轴构建正交三元组 → 标定补偿 → 变换到 Robot 系 → 提欧拉角。
// 锚点式三元组:X = 螺杆轴向,Y = 法兰 Y 投影到 X 垂直面,Z = X × Y。
std::array<CTVec3D, 3> eyeAxes;
if (!BuildFrameFromXAxis(eyeAxialDir, eyeAxes)) {
LOG_WARNING("[Algo Thread] Screw %zu: 无法由轴向构建坐标系,已跳过\n", i);
if (!PoseAxesBuilder::BuildAnchoredFrame(eyeAxialDir, flangeYInEye, eyeAxes)) {
LOG_WARNING("[Algo Thread] Screw %zu: 螺杆轴向与法兰 Y 锚点近共线,无法构造稳定副轴,已跳过\n", i);
continue;
}
if (debugParam.enableDebug && debugParam.printDetailLog) {
const CTRotationMatrix eyeRotationBefore = BuildRotationMatrix(eyeAxes);
double eyeRollBefore = 0.0;
double eyePitchBefore = 0.0;
double eyeYawBefore = 0.0;
RotationMatrixToConfiguredEulerDegrees(eyeRotationBefore, order, eyeRollBefore, eyePitchBefore, eyeYawBefore);
LOG_INFO("[Algo Thread] Screw %zu Eye Euler before compensation (order=%d): Roll=%.3f, Pitch=%.3f, Yaw=%.3f\n",
i, static_cast<int>(order), eyeRollBefore, eyePitchBefore, eyeYawBefore);
}
ApplyAxesRotation(eyeAxes, extrinsic.rotX, extrinsic.rotY, extrinsic.rotZ);
if (debugParam.enableDebug && debugParam.printDetailLog) {
const CTRotationMatrix eyeRotationAfter = BuildRotationMatrix(eyeAxes);
double eyeRollAfter = 0.0;
double eyePitchAfter = 0.0;
double eyeYawAfter = 0.0;
RotationMatrixToConfiguredEulerDegrees(eyeRotationAfter, order, eyeRollAfter, eyePitchAfter, eyeYawAfter);
LOG_INFO("[Algo Thread] Screw %zu Eye Euler after compensation (order=%d): Roll=%.3f, Pitch=%.3f, Yaw=%.3f\n",
i, static_cast<int>(order), eyeRollAfter, eyePitchAfter, eyeYawAfter);
}
std::array<CTVec3D, 3> robotAxes;
if (!TransformAxes(eyeAxes, eyeInHandTransform, robotAxes)) {
PoseAxesBuilder::PoseAngles angles;
PoseAxesBuilder::PoseDebugInfo debugInfo;
const bool poseOk = PoseAxesBuilder::ComputeRobotPoseAngles(
eyeAxes, eyeInHandTransform, extrinsic, order, rx, rz,
angles, needDebug ? &debugInfo : nullptr);
if (!poseOk) {
LOG_WARNING("[Algo Thread] Screw %zu: 轴变换到机器人坐标系失败,已跳过\n", i);
continue;
}
// Robot 系下对工具三轴做后补偿:把"算法工具系"修正到"机器人期望 TCP 系",
// 同时让最终输出欧拉角远离万向节锁;默认 0 不启用。
ApplyAxesRotation(robotAxes, extrinsic.outRotX, extrinsic.outRotY, extrinsic.outRotZ);
const CTRotationMatrix rotation = BuildRotationMatrix(robotAxes);
ScrewPosition pos;
pos.x = robotCenter.x + extrinsic.offsetX;
@ -664,12 +268,9 @@ int DetectPresenter::DetectScrew(
pos.approachX = robotApproach.x + extrinsic.offsetX;
pos.approachY = robotApproach.y + extrinsic.offsetY;
pos.approachZ = robotApproach.z + extrinsic.offsetZ;
#if 0
ExtractStableZYXEulerDegrees(rotation, pos.roll, pos.pitch, pos.yaw, rx, rz);
#else
RotationMatrixToConfiguredEulerDegrees(rotation, order, pos.roll, pos.pitch, pos.yaw);
ResolveGimbalAmbiguity(pos.roll, pos.pitch, pos.yaw, rx, rz);
#endif
pos.roll = angles.rollDeg;
pos.pitch = angles.pitchDeg;
pos.yaw = angles.yawDeg;
detectionResult.positions.push_back(pos);
ScrewInfo info;
@ -682,7 +283,25 @@ int DetectPresenter::DetectScrew(
info.rotateAngle = pos.roll;
detectionResult.screwInfoList.push_back(info);
if (debugParam.enableDebug && debugParam.printDetailLog) {
if (needDebug) {
LOG_INFO("[Algo Thread] Screw %zu Eye Euler before compensation (order=%d): Roll=%.3f, Pitch=%.3f, Yaw=%.3f\n",
i, static_cast<int>(order),
debugInfo.eyeEulerBefore.rollDeg,
debugInfo.eyeEulerBefore.pitchDeg,
debugInfo.eyeEulerBefore.yawDeg);
LOG_INFO("[Algo Thread] Screw %zu Eye Euler after compensation (order=%d): Roll=%.3f, Pitch=%.3f, Yaw=%.3f\n",
i, static_cast<int>(order),
debugInfo.eyeEulerAfter.rollDeg,
debugInfo.eyeEulerAfter.pitchDeg,
debugInfo.eyeEulerAfter.yawDeg);
// 给 outRot 标定用:这是「outRot 补偿前」的 Robot 系姿态。
// 把欧拉粘进 CloudView「工具→姿态补偿计算」的"当前"栏,期望填机械臂期望的姿态,
// 矩阵补偿 + XYZ 输出顺序,反解出来的就是 outRotX/Y/Z。
LOG_INFO("[Algo Thread] Screw %zu Robot RPY (before outRot, calibration ref): Roll=%.3f, Pitch=%.3f, Yaw=%.3f\n",
i,
debugInfo.robotEulerBeforeOutRot.rollDeg,
debugInfo.robotEulerBeforeOutRot.pitchDeg,
debugInfo.robotEulerBeforeOutRot.yawDeg);
LOG_INFO("[Algo Thread] Screw %zu Eye Coords: X=%.2f, Y=%.2f, Z=%.2f\n", i, screw.center.x, screw.center.y, screw.center.z);
LOG_INFO("[Algo Thread] Screw %zu Robot Coords: X=%.2f, Y=%.2f, Z=%.2f, RPY=%.2f/%.2f/%.2f\n", i, pos.x, pos.y, pos.z, pos.roll, pos.pitch, pos.yaw);
LOG_INFO("[Algo Thread] Screw %zu Axial Dir Eye: X=%.3f, Y=%.3f, Z=%.3f\n", i, screw.axialDir.x, screw.axialDir.y, screw.axialDir.z);
@ -753,21 +372,21 @@ int DetectPresenter::DetectToolDisk(
detectionResult.success = true;
detectionResult.errorCode = 0;
detectionResult.message = QStringLiteral("工具盘检测成功");
const CTHomogeneousMatrix handEyeMatrix = BuildHandEyeMatrix(clibMatrix);
const CTHomogeneousMatrix handEyeMatrix = PoseAxesBuilder::BuildHandEyeMatrix(clibMatrix);
const CTEulerOrder order = static_cast<CTEulerOrder>(extrinsic.eulerOrder);
double rx, ry, rz;
ResolveRobotPoseAnglesDegrees(robotPose, poseOutputOrder, rx, ry, rz);
const CTRobotPose flangePose = CTRobotPose::fromDegrees( robotPose.x, robotPose.y, robotPose.z, rx, ry, rz);
PoseAxesBuilder::ResolveRobotPoseAnglesDegrees(robotPose, poseOutputOrder, rx, ry, rz);
const CTRobotPose flangePose = CTRobotPose::fromDegrees(robotPose.x, robotPose.y, robotPose.z, rx, ry, rz);
// flangePose.rx/ry/rz 已经是真实轴角,可直接交给 sixAxisEyeInHandBuildTransform 按 eulerOrder 组装。
const CTHomogeneousMatrix eyeInHandTransform = CCoordinateTransform::sixAxisEyeInHandBuildTransform(
flangePose, order, handEyeMatrix);
// 将定位盘中心点通过手眼标定转换为机器人坐标
const CTVec3D robotCenter = eyeInHandTransform.transformPoint(ToCTVec3D(poseInfo.center));
const CTVec3D eyeXAxis = NormalizeVector(ToCTVec3D(poseInfo.xDir));
const CTVec3D eyeYAxis = NormalizeVector(ToCTVec3D(poseInfo.yDir));
const CTVec3D eyeNormalDir = NormalizeVector(ToCTVec3D(poseInfo.normalDir));
const CTVec3D eyeCenter = ToCTVec3D(poseInfo.center);
const CTVec3D robotCenter = eyeInHandTransform.transformPoint(eyeCenter);
const CTVec3D eyeXAxis = PoseAxesBuilder::NormalizeVector(ToCTVec3D(poseInfo.xDir));
const CTVec3D eyeYAxis = PoseAxesBuilder::NormalizeVector(ToCTVec3D(poseInfo.yDir));
ScrewPosition pos;
pos.x = robotCenter.x + extrinsic.offsetX;
@ -775,65 +394,69 @@ int DetectPresenter::DetectToolDisk(
pos.z = robotCenter.z + extrinsic.offsetZ;
// 接近点:Eye 系沿工具盘 X 轴偏移 offset,再经 T 变换到 Robot 系;姿态与目标点共用。
const double offset = extrinsic.approachOffset;
const CTVec3D eyeCenter = ToCTVec3D(poseInfo.center);
const CTVec3D eyeApproach(eyeCenter.x + eyeXAxis.x * offset, eyeCenter.y + eyeXAxis.y * offset, eyeCenter.z + eyeXAxis.z * offset);
const CTVec3D eyeApproach(eyeCenter.x + eyeXAxis.x * offset,
eyeCenter.y + eyeXAxis.y * offset,
eyeCenter.z + eyeXAxis.z * offset);
const CTVec3D robotApproach = eyeInHandTransform.transformPoint(eyeApproach);
pos.approachX = robotApproach.x + extrinsic.offsetX;
pos.approachY = robotApproach.y + extrinsic.offsetY;
pos.approachZ = robotApproach.z + extrinsic.offsetZ;
const std::pair<double, double> approachEyeXY(eyeApproach.x, eyeApproach.y);
// 姿态:三轴直接取算法输出 → 标定补偿 → 变换到 Robot 系 → 提欧拉角。
std::array<CTVec3D, 3> eyeAxes = {eyeXAxis, eyeYAxis, eyeNormalDir};
if (debugParam.enableDebug && debugParam.printDetailLog) {
const CTRotationMatrix eyeRotationBefore = BuildRotationMatrix(eyeAxes);
double eyeRollBefore = 0.0;
double eyePitchBefore = 0.0;
double eyeYawBefore = 0.0;
RotationMatrixToConfiguredEulerDegrees(eyeRotationBefore, order, eyeRollBefore, eyePitchBefore, eyeYawBefore);
LOG_INFO("[Algo Thread] ToolDisk Eye Euler before compensation (order=%d): Roll=%.3f, Pitch=%.3f, Yaw=%.3f\n",
static_cast<int>(order), eyeRollBefore, eyePitchBefore, eyeYawBefore);
// 锚点式三元组:算法已给出完整 X/Y/Z,但仍统一走 BuildAnchoredFrame —— 它对算法输出做
// Gram-Schmidt 正交化(算法本就正交时是恒等),保证后续欧拉提取数值稳定。
std::array<CTVec3D, 3> eyeAxes;
if (!PoseAxesBuilder::BuildAnchoredFrame(eyeXAxis, eyeYAxis, eyeAxes)) {
LOG_WARNING("[Algo Thread] ToolDisk: 算法 X/Y 轴近共线,无法构造稳定副轴\n");
return ERR_CODE(DEV_DATA_INVALID);
}
ApplyAxesRotation(eyeAxes, extrinsic.rotX, extrinsic.rotY, extrinsic.rotZ);
std::array<CTVec3D, 3> robotAxes;
if (!TransformAxes(eyeAxes, eyeInHandTransform, robotAxes)) {
const bool needDebug = debugParam.enableDebug && debugParam.printDetailLog;
PoseAxesBuilder::PoseAngles angles;
PoseAxesBuilder::PoseDebugInfo debugInfo;
if (!PoseAxesBuilder::ComputeRobotPoseAngles(
eyeAxes, eyeInHandTransform, extrinsic, order, rx, rz,
angles, needDebug ? &debugInfo : nullptr)) {
LOG_WARNING("[Algo Thread] ToolDisk: 轴变换到机器人坐标系失败\n");
return ERR_CODE(DEV_DATA_INVALID);
}
// Robot 系下对工具三轴做后补偿:把"算法工具系"修正到"机器人期望 TCP 系",
// 同时让最终输出欧拉角远离万向节锁;默认 0 不启用。
ApplyAxesRotation(robotAxes, extrinsic.outRotX, extrinsic.outRotY, extrinsic.outRotZ);
const CTRotationMatrix robotRotation = BuildRotationMatrix(robotAxes);
#if 0
ExtractStableZYXEulerDegrees(robotRotation, pos.roll, pos.pitch, pos.yaw, rx, rz);
#else
RotationMatrixToConfiguredEulerDegrees(robotRotation, order, pos.roll, pos.pitch, pos.yaw);
ResolveGimbalAmbiguity(pos.roll, pos.pitch, pos.yaw, rx, rz);
#endif
pos.roll = angles.rollDeg;
pos.pitch = angles.pitchDeg;
pos.yaw = angles.yawDeg;
detectionResult.positions.push_back(pos);
if (debugParam.enableDebug && debugParam.printDetailLog) {
const CTRotationMatrix eyeRotation = BuildRotationMatrix(eyeAxes);
double eyeRoll = 0.0;
double eyePitch = 0.0;
double eyeYaw = 0.0;
RotationMatrixToConfiguredEulerDegrees(eyeRotation, order, eyeRoll, eyePitch, eyeYaw);
if (needDebug) {
const CTRotationMatrix& eyeRotationAfter = debugInfo.eyeRotationAfter;
const CTRotationMatrix& robotRotation = debugInfo.robotRotation;
LOG_INFO("[Algo Thread] === Tool Disk Pose Debug ===\n");
LOG_INFO("[Algo Thread] Eye Rotation Matrix:\n");
LOG_INFO(" [%.6f, %.6f, %.6f]\n", eyeRotation.at(0, 0), eyeRotation.at(0, 1), eyeRotation.at(0, 2));
LOG_INFO(" [%.6f, %.6f, %.6f]\n", eyeRotation.at(1, 0), eyeRotation.at(1, 1), eyeRotation.at(1, 2));
LOG_INFO(" [%.6f, %.6f, %.6f]\n", eyeRotation.at(2, 0), eyeRotation.at(2, 1), eyeRotation.at(2, 2));
LOG_INFO("[Algo Thread] Eye Pos: (%.6f, %.6f, %.6f)\n", poseInfo.center.x, poseInfo.center.y, poseInfo.center.z);
LOG_INFO("[Algo Thread] Eye Euler after compensation (order=%d ): Roll=%.3f, Pitch=%.3f, Yaw=%.3f\n",
static_cast<int>(order), eyeRoll, eyePitch, eyeYaw);
LOG_INFO("[Algo Thread] ToolDisk Eye Euler before compensation (order=%d): Roll=%.3f, Pitch=%.3f, Yaw=%.3f\n",
static_cast<int>(order),
debugInfo.eyeEulerBefore.rollDeg,
debugInfo.eyeEulerBefore.pitchDeg,
debugInfo.eyeEulerBefore.yawDeg);
LOG_INFO("[Algo Thread] Eye Rotation Matrix (after compensation):\n");
LOG_INFO(" [%.6f, %.6f, %.6f]\n", eyeRotationAfter.at(0, 0), eyeRotationAfter.at(0, 1), eyeRotationAfter.at(0, 2));
LOG_INFO(" [%.6f, %.6f, %.6f]\n", eyeRotationAfter.at(1, 0), eyeRotationAfter.at(1, 1), eyeRotationAfter.at(1, 2));
LOG_INFO(" [%.6f, %.6f, %.6f]\n", eyeRotationAfter.at(2, 0), eyeRotationAfter.at(2, 1), eyeRotationAfter.at(2, 2));
LOG_INFO("[Algo Thread] Eye Pos: (%.6f, %.6f, %.6f)\n", poseInfo.center.x, poseInfo.center.y, poseInfo.center.z);
LOG_INFO("[Algo Thread] Eye Euler after compensation (order=%d): Roll=%.3f, Pitch=%.3f, Yaw=%.3f\n",
static_cast<int>(order),
debugInfo.eyeEulerAfter.rollDeg,
debugInfo.eyeEulerAfter.pitchDeg,
debugInfo.eyeEulerAfter.yawDeg);
LOG_INFO("[Algo Thread] Robot Rotation Matrix:\n");
LOG_INFO(" [%.6f, %.6f, %.6f]\n", robotRotation.at(0, 0), robotRotation.at(0, 1), robotRotation.at(0, 2));
LOG_INFO(" [%.6f, %.6f, %.6f]\n", robotRotation.at(1, 0), robotRotation.at(1, 1), robotRotation.at(1, 2));
LOG_INFO(" [%.6f, %.6f, %.6f]\n", robotRotation.at(2, 0), robotRotation.at(2, 1), robotRotation.at(2, 2));
LOG_INFO("[Algo Thread] Robot Pos: (%.6f, %.6f, %.6f)\n", robotCenter.x, robotCenter.y, robotCenter.z);
LOG_INFO("[Algo Thread] Robot Pos Euler (order=%d ): Roll=%.3f, Pitch=%.3f, Yaw=%.3f\n",
LOG_INFO("[Algo Thread] Robot Pos: (%.6f, %.6f, %.6f)\n", robotCenter.x, robotCenter.y, robotCenter.z);
// 给 outRot 标定用:outRot 补偿前的 Robot 系欧拉(粘进 CloudView「姿态补偿」"当前"栏)
LOG_INFO("[Algo Thread] ToolDisk Robot RPY (before outRot, calibration ref): Roll=%.3f, Pitch=%.3f, Yaw=%.3f\n",
debugInfo.robotEulerBeforeOutRot.rollDeg,
debugInfo.robotEulerBeforeOutRot.pitchDeg,
debugInfo.robotEulerBeforeOutRot.yawDeg);
LOG_INFO("[Algo Thread] Robot Pos Euler (order=%d): Roll=%.3f, Pitch=%.3f, Yaw=%.3f\n",
static_cast<int>(order), pos.roll, pos.pitch, pos.yaw);
LOG_INFO("[Algo Thread] ToolDisk Approach (offset=%.2f): X=%.2f, Y=%.2f, Z=%.2f\n",
offset, pos.approachX, pos.approachY, pos.approachZ);

View File

@ -0,0 +1,360 @@
#include "PoseAxesBuilder.h"
#include "CoordinateTransform.h"
#include "DetectPresenter.h" // for HandEyeExtrinsic
#include "ScrewPositionTCPProtocol.h" // for RobotPose6D
#include <cmath>
#ifndef M_PI
#define M_PI 3.14159265358979323846
#endif
namespace {
constexpr double kVectorEpsilon = 1e-8;
constexpr double kRotationEpsilon = 1e-9;
double DotProduct(const CTVec3D& a, const CTVec3D& b)
{
return a.x * b.x + a.y * b.y + a.z * b.z;
}
CTVec3D CrossProduct(const CTVec3D& a, const CTVec3D& b)
{
return CTVec3D(a.y * b.z - a.z * b.y,
a.z * b.x - a.x * b.z,
a.x * b.y - a.y * b.x);
}
double WrapDegreesTo180(double deg)
{
while (deg > 180.0) {
deg -= 360.0;
}
while (deg <= -180.0) {
deg += 360.0;
}
return deg;
}
// 对 Tait-Bryan 欧拉角做等价归一化,使 pitch ∈ [-90°, 90°]。
// 依据:(α, β, γ) ≡ (α±180°, 180°-β, γ±180°) 以及 (α±180°, -180°-β, γ±180°),
// 适用于 CTEulerOrder 中所有 12 种 Tait-Bryan 顺序(内旋/外旋一致)。
void NormalizePitchRange(double& rollDeg, double& pitchDeg, double& yawDeg)
{
pitchDeg = WrapDegreesTo180(pitchDeg);
if (pitchDeg > 90.0) {
pitchDeg = 180.0 - pitchDeg;
rollDeg += 180.0;
yawDeg += 180.0;
} else if (pitchDeg < -90.0) {
pitchDeg = -180.0 - pitchDeg;
rollDeg += 180.0;
yawDeg += 180.0;
}
rollDeg = WrapDegreesTo180(rollDeg);
yawDeg = WrapDegreesTo180(yawDeg);
}
} // namespace
namespace PoseAxesBuilder
{
CTVec3D NormalizeVector(const CTVec3D& v)
{
const double length = v.norm();
if (length < kVectorEpsilon) {
return CTVec3D();
}
return v * (1.0 / length);
}
bool IsValidVector(const CTVec3D& v)
{
return v.norm() >= kVectorEpsilon;
}
CTHomogeneousMatrix BuildHandEyeMatrix(const double clibMatrix[16])
{
CTHomogeneousMatrix handEyeMatrix;
for (int row = 0; row < 4; ++row) {
for (int col = 0; col < 4; ++col) {
handEyeMatrix.at(row, col) = clibMatrix[row * 4 + col];
}
}
return handEyeMatrix;
}
CTVec3D FlangeAxisToEye(const CTHomogeneousMatrix& handEyeMatrix,
const CTVec3D& flangeLocalAxis)
{
// handEyeMatrix: T_flange_camera,旋转部分 R 把 Eye 系向量映射到 Flange 系。
// 反向:把 Flange 系向量映射到 Eye 系 = R^T·v。
const CTRotationMatrix rotInv = handEyeMatrix.getRotation().transpose();
return NormalizeVector(rotInv.transform(flangeLocalAxis));
}
void ResolveRobotPoseAnglesDegrees(const RobotPose6D& robotPose,
int poseOutputOrder,
double& rxDeg,
double& ryDeg,
double& rzDeg)
{
switch (poseOutputOrder) {
case 1:
rxDeg = robotPose.a;
ryDeg = robotPose.b;
rzDeg = robotPose.c;
break;
case 2:
rxDeg = robotPose.b;
ryDeg = robotPose.a;
rzDeg = robotPose.c;
break;
case 3:
rxDeg = robotPose.c;
ryDeg = robotPose.a;
rzDeg = robotPose.b;
break;
case 4:
rxDeg = robotPose.b;
ryDeg = robotPose.c;
rzDeg = robotPose.a;
break;
case 5:
rxDeg = robotPose.c;
ryDeg = robotPose.b;
rzDeg = robotPose.a;
break;
case 0:
default:
rxDeg = robotPose.a;
ryDeg = robotPose.b;
rzDeg = robotPose.c;
break;
}
}
bool BuildAnchoredFrame(const CTVec3D& primary,
const CTVec3D& referenceY,
std::array<CTVec3D, 3>& axes,
double minPerpendicularity)
{
const CTVec3D xAxis = NormalizeVector(primary);
if (!IsValidVector(xAxis)) {
return false;
}
const CTVec3D refY = NormalizeVector(referenceY);
if (!IsValidVector(refY)) {
return false;
}
// Gram-Schmidt:refY 投影到与 X 垂直的平面。
// |yRaw| = sin(refY 与 X 的夹角)。小于阈值则两者近共线、副轴方向无意义。
const CTVec3D yRaw = refY - xAxis * DotProduct(refY, xAxis);
if (yRaw.norm() < minPerpendicularity) {
return false;
}
const CTVec3D yAxis = NormalizeVector(yRaw);
const CTVec3D zAxis = NormalizeVector(CrossProduct(xAxis, yAxis));
if (!IsValidVector(zAxis)) {
return false;
}
axes = {xAxis, yAxis, zAxis};
return true;
}
void ApplyAxesRotation(std::array<CTVec3D, 3>& axes,
double rotXDeg, double rotYDeg, double rotZDeg)
{
if (std::fabs(rotXDeg) < kRotationEpsilon &&
std::fabs(rotYDeg) < kRotationEpsilon &&
std::fabs(rotZDeg) < kRotationEpsilon) {
return;
}
const double rx = rotXDeg * M_PI / 180.0;
const double ry = rotYDeg * M_PI / 180.0;
const double rz = rotZDeg * M_PI / 180.0;
const double cx = std::cos(rx), sx = std::sin(rx);
const double cy = std::cos(ry), sy = std::sin(ry);
const double cz = std::cos(rz), sz = std::sin(rz);
// R = Rx(rx) * Ry(ry) * Rz(rz);对工具三轴(列向量)右乘 R,等价于在工具自身坐标系
// 内按内旋 XYZ 顺序施加补偿旋转。
double R[3][3];
R[0][0] = cy * cz;
R[0][1] = -cy * sz;
R[0][2] = sy;
R[1][0] = sx * sy * cz + cx * sz;
R[1][1] = -sx * sy * sz + cx * cz;
R[1][2] = -sx * cy;
R[2][0] = -cx * sy * cz + sx * sz;
R[2][1] = cx * sy * sz + sx * cz;
R[2][2] = cx * cy;
const CTVec3D oldX = axes[0];
const CTVec3D oldY = axes[1];
const CTVec3D oldZ = axes[2];
axes[0] = oldX * R[0][0] + oldY * R[1][0] + oldZ * R[2][0];
axes[1] = oldX * R[0][1] + oldY * R[1][1] + oldZ * R[2][1];
axes[2] = oldX * R[0][2] + oldY * R[1][2] + oldZ * R[2][2];
}
bool TransformAxes(const std::array<CTVec3D, 3>& srcAxes,
const CTHomogeneousMatrix& transform,
std::array<CTVec3D, 3>& dstAxes)
{
for (size_t i = 0; i < srcAxes.size(); ++i) {
dstAxes[i] = NormalizeVector(transform.transformVector(srcAxes[i]));
if (!IsValidVector(dstAxes[i])) {
return false;
}
}
return true;
}
CTRotationMatrix BuildRotationMatrix(const std::array<CTVec3D, 3>& axes)
{
CTRotationMatrix rotation;
rotation.at(0, 0) = axes[0].x;
rotation.at(0, 1) = axes[1].x;
rotation.at(0, 2) = axes[2].x;
rotation.at(1, 0) = axes[0].y;
rotation.at(1, 1) = axes[1].y;
rotation.at(1, 2) = axes[2].y;
rotation.at(2, 0) = axes[0].z;
rotation.at(2, 1) = axes[1].z;
rotation.at(2, 2) = axes[2].z;
return rotation;
}
void RotationMatrixToConfiguredEulerDegrees(const CTRotationMatrix& rotation,
CTEulerOrder order,
double& rollDeg,
double& pitchDeg,
double& yawDeg)
{
const CTEulerAngles euler = CCoordinateTransform::rotationMatrixToEuler(rotation, order);
euler.toDegrees(rollDeg, pitchDeg, yawDeg);
NormalizePitchRange(rollDeg, pitchDeg, yawDeg);
}
// 万向锁消歧:当 |pitch| 接近 90° 时,Rz(yaw)·Ry(pitch)·Rx(roll) 的分解退化成一个自由度。
// 严格展开可得:
// pitch = +90° → R 只依赖 (roll - yaw);任意满足 roll - yaw = const 的组合都表示同一 R
// pitch = -90° → R 只依赖 (roll + yaw);任意满足 roll + yaw = const 的组合都表示同一 R
// 本函数把 yaw 锚定到 refYaw 附近,把剩余自由度放到 roll,让不同帧的输出稳定落在
// 参考(通常是机器人法兰的 rx/rz)附近。阈值覆盖 80°-85°:
// |pitch| < 80°:完全不改(保留真实分解)
// |pitch| ≥ 85°:完全锁到参考(对 R 影响最小)
// 80° ≤ |pitch| < 85°:线性插值过渡
void ResolveGimbalAmbiguity(double& rollDeg, double pitchDeg, double& yawDeg,
double refRollDeg, double refYawDeg)
{
constexpr double kSoftThresholdDeg = 80.0;
constexpr double kHardThresholdDeg = 85.0;
const double absPitch = std::abs(pitchDeg);
if (absPitch < kSoftThresholdDeg) {
return;
}
// 不变量方向与 pitch 符号的关系(由 Rz·Ry·Rx 展开式导出):
// pitch > 0 → inv = roll - yaw
// pitch < 0 → inv = roll + yaw
const bool positivePitch = (pitchDeg >= 0.0);
const double sum = positivePitch ? (rollDeg - yawDeg) : (rollDeg + yawDeg);
const double refSum = positivePitch ? (refRollDeg - refYawDeg) : (refRollDeg + refYawDeg);
double adjustedSum = sum;
while (adjustedSum - refSum > 180.0) { adjustedSum -= 360.0; }
while (adjustedSum - refSum < -180.0) { adjustedSum += 360.0; }
const double fullYaw = WrapDegreesTo180(refYawDeg);
const double fullRoll = WrapDegreesTo180(positivePitch ? (adjustedSum + fullYaw)
: (adjustedSum - fullYaw));
if (absPitch >= kHardThresholdDeg) {
rollDeg = fullRoll;
yawDeg = fullYaw;
return;
}
const double alpha = (absPitch - kSoftThresholdDeg) /
(kHardThresholdDeg - kSoftThresholdDeg);
auto lerpAngle = [alpha](double from, double to) {
const double diff = WrapDegreesTo180(to - from);
return WrapDegreesTo180(from + alpha * diff);
};
rollDeg = lerpAngle(rollDeg, fullRoll);
yawDeg = lerpAngle(yawDeg, fullYaw);
}
bool ComputeRobotPoseAngles(const std::array<CTVec3D, 3>& eyeAxes,
const CTHomogeneousMatrix& eyeInHandTransform,
const HandEyeExtrinsic& extrinsic,
CTEulerOrder eulerOrder,
double refRollDeg,
double refYawDeg,
PoseAngles& outAngles,
PoseDebugInfo* outDebugInfo)
{
std::array<CTVec3D, 3> eyeAxesWorking = eyeAxes;
if (outDebugInfo) {
const CTRotationMatrix rEyeBefore = BuildRotationMatrix(eyeAxesWorking);
RotationMatrixToConfiguredEulerDegrees(rEyeBefore, eulerOrder,
outDebugInfo->eyeEulerBefore.rollDeg,
outDebugInfo->eyeEulerBefore.pitchDeg,
outDebugInfo->eyeEulerBefore.yawDeg);
}
// 1) Eye 系内补偿
ApplyAxesRotation(eyeAxesWorking, extrinsic.rotX, extrinsic.rotY, extrinsic.rotZ);
if (outDebugInfo) {
outDebugInfo->eyeRotationAfter = BuildRotationMatrix(eyeAxesWorking);
RotationMatrixToConfiguredEulerDegrees(outDebugInfo->eyeRotationAfter, eulerOrder,
outDebugInfo->eyeEulerAfter.rollDeg,
outDebugInfo->eyeEulerAfter.pitchDeg,
outDebugInfo->eyeEulerAfter.yawDeg);
}
// 2) Eye → Robot 变换
std::array<CTVec3D, 3> robotAxes;
if (!TransformAxes(eyeAxesWorking, eyeInHandTransform, robotAxes)) {
return false;
}
// 2.5) outRot 补偿之前的 Robot 系姿态:这是 CloudView「姿态补偿」工具应当读到的「当前」。
// 把日志里打印的欧拉粘进"当前"栏,期望填机械臂期望的姿态,矩阵补偿 + XYZ 输出顺序,
// 反解出来的就是 outRotX/Y/Z。
if (outDebugInfo) {
const CTRotationMatrix rRobotBeforeOutRot = BuildRotationMatrix(robotAxes);
RotationMatrixToConfiguredEulerDegrees(rRobotBeforeOutRot, eulerOrder,
outDebugInfo->robotEulerBeforeOutRot.rollDeg,
outDebugInfo->robotEulerBeforeOutRot.pitchDeg,
outDebugInfo->robotEulerBeforeOutRot.yawDeg);
}
// 3) Robot 系内补偿
ApplyAxesRotation(robotAxes, extrinsic.outRotX, extrinsic.outRotY, extrinsic.outRotZ);
// 4) 提欧拉角 + 万向锁消歧
const CTRotationMatrix robotRotation = BuildRotationMatrix(robotAxes);
if (outDebugInfo) {
outDebugInfo->robotRotation = robotRotation;
}
RotationMatrixToConfiguredEulerDegrees(robotRotation, eulerOrder,
outAngles.rollDeg, outAngles.pitchDeg, outAngles.yawDeg);
ResolveGimbalAmbiguity(outAngles.rollDeg, outAngles.pitchDeg, outAngles.yawDeg,
refRollDeg, refYawDeg);
return true;
}
} // namespace PoseAxesBuilder

View File

@ -53,6 +53,7 @@ SOURCES += \
Presenter/Src/ConfigManager.cpp \
Presenter/Src/ScrewPositionPresenter.cpp \
Presenter/Src/DetectPresenter.cpp \
Presenter/Src/PoseAxesBuilder.cpp \
Presenter/Src/ScrewPositionTCPProtocol.cpp \
Presenter/Src/TCPServerMethods.cpp \
main.cpp \
@ -65,6 +66,7 @@ HEADERS += \
Presenter/Inc/ConfigManager.h \
Presenter/Inc/ScrewPositionPresenter.h \
Presenter/Inc/DetectPresenter.h \
Presenter/Inc/PoseAxesBuilder.h \
Presenter/Inc/ScrewPositionTCPProtocol.h \
IYScrewPositionStatus.h \
mainwindow.h \

View File

@ -3,7 +3,7 @@
#define SCREWPOSITION_APP_NAME "螺杆定位"
#define SCREWPOSITION_VERSION_STRING "1.1.10"
#define SCREWPOSITION_VERSION_STRING "1.1.11"
#define SCREWPOSITION_BUILD_STRING "1"
#define SCREWPOSITION_FULL_VERSION_STRING "V" SCREWPOSITION_VERSION_STRING "_" SCREWPOSITION_BUILD_STRING

View File

@ -1,3 +1,11 @@
# 1.1.11 2026-05-22
## build_1
1. 修改螺杆的手眼转换
# 1.1.10 2026-05-15
## build_1
1. 更新算法:螺杆检测
# 1.1.10 2026-05-15
## build_1
1. 更新算法:螺杆检测

View File

@ -5,7 +5,7 @@
#define WORKPIECEHOLE_APP_NAME "工件孔定位"
// 版本字符串
#define WORKPIECEHOLE_VERSION_STRING "1.1.5"
#define WORKPIECEHOLE_VERSION_STRING "1.1.6"
#define WORKPIECEHOLE_BUILD_STRING "1"
#define WORKPIECEHOLE_FULL_VERSION_STRING "V" WORKPIECEHOLE_VERSION_STRING "_" WORKPIECEHOLE_BUILD_STRING

View File

@ -1,3 +1,8 @@
# 1.1.6
## build_1 2026-05-19
1. 更新算法:修正一层的时候无结果
# 1.1.5
## build_1 2026-05-05
1. 更新算法:异物检测修正

View File

@ -166,6 +166,14 @@ SG_APISHARED_EXPORT void wd_lineDataSegment_dist(
const int minSegSize
);
//根据距离连续性分段
SG_APISHARED_EXPORT void wd_lineDataSegment_dist_2(
std::vector< SVzNL3DPosition>& lineData,
std::vector<SSG_RUN>& segs,
const double maxDistTh,
const int minSegSize
);
SG_APISHARED_EXPORT void wd_lineDataSegment_zDist(
std::vector< SVzNL3DPosition>& lineData,
std::vector< SVzNL3DPosition>& vldPts,
@ -205,6 +213,29 @@ SG_APISHARED_EXPORT void wd_getRodArcFeature_peakCornerMethod(
std::vector<SWD_rodArcFeature>& line_rodArcs //环
);
/// <summary>
/// 提取激光线上的圆环的上半段弧。
/// seg端点:z距离大于门限
/// nPointIdx被重新定义成Feature类型
/// 算法流程:
/// (1)逐段计算前向角和后向角
/// (2)搜索同方向的拐角连续段
/// (2)搜索Z极值,
/// (2)逐点计算拐角,顺时针为负,逆时针为正
/// (3)搜索正拐角的极大值。
/// (4)判断拐角是否为跳变
///
/// </summary>
SG_APISHARED_EXPORT void wd_getRodArcFeature_segmentPeakCornerMethod(
std::vector< SVzNL3DPosition>& lineData,
int lineIdx,
const double maxDistTh,
const int minSegSize,
const double peakChkWin,
const SSG_cornerParam cornerPara,
std::vector<SWD_rodArcFeature>& line_rodArcs //环
);
//提取corner极值(较早实现函数可以使用此函数进行代码优化)
SG_APISHARED_EXPORT void wd_searchCornerPeaks(
std::vector< SSG_pntDirAngle>& corners,

View File

@ -566,7 +566,7 @@ void TuoPuFa_holePosition_test(void)
};
SVzNLRange fileIdx[TPF_TEST_GROUP] = {
{6,6}, {1, 16}, {1,6}
{6,6}, {1, 16}, {1,19}
};
const char* ver = wd_workpieceHolePositioningVersion();