#include "graphicsscene.h" // qt #include #include #include #include #include namespace orphex { namespace constants { const QLineF laserPlane{QPointF{-1000, 0}, QPointF{1000, 0}}; const auto laserColor = QColor{Qt::red}; const auto sensorColor = QColor{Qt::red}; } /*! * \brief vFoVDegrees - get "vertical" (YZ projection) Field of View (degrees) * \param vSensorPlane - sensor plane (YZ projection) * \param vLaserRange - laser range (YZ projection) * \return - Field of View angle (degrees) */ double vFoVDegrees(const QLineF& vSensorPlane, const QLineF& vLaserRange); /*! * \brief lenseBodyRect - create 2d rectangle representing lense body * \param diameterMm - body diameter (mm) * \param lengthMm - body length (mm) * \return - corresponding 2d body rect */ QRectF lenseBodyRect(const double diameterMm, const double lengthMm); /*! * \brief laserBodyRect - create 2d rectangle representing laser body * \param diameterMm - body diameter (mm) * \param lengthMm - body length (mm) * \return - corresponding 2d body rect */ QRectF laserBodyRect(const double diameterMm, const double lengthMm); /*! * \brief laserBodyRect - create 2d rectangle representing laser body * \param diameterMm - body diameter (mm) * \param lengthMm - body length (mm) * \return - corresponding 2d body rect */ /*! * \brief scannerBodyFrontWallRect - create 2d rectangle representing front wall * of scanner body * \param thicknessMm - wall thickness (mm) * \param offsetMm - horizontal offset from lense center (mm) * \return - corresponding 2d body rect */ QRectF scannerBodyFrontWallRect( const double thicknessMm, const double offsetMm ); QList xzLaserAreaPolygon(const double laserAngle); } // namespace orphex GraphicsScene::GraphicsScene(QObject* parent) : QGraphicsScene{parent} { setSceneRect(QRectF{QPointF{-1000, -1000}, QPointF{1000, 1000}}); m_lenseItem = addEllipse( QRectF{QPointF{-1, -15}, QPointF{1, 15}}, QPen{Qt::yellow, 0} ); m_lenseItem->setTransformOriginPoint(m_lenseItem->boundingRect().center()); m_yzSensorItem = new QGraphicsLineItem{QLineF{}, m_lenseItem}; m_yzSensorItem->setPen(QPen{orphex::constants::sensorColor, 0}); m_xzSensorItem = addRect({}, QPen{orphex::constants::sensorColor, 0}); m_lineOfActionItem = new QGraphicsLineItem{ QLineF{QPointF{0, -1000}, QPointF{0, 1000}}, m_lenseItem }; m_lineOfActionItem->setPen(QPen{Qt::green, 0}); m_opticalAxisItem = new QGraphicsLineItem{ QLineF{QPointF{-1000, 0}, QPointF{1000, 0}}, m_lenseItem }; m_opticalAxisItem->setPen(QPen{Qt::gray, 0}); m_lenseVerticalPlaneItem = addLine( QLineF{QPointF{0, -1000}, QPointF{0, 1000}}, QPen{Qt::black, 0} ); m_lenseBodyItem = addRect( QRectF{}, QPen{m_lenseItem->pen().color(), 0}, QBrush{m_lenseItem->pen().color(), Qt::DiagCrossPattern} ); m_lenseBodyItem->setParentItem(m_lenseItem); m_lenseBodyItem->setOpacity(0.1); m_laserBodyItem = addRect( QRectF{}, QPen{Qt::black, 0}, QBrush{Qt::black, Qt::DiagCrossPattern} ); m_scannerBodyFrontWallItem = addRect( QRectF{}, QPen{Qt::lightGray, 0}, QBrush{Qt::lightGray, Qt::DiagCrossPattern} ); using namespace orphex::constants; m_laserPlaneItem = addLine(laserPlane, QPen{Qt::black, 0}); m_laserPlaneItem->stackBefore(m_lenseItem); m_reverseLaserPlaneItem = new QGraphicsLineItem(QLineF{}, m_lenseItem); m_reverseLaserPlaneItem->setPen(QPen{Qt::magenta, 2}); m_reverseLaserPlaneItem->setVisible(false); m_desiredLaserPlaneItem = new QGraphicsLineItem(QLineF{}, m_lenseItem); m_desiredLaserPlaneItem->setPen(QPen{Qt::yellow, 0.1}); m_desiredLaserPlaneItem->setOpacity(0.5); m_desiredImagePlaneItem = new QGraphicsLineItem(QLineF{}, m_lenseItem); m_desiredImagePlaneItem->setPen(QPen{Qt::yellow, 0.15}); m_desiredImagePlaneItem->stackBefore(m_yzSensorItem); m_desiredRangeAreaItem = new QGraphicsPolygonItem{m_lenseItem}; m_desiredRangeAreaItem->setPen( QPen{m_desiredLaserPlaneItem->pen().color(), 0} ); m_desiredRangeAreaItem->setBrush( QBrush{m_desiredLaserPlaneItem->pen().color()} ); m_desiredRangeAreaItem->setOpacity(0.1); m_actualRangeItem = new QGraphicsLineItem(QLineF{}, m_lenseItem); m_actualRangeItem->setPen(QPen{Qt::red, 0.1}); m_actualRangeItem->setOpacity(0.5); m_actualRangeAreaItem = new QGraphicsPolygonItem{m_lenseItem}; m_actualRangeAreaItem->setPen(QPen{m_yzSensorItem->pen().color(), 0}); m_actualRangeAreaItem->setBrush(QBrush{m_yzSensorItem->pen().color()}); m_actualRangeAreaItem->setOpacity(0.1); m_xzActualRangeAreaItem = addPolygon( {}, m_actualRangeItem->pen(), m_actualRangeAreaItem->brush() ); m_xzActualRangeAreaItem->setOpacity(0.1); // m_xzActualRangeAreaItem->setParentItem(m_lenseItem); m_sensorLenseIntersectionItem = new QGraphicsEllipseItem{ QRectF{QPointF{-1, -1}, QPointF{1, 1}}, m_lenseItem }; m_sensorLenseIntersectionItem->setPen(QPen{Qt::red, 0.1}); const auto sensorLenseTextItem = new QGraphicsTextItem{ tr("Scheimpflug: sensor/lense"), m_sensorLenseIntersectionItem }; sensorLenseTextItem->setPos(QPointF{-6, -8}); auto font = sensorLenseTextItem->font(); font.setPixelSize(2); sensorLenseTextItem->setFont(font); sensorLenseTextItem->setDefaultTextColor( m_sensorLenseIntersectionItem->pen().color() ); m_lenseLaserIntersectionItem = new QGraphicsEllipseItem{ QRectF{QPointF{-1, -1}, QPointF{1, 1}}, m_lenseItem }; m_lenseLaserIntersectionItem->setPen(QPen{Qt::blue, 0}); const auto lenseLaserTextItem = new QGraphicsTextItem{ tr("Scheimpflug: lense/laser"), m_lenseLaserIntersectionItem }; lenseLaserTextItem->setPos(QPointF{-6, -2}); lenseLaserTextItem->setFont(font); lenseLaserTextItem->setDefaultTextColor( m_lenseLaserIntersectionItem->pen().color() ); m_xzHardwareLaserPlaneItem = addPolygon( QPolygonF{}, // QPen{orphex::constants::laserColor, 0.}, QPen{Qt::black, 0.}, // QBrush{orphex::constants::laserColor, Qt::DiagCrossPattern} QBrush{Qt::black, Qt::DiagCrossPattern} ); m_xzHardwareLaserPlaneItem->setOpacity(0.2); m_xzHardwareLaserPlaneItem->setParentItem(m_laserBodyItem); m_xzDebugItem = addPolygon( QPolygonF{}, QPen{Qt::green, 0.}, QBrush{Qt::green, Qt::DiagCrossPattern} ); m_xzDebugItem->setOpacity(0.1); m_xzDebugItem->setParentItem(m_laserBodyItem); } void GraphicsScene::update(OpticalDesign* design) { qDebug() << "update"; if (!design) { qCritical() << Q_FUNC_INFO << "design is nullptr"; return; } using namespace orphex::constants; const auto F = design->get_focalDistanceMm(); const auto H = design->get_lenseYPosMm(); const auto w = design->get_sensorPixelsWidth(); const auto h = design->get_sensorPixelsHeight(); const auto wMm = design->get_sensorWidthMm(); const auto hMm = design->get_sensorHeightMm(); const auto oAngle = design->get_opticalAxisAngleDegrees(); m_lenseItem->setRotation(m_lenseItem->rotation()); m_lenseItem->setRotation(-oAngle); m_lenseItem->setPos(QPointF{0, -H}); const auto objectToImage = [F](const QPointF& objectPoint) -> QPointF { // please note that object X is negative const auto imagePointX = 1. / (1. / F - 1. / -objectPoint.x()); const auto magnification = imagePointX / objectPoint.x(); const auto imagePointY = magnification * objectPoint.y(); return QPointF{imagePointX, imagePointY}; }; const auto imageToObject = [F](const QPointF& imagePoint) -> QPointF { const auto objectPointX = 1. / (1. / imagePoint.x() - 1. / F); const auto magnification = imagePoint.x() / objectPointX; const auto objectPointY = imagePoint.y() / magnification; return QPointF{objectPointX, objectPointY}; }; // calculate sensor plane const auto& oai = m_opticalAxisItem; const auto& oail = m_opticalAxisItem->line(); const QLineF opticalAxisScene{ oai->mapToScene(oail.p1()), oai->mapToScene(oail.p2()) }; QPointF intersection{}; const auto type = m_laserPlaneItem->line().intersects(opticalAxisScene, &intersection); if (type == QLineF::NoIntersection) { qCritical() << "no intersection between laser plane and optical axis"; return; } if (!qFuzzyCompare(intersection.y(), 0.)) { qCritical() << "laser plane/optical axis intersection point y != 0"; return; } // take 2 points - to the left and to the right from intersection point; const auto leftP = intersection / 4; // const auto rightP = intersection + leftP; const auto rightP = intersection * 1.5; // create desired line on laser plane m_desiredLaserPlaneItem->setLine( {m_lenseItem->mapFromScene(QPointF{-design->get_zBaseMm(), 0}), m_lenseItem->mapFromScene( QPointF{-(design->get_zBaseMm() + design->get_zRangeMm()), 0} )} ); // calculate image plane const auto debugL = m_desiredLaserPlaneItem->line(); const QLineF debugImageL{ objectToImage(debugL.p1()), objectToImage(debugL.p2()) }; m_desiredImagePlaneItem->setLine(debugImageL); // create sensor line on image plane QPointF sensorCenterPos{}; if (m_opticalAxisItem->line().intersects( m_desiredImagePlaneItem->line(), &sensorCenterPos ) == QLineF::NoIntersection) { qCritical() << "no intersection between sensor plane and optical axis"; return; } const auto sensorHalfHeight = design->get_sensorHeightMm() / 2.; QLineF sensorLine{QPointF{0, 0}, QPointF{0, sensorHalfHeight}}; sensorLine.translate(sensorCenterPos); sensorLine.setAngle(m_desiredImagePlaneItem->line().angle()); sensorLine.setP1(sensorCenterPos + (sensorCenterPos - sensorLine.p2())); // at this momet sensor is centered and rotated. apply vertical offset const auto offsetRatio = design->get_sensorVerticalOffsetMm() / sensorLine.length(); const auto offset = sensorLine.pointAt(1 + offsetRatio) - sensorLine.p2(); sensorLine.setPoints(sensorLine.p1() + offset, sensorLine.p2() + offset); m_yzSensorItem->setLine(sensorLine); const auto sensorHalfWidth = design->get_sensorWidthMm() / 2.; const QRectF xzSensorRect{ QPointF{ // m_yzSensorItem->mapToScene(sensorLine.p1()).x(), sensorLine.p1().x(), sensorHalfWidth }, QPointF{ // m_yzSensorItem->mapToScene(sensorLine.p2()).x(), sensorLine.p2().x(), -sensorHalfWidth } }; m_xzSensorItem->setRect(xzSensorRect); // const auto yzFoVAngle = QLineF{m_lenseItem->pos(), } // const auto axisAngleD = design->get_opticalAxisAngleDegrees(); // const QLineF sceneAxis{ // m_opticalAxisItem->mapToScene(m_opticalAxisItem->line().p1()), // m_opticalAxisItem->mapToScene(m_opticalAxisItem->line().p1()) // }; // for (auto& p : xzActualRangePolygon) // { // // qDebug() << "AZAZA hyp:" << p.x(); // // p.setX(sceneAxis.x1()); // // qt_noop(); // // p.setX(std::sin(qDegreesToRadians(axisAngleD)) * p.x()); // // p.setX(std::cos(qDegreesToRadians(axisAngleD)) * p.x()); // // p.setX(p.x() / std::cos(qDegreesToRadians(axisAngleD))); // // p.setX(m_lenseItem->mapFromScene(p).x()); // p = QPointF // } // m_xzActualRangeAreaItem->setRotation(180.); const auto p1 = xzSensorRect.topLeft(); const auto p2 = xzSensorRect.bottomRight(); // find laser plane range which corresponds to sensor position m_actualRangeItem->setLine({ imageToObject(m_yzSensorItem->line().p1()), imageToObject(m_yzSensorItem->line().p2()), }); // note that object area has negative coords // const auto tmp = m_actualRangeItem->line(); // const auto tmp2 = m_actualRangeItem->mapToScene(tmp.p1()); design->set_actualZBaseMm( -m_actualRangeItem->mapToScene(m_actualRangeItem->line().p1()).x() /*+ design->fullBodyOffset()*/ ); design->set_actualZRangeMm(m_actualRangeItem->line().length()); // convert debug image plane to laser plane just to verify they are the same // TODO: check with code and log errors? const QLineF reverseLaserPlaneLine{ imageToObject(m_desiredImagePlaneItem->line().p1()), imageToObject(m_desiredImagePlaneItem->line().p2()) }; m_reverseLaserPlaneItem->setLine(reverseLaserPlaneLine); // fill desired range area m_desiredRangeAreaItem->setPolygon( QPolygonF{ {m_desiredImagePlaneItem->line().p1(), m_desiredImagePlaneItem->line().p2(), m_desiredLaserPlaneItem->line().p2(), m_desiredLaserPlaneItem->line().p1()} } ); // fill actual range area m_actualRangeAreaItem->setPolygon( QPolygonF{ {m_yzSensorItem->line().p1(), m_yzSensorItem->line().p2(), m_actualRangeItem->line().p2(), m_actualRangeItem->line().p1()} } ); design->set_vFoVDegrees( orphex::vFoVDegrees(m_yzSensorItem->line(), m_actualRangeItem->line()) ); design->set_sensorLenseAngleDegrees( m_desiredImagePlaneItem->line().angleTo(m_lineOfActionItem->line()) ); // TODO: take back focal length into account // check Scheimpflug principle // TODO: draw lines? QPointF sensorLenseIntersection{}; if (m_yzSensorItem->line().intersects( m_lineOfActionItem->line(), &sensorLenseIntersection ) == QLineF::NoIntersection) { qWarning() << "no intersection between sensor plane and lense plane"; return; } m_sensorLenseIntersectionItem->setPos(sensorLenseIntersection); QPointF lenseLaserIntersection{}; if (m_lineOfActionItem->line().intersects( m_actualRangeItem->line(), &lenseLaserIntersection ) == QLineF::NoIntersection) { qWarning() << "no intersection between laser plane and lense plane"; return; } m_lenseLaserIntersectionItem->setPos(lenseLaserIntersection); if (!qFuzzyCompare(sensorLenseIntersection, lenseLaserIntersection)) { qWarning() << "The Scheimpflug principle is not observed:" << Qt::endl << "sensor/lense intersection:" << sensorLenseIntersection << Qt::endl << "lense/laser intersection:" << lenseLaserIntersection; } // TODO: move to settings // doesn't work (doesn't zoom) constexpr bool autoScale{false}; if (autoScale) { // region of interest QRectF ROI{}; ROI |= m_actualRangeAreaItem->boundingRect(); ROI |= m_desiredRangeAreaItem->boundingRect(); ROI |= m_lenseItem->boundingRect(); ROI |= m_yzSensorItem->boundingRect(); ROI |= m_sensorLenseIntersectionItem->boundingRect(); ROI |= m_lenseLaserIntersectionItem->boundingRect(); for (const auto& view : views()) { // TODO: move to settings constexpr double margin{5.}; view->setSceneRect(ROI); } } // calculate distance between sensor and lense on optical axis QPointF sensorAxisIntersection{}; if (m_yzSensorItem->line().intersects( m_opticalAxisItem->line(), &sensorAxisIntersection ) == QLineF::NoIntersection) { qCritical() << "no intersection between sensor and optical axis"; return; } design->set_lenseSensorDistanceMm( QLineF{QPointF{0, 0}, sensorAxisIntersection}.length() ); // calculate depth of field QPointF axisLaserIntersection{}; if (m_opticalAxisItem->line().intersects( m_actualRangeItem->line(), &axisLaserIntersection ) == QLineF::NoIntersection) { qCritical() << "no intersection between laser and optical axis"; return; } // const auto zM = std::min( // design->get_sensorPixelsHeight(), // design->get_sensorPixelsWidth() // ) / // 1000. / 1000.; // try z based on sensor cell size // const auto zMm = std::min( // design->get_sensorCellHeightUm(), // design->get_sensorCellHeightUm() // ) / // 1000.; // try z based on 1/16 sensor cell size (calibration step) const auto zMm = std::min( design->get_sensorCellHeightUm(), design->get_sensorCellWidthUm() ) / 1000. / 16.; // try z based on E. Goldovsky method - 0.2% of sensor height // const auto zMm = design->get_sensorHeightMm() * 0.2 / 100.; // const auto RM = // QLineF{QPointF{0., 0.}, axisLaserIntersection}.length() * 1000.; const auto RMidMm = QLineF{QPointF{0., 0.}, axisLaserIntersection}.length(); // const auto RFrontMm = QLineF{QPointF{0., 0.}, // axisLaserIntersection}.length(); const auto fM = // design->get_backFocalDistanceMm() * 1000.; // Gordijchuk & Pell, 1979, say to use focal distance. // Wikipedia, based on the same book, says to use back focal distance. WTF? // const auto fMm = design->get_backFocalDistanceMm(); const auto fMm = design->get_focalDistanceMm(); const auto K = static_cast(design->get_lenseAperture()) / 10.; using FromSharpDistMm = double; using BackSharpDistMm = double; using DofResult = std::tuple; const auto calculateDof = [zMm, fMm, K](const auto RMm) -> DofResult { const auto ff = fMm * fMm; const auto Rff = RMm * ff; const auto Kfz = K * fMm * zMm; const auto KRz = K * RMm * zMm; const auto R1Mm = Rff / (ff - Kfz + KRz); const auto R2Mm = Rff / (ff + Kfz - KRz); return {R1Mm, R2Mm}; }; const auto ff = fMm * fMm; const auto Rff = RMidMm * ff; const auto Kfz = K * fMm * zMm; const auto KRz = K * RMidMm * zMm; // const auto R1M = Rff / (ff - Kfz + KRz); // const auto R2M = Rff / (ff + Kfz - KRz); // const auto R1Mm = Rff / (ff - Kfz + KRz); // const auto R2Mm = Rff / (ff + Kfz - KRz); const auto [R1Mm, R2Mm] = calculateDof(RMidMm); design->set_frontSharpDistanceMm(R1Mm); design->set_backSharpDistanceMm(R2Mm); const auto depthOfFieldMm = R2Mm - R1Mm; design->set_depthOfFieldMm(depthOfFieldMm); if (qFuzzyIsNull(depthOfFieldMm)) { emit design->depthOfFieldMmChanged(); emit design->depthOfFieldMmChanged(depthOfFieldMm); } m_lenseBodyItem->setRect( orphex::lenseBodyRect( design->get_lenseBodyMaxDiameterMm(), design->get_lenseBodyLengthMm() ) ); m_laserBodyItem->setRect( orphex::laserBodyRect( design->get_laserBodyDiameterMm(), design->get_laserBodyLengthMm() ) ); m_scannerBodyFrontWallItem->setRect( orphex::scannerBodyFrontWallRect( design->get_scannerBodyWallThicknessMm(), design->get_scannerBodyFrontWallOffsetMm() ) ); { m_xzHardwareLaserPlaneItem->setPolygon( orphex::xzLaserAreaPolygon(design->get_laserAngleDegrees()) ); } { const auto item = m_actualRangeItem; const auto line = item->line(); const auto x1 = item->mapToScene(line.p1()).x(); const auto x2 = item->mapToScene(line.p2()).x(); QPolygonF xzActualRangePolygon{QList{ QPointF{x1, imageToObject(m_xzSensorItem->rect().topLeft()).y()}, QPointF{x1, imageToObject(m_xzSensorItem->rect().bottomLeft()).y()}, QPointF{ x2, imageToObject(m_xzSensorItem->rect().bottomRight()).y() }, QPointF{x2, imageToObject(m_xzSensorItem->rect().topRight()).y()}, }}; m_xzActualRangeAreaItem->setPolygon(xzActualRangePolygon); m_xzDebugItem->setPolygon( QPolygonF{QList{ QPointF{0., 0.}, QPointF{ x1, imageToObject(m_xzSensorItem->rect().topLeft()).y() }, QPointF{ x1, imageToObject(m_xzSensorItem->rect().bottomLeft()).y() }, }} ); } } double orphex::vFoVDegrees( const QLineF& vSensorPlane, const QLineF& vLaserRange ) { const auto sl = vSensorPlane; const auto arl = vLaserRange; const QLineF l0{sl.p1(), arl.p1()}; const QLineF l1{sl.p2(), arl.p2()}; return l1.angleTo(l0); } QRectF orphex::lenseBodyRect(const double diameterMm, const double lengthMm) { return QRectF{ QPointF{-lengthMm, -diameterMm / 2.}, QSizeF{lengthMm, diameterMm} }; } QRectF orphex::laserBodyRect(const double diameterMm, const double lengthMm) { return QRectF{QPointF{0., -diameterMm / 2.}, QSizeF{lengthMm, diameterMm}}; } QRectF orphex::scannerBodyFrontWallRect( const double thicknessMm, const double offsetMm ) { return QRectF{ QPointF{offsetMm - thicknessMm, -1000.}, QPointF{offsetMm, 1000.} }; } QList orphex::xzLaserAreaPolygon(const double laserAngle) { QLineF line{QPointF{0., 0.}, QPointF{-1000., 0.}}; line.setAngle(180. + laserAngle / 2.); QList result{QPointF{0., 0.}, line.p2()}; line.setAngle(180. - laserAngle / 2.); result << line.p2(); return result; }