diff --git a/base/CMakeLists.txt b/base/CMakeLists.txt index e16b7821a..2762aeea5 100755 --- a/base/CMakeLists.txt +++ b/base/CMakeLists.txt @@ -322,6 +322,7 @@ SET(IP_FILES src/ImageEncoderCV.cpp src/RotateCV.cpp src/AffineTransform.cpp + src/PerspectiveTransform.cpp src/BrightnessContrastControlXform.cpp src/VirtualPTZ.cpp src/WebCamSource.cpp @@ -355,6 +356,7 @@ SET(IP_FILES_H include/ImageEncoderCV.h include/RotateCV.h include/AffineTransform.h + include/PerspectiveTransform.h include/BrightnessContrastControlXform.h include/VirtualPTZ.h include/WebCamSource.h @@ -600,6 +602,7 @@ SET(UT_FILES test/ImageEncodeCV_tests.cpp test/rotatecv_tests.cpp test/affinetransform_tests.cpp + test/perspectivetransform_tests.cpp test/brightness_contrast_tests.cpp test/virtualptz_tests.cpp test/webcam_source_tests.cpp diff --git a/base/include/PerspectiveTransform.h b/base/include/PerspectiveTransform.h new file mode 100644 index 000000000..e17b95ce3 --- /dev/null +++ b/base/include/PerspectiveTransform.h @@ -0,0 +1,114 @@ +#pragma once +#include "FrameMetadata.h" +#include "Module.h" +#include "Logger.h" + +class PerspectiveTransformProps : public ModuleProps +{ +public: + enum Mode + { + BASIC, + DYNAMIC + }; + + // Default constructor for serialization + PerspectiveTransformProps() : mode(BASIC) {} + + // Constructor for BASIC mode + PerspectiveTransformProps(const std::vector &_srcPoints, const std::vector &_dstPoints) + : mode(BASIC) + { + srcPoints = _srcPoints; + dstPoints = _dstPoints; + } + + // Constructor for DYNAMIC mode + PerspectiveTransformProps(Mode _mode) : mode(_mode) + { + if (_mode != DYNAMIC) + { + throw AIPException(AIP_FATAL, "This constructor only supports DYNAMIC mode"); + } + } + + Mode mode; + std::vector srcPoints; + std::vector dstPoints; + + size_t getSerializeSize() + { + return ModuleProps::getSerializeSize() + sizeof(Mode) + + sizeof(size_t) + srcPoints.size() * sizeof(cv::Point2f) + + sizeof(size_t) + dstPoints.size() * sizeof(cv::Point2f); + } + +private: + friend class boost::serialization::access; + + template + void save(Archive &ar, const unsigned int version) const + { + ar &boost::serialization::base_object(*this); + ar &mode; + + size_t srcSize = srcPoints.size(); + size_t dstSize = dstPoints.size(); + ar &srcSize &dstSize; + + for (size_t i = 0; i < srcSize; ++i) { + ar &srcPoints[i].x &srcPoints[i].y; + } + for (size_t i = 0; i < dstSize; ++i) { + ar &dstPoints[i].x &dstPoints[i].y; + } + } + + template + void load(Archive &ar, const unsigned int version) + { + ar &boost::serialization::base_object(*this); + ar &mode; + + size_t srcSize, dstSize; + ar &srcSize &dstSize; + + srcPoints.resize(srcSize); + dstPoints.resize(dstSize); + + for (size_t i = 0; i < srcSize; ++i) { + ar &srcPoints[i].x &srcPoints[i].y; + } + for (size_t i = 0; i < dstSize; ++i) { + ar &dstPoints[i].x &dstPoints[i].y; + } + } + + BOOST_SERIALIZATION_SPLIT_MEMBER() +}; + +class PerspectiveTransform : public Module +{ +public: + PerspectiveTransform(PerspectiveTransformProps _props); + virtual ~PerspectiveTransform(); + bool init(); + bool term(); + void setProps(PerspectiveTransformProps &props); + PerspectiveTransformProps getProps(); + +protected: + bool process(frame_container &frames); + bool processSOS(frame_sp &frame); + bool validateInputPins(); + bool validateOutputPins(); + void addInputPin(framemetadata_sp &metadata, string &pinId); + std::string addOutputPin(framemetadata_sp &metadata); + bool handlePropsChange(frame_sp &frame); + +private: + int mFrameType; + PerspectiveTransformProps mProps; + class Detail; + boost::shared_ptr mDetail; +}; \ No newline at end of file diff --git a/base/src/PerspectiveTransform.cpp b/base/src/PerspectiveTransform.cpp new file mode 100644 index 000000000..066da4cc2 --- /dev/null +++ b/base/src/PerspectiveTransform.cpp @@ -0,0 +1,325 @@ +#include "PerspectiveTransform.h" +#include "FrameMetadata.h" +#include "RawImageMetadata.h" +#include "RawImagePlanarMetadata.h" +#include "ArrayMetadata.h" +#include "FrameMetadataFactory.h" +#include "Frame.h" +#include "Logger.h" +#include +#include "Utils.h" + +class PerspectiveTransform::Detail +{ +public: + Detail(PerspectiveTransformProps &_props) : mProps(_props) + { + // Only compute transformation matrix in BASIC mode, for DYNAMIC mode, it will be computed per frame + if (mProps.mode == PerspectiveTransformProps::BASIC) + { + transformMatrix = cv::getPerspectiveTransform(mProps.srcPoints, mProps.dstPoints); + } + } + ~Detail() {} + + void initMatImages(framemetadata_sp &input) + { + iImg = Utils::getMatHeader(FrameMetadataFactory::downcast(input)); + oImg = Utils::getMatHeader(FrameMetadataFactory::downcast(mOutputMetadata)); + } + + void updateTransformMatrix(const std::vector& srcPoints, const std::vector& dstPoints) + { + transformMatrix = cv::getPerspectiveTransform(srcPoints, dstPoints); + } + +public: + framemetadata_sp mOutputMetadata; + std::string mOutputPinId; + std::string mSrcPointsPinId; + std::string mDstPointsPinId; + cv::Mat iImg; + cv::Mat oImg; + cv::Mat transformMatrix; + +private: + PerspectiveTransformProps mProps; +}; + +PerspectiveTransform::PerspectiveTransform(PerspectiveTransformProps _props) : Module(TRANSFORM, "PerspectiveTransform", _props), mProps(_props), mFrameType(FrameMetadata::GENERAL) +{ + mDetail.reset(new Detail(_props)); +} + +PerspectiveTransform::~PerspectiveTransform() {} + +bool PerspectiveTransform::validateInputPins() +{ + if (mProps.mode == PerspectiveTransformProps::BASIC) + { + if (getNumberOfInputPins() != 1) + { + LOG_ERROR << "<" << getId() << ">::validateInputPins BASIC mode expects 1 input pin. Actual<" << getNumberOfInputPins() << ">"; + return false; + } + } + else if (mProps.mode == PerspectiveTransformProps::DYNAMIC) + { + int numPins = getNumberOfInputPins(); + if (numPins < 1 || numPins > 3) + { + LOG_ERROR << "<" << getId() << ">::validateInputPins DYNAMIC mode expects 1-3 input pins during setup. Actual<" << numPins << ">"; + return false; + } + + if (numPins > 0) + { + auto inputMetadata = getInputMetadata(); + int imageCount = 0, arrayCount = 0; + + for (auto const& elem: inputMetadata) + { + if (elem.second->getFrameType() == FrameMetadata::RAW_IMAGE) + { + imageCount++; + } + else if (elem.second->getFrameType() == FrameMetadata::ARRAY) + { + arrayCount++; + } + } + + if (imageCount > 1 || arrayCount > 2) + { + LOG_ERROR << "<" << getId() << ">::validateInputPins DYNAMIC mode expects at most 1 RAW_IMAGE and 2 ARRAY inputs. Actual: RAW_IMAGE<" << imageCount << "> ARRAY<" << arrayCount << ">"; + return false; + } + } + } + + framemetadata_sp imageMetadata; + if (mProps.mode == PerspectiveTransformProps::BASIC) + { + imageMetadata = getFirstInputMetadata(); + } + else + { + auto inputMetadata = getInputMetadata(); + for (auto const& elem: inputMetadata) + { + if (elem.second->getFrameType() == FrameMetadata::RAW_IMAGE) + { + imageMetadata = elem.second; + break; + } + } + } + + // Only validate image metadata if we have it + if (imageMetadata) + { + if (imageMetadata->getFrameType() != FrameMetadata::RAW_IMAGE) + { + LOG_ERROR << "<" << getId() << ">::validateInputPins input frameType is expected to be Raw_Image. Actual<" << imageMetadata->getFrameType() << ">"; + return false; + } + + auto rawMetadata = FrameMetadataFactory::downcast(imageMetadata); + auto imageType = rawMetadata->getImageType(); + switch (imageType) + { + case ImageMetadata::MONO: + case ImageMetadata::BGR: + case ImageMetadata::BGRA: + case ImageMetadata::RGB: + case ImageMetadata::RGBA: + break; + default: + throw AIPException(AIP_NOTIMPLEMENTED, "Encoder not supported for ImageType<" + std::to_string(imageType) + ">"); + } + } + + return true; +} + +bool PerspectiveTransform::validateOutputPins() +{ + if (getNumberOfOutputPins() != 1) + { + LOG_ERROR << "<" << getId() << ">::validateOutputPins size is expected to be 1. Actual<" << getNumberOfOutputPins() << ">"; + return false; + } + + framemetadata_sp metadata = getFirstOutputMetadata(); + FrameMetadata::FrameType frameType = metadata->getFrameType(); + if (frameType != FrameMetadata::RAW_IMAGE) + { + LOG_ERROR << "<" << getId() << ">::validateOutputPins input frameType is expected to be RAW_IMAGE. Actual<" << frameType << ">"; + return false; + } + + return true; +} + +void PerspectiveTransform::addInputPin(framemetadata_sp &metadata, string &pinId) +{ + Module::addInputPin(metadata, pinId); + + if (metadata->getFrameType() == FrameMetadata::RAW_IMAGE) + { + auto rawMetadata = FrameMetadataFactory::downcast(metadata); + mDetail->mOutputMetadata = boost::shared_ptr(new RawImageMetadata(rawMetadata->getWidth(), rawMetadata->getHeight(), rawMetadata->getImageType(), rawMetadata->getType(), 0, rawMetadata->getDepth(), FrameMetadata::HOST, true)); + mDetail->initMatImages(metadata); + mDetail->mOutputMetadata->copyHint(*metadata.get()); + mDetail->mOutputPinId = addOutputPin(mDetail->mOutputMetadata); + } + else if (metadata->getFrameType() == FrameMetadata::ARRAY && mProps.mode == PerspectiveTransformProps::DYNAMIC) + { + if (mDetail->mSrcPointsPinId.empty()) + { + mDetail->mSrcPointsPinId = pinId; + LOG_INFO << "<" << getId() << "> Source points input pin: " << pinId; + } + else if (mDetail->mDstPointsPinId.empty()) + { + mDetail->mDstPointsPinId = pinId; + LOG_INFO << "<" << getId() << "> Destination points input pin: " << pinId; + } + else + { + LOG_ERROR << "<" << getId() << "> Too many ARRAY input pins. Expected 2 for DYNAMIC mode."; + } + } +} + +std::string PerspectiveTransform::addOutputPin(framemetadata_sp &metadata) +{ + return Module::addOutputPin(metadata); +} + +bool PerspectiveTransform::init() +{ + if (mProps.mode == PerspectiveTransformProps::DYNAMIC) + { + if (getNumberOfInputPins() != 3) + { + LOG_ERROR << "<" << getId() << ">::init DYNAMIC mode requires exactly 3 input pins. Actual<" << getNumberOfInputPins() << ">"; + return false; + } + + // Validate that we have exactly 1 RAW_IMAGE and 2 ARRAY inputs + auto inputMetadata = getInputMetadata(); + int imageCount = 0, arrayCount = 0; + + for (auto const& elem: inputMetadata) + { + if (elem.second->getFrameType() == FrameMetadata::RAW_IMAGE) + { + imageCount++; + } + else if (elem.second->getFrameType() == FrameMetadata::ARRAY) + { + arrayCount++; + } + } + + if (imageCount != 1 || arrayCount != 2) + { + LOG_ERROR << "<" << getId() << ">::init DYNAMIC mode requires exactly 1 RAW_IMAGE and 2 ARRAY inputs. Actual: RAW_IMAGE<" << imageCount << "> ARRAY<" << arrayCount << ">"; + return false; + } + + // Ensure we have identified both point pin IDs + if (mDetail->mSrcPointsPinId.empty() || mDetail->mDstPointsPinId.empty()) + { + LOG_ERROR << "<" << getId() << ">::init DYNAMIC mode requires both source and destination point pin IDs to be set"; + return false; + } + } + + return Module::init(); +} + +bool PerspectiveTransform::term() +{ + return Module::term(); +} + +bool PerspectiveTransform::process(frame_container &frames) +{ + auto frame = getFrameByType(frames, FrameMetadata::RAW_IMAGE); + if (isFrameEmpty(frame)) + { + return true; + } + + // Handle DYNAMIC mode - extract points from input pins + if (mProps.mode == PerspectiveTransformProps::DYNAMIC) + { + auto srcPointsFrame = frames.find(mDetail->mSrcPointsPinId); + if (srcPointsFrame == frames.end() || isFrameEmpty(srcPointsFrame->second)) + { + LOG_ERROR << "<" << getId() << "> Source points frame not found or empty in DYNAMIC mode"; + return true; + } + + auto dstPointsFrame = frames.find(mDetail->mDstPointsPinId); + if (dstPointsFrame == frames.end() || isFrameEmpty(dstPointsFrame->second)) + { + LOG_ERROR << "<" << getId() << "> Destination points frame not found or empty in DYNAMIC mode"; + return true; + } + + auto srcPointsData = static_cast(srcPointsFrame->second->data()); + auto dstPointsData = static_cast(dstPointsFrame->second->data()); + + std::vector srcPoints(srcPointsData, srcPointsData + 4); + std::vector dstPoints(dstPointsData, dstPointsData + 4); + + // Update transformation matrix with dynamic points + mDetail->updateTransformMatrix(srcPoints, dstPoints); + } + + auto outFrame = makeFrame(); + + mDetail->iImg.data = static_cast(frame->data()); + mDetail->oImg.data = static_cast(outFrame->data()); + + LOG_INFO << "Transformation Matrix: " << mDetail->transformMatrix; + + cv::warpPerspective(mDetail->iImg, mDetail->oImg, mDetail->transformMatrix, mDetail->oImg.size()); + frames.insert(make_pair(mDetail->mOutputPinId, outFrame)); + send(frames); + return true; +} + +bool PerspectiveTransform::processSOS(frame_sp &frame) +{ + auto metadata = frame->getMetadata(); + return true; +} + +void PerspectiveTransform::setProps(PerspectiveTransformProps &props) +{ + Module::addPropsToQueue(props); +} + +PerspectiveTransformProps PerspectiveTransform::getProps() +{ + return mProps; +} + +bool PerspectiveTransform::handlePropsChange(frame_sp &frame) +{ + PerspectiveTransformProps props; + bool ret = Module::handlePropsChange(frame, props); + + // Update the transformation matrix if mode changed to BASIC or points changed + if (props.mode == PerspectiveTransformProps::BASIC) + { + mDetail->updateTransformMatrix(props.srcPoints, props.dstPoints); + } + + mProps = props; + return ret; +} \ No newline at end of file diff --git a/base/test/perspectivetransform_tests.cpp b/base/test/perspectivetransform_tests.cpp new file mode 100644 index 000000000..7a50ebe9e --- /dev/null +++ b/base/test/perspectivetransform_tests.cpp @@ -0,0 +1,410 @@ +#include "stdafx.h" +#include +#include + +#include "FileReaderModule.h" +#include "ExternalSinkModule.h" +#include "ExternalSourceModule.h" +#include "FrameMetadata.h" +#include "FrameMetadataFactory.h" +#include "Frame.h" +#include "Logger.h" +#include "AIPExceptions.h" +#include "ArrayMetadata.h" + +#include "test_utils.h" +#include "PerspectiveTransform.h" +#include "PipeLine.h" +#include "StatSink.h" + +// Test wrapper class to access protected methods +class TestPerspectiveTransform : public PerspectiveTransform +{ +public: + TestPerspectiveTransform(PerspectiveTransformProps _props) : PerspectiveTransform(_props) {} + virtual ~TestPerspectiveTransform() {} + + // Expose protected methods for testing + void addInputPin(framemetadata_sp &metadata, string &pinId) { + return PerspectiveTransform::addInputPin(metadata, pinId); + } + bool validateInputPins() { return PerspectiveTransform::validateInputPins(); } + bool validateOutputPins() { return PerspectiveTransform::validateOutputPins(); } + frame_sp makeFrame(size_t size) { return PerspectiveTransform::makeFrame(size); } + bool send(frame_container& frames) { return PerspectiveTransform::send(frames); } +}; + +BOOST_AUTO_TEST_SUITE(perspectivetransform_tests) + +BOOST_AUTO_TEST_CASE(mono_1920x960) +{ + auto fileReader = boost::shared_ptr(new FileReaderModule(FileReaderModuleProps("./data/mono_1920x960.raw"))); + auto metadata = framemetadata_sp(new RawImageMetadata(1920, 960, ImageMetadata::ImageType::MONO, CV_8UC1, 0, CV_8U, FrameMetadata::HOST, true)); + fileReader->addOutputPin(metadata); + + // Define source and destination points for perspective transformation + std::vector srcPoints = { + {0, 0}, + {1920, 0}, + {1920, 960}, + {0, 960}}; + std::vector dstPoints = { + {100, 200}, + {1820, 50}, + {1820, 950}, + {100, 800} + }; + + auto perspectiveTransform = boost::shared_ptr(new PerspectiveTransform(PerspectiveTransformProps(srcPoints, dstPoints))); + fileReader->setNext(perspectiveTransform); + + auto sink = boost::shared_ptr(new ExternalSinkModule()); + perspectiveTransform->setNext(sink); + + BOOST_TEST(fileReader->init()); + BOOST_TEST(perspectiveTransform->init()); + BOOST_TEST(sink->init()); + + fileReader->step(); + perspectiveTransform->step(); + + auto frames = sink->pop(); + BOOST_TEST(frames.size() == 1); + + auto outputFrame = frames.cbegin()->second; + BOOST_TEST(outputFrame->getMetadata()->getFrameType() == FrameMetadata::RAW_IMAGE); + + auto rawMetadata = FrameMetadataFactory::downcast(outputFrame->getMetadata()); + BOOST_TEST(rawMetadata->getDepth() == CV_8U); + + Test_Utils::saveOrCompare("./data/PerspectiveTransform_outputs/perspectivetransform-mono_1920x960_transform.raw", const_cast(static_cast(outputFrame->data())), outputFrame->size(), 0); +} + +BOOST_AUTO_TEST_CASE(bgr_1080x720) +{ + auto fileReader = boost::shared_ptr(new FileReaderModule(FileReaderModuleProps("./data/BGR_1080x720.raw"))); + auto metadata = framemetadata_sp(new RawImageMetadata(1080, 720, ImageMetadata::ImageType::BGR, CV_8UC3, 0, CV_8U, FrameMetadata::HOST, true)); + fileReader->addOutputPin(metadata); + + std::vector srcPoints = { + {0, 0}, + {1080, 0}, + {1080, 720}, + {0, 720}}; + std::vector dstPoints = { + {50, 100}, + {1030, 50}, + {1030, 670}, + {50, 620}}; + + auto perspectiveTransform = boost::shared_ptr(new PerspectiveTransform(PerspectiveTransformProps(srcPoints, dstPoints))); + fileReader->setNext(perspectiveTransform); + + auto sink = boost::shared_ptr(new ExternalSinkModule()); + perspectiveTransform->setNext(sink); + + BOOST_TEST(fileReader->init()); + BOOST_TEST(perspectiveTransform->init()); + BOOST_TEST(sink->init()); + + fileReader->step(); + perspectiveTransform->step(); + + auto frames = sink->pop(); + BOOST_TEST(frames.size() == 1); + + auto outputFrame = frames.cbegin()->second; + BOOST_TEST(outputFrame->getMetadata()->getFrameType() == FrameMetadata::RAW_IMAGE); + + auto rawMetadata = FrameMetadataFactory::downcast(outputFrame->getMetadata()); + BOOST_TEST(rawMetadata->getDepth() == CV_8U); + + Test_Utils::saveOrCompare("./data/PerspectiveTransform_outputs/perspectivetransform-bgr_1080x720_transform.raw", const_cast(static_cast(outputFrame->data())), outputFrame->size(), 0); +} + +BOOST_AUTO_TEST_CASE(rgb_320x180) +{ + auto fileReader = boost::shared_ptr(new FileReaderModule(FileReaderModuleProps("./data/RGB_320x180.raw"))); + auto metadata = framemetadata_sp(new RawImageMetadata(320, 180, ImageMetadata::ImageType::RGB, CV_8UC3, 0, CV_8U, FrameMetadata::HOST, true)); + fileReader->addOutputPin(metadata); + + std::vector srcPoints = { + {0, 0}, + {320, 0}, + {320, 180}, + {0, 180}}; + std::vector dstPoints = { + {20, 30}, + {300, 10}, + {310, 170}, + {10, 160}}; + + auto perspectiveTransform = boost::shared_ptr(new PerspectiveTransform(PerspectiveTransformProps(srcPoints, dstPoints))); + fileReader->setNext(perspectiveTransform); + + auto sink = boost::shared_ptr(new ExternalSinkModule()); + perspectiveTransform->setNext(sink); + + BOOST_TEST(fileReader->init()); + BOOST_TEST(perspectiveTransform->init()); + BOOST_TEST(sink->init()); + + fileReader->step(); + perspectiveTransform->step(); + + auto frames = sink->pop(); + BOOST_TEST(frames.size() == 1); + + auto outputFrame = frames.cbegin()->second; + BOOST_TEST(outputFrame->getMetadata()->getFrameType() == FrameMetadata::RAW_IMAGE); + + auto rawMetadata = FrameMetadataFactory::downcast(outputFrame->getMetadata()); + BOOST_TEST(rawMetadata->getDepth() == CV_8U); + + Test_Utils::saveOrCompare("./data/PerspectiveTransform_outputs/perspectivetransform-rgb_320x180_transform.raw", const_cast(static_cast(outputFrame->data())), outputFrame->size(), 0); +} + +BOOST_AUTO_TEST_CASE(dynamic_mode_rgb_320x180) +{ + // Read image using FileReaderModule + auto fileReader = boost::shared_ptr(new FileReaderModule(FileReaderModuleProps("./data/RGB_320x180.raw"))); + auto imageMetadata = framemetadata_sp(new RawImageMetadata(320, 180, ImageMetadata::RGB, CV_8UC3, 0, CV_8U, FrameMetadata::HOST, true)); + fileReader->addOutputPin(imageMetadata); + + auto tempSink = boost::shared_ptr(new ExternalSinkModule()); + fileReader->setNext(tempSink); + + BOOST_TEST(fileReader->init()); + BOOST_TEST(tempSink->init()); + + fileReader->step(); + auto imageFrames = tempSink->pop(); + BOOST_TEST(imageFrames.size() == 1); + auto imageData = imageFrames.cbegin()->second; + + // Create source for all inputs (image + points) + auto combinedSource = boost::shared_ptr(new ExternalSourceModule()); + auto imagePinId = combinedSource->addOutputPin(imageMetadata); + + auto srcPointsMetadata = framemetadata_sp(new ArrayMetadata()); + FrameMetadataFactory::downcast(srcPointsMetadata)->setData(4, CV_32FC2, sizeof(cv::Point2f)); + auto srcPointsPinId = combinedSource->addOutputPin(srcPointsMetadata); + + auto dstPointsMetadata = framemetadata_sp(new ArrayMetadata()); + FrameMetadataFactory::downcast(dstPointsMetadata)->setData(4, CV_32FC2, sizeof(cv::Point2f)); + auto dstPointsPinId = combinedSource->addOutputPin(dstPointsMetadata); + + // Create perspective transform in DYNAMIC mode + auto perspectiveTransform = boost::shared_ptr(new TestPerspectiveTransform(PerspectiveTransformProps(PerspectiveTransformProps::DYNAMIC))); + + auto sink = boost::shared_ptr(new ExternalSinkModule()); + combinedSource->setNext(perspectiveTransform); + perspectiveTransform->setNext(sink); + + BOOST_TEST(combinedSource->init()); + BOOST_TEST(perspectiveTransform->init()); + BOOST_TEST(sink->init()); + + // Create frames with actual file data + auto imageFrame = combinedSource->makeFrame(imageMetadata->getDataSize(), imagePinId); + memcpy(imageFrame->data(), imageData->data(), imageMetadata->getDataSize()); + + std::vector srcPoints = { + cv::Point2f(0, 0), + cv::Point2f(320, 0), + cv::Point2f(320, 180), + cv::Point2f(0, 180) + }; + auto srcPointsFrame = combinedSource->makeFrame(srcPointsMetadata->getDataSize(), srcPointsPinId); + memcpy(srcPointsFrame->data(), srcPoints.data(), 4 * sizeof(cv::Point2f)); + + std::vector dstPoints = { + cv::Point2f(20, 30), + cv::Point2f(300, 10), + cv::Point2f(310, 170), + cv::Point2f(10, 160) + }; + auto dstPointsFrame = combinedSource->makeFrame(dstPointsMetadata->getDataSize(), dstPointsPinId); + memcpy(dstPointsFrame->data(), dstPoints.data(), 4 * sizeof(cv::Point2f)); + + frame_container allFrames; + allFrames.insert(make_pair(imagePinId, imageFrame)); + allFrames.insert(make_pair(srcPointsPinId, srcPointsFrame)); + allFrames.insert(make_pair(dstPointsPinId, dstPointsFrame)); + combinedSource->send(allFrames); + + perspectiveTransform->step(); + + auto frames = sink->try_pop(); + BOOST_TEST(!frames.empty()); + + if (!frames.empty()) { + auto outputFrame = frames.cbegin()->second; + BOOST_TEST(outputFrame->getMetadata()->getFrameType() == FrameMetadata::RAW_IMAGE); + + auto rawMetadata = FrameMetadataFactory::downcast(outputFrame->getMetadata()); + BOOST_TEST(rawMetadata->getDepth() == CV_8U); + + Test_Utils::saveOrCompare("./data/PerspectiveTransform_outputs/perspectivetransform-dynamic_rgb_320x180.raw", const_cast(static_cast(outputFrame->data())), outputFrame->size(), 0); + } +} + +BOOST_AUTO_TEST_CASE(getsetprops_basic_to_dynamic) +{ + // Start with BASIC mode + std::vector initialSrcPoints = { + cv::Point2f(0, 0), + cv::Point2f(320, 0), + cv::Point2f(320, 180), + cv::Point2f(0, 180) + }; + std::vector initialDstPoints = { + cv::Point2f(50, 50), + cv::Point2f(270, 50), + cv::Point2f(270, 130), + cv::Point2f(50, 130) + }; + + auto perspectiveTransform = boost::shared_ptr(new TestPerspectiveTransform(PerspectiveTransformProps(initialSrcPoints, initialDstPoints))); + + // Set up a minimal pipeline to allow setProps to work + auto imageMetadata = framemetadata_sp(new RawImageMetadata(320, 180, ImageMetadata::RGB, CV_8UC3, 0, CV_8U, FrameMetadata::HOST, true)); + auto imageSource = boost::shared_ptr(new ExternalSourceModule()); + auto imagePinId = imageSource->addOutputPin(imageMetadata); + + auto sink = boost::shared_ptr(new ExternalSinkModule()); + imageSource->setNext(perspectiveTransform); + perspectiveTransform->setNext(sink); + + BOOST_TEST(imageSource->init()); + BOOST_TEST(perspectiveTransform->init()); + BOOST_TEST(sink->init()); + + // Test getProps returns correct initial values + auto currentProps = perspectiveTransform->getProps(); + BOOST_TEST(currentProps.mode == PerspectiveTransformProps::BASIC); + BOOST_TEST(currentProps.srcPoints.size() == 4); + BOOST_TEST(currentProps.dstPoints.size() == 4); + BOOST_TEST(currentProps.srcPoints[0].x == 0); + BOOST_TEST(currentProps.srcPoints[0].y == 0); + BOOST_TEST(currentProps.dstPoints[0].x == 50); + BOOST_TEST(currentProps.dstPoints[0].y == 50); + + // Test setProps functionality - create different points to verify change + std::vector newSrcPoints = { + cv::Point2f(10, 10), + cv::Point2f(310, 10), + cv::Point2f(310, 170), + cv::Point2f(10, 170) + }; + std::vector newDstPoints = { + cv::Point2f(20, 20), + cv::Point2f(300, 20), + cv::Point2f(300, 160), + cv::Point2f(20, 160) + }; + + PerspectiveTransformProps newProps(newSrcPoints, newDstPoints); + perspectiveTransform->setProps(newProps); +} + +BOOST_AUTO_TEST_CASE(pins_override_static_props) +{ + // In DYNAMIC mode, pin inputs control the transformation rather than any constructor parameters (showing pins take precedence) + std::vector unusedStaticSrcPoints = { + cv::Point2f(0, 0), + cv::Point2f(320, 0), + cv::Point2f(320, 180), + cv::Point2f(0, 180) + }; + std::vector unusedStaticDstPoints = { + cv::Point2f(50, 50), // These static points are ignored in DYNAMIC mode + cv::Point2f(270, 50), + cv::Point2f(270, 130), + cv::Point2f(50, 130) + }; + + auto fileReader = boost::shared_ptr(new FileReaderModule(FileReaderModuleProps("./data/RGB_320x180.raw"))); + auto imageMetadata = framemetadata_sp(new RawImageMetadata(320, 180, ImageMetadata::RGB, CV_8UC3, 0, CV_8U, FrameMetadata::HOST, true)); + fileReader->addOutputPin(imageMetadata); + + auto tempSink = boost::shared_ptr(new ExternalSinkModule()); + fileReader->setNext(tempSink); + + BOOST_TEST(fileReader->init()); + BOOST_TEST(tempSink->init()); + + fileReader->step(); + auto imageFrames = tempSink->pop(); + BOOST_TEST(imageFrames.size() == 1); + auto imageData = imageFrames.cbegin()->second; + + // Create source with dynamic pin inputs that should override static props + auto combinedSource = boost::shared_ptr(new ExternalSourceModule()); + auto imagePinId = combinedSource->addOutputPin(imageMetadata); + + auto srcPointsMetadata = framemetadata_sp(new ArrayMetadata()); + FrameMetadataFactory::downcast(srcPointsMetadata)->setData(4, CV_32FC2, sizeof(cv::Point2f)); + auto srcPointsPinId = combinedSource->addOutputPin(srcPointsMetadata); + + auto dstPointsMetadata = framemetadata_sp(new ArrayMetadata()); + FrameMetadataFactory::downcast(dstPointsMetadata)->setData(4, CV_32FC2, sizeof(cv::Point2f)); + auto dstPointsPinId = combinedSource->addOutputPin(dstPointsMetadata); + + // Create module directly in DYNAMIC mode to enable pin inputs + auto perspectiveTransform = boost::shared_ptr(new TestPerspectiveTransform(PerspectiveTransformProps(PerspectiveTransformProps::DYNAMIC))); + + auto sink = boost::shared_ptr(new ExternalSinkModule()); + combinedSource->setNext(perspectiveTransform); + perspectiveTransform->setNext(sink); + + BOOST_TEST(combinedSource->init()); + BOOST_TEST(perspectiveTransform->init()); + BOOST_TEST(sink->init()); + + auto imageFrame = combinedSource->makeFrame(imageMetadata->getDataSize(), imagePinId); + memcpy(imageFrame->data(), imageData->data(), imageMetadata->getDataSize()); + + // Pin input points - these control the transformation in DYNAMIC mode + std::vector dynamicSrcPoints = { + cv::Point2f(0, 0), + cv::Point2f(320, 0), + cv::Point2f(320, 180), + cv::Point2f(0, 180) + }; + std::vector dynamicDstPoints = { + cv::Point2f(20, 30), + cv::Point2f(300, 10), + cv::Point2f(310, 170), + cv::Point2f(10, 160) + }; + + auto srcPointsFrame = combinedSource->makeFrame(srcPointsMetadata->getDataSize(), srcPointsPinId); + memcpy(srcPointsFrame->data(), dynamicSrcPoints.data(), 4 * sizeof(cv::Point2f)); + + auto dstPointsFrame = combinedSource->makeFrame(dstPointsMetadata->getDataSize(), dstPointsPinId); + memcpy(dstPointsFrame->data(), dynamicDstPoints.data(), 4 * sizeof(cv::Point2f)); + + frame_container allFrames; + allFrames.insert(make_pair(imagePinId, imageFrame)); + allFrames.insert(make_pair(srcPointsPinId, srcPointsFrame)); + allFrames.insert(make_pair(dstPointsPinId, dstPointsFrame)); + combinedSource->send(allFrames); + + perspectiveTransform->step(); + + auto frames = sink->try_pop(); + BOOST_TEST(!frames.empty()); + + if (!frames.empty()) { + auto outputFrame = frames.cbegin()->second; + BOOST_TEST(outputFrame->getMetadata()->getFrameType() == FrameMetadata::RAW_IMAGE); + + auto rawMetadata = FrameMetadataFactory::downcast(outputFrame->getMetadata()); + BOOST_TEST(rawMetadata->getDepth() == CV_8U); + + Test_Utils::saveOrCompare("./data/PerspectiveTransform_outputs/perspectivetransform-pins_override_static.raw", const_cast(static_cast(outputFrame->data())), outputFrame->size(), 0); + } +} + +BOOST_AUTO_TEST_SUITE_END() \ No newline at end of file diff --git a/data/PerspectiveTransform_outputs/perspectivetransform-bgr_1080x720_transform.raw b/data/PerspectiveTransform_outputs/perspectivetransform-bgr_1080x720_transform.raw new file mode 100644 index 000000000..741f35e91 Binary files /dev/null and b/data/PerspectiveTransform_outputs/perspectivetransform-bgr_1080x720_transform.raw differ diff --git a/data/PerspectiveTransform_outputs/perspectivetransform-dynamic_rgb_320x180.raw b/data/PerspectiveTransform_outputs/perspectivetransform-dynamic_rgb_320x180.raw new file mode 100644 index 000000000..01f4be7d8 Binary files /dev/null and b/data/PerspectiveTransform_outputs/perspectivetransform-dynamic_rgb_320x180.raw differ diff --git a/data/PerspectiveTransform_outputs/perspectivetransform-mono_1920x960_transform.raw b/data/PerspectiveTransform_outputs/perspectivetransform-mono_1920x960_transform.raw new file mode 100644 index 000000000..3c0ff1ed1 Binary files /dev/null and b/data/PerspectiveTransform_outputs/perspectivetransform-mono_1920x960_transform.raw differ diff --git a/data/PerspectiveTransform_outputs/perspectivetransform-pins_override_static.raw b/data/PerspectiveTransform_outputs/perspectivetransform-pins_override_static.raw new file mode 100644 index 000000000..01f4be7d8 Binary files /dev/null and b/data/PerspectiveTransform_outputs/perspectivetransform-pins_override_static.raw differ diff --git a/data/PerspectiveTransform_outputs/perspectivetransform-rgb_320x180_transform.raw b/data/PerspectiveTransform_outputs/perspectivetransform-rgb_320x180_transform.raw new file mode 100644 index 000000000..01f4be7d8 Binary files /dev/null and b/data/PerspectiveTransform_outputs/perspectivetransform-rgb_320x180_transform.raw differ