Se mejora notablemente la calibracion usando tablero de ajedrez y se usa opencl para procesamiento
This commit is contained in:
parent
c13794ada0
commit
bf5e058030
6 changed files with 854 additions and 466 deletions
|
|
@ -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}
|
||||||
)
|
)
|
||||||
|
|
|
||||||
130
main_window.cpp
130
main_window.cpp
|
|
@ -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));
|
bool success = m_glWidget->calibrateWithChessboard(gx, gy, sqSize);
|
||||||
painter.setPen(QPen(QColor(170, 175, 185), 1.5));
|
|
||||||
painter.drawEllipse(QPointF(x, y), r, r * 0.5);
|
if (success) {
|
||||||
|
// Keep green when calibrated successfully
|
||||||
|
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);
|
||||||
|
|
||||||
// Draw reference pattern bar (200px = 10mm)
|
if (success) {
|
||||||
int scaleX = 150;
|
int squaresX = gx + 1;
|
||||||
int scaleY = 560;
|
int squaresY = gy + 1;
|
||||||
int scaleLenPx = 200;
|
|
||||||
|
|
||||||
painter.setBrush(QColor(230, 230, 230));
|
QMessageBox::information(this, "Calibración Exitosa",
|
||||||
painter.setPen(Qt::NoPen);
|
QString("Se detectó automáticamente un tablero de ajedrez.\n\n"
|
||||||
painter.drawRoundedRect(scaleX - 20, scaleY - 30, scaleLenPx + 40, 70, 8, 8);
|
"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."));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
QPen rulerPen(Qt::black, 4, Qt::SolidLine, Qt::SquareCap);
|
// Reset calibration button color to red when a new image is loaded
|
||||||
painter.setPen(rulerPen);
|
connect(m_glWidget, &MeasurementWidget::imageLoaded, this, [btnCalibChessboard]() {
|
||||||
painter.drawLine(scaleX, scaleY, scaleX + scaleLenPx, scaleY);
|
btnCalibChessboard->setStyleSheet("font-weight: bold; background-color: #b71c1c; color: white; border-radius: 4px;");
|
||||||
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));
|
|
||||||
painter.drawText(QRect(scaleX - 20, scaleY + 10, scaleLenPx + 40, 25), Qt::AlignCenter, "PATRÓN DE PRUEBA: 10.0 mm");
|
|
||||||
|
|
||||||
img.save("/home/sebastian/repos/weld_measure/sample_weld.png");
|
|
||||||
m_glWidget->setImage(img);
|
|
||||||
|
|
||||||
// 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)
|
||||||
|
|
|
||||||
|
|
@ -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
745
measurement_widget.cpp
Normal 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;
|
||||||
|
}
|
||||||
|
|
@ -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
|
||||||
|
|
@ -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();
|
|
||||||
}
|
|
||||||
Loading…
Reference in a new issue