Se mejora notablemente la calibracion usando tablero de ajedrez y se usa opencl para procesamiento

This commit is contained in:
sebastian 2026-08-12 11:38:20 -03:00
parent c13794ada0
commit bf5e058030
6 changed files with 854 additions and 466 deletions

View file

@ -9,17 +9,19 @@ set(CMAKE_AUTORCC ON)
set(CMAKE_AUTOUIC ON) set(CMAKE_AUTOUIC ON)
find_package(Qt6 REQUIRED COMPONENTS Widgets OpenGLWidgets OpenGL) find_package(Qt6 REQUIRED COMPONENTS Widgets OpenGLWidgets OpenGL)
find_package(OpenCV REQUIRED)
add_executable(weld_measure add_executable(weld_measure
main.cpp main.cpp
main_window.h main_window.h
main_window.cpp main_window.cpp
weld_opengl_widget.h measurement_widget.h
weld_opengl_widget.cpp measurement_widget.cpp
) )
target_link_libraries(weld_measure PRIVATE target_link_libraries(weld_measure PRIVATE
Qt6::Widgets Qt6::Widgets
Qt6::OpenGLWidgets Qt6::OpenGLWidgets
Qt6::OpenGL Qt6::OpenGL
${OpenCV_LIBS}
) )

View file

@ -8,6 +8,7 @@
#include <QMessageBox> #include <QMessageBox>
#include <QPainter> #include <QPainter>
#include <QtMath> #include <QtMath>
#include <QTabWidget>
MainWindow::MainWindow(QWidget *parent) MainWindow::MainWindow(QWidget *parent)
: QMainWindow(parent) : QMainWindow(parent)
@ -16,7 +17,6 @@ MainWindow::MainWindow(QWidget *parent)
resize(1200, 800); resize(1200, 800);
createUI(); createUI();
createDefaultSampleImage();
} }
void MainWindow::createUI() void MainWindow::createUI()
@ -46,23 +46,26 @@ void MainWindow::createUI()
panelLayout->addWidget(grpFile); panelLayout->addWidget(grpFile);
// Calibration setup section // Calibration setup section
QGroupBox *grpCalib = new QGroupBox("2. Calibración por Escala", panel); QGroupBox *grpCalib = new QGroupBox("2. Calibración", panel);
QVBoxLayout *calibLayout = new QVBoxLayout(grpCalib); QVBoxLayout *calibLayout = new QVBoxLayout(grpCalib);
QFormLayout *formCalib = new QFormLayout(); QFormLayout *formGrid = new QFormLayout();
m_spinCalibMm = new QDoubleSpinBox(grpCalib); m_spinSquareSize = new QDoubleSpinBox(grpCalib);
m_spinCalibMm->setRange(0.001, 10000.0); m_spinSquareSize->setRange(0.1, 1000.0);
m_spinCalibMm->setValue(10.0); m_spinSquareSize->setValue(30.0);
m_spinCalibMm->setSuffix(" mm"); m_spinSquareSize->setSuffix(" mm");
m_spinCalibMm->setDecimals(3); m_spinSquareSize->setDecimals(1);
formCalib->addRow("Largo Real Patrón:", m_spinCalibMm);
calibLayout->addLayout(formCalib); formGrid->addRow("Lado Cuadrado (Ajedrez):", m_spinSquareSize);
calibLayout->addLayout(formGrid);
QPushButton *btnCalibChessboard = new QPushButton("Autocalibrar Ajedrez", grpCalib);
btnCalibChessboard->setFixedHeight(30);
btnCalibChessboard->setStyleSheet("font-weight: bold; background-color: #b71c1c; color: white; border-radius: 4px;");
calibLayout->addWidget(btnCalibChessboard);
m_btnCalibrate = new QPushButton("Calibrar Escala", grpCalib);
m_btnCalibrate->setFixedHeight(34);
m_btnCalibrate->setStyleSheet("font-weight: bold; background-color: #2b5b84; color: white; border-radius: 4px;");
calibLayout->addWidget(m_btnCalibrate);
panelLayout->addWidget(grpCalib); panelLayout->addWidget(grpCalib);
// Measurement readout section // Measurement readout section
@ -80,59 +83,71 @@ void MainWindow::createUI()
panelLayout->addStretch(); panelLayout->addStretch();
// OpenGL Central Viewport Widget // OpenGL Central Viewport Widget
m_glWidget = new WeldOpenGLWidget(centralWidget); m_glWidget = new MeasurementWidget(centralWidget);
mainLayout->addWidget(panel); mainLayout->addWidget(panel);
mainLayout->addWidget(m_glWidget, 1); mainLayout->addWidget(m_glWidget, 1);
// Connect signals & slots // Connect signals & slots
connect(btnOpen, &QPushButton::clicked, this, &MainWindow::openImage); connect(btnOpen, &QPushButton::clicked, this, &MainWindow::openImage);
connect(m_btnCalibrate, &QPushButton::clicked, this, &MainWindow::onCalibrateClicked);
connect(m_glWidget, &WeldOpenGLWidget::measurementUpdated, this, &MainWindow::updateMeasurementInfo); connect(m_glWidget, &MeasurementWidget::measurementUpdated, this, &MainWindow::updateMeasurementInfo);
}
void MainWindow::createDefaultSampleImage() // Connect progress signal to dynamically update the gradient color of the button!
{ connect(m_glWidget, &MeasurementWidget::calibrationProgress, this, [btnCalibChessboard](int percent) {
QImage img(1000, 700, QImage::Format_RGB32); double ratio = percent / 100.0;
img.fill(QColor(40, 42, 48)); btnCalibChessboard->setStyleSheet(QString(
"font-weight: bold; color: white; border-radius: 4px;"
"background: qlineargradient(x1:0, y1:0, x2:1, y2:0, stop:0 #2e7d32, stop:%1 #2e7d32, stop:%2 #b71c1c, stop:1 #b71c1c);"
).arg(ratio).arg(qMin(1.0, ratio + 0.001)));
});
QPainter painter(&img); // Autocalibrate using chessboard
painter.setRenderHint(QPainter::Antialiasing); connect(btnCalibChessboard, &QPushButton::clicked, this, [this, btnCalibChessboard, btnOpen]() {
btnCalibChessboard->setEnabled(false);
btnOpen->setEnabled(false);
// Draw simulated weld seam int gx = 0; // Pass 0 to auto-detect
for (int x = 100; x < 900; x += 6) { int gy = 0; // Pass 0 to auto-detect
double y = 350 + qSin(x / 30.0) * 15 + qCos(x / 7.0) * 8; double sqSize = m_spinSquareSize->value();
double r = 25 + qSin(x / 10.0) * 5;
painter.setBrush(QColor(130, 135, 145));
painter.setPen(QPen(QColor(170, 175, 185), 1.5));
painter.drawEllipse(QPointF(x, y), r, r * 0.5);
}
// Draw reference pattern bar (200px = 10mm) bool success = m_glWidget->calibrateWithChessboard(gx, gy, sqSize);
int scaleX = 150;
int scaleY = 560;
int scaleLenPx = 200;
painter.setBrush(QColor(230, 230, 230)); if (success) {
painter.setPen(Qt::NoPen); // Keep green when calibrated successfully
painter.drawRoundedRect(scaleX - 20, scaleY - 30, scaleLenPx + 40, 70, 8, 8); btnCalibChessboard->setStyleSheet("font-weight: bold; background-color: #2e7d32; color: white; border-radius: 4px;");
} else {
// Return to red if calibration failed
btnCalibChessboard->setStyleSheet("font-weight: bold; background-color: #b71c1c; color: white; border-radius: 4px;");
}
btnCalibChessboard->setEnabled(true);
btnOpen->setEnabled(true);
QPen rulerPen(Qt::black, 4, Qt::SolidLine, Qt::SquareCap); if (success) {
painter.setPen(rulerPen); int squaresX = gx + 1;
painter.drawLine(scaleX, scaleY, scaleX + scaleLenPx, scaleY); int squaresY = gy + 1;
painter.drawLine(scaleX, scaleY - 15, scaleX, scaleY + 15);
painter.drawLine(scaleX + scaleLenPx, scaleY - 15, scaleX + scaleLenPx, scaleY + 15);
painter.setFont(QFont("Arial", 11, QFont::Bold)); QMessageBox::information(this, "Calibración Exitosa",
painter.drawText(QRect(scaleX - 20, scaleY + 10, scaleLenPx + 40, 25), Qt::AlignCenter, "PATRÓN DE PRUEBA: 10.0 mm"); QString("Se detectó automáticamente un tablero de ajedrez.\n\n"
"Dimensiones identificadas:\n"
"- Intersecciones internas: %1 x %2\n"
"- Cuadrados totales: %3 x %4\n"
"- Medida del lado: %5 mm\n\n"
"La distorsión del lente y la perspectiva han sido calibradas con éxito.")
.arg(gx).arg(gy).arg(squaresX).arg(squaresY).arg(sqSize, 0, 'f', 1));
} else {
QMessageBox::warning(this, "Error de Calibración",
QString("No se pudo detectar automáticamente ningún tablero de ajedrez en la imagen.\n\n"
"Por favor verifica:\n"
"1. Que el tablero esté bien iluminado y plano.\n"
"2. Que el tablero esté completamente visible en la foto."));
}
});
img.save("/home/sebastian/repos/weld_measure/sample_weld.png"); // Reset calibration button color to red when a new image is loaded
m_glWidget->setImage(img); connect(m_glWidget, &MeasurementWidget::imageLoaded, this, [btnCalibChessboard]() {
btnCalibChessboard->setStyleSheet("font-weight: bold; background-color: #b71c1c; color: white; border-radius: 4px;");
// Position single line right over the sample pattern bar (200px = 10mm) });
m_glWidget->setLinePoints(QPointF(scaleX, scaleY), QPointF(scaleX + scaleLenPx, scaleY));
m_glWidget->calibrateWithCurrentLine(10.0);
} }
void MainWindow::openImage() void MainWindow::openImage()
@ -145,15 +160,6 @@ void MainWindow::openImage()
} }
} }
void MainWindow::onCalibrateClicked()
{
double knownMm = m_spinCalibMm->value();
m_glWidget->calibrateWithCurrentLine(knownMm);
QMessageBox::information(this, "Calibración Exitosa",
QString("Escala fijada correctamente:\n%1 mm/px\n\nAhora puede mover la misma línea para medir cualquier parte de la imagen.")
.arg(m_glWidget->mmPerPixel(), 0, 'g', 5));
}
void MainWindow::updateMeasurementInfo(double px, double mm) void MainWindow::updateMeasurementInfo(double px, double mm)

View file

@ -3,9 +3,11 @@
#include <QMainWindow> #include <QMainWindow>
#include <QDoubleSpinBox> #include <QDoubleSpinBox>
#include <QSpinBox>
#include <QLabel> #include <QLabel>
#include <QPushButton> #include <QPushButton>
#include "weld_opengl_widget.h" #include <QProgressBar>
#include "measurement_widget.h"
class MainWindow : public QMainWindow class MainWindow : public QMainWindow
{ {
@ -17,18 +19,16 @@ public:
private slots: private slots:
void openImage(); void openImage();
void onCalibrateClicked();
void updateMeasurementInfo(double px, double mm); void updateMeasurementInfo(double px, double mm);
private: private:
void createUI(); void createUI();
void createDefaultSampleImage();
WeldOpenGLWidget *m_glWidget = nullptr; MeasurementWidget *m_glWidget = nullptr;
QDoubleSpinBox *m_spinCalibMm = nullptr;
QPushButton *m_btnCalibrate = nullptr;
QLabel *m_lblMeasurement = nullptr; QLabel *m_lblMeasurement = nullptr;
QDoubleSpinBox *m_spinSquareSize = nullptr;
}; };
#endif // MAIN_WINDOW_H #endif // MAIN_WINDOW_H

745
measurement_widget.cpp Normal file
View file

@ -0,0 +1,745 @@
#include "measurement_widget.h"
#include <QPainter>
#include <QMouseEvent>
#include <QWheelEvent>
#include <QtMath>
#include <QDebug>
#include <QPolygonF>
#include <QTransform>
#include <opencv2/calib3d.hpp>
#include <opencv2/imgproc.hpp>
#include <QCoreApplication>
#include <QThread>
MeasurementWidget::MeasurementWidget(QWidget *parent)
: QOpenGLWidget(parent)
{
setMouseTracking(true);
}
MeasurementWidget::~MeasurementWidget()
{
makeCurrent();
if (m_texture) {
delete m_texture;
m_texture = nullptr;
}
doneCurrent();
}
void MeasurementWidget::initializeGL()
{
initializeOpenGLFunctions();
glClearColor(0.15f, 0.15f, 0.18f, 1.0f);
}
void MeasurementWidget::resizeGL(int w, int h)
{
glViewport(0, 0, w, h);
}
bool MeasurementWidget::loadImage(const QString &filePath)
{
QImage img(filePath);
if (img.isNull()) return false;
setImage(img);
return true;
}
void MeasurementWidget::setImage(const QImage &image)
{
m_image = image.convertToFormat(QImage::Format_RGBA8888);
m_textureNeedsUpdate = true;
// Reset zoom and center image
m_zoomLevel = 1.0;
m_panOffset = QPointF(0, 0);
// Initialize single measurement/calibration line
int w = m_image.width();
int h = m_image.height();
m_head = QPointF(w * 0.3, h * 0.5);
m_tail = QPointF(w * 0.7, h * 0.5);
m_hasLine = true;
// Reset calibration parameters
m_mmPerPixel = 0.0;
m_isCalibrated = false;
m_hasHomography = false;
m_calibrationMode = false;
m_activeCornerIdx = -1;
m_calibCorners.clear();
emit imageLoaded(w, h);
emit calibrationUpdated(m_mmPerPixel);
emit measurementUpdated(measuredPx(), measuredMm());
update();
}
double MeasurementWidget::measuredPx() const
{
if (!m_hasLine) return 0.0;
return QLineF(m_head, m_tail).length();
}
double MeasurementWidget::measuredMm() const
{
if (m_hasHomography) {
QPointF h = m_head;
QPointF t = m_tail;
if (m_hasLensCalibration) {
std::vector<cv::Point2f> src = { cv::Point2f(m_head.x(), m_head.y()), cv::Point2f(m_tail.x(), m_tail.y()) };
std::vector<cv::Point2f> dst;
cv::undistortPoints(src, dst, m_cameraMatrix, m_distCoeffs, cv::Mat(), m_cameraMatrix);
h = QPointF(dst[0].x, dst[0].y);
t = QPointF(dst[1].x, dst[1].y);
}
QPointF headReal = m_homography.map(h);
QPointF tailReal = m_homography.map(t);
return QLineF(headReal, tailReal).length();
}
return measuredPx() * m_mmPerPixel;
}
void MeasurementWidget::resetLine()
{
if (!m_image.isNull()) {
int w = m_image.width();
int h = m_image.height();
m_head = QPointF(w * 0.3, h * 0.5);
m_tail = QPointF(w * 0.7, h * 0.5);
m_hasLine = true;
emit measurementUpdated(measuredPx(), measuredMm());
update();
}
}
void MeasurementWidget::setLinePoints(const QPointF &head, const QPointF &tail)
{
m_head = head;
m_tail = tail;
m_hasLine = true;
emit measurementUpdated(measuredPx(), measuredMm());
update();
}
void MeasurementWidget::paintGL()
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
if (m_image.isNull()) return;
makeCurrent();
if (m_textureNeedsUpdate) {
if (m_texture) delete m_texture;
m_texture = new QOpenGLTexture(m_image);
m_texture->setMinificationFilter(QOpenGLTexture::Linear);
m_texture->setMagnificationFilter(QOpenGLTexture::Linear);
m_textureNeedsUpdate = false;
}
// Render 2D image via QPainter over OpenGL viewport
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing, true);
painter.setRenderHint(QPainter::TextAntialiasing, true);
painter.setRenderHint(QPainter::SmoothPixmapTransform, true);
// Calculate image destination rectangle with zoom and pan
double widgetAspect = (double)width() / height();
double imageAspect = (double)m_image.width() / m_image.height();
double drawW, drawH;
if (widgetAspect > imageAspect) {
drawH = height() * m_zoomLevel;
drawW = drawH * imageAspect;
} else {
drawW = width() * m_zoomLevel;
drawH = drawW / imageAspect;
}
double drawX = (width() - drawW) / 2.0 + m_panOffset.x();
double drawY = (height() - drawH) / 2.0 + m_panOffset.y();
QRectF targetRect(drawX, drawY, drawW, drawH);
painter.drawImage(targetRect, m_image);
// Draw the single measurement/calibration line
if (m_hasLine && !m_calibrationMode) {
QColor color(255, 45, 45); // Smooth Vivid Red
drawLineOverlay(painter, m_head, m_tail, color);
}
if (m_calibrationMode) {
// Draw the 4 corners and connecting lines
QPolygonF wPoly;
for (const auto &p : m_calibCorners) {
wPoly << imageToWidgetCoords(p);
}
// Draw lines connecting them to form a quad
QPen polyPen(QColor(0, 191, 255), 2.0, Qt::DashLine); // Deep Sky Blue
painter.setPen(polyPen);
painter.setBrush(Qt::NoBrush);
if (wPoly.size() == 4) {
painter.drawPolygon(wPoly);
}
// Draw handles at each corner
double handleRadius = 8.0;
painter.setPen(QPen(QColor(0, 191, 255), 2.0));
QStringList labels = {"1 (Sup-Izq)", "2 (Sup-Der)", "3 (Inf-Der)", "4 (Inf-Izq)"};
for (int i = 0; i < wPoly.size(); ++i) {
if (i == m_activeCornerIdx) {
painter.setBrush(QBrush(QColor(255, 165, 0, 200))); // Orange when dragging
} else {
painter.setBrush(QBrush(QColor(0, 191, 255, 150)));
}
painter.drawEllipse(wPoly[i], handleRadius, handleRadius);
// Draw label
painter.setFont(QFont("Arial", 8, QFont::Bold));
painter.setPen(Qt::white);
painter.drawText(wPoly[i] + QPointF(12, 4), labels[i]);
painter.setPen(QPen(QColor(0, 191, 255), 2.0)); // Restore pen
}
}
}
void MeasurementWidget::drawLineOverlay(QPainter &painter, const QPointF &imgHead, const QPointF &imgTail,
const QColor &color)
{
QPointF wHead = imageToWidgetCoords(imgHead);
QPointF wTail = imageToWidgetCoords(imgTail);
painter.setRenderHint(QPainter::Antialiasing, true);
// Pure single red line (1.0px thin stroke with antialiasing)
QPen linePen(color, 1.0, Qt::SolidLine, Qt::RoundCap);
painter.setPen(linePen);
painter.drawLine(wHead, wTail);
// Compact drag handles (short perpendicular ticks)
QLineF line(wHead, wTail);
double dx = line.dx();
double dy = line.dy();
double len = line.length();
if (len > 0.0) {
double px = -dy / len;
double py = dx / len;
double tickHalfLen = 6.0; // short length perpendicular to measurement line
QPointF headP1 = wHead + QPointF(px * tickHalfLen, py * tickHalfLen);
QPointF headP2 = wHead - QPointF(px * tickHalfLen, py * tickHalfLen);
painter.drawLine(headP1, headP2);
QPointF tailP1 = wTail + QPointF(px * tickHalfLen, py * tickHalfLen);
QPointF tailP2 = wTail - QPointF(px * tickHalfLen, py * tickHalfLen);
painter.drawLine(tailP1, tailP2);
}
// End labels
painter.setFont(QFont("Arial", 8, QFont::Bold));
painter.setPen(Qt::white);
painter.drawText(wHead + QPointF(10, -6), "A");
painter.drawText(wTail + QPointF(10, -6), "B");
}
QPointF MeasurementWidget::widgetToImageCoords(const QPointF &widgetPos) const
{
if (m_image.isNull()) return QPointF();
double widgetAspect = (double)width() / height();
double imageAspect = (double)m_image.width() / m_image.height();
double drawW, drawH;
if (widgetAspect > imageAspect) {
drawH = height() * m_zoomLevel;
drawW = drawH * imageAspect;
} else {
drawW = width() * m_zoomLevel;
drawH = drawW / imageAspect;
}
double drawX = (width() - drawW) / 2.0 + m_panOffset.x();
double drawY = (height() - drawH) / 2.0 + m_panOffset.y();
double normX = (widgetPos.x() - drawX) / drawW;
double normY = (widgetPos.y() - drawY) / drawH;
return QPointF(normX * m_image.width(), normY * m_image.height());
}
QPointF MeasurementWidget::imageToWidgetCoords(const QPointF &imgPos) const
{
if (m_image.isNull()) return QPointF();
double widgetAspect = (double)width() / height();
double imageAspect = (double)m_image.width() / m_image.height();
double drawW, drawH;
if (widgetAspect > imageAspect) {
drawH = height() * m_zoomLevel;
drawW = drawH * imageAspect;
} else {
drawW = width() * m_zoomLevel;
drawH = drawW / imageAspect;
}
double drawX = (width() - drawW) / 2.0 + m_panOffset.x();
double drawY = (height() - drawH) / 2.0 + m_panOffset.y();
double normX = imgPos.x() / m_image.width();
double normY = imgPos.y() / m_image.height();
return QPointF(drawX + normX * drawW, drawY + normY * drawH);
}
MeasurementWidget::DragHandle MeasurementWidget::hitTestLine(const QPointF &imgPos, const QPointF &head, const QPointF &tail) const
{
QPointF wPos = imageToWidgetCoords(imgPos);
QPointF wHead = imageToWidgetCoords(head);
QPointF wTail = imageToWidgetCoords(tail);
const double handleRadius = 15.0; // Hit test tolerance in pixels
if (QLineF(wPos, wHead).length() <= handleRadius) return DragHandle::Head;
if (QLineF(wPos, wTail).length() <= handleRadius) return DragHandle::Tail;
// Check if clicked near line body
QLineF line(wHead, wTail);
double lineLen = line.length();
if (lineLen > 0) {
double u = ((wPos.x() - wHead.x()) * (wTail.x() - wHead.x()) + (wPos.y() - wHead.y()) * (wTail.y() - wHead.y())) / (lineLen * lineLen);
if (u >= 0.0 && u <= 1.0) {
QPointF proj = wHead + u * (wTail - wHead);
if (QLineF(wPos, proj).length() <= handleRadius) {
return DragHandle::Body;
}
}
}
return DragHandle::None;
}
void MeasurementWidget::mousePressEvent(QMouseEvent *event)
{
if (m_image.isNull()) return;
if (m_calibrationMode) {
if (event->button() == Qt::LeftButton) {
QPointF imgPos = widgetToImageCoords(event->position());
QPointF wPos = event->position();
m_activeCornerIdx = -1;
const double handleRadius = 15.0;
for (int i = 0; i < m_calibCorners.size(); ++i) {
QPointF wCorner = imageToWidgetCoords(m_calibCorners[i]);
if (QLineF(wPos, wCorner).length() <= handleRadius) {
m_activeCornerIdx = i;
m_dragStartImgPos = imgPos;
update();
break;
}
}
if (m_activeCornerIdx == -1) {
m_isPanning = true;
m_lastMousePos = event->pos();
setCursor(Qt::ClosedHandCursor);
}
return;
}
if (event->button() == Qt::RightButton) {
m_isPanning = true;
m_lastMousePos = event->pos();
setCursor(Qt::ClosedHandCursor);
return;
}
return;
}
if (event->button() == Qt::LeftButton) {
QPointF imgPos = widgetToImageCoords(event->position());
m_activeHandle = hitTestLine(imgPos, m_head, m_tail);
if (m_activeHandle != DragHandle::None) {
m_dragStartImgPos = imgPos;
m_dragStartHead = m_head;
m_dragStartTail = m_tail;
update();
} else {
// Clicked background -> Pan image on left click drag
m_isPanning = true;
m_lastMousePos = event->pos();
setCursor(Qt::ClosedHandCursor);
}
return;
}
if (event->button() == Qt::RightButton) {
m_isPanning = true;
m_lastMousePos = event->pos();
setCursor(Qt::ClosedHandCursor);
return;
}
}
void MeasurementWidget::mouseMoveEvent(QMouseEvent *event)
{
if (m_isPanning) {
QPoint delta = event->pos() - m_lastMousePos;
m_panOffset += QPointF(delta.x(), delta.y());
m_lastMousePos = event->pos();
update();
return;
}
QPointF imgPos = widgetToImageCoords(event->position());
if (m_calibrationMode) {
if (m_activeCornerIdx != -1) {
double x = qBound(0.0, imgPos.x(), (double)m_image.width());
double y = qBound(0.0, imgPos.y(), (double)m_image.height());
m_calibCorners[m_activeCornerIdx] = QPointF(x, y);
update();
return;
}
// Update cursor on hover
QPointF wPos = event->position();
bool overCorner = false;
const double handleRadius = 15.0;
for (int i = 0; i < m_calibCorners.size(); ++i) {
QPointF wCorner = imageToWidgetCoords(m_calibCorners[i]);
if (QLineF(wPos, wCorner).length() <= handleRadius) {
overCorner = true;
break;
}
}
if (overCorner) {
setCursor(Qt::PointingHandCursor);
} else {
setCursor(Qt::OpenHandCursor);
}
return;
}
if (m_activeHandle != DragHandle::None) {
if (m_activeHandle == DragHandle::Head) {
m_head = imgPos;
} else if (m_activeHandle == DragHandle::Tail) {
m_tail = imgPos;
} else if (m_activeHandle == DragHandle::Body) {
QPointF delta = imgPos - m_dragStartImgPos;
m_head = m_dragStartHead + delta;
m_tail = m_dragStartTail + delta;
}
emit measurementUpdated(measuredPx(), measuredMm());
update();
return;
}
// Update cursor feedback based on hover
DragHandle hover = hitTestLine(imgPos, m_head, m_tail);
if (hover == DragHandle::Head || hover == DragHandle::Tail) {
setCursor(Qt::CrossCursor);
} else if (hover == DragHandle::Body) {
setCursor(Qt::SizeAllCursor);
} else {
setCursor(Qt::OpenHandCursor);
}
}
void MeasurementWidget::mouseReleaseEvent(QMouseEvent *event)
{
if (m_calibrationMode) {
if (m_isPanning && (event->button() == Qt::LeftButton || event->button() == Qt::RightButton)) {
m_isPanning = false;
setCursor(Qt::OpenHandCursor);
}
if (event->button() == Qt::LeftButton && m_activeCornerIdx != -1) {
m_activeCornerIdx = -1;
setCursor(Qt::OpenHandCursor);
}
return;
}
if (m_isPanning && (event->button() == Qt::LeftButton || event->button() == Qt::RightButton)) {
m_isPanning = false;
QPointF imgPos = widgetToImageCoords(event->position());
DragHandle hover = hitTestLine(imgPos, m_head, m_tail);
if (hover == DragHandle::Head || hover == DragHandle::Tail) setCursor(Qt::CrossCursor);
else if (hover == DragHandle::Body) setCursor(Qt::SizeAllCursor);
else setCursor(Qt::OpenHandCursor);
}
if (event->button() == Qt::LeftButton && m_activeHandle != DragHandle::None) {
m_activeHandle = DragHandle::None;
QPointF imgPos = widgetToImageCoords(event->position());
DragHandle hover = hitTestLine(imgPos, m_head, m_tail);
if (hover == DragHandle::Head || hover == DragHandle::Tail) setCursor(Qt::CrossCursor);
else if (hover == DragHandle::Body) setCursor(Qt::SizeAllCursor);
else setCursor(Qt::OpenHandCursor);
}
}
void MeasurementWidget::wheelEvent(QWheelEvent *event)
{
double zoomFactor = 1.15;
if (event->angleDelta().y() < 0) {
zoomFactor = 1.0 / zoomFactor;
}
double newZoom = m_zoomLevel * zoomFactor;
if (newZoom < 0.2) newZoom = 0.2;
if (newZoom > 25.0) newZoom = 25.0;
QPointF mousePos = event->position();
QPointF imgPosBefore = widgetToImageCoords(mousePos);
m_zoomLevel = newZoom;
QPointF widgetPosAfter = imageToWidgetCoords(imgPosBefore);
m_panOffset += (mousePos - widgetPosAfter);
update();
}
void MeasurementWidget::setCalibrationMode(bool active)
{
if (m_image.isNull()) return;
m_calibrationMode = active;
if (active && m_calibCorners.isEmpty()) {
double w = m_image.width();
double h = m_image.height();
m_calibCorners.clear();
m_calibCorners.append(QPointF(w * 0.1, h * 0.1));
m_calibCorners.append(QPointF(w * 0.9, h * 0.1));
m_calibCorners.append(QPointF(w * 0.9, h * 0.9));
m_calibCorners.append(QPointF(w * 0.1, h * 0.9));
}
update();
}
void MeasurementWidget::cancelCalibrationMode()
{
m_calibrationMode = false;
m_activeCornerIdx = -1;
update();
}
void MeasurementWidget::applyPerspectiveCalibration(double realX, double realY)
{
if (m_image.isNull() || m_calibCorners.size() != 4) return;
QPolygonF srcQuad;
srcQuad << m_calibCorners[0] << m_calibCorners[1] << m_calibCorners[2] << m_calibCorners[3];
QPolygonF destQuad;
destQuad << QPointF(0, 0) << QPointF(realX, 0) << QPointF(realX, realY) << QPointF(0, realY);
if (QTransform::quadToQuad(srcQuad, destQuad, m_homography)) {
m_hasHomography = true;
m_isCalibrated = true;
m_calibrationMode = false;
m_activeCornerIdx = -1;
// Calculate a representative/average scale factor for compatibility
double imgW = QLineF(m_calibCorners[0], m_calibCorners[1]).length();
if (imgW > 0) {
m_mmPerPixel = realX / imgW;
}
emit calibrationUpdated(m_mmPerPixel);
emit measurementUpdated(measuredPx(), measuredMm());
update();
}
}
void MeasurementWidget::resetCalibration()
{
m_mmPerPixel = 0.0;
m_isCalibrated = false;
m_hasHomography = false;
m_hasLensCalibration = false;
m_calibrationMode = false;
m_activeCornerIdx = -1;
m_calibCorners.clear();
emit calibrationUpdated(m_mmPerPixel);
emit measurementUpdated(measuredPx(), measuredMm());
update();
}
bool MeasurementWidget::calibrateWithChessboard(int &innerCornersX, int &innerCornersY, double squareSizeMm)
{
if (m_image.isNull()) return false;
// Convert QImage to cv::Mat
cv::Mat mat(m_image.height(), m_image.width(), CV_8UC4,
const_cast<uchar*>(m_image.bits()),
m_image.bytesPerLine());
cv::Mat gray;
cv::cvtColor(mat, gray, cv::COLOR_RGBA2GRAY);
// If the image is very high resolution, resize it for the initial corner detection search
// to reduce high-frequency printer noise/banding and speed up detection significantly (using 600px width).
cv::Mat graySearch = gray;
double scale = 1.0;
if (gray.cols > 600) {
scale = 600.0 / gray.cols;
int newHeight = static_cast<int>(gray.rows * scale);
cv::resize(gray, graySearch, cv::Size(600, newHeight), 0, 0, cv::INTER_AREA);
}
std::vector<cv::Point2f> searchCorners;
bool found = false;
int bestX = 0;
int bestY = 0;
if (innerCornersX > 0 && innerCornersY > 0) {
// Try user specified dimensions
cv::Size patternSize(innerCornersX, innerCornersY);
found = cv::findChessboardCorners(graySearch, patternSize, searchCorners,
cv::CALIB_CB_ADAPTIVE_THRESH | cv::CALIB_CB_NORMALIZE_IMAGE);
if (!found) {
cv::Mat graySearchInv = 255 - graySearch;
found = cv::findChessboardCorners(graySearchInv, patternSize, searchCorners,
cv::CALIB_CB_ADAPTIVE_THRESH | cv::CALIB_CB_NORMALIZE_IMAGE);
}
if (found) {
bestX = innerCornersX;
bestY = innerCornersY;
}
} else {
// Auto-detect size: scan common sizes from largest to smallest (10 down to 4)
int totalSteps = (10 - 4 + 1) * (10 - 4 + 1);
int step = 0;
for (int cx = 10; cx >= 4; --cx) {
for (int cy = 10; cy >= 4; --cy) {
step++;
emit calibrationProgress(step * 100 / totalSteps);
QCoreApplication::processEvents();
QThread::msleep(8); // Tiny sleep for smooth visual progress bar
cv::Size patternSize(cx, cy);
found = cv::findChessboardCorners(graySearch, patternSize, searchCorners,
cv::CALIB_CB_ADAPTIVE_THRESH | cv::CALIB_CB_NORMALIZE_IMAGE);
if (!found) {
cv::Mat graySearchInv = 255 - graySearch;
found = cv::findChessboardCorners(graySearchInv, patternSize, searchCorners,
cv::CALIB_CB_ADAPTIVE_THRESH | cv::CALIB_CB_NORMALIZE_IMAGE);
}
if (found) {
bestX = cx;
bestY = cy;
emit calibrationProgress(100);
QCoreApplication::processEvents();
break;
}
}
if (found) break;
}
}
if (!found) {
return false;
}
// Assign detected dimensions back to reference variables to update UI
innerCornersX = bestX;
innerCornersY = bestY;
// Scale corner coordinates back to the original image resolution
std::vector<cv::Point2f> corners;
for (const auto &p : searchCorners) {
corners.push_back(cv::Point2f(p.x / scale, p.y / scale));
}
// Refine corner positions on the original high-res image
cv::cornerSubPix(gray, corners, cv::Size(11, 11), cv::Size(-1, -1),
cv::TermCriteria(cv::TermCriteria::EPS + cv::TermCriteria::COUNT, 30, 0.1));
// Prepare calibration points
std::vector<std::vector<cv::Point3f>> objectPoints;
std::vector<std::vector<cv::Point2f>> imagePoints;
std::vector<cv::Point3f> obj;
for (int i = 0; i < innerCornersY; ++i) {
for (int j = 0; j < innerCornersX; ++j) {
obj.push_back(cv::Point3f(j * squareSizeMm, i * squareSizeMm, 0.0f));
}
}
objectPoints.push_back(obj);
imagePoints.push_back(corners);
// Calibrate camera
m_cameraMatrix = cv::Mat::eye(3, 3, CV_64F);
m_distCoeffs = cv::Mat::zeros(5, 1, CV_64F);
std::vector<cv::Mat> rvecs, tvecs;
double rms = cv::calibrateCamera(objectPoints, imagePoints,
cv::Size(m_image.width(), m_image.height()),
m_cameraMatrix, m_distCoeffs, rvecs, tvecs,
cv::CALIB_FIX_K3);
qDebug() << "Calibration RMS error:" << rms;
// Extreme corners:
// Top-Left (index 0) -> maps to (0, 0)
// Top-Right (index innerCornersX - 1) -> maps to ((innerCornersX-1)*sq, 0)
// Bottom-Right (index innerCornersX * innerCornersY - 1) -> maps to ((innerCornersX-1)*sq, (innerCornersY-1)*sq)
// Bottom-Left (index innerCornersX * (innerCornersY - 1)) -> maps to (0, (innerCornersY-1)*sq)
std::vector<cv::Point2f> srcPointsDistorted = {
corners[0],
corners[innerCornersX - 1],
corners[innerCornersX * innerCornersY - 1],
corners[innerCornersX * (innerCornersY - 1)]
};
// Undistort these 4 points
std::vector<cv::Point2f> srcPointsUndistorted;
cv::undistortPoints(srcPointsDistorted, srcPointsUndistorted, m_cameraMatrix, m_distCoeffs, cv::Mat(), m_cameraMatrix);
QPolygonF srcQuad;
srcQuad << QPointF(srcPointsUndistorted[0].x, srcPointsUndistorted[0].y)
<< QPointF(srcPointsUndistorted[1].x, srcPointsUndistorted[1].y)
<< QPointF(srcPointsUndistorted[2].x, srcPointsUndistorted[2].y)
<< QPointF(srcPointsUndistorted[3].x, srcPointsUndistorted[3].y);
double realX = (innerCornersX - 1) * squareSizeMm;
double realY = (innerCornersY - 1) * squareSizeMm;
QPolygonF destQuad;
destQuad << QPointF(0, 0) << QPointF(realX, 0) << QPointF(realX, realY) << QPointF(0, realY);
if (QTransform::quadToQuad(srcQuad, destQuad, m_homography)) {
m_hasHomography = true;
m_hasLensCalibration = true;
m_isCalibrated = true;
m_calibrationMode = false;
m_activeCornerIdx = -1;
// Compute average scale
double imgW = QLineF(srcQuad[0], srcQuad[1]).length();
if (imgW > 0) {
m_mmPerPixel = realX / imgW;
}
emit calibrationUpdated(m_mmPerPixel);
emit measurementUpdated(measuredPx(), measuredMm());
update();
return true;
}
return false;
}

View file

@ -1,5 +1,5 @@
#ifndef WELD_OPENGL_WIDGET_H #ifndef MEASUREMENT_WIDGET_H
#define WELD_OPENGL_WIDGET_H #define MEASUREMENT_WIDGET_H
#include <QOpenGLWidget> #include <QOpenGLWidget>
#include <QOpenGLFunctions> #include <QOpenGLFunctions>
@ -7,8 +7,9 @@
#include <QPointF> #include <QPointF>
#include <QLineF> #include <QLineF>
#include <QOpenGLTexture> #include <QOpenGLTexture>
#include <opencv2/core.hpp>
class WeldOpenGLWidget : public QOpenGLWidget, protected QOpenGLFunctions class MeasurementWidget : public QOpenGLWidget, protected QOpenGLFunctions
{ {
Q_OBJECT Q_OBJECT
@ -20,13 +21,12 @@ public:
Body Body
}; };
explicit WeldOpenGLWidget(QWidget *parent = nullptr); explicit MeasurementWidget(QWidget *parent = nullptr);
~WeldOpenGLWidget() override; ~MeasurementWidget() override;
bool loadImage(const QString &filePath); bool loadImage(const QString &filePath);
void setImage(const QImage &image); void setImage(const QImage &image);
void calibrateWithCurrentLine(double knownMm);
bool isCalibrated() const { return m_isCalibrated; } bool isCalibrated() const { return m_isCalibrated; }
double mmPerPixel() const { return m_mmPerPixel; } double mmPerPixel() const { return m_mmPerPixel; }
double measuredPx() const; double measuredPx() const;
@ -35,10 +35,18 @@ public:
void resetLine(); void resetLine();
void setLinePoints(const QPointF &head, const QPointF &tail); void setLinePoints(const QPointF &head, const QPointF &tail);
void setCalibrationMode(bool active);
bool isCalibrationMode() const { return m_calibrationMode; }
void applyPerspectiveCalibration(double realX, double realY);
void cancelCalibrationMode();
void resetCalibration();
bool calibrateWithChessboard(int &innerCornersX, int &innerCornersY, double squareSizeMm);
signals: signals:
void calibrationUpdated(double mmPerPx); void calibrationUpdated(double mmPerPx);
void measurementUpdated(double px, double mm); void measurementUpdated(double px, double mm);
void imageLoaded(int width, int height); void imageLoaded(int width, int height);
void calibrationProgress(int percent);
protected: protected:
void initializeGL() override; void initializeGL() override;
@ -71,6 +79,20 @@ private:
double m_mmPerPixel = 0.0; double m_mmPerPixel = 0.0;
bool m_isCalibrated = false; bool m_isCalibrated = false;
// Homography matrix for perspective calibration
QTransform m_homography;
bool m_hasHomography = false;
// Lens Calibration Matrix & Coefficients
cv::Mat m_cameraMatrix;
cv::Mat m_distCoeffs;
bool m_hasLensCalibration = false;
// Perspective Calibration state
bool m_calibrationMode = false;
QList<QPointF> m_calibCorners;
int m_activeCornerIdx = -1;
// Mouse interaction state // Mouse interaction state
DragHandle m_activeHandle = DragHandle::None; DragHandle m_activeHandle = DragHandle::None;
QPointF m_dragStartImgPos; QPointF m_dragStartImgPos;
@ -84,4 +106,4 @@ private:
QPoint m_lastMousePos; QPoint m_lastMousePos;
}; };
#endif // WELD_OPENGL_WIDGET_H #endif // MEASUREMENT_WIDGET_H

View file

@ -1,387 +0,0 @@
#include "weld_opengl_widget.h"
#include <QPainter>
#include <QMouseEvent>
#include <QWheelEvent>
#include <QtMath>
#include <QDebug>
WeldOpenGLWidget::WeldOpenGLWidget(QWidget *parent)
: QOpenGLWidget(parent)
{
setMouseTracking(true);
}
WeldOpenGLWidget::~WeldOpenGLWidget()
{
makeCurrent();
if (m_texture) {
delete m_texture;
m_texture = nullptr;
}
doneCurrent();
}
void WeldOpenGLWidget::initializeGL()
{
initializeOpenGLFunctions();
glClearColor(0.15f, 0.15f, 0.18f, 1.0f);
}
void WeldOpenGLWidget::resizeGL(int w, int h)
{
glViewport(0, 0, w, h);
}
bool WeldOpenGLWidget::loadImage(const QString &filePath)
{
QImage img(filePath);
if (img.isNull()) return false;
setImage(img);
return true;
}
void WeldOpenGLWidget::setImage(const QImage &image)
{
m_image = image.convertToFormat(QImage::Format_RGBA8888);
m_textureNeedsUpdate = true;
// Reset zoom and center image
m_zoomLevel = 1.0;
m_panOffset = QPointF(0, 0);
// Initialize single measurement/calibration line
int w = m_image.width();
int h = m_image.height();
m_head = QPointF(w * 0.3, h * 0.5);
m_tail = QPointF(w * 0.7, h * 0.5);
m_hasLine = true;
emit imageLoaded(w, h);
emit calibrationUpdated(m_mmPerPixel);
emit measurementUpdated(measuredPx(), measuredMm());
update();
}
void WeldOpenGLWidget::calibrateWithCurrentLine(double knownMm)
{
double px = measuredPx();
if (px > 0.0 && knownMm > 0.0) {
m_mmPerPixel = knownMm / px;
m_isCalibrated = true;
}
emit calibrationUpdated(m_mmPerPixel);
emit measurementUpdated(measuredPx(), measuredMm());
update();
}
double WeldOpenGLWidget::measuredPx() const
{
if (!m_hasLine) return 0.0;
return QLineF(m_head, m_tail).length();
}
double WeldOpenGLWidget::measuredMm() const
{
return measuredPx() * m_mmPerPixel;
}
void WeldOpenGLWidget::resetLine()
{
if (!m_image.isNull()) {
int w = m_image.width();
int h = m_image.height();
m_head = QPointF(w * 0.3, h * 0.5);
m_tail = QPointF(w * 0.7, h * 0.5);
m_hasLine = true;
emit measurementUpdated(measuredPx(), measuredMm());
update();
}
}
void WeldOpenGLWidget::setLinePoints(const QPointF &head, const QPointF &tail)
{
m_head = head;
m_tail = tail;
m_hasLine = true;
emit measurementUpdated(measuredPx(), measuredMm());
update();
}
void WeldOpenGLWidget::paintGL()
{
glClear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
if (m_image.isNull()) return;
makeCurrent();
if (m_textureNeedsUpdate) {
if (m_texture) delete m_texture;
m_texture = new QOpenGLTexture(m_image);
m_texture->setMinificationFilter(QOpenGLTexture::Linear);
m_texture->setMagnificationFilter(QOpenGLTexture::Linear);
m_textureNeedsUpdate = false;
}
// Render 2D image via QPainter over OpenGL viewport
QPainter painter(this);
painter.setRenderHint(QPainter::Antialiasing, true);
painter.setRenderHint(QPainter::TextAntialiasing, true);
painter.setRenderHint(QPainter::SmoothPixmapTransform, true);
// Calculate image destination rectangle with zoom and pan
double widgetAspect = (double)width() / height();
double imageAspect = (double)m_image.width() / m_image.height();
double drawW, drawH;
if (widgetAspect > imageAspect) {
drawH = height() * m_zoomLevel;
drawW = drawH * imageAspect;
} else {
drawW = width() * m_zoomLevel;
drawH = drawW / imageAspect;
}
double drawX = (width() - drawW) / 2.0 + m_panOffset.x();
double drawY = (height() - drawH) / 2.0 + m_panOffset.y();
QRectF targetRect(drawX, drawY, drawW, drawH);
painter.drawImage(targetRect, m_image);
// Draw the single measurement/calibration line
if (m_hasLine) {
QColor color(255, 45, 45); // Smooth Vivid Red
drawLineOverlay(painter, m_head, m_tail, color);
}
}
void WeldOpenGLWidget::drawLineOverlay(QPainter &painter, const QPointF &imgHead, const QPointF &imgTail,
const QColor &color)
{
QPointF wHead = imageToWidgetCoords(imgHead);
QPointF wTail = imageToWidgetCoords(imgTail);
painter.setRenderHint(QPainter::Antialiasing, true);
// Pure single red line (1.0px thin stroke with antialiasing)
QPen linePen(color, 1.0, Qt::SolidLine, Qt::RoundCap);
painter.setPen(linePen);
painter.drawLine(wHead, wTail);
// Compact drag handles (short perpendicular ticks)
QLineF line(wHead, wTail);
double dx = line.dx();
double dy = line.dy();
double len = line.length();
if (len > 0.0) {
double px = -dy / len;
double py = dx / len;
double tickHalfLen = 6.0; // short length perpendicular to measurement line
QPointF headP1 = wHead + QPointF(px * tickHalfLen, py * tickHalfLen);
QPointF headP2 = wHead - QPointF(px * tickHalfLen, py * tickHalfLen);
painter.drawLine(headP1, headP2);
QPointF tailP1 = wTail + QPointF(px * tickHalfLen, py * tickHalfLen);
QPointF tailP2 = wTail - QPointF(px * tickHalfLen, py * tickHalfLen);
painter.drawLine(tailP1, tailP2);
}
// End labels
painter.setFont(QFont("Arial", 8, QFont::Bold));
painter.setPen(Qt::white);
painter.drawText(wHead + QPointF(10, -6), "A");
painter.drawText(wTail + QPointF(10, -6), "B");
}
QPointF WeldOpenGLWidget::widgetToImageCoords(const QPointF &widgetPos) const
{
if (m_image.isNull()) return QPointF();
double widgetAspect = (double)width() / height();
double imageAspect = (double)m_image.width() / m_image.height();
double drawW, drawH;
if (widgetAspect > imageAspect) {
drawH = height() * m_zoomLevel;
drawW = drawH * imageAspect;
} else {
drawW = width() * m_zoomLevel;
drawH = drawW / imageAspect;
}
double drawX = (width() - drawW) / 2.0 + m_panOffset.x();
double drawY = (height() - drawH) / 2.0 + m_panOffset.y();
double normX = (widgetPos.x() - drawX) / drawW;
double normY = (widgetPos.y() - drawY) / drawH;
return QPointF(normX * m_image.width(), normY * m_image.height());
}
QPointF WeldOpenGLWidget::imageToWidgetCoords(const QPointF &imgPos) const
{
if (m_image.isNull()) return QPointF();
double widgetAspect = (double)width() / height();
double imageAspect = (double)m_image.width() / m_image.height();
double drawW, drawH;
if (widgetAspect > imageAspect) {
drawH = height() * m_zoomLevel;
drawW = drawH * imageAspect;
} else {
drawW = width() * m_zoomLevel;
drawH = drawW / imageAspect;
}
double drawX = (width() - drawW) / 2.0 + m_panOffset.x();
double drawY = (height() - drawH) / 2.0 + m_panOffset.y();
double normX = imgPos.x() / m_image.width();
double normY = imgPos.y() / m_image.height();
return QPointF(drawX + normX * drawW, drawY + normY * drawH);
}
WeldOpenGLWidget::DragHandle WeldOpenGLWidget::hitTestLine(const QPointF &imgPos, const QPointF &head, const QPointF &tail) const
{
QPointF wPos = imageToWidgetCoords(imgPos);
QPointF wHead = imageToWidgetCoords(head);
QPointF wTail = imageToWidgetCoords(tail);
const double handleRadius = 15.0; // Hit test tolerance in pixels
if (QLineF(wPos, wHead).length() <= handleRadius) return DragHandle::Head;
if (QLineF(wPos, wTail).length() <= handleRadius) return DragHandle::Tail;
// Check if clicked near line body
QLineF line(wHead, wTail);
double lineLen = line.length();
if (lineLen > 0) {
double u = ((wPos.x() - wHead.x()) * (wTail.x() - wHead.x()) + (wPos.y() - wHead.y()) * (wTail.y() - wHead.y())) / (lineLen * lineLen);
if (u >= 0.0 && u <= 1.0) {
QPointF proj = wHead + u * (wTail - wHead);
if (QLineF(wPos, proj).length() <= handleRadius) {
return DragHandle::Body;
}
}
}
return DragHandle::None;
}
void WeldOpenGLWidget::mousePressEvent(QMouseEvent *event)
{
if (m_image.isNull()) return;
if (event->button() == Qt::LeftButton) {
QPointF imgPos = widgetToImageCoords(event->position());
m_activeHandle = hitTestLine(imgPos, m_head, m_tail);
if (m_activeHandle != DragHandle::None) {
m_dragStartImgPos = imgPos;
m_dragStartHead = m_head;
m_dragStartTail = m_tail;
update();
} else {
// Clicked background -> Pan image on left click drag
m_isPanning = true;
m_lastMousePos = event->pos();
setCursor(Qt::ClosedHandCursor);
}
return;
}
if (event->button() == Qt::RightButton) {
m_isPanning = true;
m_lastMousePos = event->pos();
setCursor(Qt::ClosedHandCursor);
return;
}
}
void WeldOpenGLWidget::mouseMoveEvent(QMouseEvent *event)
{
if (m_isPanning) {
QPoint delta = event->pos() - m_lastMousePos;
m_panOffset += QPointF(delta.x(), delta.y());
m_lastMousePos = event->pos();
update();
return;
}
QPointF imgPos = widgetToImageCoords(event->position());
if (m_activeHandle != DragHandle::None) {
if (m_activeHandle == DragHandle::Head) {
m_head = imgPos;
} else if (m_activeHandle == DragHandle::Tail) {
m_tail = imgPos;
} else if (m_activeHandle == DragHandle::Body) {
QPointF delta = imgPos - m_dragStartImgPos;
m_head = m_dragStartHead + delta;
m_tail = m_dragStartTail + delta;
}
emit measurementUpdated(measuredPx(), measuredMm());
update();
return;
}
// Update cursor feedback based on hover
DragHandle hover = hitTestLine(imgPos, m_head, m_tail);
if (hover == DragHandle::Head || hover == DragHandle::Tail) {
setCursor(Qt::CrossCursor);
} else if (hover == DragHandle::Body) {
setCursor(Qt::SizeAllCursor);
} else {
setCursor(Qt::OpenHandCursor);
}
}
void WeldOpenGLWidget::mouseReleaseEvent(QMouseEvent *event)
{
if (m_isPanning && (event->button() == Qt::LeftButton || event->button() == Qt::RightButton)) {
m_isPanning = false;
QPointF imgPos = widgetToImageCoords(event->position());
DragHandle hover = hitTestLine(imgPos, m_head, m_tail);
if (hover == DragHandle::Head || hover == DragHandle::Tail) setCursor(Qt::CrossCursor);
else if (hover == DragHandle::Body) setCursor(Qt::SizeAllCursor);
else setCursor(Qt::OpenHandCursor);
}
if (event->button() == Qt::LeftButton && m_activeHandle != DragHandle::None) {
m_activeHandle = DragHandle::None;
QPointF imgPos = widgetToImageCoords(event->position());
DragHandle hover = hitTestLine(imgPos, m_head, m_tail);
if (hover == DragHandle::Head || hover == DragHandle::Tail) setCursor(Qt::CrossCursor);
else if (hover == DragHandle::Body) setCursor(Qt::SizeAllCursor);
else setCursor(Qt::OpenHandCursor);
}
}
void WeldOpenGLWidget::wheelEvent(QWheelEvent *event)
{
double zoomFactor = 1.15;
if (event->angleDelta().y() < 0) {
zoomFactor = 1.0 / zoomFactor;
}
double newZoom = m_zoomLevel * zoomFactor;
if (newZoom < 0.2) newZoom = 0.2;
if (newZoom > 25.0) newZoom = 25.0;
QPointF mousePos = event->position();
QPointF imgPosBefore = widgetToImageCoords(mousePos);
m_zoomLevel = newZoom;
QPointF widgetPosAfter = imageToWidgetCoords(imgPosBefore);
m_panOffset += (mousePos - widgetPosAfter);
update();
}