mirror of
https://github.com/OPM/ResInsight.git
synced 2025-02-25 18:55:39 -06:00
Add helper class for polygon and 2D binary image operations
This commit is contained in:
parent
62bb40c4f4
commit
882e64f790
@ -21,7 +21,7 @@
|
||||
#include "Polygons/RimPolygon.h"
|
||||
#include "Polygons/RimPolygonInView.h"
|
||||
|
||||
#include "RigCellGeometryTools.h"
|
||||
#include "RigPolygonTools.h"
|
||||
|
||||
#include "cafSelectionManager.h"
|
||||
|
||||
@ -46,21 +46,47 @@ void RicSimplifyPolygonFeature::onActionTriggered( bool isChecked )
|
||||
auto selPolygons = selectedPolygons();
|
||||
if ( selPolygons.empty() ) return;
|
||||
|
||||
const double defaultEpsilon = 10.0;
|
||||
|
||||
bool ok;
|
||||
auto epsilon =
|
||||
QInputDialog::getDouble( nullptr, "Simplify Polygon Threshold", "Threshold:", defaultEpsilon, 1.0, 1000.0, 1, &ok, Qt::WindowFlags(), 1 );
|
||||
|
||||
if ( !ok ) return;
|
||||
std::vector<std::vector<cvf::Vec3d>> originalCoords;
|
||||
|
||||
for ( auto sourcePolygon : selPolygons )
|
||||
{
|
||||
auto coords = sourcePolygon->pointsInDomainCoords();
|
||||
RigCellGeometryTools::simplifyPolygon( &coords, epsilon );
|
||||
originalCoords.push_back( sourcePolygon->pointsInDomainCoords() );
|
||||
}
|
||||
|
||||
sourcePolygon->setPointsInDomainCoords( coords );
|
||||
sourcePolygon->coordinatesChanged.send();
|
||||
const int defaultEpsilon = 10;
|
||||
|
||||
QInputDialog inputDialog;
|
||||
inputDialog.setWindowTitle( "Simplify Polygon" );
|
||||
inputDialog.setLabelText( "Threshold (larger value removes more points) :" );
|
||||
inputDialog.setInputMode( QInputDialog::IntInput );
|
||||
inputDialog.setIntRange( 10, 200 );
|
||||
inputDialog.setIntValue( defaultEpsilon );
|
||||
|
||||
connect( &inputDialog,
|
||||
&QInputDialog::intValueChanged,
|
||||
[&originalCoords, &selPolygons]( int value )
|
||||
{
|
||||
for ( size_t i = 0; i < originalCoords.size(); i++ )
|
||||
{
|
||||
auto coords = originalCoords[i];
|
||||
RigPolygonTools::simplifyPolygon( coords, value );
|
||||
|
||||
auto sourcePolygon = selPolygons[i];
|
||||
sourcePolygon->setPointsInDomainCoords( coords );
|
||||
sourcePolygon->coordinatesChanged.send();
|
||||
}
|
||||
} );
|
||||
|
||||
if ( inputDialog.exec() == QDialog::Rejected )
|
||||
{
|
||||
for ( size_t i = 0; i < originalCoords.size(); i++ )
|
||||
{
|
||||
auto coords = originalCoords[i];
|
||||
|
||||
auto sourcePolygon = selPolygons[i];
|
||||
sourcePolygon->setPointsInDomainCoords( coords );
|
||||
sourcePolygon->coordinatesChanged.send();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -89,6 +89,7 @@ set(SOURCE_GROUP_HEADER_FILES
|
||||
${CMAKE_CURRENT_LIST_DIR}/RigEclipseContourMapProjection.h
|
||||
${CMAKE_CURRENT_LIST_DIR}/RigGeoMechContourMapProjection.h
|
||||
${CMAKE_CURRENT_LIST_DIR}/RigStatisticsContourMapProjection.h
|
||||
${CMAKE_CURRENT_LIST_DIR}/RigPolygonTools.h
|
||||
)
|
||||
|
||||
set(SOURCE_GROUP_SOURCE_FILES
|
||||
@ -180,6 +181,7 @@ set(SOURCE_GROUP_SOURCE_FILES
|
||||
${CMAKE_CURRENT_LIST_DIR}/RigEclipseContourMapProjection.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/RigGeoMechContourMapProjection.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/RigStatisticsContourMapProjection.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/RigPolygonTools.cpp
|
||||
)
|
||||
|
||||
list(APPEND CODE_HEADER_FILES ${SOURCE_GROUP_HEADER_FILES})
|
||||
|
@ -233,50 +233,6 @@ void RigCellGeometryTools::createPolygonFromLineSegments( std::list<std::pair<cv
|
||||
}
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// Ramer-Douglas-Peucker simplification algorithm
|
||||
///
|
||||
/// https://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
void RigCellGeometryTools::simplifyPolygon( std::vector<cvf::Vec3d>* vertices, double epsilon )
|
||||
{
|
||||
CVF_ASSERT( vertices );
|
||||
if ( vertices->size() < 3 ) return;
|
||||
|
||||
std::pair<size_t, double> maxDistPoint( 0u, 0.0 );
|
||||
|
||||
for ( size_t i = 1; i < vertices->size() - 1; ++i )
|
||||
{
|
||||
cvf::Vec3d v = vertices->at( i );
|
||||
double u;
|
||||
cvf::Vec3d v_proj = cvf::GeometryTools::projectPointOnLine( vertices->front(), vertices->back(), v, &u );
|
||||
double distance = ( v_proj - v ).length();
|
||||
if ( distance > maxDistPoint.second )
|
||||
{
|
||||
maxDistPoint = std::make_pair( i, distance );
|
||||
}
|
||||
}
|
||||
|
||||
if ( maxDistPoint.second > epsilon )
|
||||
{
|
||||
std::vector<cvf::Vec3d> newVertices1( vertices->begin(), vertices->begin() + maxDistPoint.first + 1 );
|
||||
std::vector<cvf::Vec3d> newVertices2( vertices->begin() + maxDistPoint.first, vertices->end() );
|
||||
|
||||
// Recurse
|
||||
simplifyPolygon( &newVertices1, epsilon );
|
||||
simplifyPolygon( &newVertices2, epsilon );
|
||||
|
||||
std::vector<cvf::Vec3d> newVertices( newVertices1.begin(), newVertices1.end() - 1 );
|
||||
newVertices.insert( newVertices.end(), newVertices2.begin(), newVertices2.end() );
|
||||
*vertices = newVertices;
|
||||
}
|
||||
else
|
||||
{
|
||||
std::vector<cvf::Vec3d> newVertices = { vertices->front(), vertices->back() };
|
||||
*vertices = newVertices;
|
||||
}
|
||||
}
|
||||
|
||||
//==================================================================================================
|
||||
///
|
||||
//==================================================================================================
|
||||
|
@ -39,7 +39,6 @@ public:
|
||||
static void createPolygonFromLineSegments( std::list<std::pair<cvf::Vec3d, cvf::Vec3d>>& intersectionLineSegments,
|
||||
std::vector<std::vector<cvf::Vec3d>>& polygons,
|
||||
double tolerance = 1.0e-4 );
|
||||
static void simplifyPolygon( std::vector<cvf::Vec3d>* vertices, double epsilon );
|
||||
|
||||
static void findCellLocalXYZ( const std::array<cvf::Vec3d, 8>& hexCorners,
|
||||
cvf::Vec3d& localXdirection,
|
||||
|
@ -48,7 +48,6 @@ public:
|
||||
cvf::Vec2ui numberOfVerticesIJ() const;
|
||||
|
||||
cvf::uint numberOfCells() const;
|
||||
cvf::uint numberOfValidCells() const;
|
||||
size_t numberOfVertices() const;
|
||||
|
||||
cvf::Vec3d origin3d() const;
|
||||
|
@ -121,6 +121,20 @@ double RigContourMapProjection::valueAtVertex( unsigned int i, unsigned int j )
|
||||
return std::numeric_limits<double>::infinity();
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
double RigContourMapProjection::filteredValueAtVertex( unsigned int i, unsigned int j ) const
|
||||
{
|
||||
size_t index = m_contourMapGrid.vertexIndexFromIJ( i, j );
|
||||
if ( index < numberOfVertices() )
|
||||
{
|
||||
auto values = aggregatedVertexResultsFiltered();
|
||||
return values.at( index );
|
||||
}
|
||||
return std::numeric_limits<double>::infinity();
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
@ -187,6 +201,14 @@ cvf::Vec3d RigContourMapProjection::origin3d() const
|
||||
return m_contourMapGrid.expandedBoundingBox().min();
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
double RigContourMapProjection::topDepthBoundingBox() const
|
||||
{
|
||||
return m_contourMapGrid.expandedBoundingBox().max().z();
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
@ -440,9 +462,9 @@ const std::vector<double>& RigContourMapProjection::aggregatedResults() const
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
std::vector<double> RigContourMapProjection::aggregatedVertexResultsFiltered() const
|
||||
{
|
||||
std::vector<double> filteredResults = m_aggregatedVertexResults;
|
||||
if ( m_valueFilter )
|
||||
{
|
||||
std::vector<double> filteredResults = m_aggregatedVertexResults;
|
||||
std::transform( filteredResults.begin(),
|
||||
filteredResults.end(),
|
||||
filteredResults.begin(),
|
||||
@ -450,9 +472,10 @@ std::vector<double> RigContourMapProjection::aggregatedVertexResultsFiltered() c
|
||||
return ( value < m_valueFilter->first || value > m_valueFilter->second ) ? std::numeric_limits<double>::infinity()
|
||||
: value;
|
||||
} );
|
||||
return filteredResults;
|
||||
}
|
||||
|
||||
return filteredResults;
|
||||
return m_aggregatedVertexResults;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
|
@ -59,6 +59,7 @@ public:
|
||||
cvf::Vec2ui numberOfVerticesIJ() const;
|
||||
|
||||
double valueAtVertex( unsigned int i, unsigned int j ) const;
|
||||
double filteredValueAtVertex( unsigned int i, unsigned int j ) const;
|
||||
|
||||
unsigned int numberOfCells() const;
|
||||
unsigned int numberOfValidCells() const;
|
||||
@ -66,6 +67,7 @@ public:
|
||||
|
||||
bool checkForMapIntersection( const cvf::Vec3d& domainPoint3d, cvf::Vec2d* contourMapPoint, double* valueAtPoint ) const;
|
||||
cvf::Vec3d origin3d() const;
|
||||
double topDepthBoundingBox() const;
|
||||
|
||||
std::vector<double> xVertexPositions() const;
|
||||
std::vector<double> yVertexPositions() const;
|
||||
|
@ -23,6 +23,7 @@
|
||||
#include "RigCellGeometryTools.h"
|
||||
#include "RigContourMapGrid.h"
|
||||
#include "RigContourMapProjection.h"
|
||||
#include "RigPolygonTools.h"
|
||||
|
||||
#include "cvfGeometryUtils.h"
|
||||
|
||||
@ -323,7 +324,7 @@ std::pair<std::vector<RigContourMapTrianglesGenerator::ContourPolygons>, std::ve
|
||||
|
||||
for ( RigContourPolygonsTools::ContourPolygon& polygon : contourPolygons[i] )
|
||||
{
|
||||
RigCellGeometryTools::simplifyPolygon( &polygon.vertices, simplifyEpsilon );
|
||||
RigPolygonTools::simplifyPolygon( polygon.vertices, simplifyEpsilon );
|
||||
}
|
||||
}
|
||||
|
||||
|
327
ApplicationLibCode/ReservoirDataModel/RigPolygonTools.cpp
Normal file
327
ApplicationLibCode/ReservoirDataModel/RigPolygonTools.cpp
Normal file
@ -0,0 +1,327 @@
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Copyright (C) 2025 Equinor ASA
|
||||
//
|
||||
// ResInsight is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
// FITNESS FOR A PARTICULAR PURPOSE.
|
||||
//
|
||||
// See the GNU General Public License at <http://www.gnu.org/licenses/gpl.html>
|
||||
// for more details.
|
||||
//
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include "RigPolygonTools.h"
|
||||
|
||||
#include "cvfGeometryTools.h"
|
||||
|
||||
#include <optional>
|
||||
#include <stack>
|
||||
#include <utility>
|
||||
|
||||
namespace RigPolygonTools
|
||||
{
|
||||
namespace internal
|
||||
{
|
||||
// Function to check if a point is valid and within bounds
|
||||
bool isValid( int x, int y, int rows, int cols, const IntegerImage& image, const IntegerImage& visited )
|
||||
{
|
||||
return x >= 0 && x < rows && y >= 0 && y < cols && image[x][y] == 1 && !visited[x][y];
|
||||
}
|
||||
|
||||
bool isValidImage( const IntegerImage& image )
|
||||
{
|
||||
if ( image.empty() ) return false;
|
||||
auto rowSize = image[0].size();
|
||||
for ( const auto& row : image )
|
||||
{
|
||||
if ( row.size() != rowSize ) return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
void floodFill( IntegerImage& image, int x, int y, int oldColor, int newColor )
|
||||
{
|
||||
if ( !isValidImage( image ) ) return;
|
||||
|
||||
auto rows = static_cast<int>( image.size() );
|
||||
auto cols = static_cast<int>( image[0].size() );
|
||||
std::stack<std::pair<int, int>> stack;
|
||||
stack.push( { x, y } );
|
||||
|
||||
while ( !stack.empty() )
|
||||
{
|
||||
auto [cx, cy] = stack.top();
|
||||
stack.pop();
|
||||
|
||||
if ( cx < 0 || cy < 0 || cx >= rows || cy >= cols || image[cx][cy] != oldColor ) continue;
|
||||
|
||||
image[cx][cy] = newColor;
|
||||
|
||||
stack.push( { cx + 1, cy } );
|
||||
stack.push( { cx - 1, cy } );
|
||||
stack.push( { cx, cy + 1 } );
|
||||
stack.push( { cx, cy - 1 } );
|
||||
}
|
||||
}
|
||||
|
||||
}; // namespace internal
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
IntegerImage erode( IntegerImage image, int kernelSize )
|
||||
{
|
||||
if ( !internal::isValidImage( image ) ) return {};
|
||||
if ( kernelSize <= 0 ) return {};
|
||||
|
||||
auto rows = static_cast<int>( image.size() );
|
||||
auto cols = static_cast<int>( image[0].size() );
|
||||
int offset = kernelSize / 2;
|
||||
IntegerImage eroded( rows, std::vector<int>( cols, 0 ) );
|
||||
|
||||
for ( int i = offset; i < rows - offset; ++i )
|
||||
{
|
||||
for ( int j = offset; j < cols - offset; ++j )
|
||||
{
|
||||
bool erodePixel = true;
|
||||
for ( int ki = -offset; ki <= offset; ++ki )
|
||||
{
|
||||
for ( int kj = -offset; kj <= offset; ++kj )
|
||||
{
|
||||
if ( image[i + ki][j + kj] == 0 )
|
||||
{
|
||||
erodePixel = false;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ( !erodePixel ) break;
|
||||
}
|
||||
eroded[i][j] = erodePixel ? 1 : 0;
|
||||
}
|
||||
}
|
||||
|
||||
return eroded;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
IntegerImage dilate( IntegerImage image, int kernelSize )
|
||||
{
|
||||
if ( !internal::isValidImage( image ) ) return {};
|
||||
if ( kernelSize <= 0 ) return {};
|
||||
|
||||
auto rows = static_cast<int>( image.size() );
|
||||
auto cols = static_cast<int>( image[0].size() );
|
||||
int offset = kernelSize / 2;
|
||||
IntegerImage dilated( rows, std::vector<int>( cols, 0 ) );
|
||||
|
||||
for ( int i = offset; i < rows - offset; ++i )
|
||||
{
|
||||
for ( int j = offset; j < cols - offset; ++j )
|
||||
{
|
||||
bool dilatePixel = false;
|
||||
for ( int ki = -offset; ki <= offset; ++ki )
|
||||
{
|
||||
for ( int kj = -offset; kj <= offset; ++kj )
|
||||
{
|
||||
if ( image[i + ki][j + kj] == 1 )
|
||||
{
|
||||
dilatePixel = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ( dilatePixel ) break;
|
||||
}
|
||||
dilated[i][j] = dilatePixel ? 1 : 0;
|
||||
}
|
||||
}
|
||||
|
||||
return dilated;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
IntegerImage fillInterior( IntegerImage sourceImage )
|
||||
{
|
||||
if ( !internal::isValidImage( sourceImage ) ) return {};
|
||||
|
||||
auto image = sourceImage;
|
||||
auto rows = static_cast<int>( image.size() );
|
||||
auto cols = static_cast<int>( image[0].size() );
|
||||
|
||||
// Flood fill the exterior (starting from the borders)
|
||||
for ( int i = 0; i < rows; ++i )
|
||||
{
|
||||
if ( image[i][0] == 0 ) internal::floodFill( image, i, 0, 0, -1 );
|
||||
if ( image[i][cols - 1] == 0 ) internal::floodFill( image, i, cols - 1, 0, -1 );
|
||||
}
|
||||
for ( int j = 0; j < cols; ++j )
|
||||
{
|
||||
if ( image[0][j] == 0 ) internal::floodFill( image, 0, j, 0, -1 );
|
||||
if ( image[rows - 1][j] == 0 ) internal::floodFill( image, rows - 1, j, 0, -1 );
|
||||
}
|
||||
|
||||
// Fill interior holes (remaining 0s)
|
||||
for ( int i = 0; i < rows; ++i )
|
||||
{
|
||||
for ( int j = 0; j < cols; ++j )
|
||||
{
|
||||
if ( image[i][j] == 0 ) image[i][j] = 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Restore the exterior
|
||||
for ( int i = 0; i < rows; ++i )
|
||||
{
|
||||
for ( int j = 0; j < cols; ++j )
|
||||
{
|
||||
if ( image[i][j] == -1 ) image[i][j] = 0;
|
||||
}
|
||||
}
|
||||
|
||||
return image;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
///
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
std::vector<std::pair<int, int>> boundary( const IntegerImage& image )
|
||||
{
|
||||
if ( !internal::isValidImage( image ) ) return {};
|
||||
|
||||
std::vector<std::pair<int, int>> boundaries;
|
||||
|
||||
// Get dimensions of the image
|
||||
int rows = static_cast<int>( image.size() );
|
||||
int cols = static_cast<int>( image[0].size() );
|
||||
|
||||
// Direction vectors for clockwise search (8-connectivity)
|
||||
const std::vector<std::pair<int, int>> directions = { { -1, 0 }, { -1, 1 }, { 0, 1 }, { 1, 1 }, { 1, 0 }, { 1, -1 }, { 0, -1 }, { -1, -1 } };
|
||||
|
||||
// Helper lambda to check if a pixel is a valid boundary pixel
|
||||
auto isBoundaryPixel = [&]( int x, int y )
|
||||
{
|
||||
if ( x < 0 || x >= rows || y < 0 || y >= cols || image[x][y] == 0 ) return false;
|
||||
// Check if it's adjacent to a background pixel
|
||||
for ( const auto& [dx, dy] : directions )
|
||||
{
|
||||
int nx = x + dx, ny = y + dy;
|
||||
if ( nx < 0 || nx >= rows || ny < 0 || ny >= cols || image[nx][ny] == 0 )
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
||||
// Find the starting boundary pixel
|
||||
std::pair<int, int> start( -1, -1 );
|
||||
for ( int row = 0; row < rows; ++row )
|
||||
{
|
||||
for ( int col = 0; col < cols; ++col )
|
||||
{
|
||||
if ( isBoundaryPixel( row, col ) )
|
||||
{
|
||||
start = { row, col };
|
||||
break;
|
||||
}
|
||||
}
|
||||
if ( start.first != -1 ) break;
|
||||
}
|
||||
|
||||
if ( start.first == -1 ) return boundaries; // No boundary found
|
||||
|
||||
// Contour following algorithm
|
||||
std::pair<int, int> current = start;
|
||||
int direction = 0; // Start search direction (arbitrary)
|
||||
do
|
||||
{
|
||||
boundaries.push_back( current );
|
||||
bool foundNext = false;
|
||||
|
||||
// Look for the next boundary pixel in a clockwise direction
|
||||
for ( int i = 0; i < 8; ++i )
|
||||
{
|
||||
int newDir = ( direction + i ) % 8;
|
||||
int nx = current.first + directions[newDir].first;
|
||||
int ny = current.second + directions[newDir].second;
|
||||
|
||||
if ( isBoundaryPixel( nx, ny ) )
|
||||
{
|
||||
current = { nx, ny };
|
||||
direction = ( newDir + 6 ) % 8; // Adjust direction for next search
|
||||
foundNext = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If no next pixel is found, the boundary is invalid or incomplete
|
||||
if ( !foundNext ) break;
|
||||
|
||||
} while ( current != start ); // Stop when we loop back to the start
|
||||
|
||||
return boundaries;
|
||||
}
|
||||
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
/// Ramer-Douglas-Peucker simplification algorithm
|
||||
///
|
||||
/// https://en.wikipedia.org/wiki/Ramer%E2%80%93Douglas%E2%80%93Peucker_algorithm
|
||||
//--------------------------------------------------------------------------------------------------
|
||||
void simplifyPolygon( std::vector<cvf::Vec3d>& vertices, double epsilon )
|
||||
{
|
||||
// If the polygon has fewer than 3 vertices, it cannot be simplified.
|
||||
if ( vertices.size() < 3 ) return;
|
||||
|
||||
// Find the point with the maximum perpendicular distance from the line connecting the endpoints.
|
||||
std::optional<std::pair<size_t, double>> maxDistPoint;
|
||||
|
||||
for ( size_t i = 1; i < vertices.size() - 1; ++i )
|
||||
{
|
||||
const cvf::Vec3d& point = vertices[i];
|
||||
cvf::Vec3d projected = cvf::GeometryTools::projectPointOnLine( vertices.front(), vertices.back(), point );
|
||||
double distance = ( projected - point ).length();
|
||||
|
||||
if ( !maxDistPoint || distance > maxDistPoint->second )
|
||||
{
|
||||
maxDistPoint = std::make_pair( i, distance );
|
||||
}
|
||||
}
|
||||
|
||||
// If the maximum distance exceeds epsilon, split and simplify recursively.
|
||||
if ( maxDistPoint && maxDistPoint->second > epsilon )
|
||||
{
|
||||
size_t splitIndex = maxDistPoint->first;
|
||||
|
||||
// Divide the vertices into two segments.
|
||||
std::vector<cvf::Vec3d> segment1( vertices.begin(), vertices.begin() + splitIndex + 1 );
|
||||
std::vector<cvf::Vec3d> segment2( vertices.begin() + splitIndex, vertices.end() );
|
||||
|
||||
// Recursively simplify both segments.
|
||||
simplifyPolygon( segment1, epsilon );
|
||||
simplifyPolygon( segment2, epsilon );
|
||||
|
||||
// Combine the simplified segments, avoiding duplication at the split point.
|
||||
vertices = std::move( segment1 );
|
||||
vertices.pop_back(); // Remove duplicate at the split point.
|
||||
vertices.insert( vertices.end(), segment2.begin(), segment2.end() );
|
||||
}
|
||||
else
|
||||
{
|
||||
// If no point exceeds the threshold, reduce to endpoints.
|
||||
vertices = { vertices.front(), vertices.back() };
|
||||
}
|
||||
}
|
||||
} // namespace RigPolygonTools
|
39
ApplicationLibCode/ReservoirDataModel/RigPolygonTools.h
Normal file
39
ApplicationLibCode/ReservoirDataModel/RigPolygonTools.h
Normal file
@ -0,0 +1,39 @@
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Copyright (C) 2025 Equinor ASA
|
||||
//
|
||||
// ResInsight is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
// FITNESS FOR A PARTICULAR PURPOSE.
|
||||
//
|
||||
// See the GNU General Public License at <http://www.gnu.org/licenses/gpl.html>
|
||||
// for more details.
|
||||
//
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#pragma once
|
||||
|
||||
#include "cvfVector3.h"
|
||||
|
||||
#include <vector>
|
||||
|
||||
namespace RigPolygonTools
|
||||
{
|
||||
// Integer images are assumed to be 2D arrays of integers, where 0 is background and 1 is foreground.
|
||||
using IntegerImage = std::vector<std::vector<int>>;
|
||||
|
||||
IntegerImage erode( IntegerImage image, int kernelSize );
|
||||
IntegerImage dilate( IntegerImage image, int kernelSize );
|
||||
IntegerImage fillInterior( IntegerImage sourceImage );
|
||||
|
||||
std::vector<std::pair<int, int>> boundary( const IntegerImage& image );
|
||||
|
||||
// Recursive function modifying the incoming vertices
|
||||
void simplifyPolygon( std::vector<cvf::Vec3d>& vertices, double epsilon );
|
||||
|
||||
} // namespace RigPolygonTools
|
@ -106,6 +106,7 @@ set(SOURCE_UNITTEST_FILES
|
||||
${CMAKE_CURRENT_LIST_DIR}/RifOsduWellPathReader-Test.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/RigVfpTables-Test.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/RiaResultName-Test.cpp
|
||||
${CMAKE_CURRENT_LIST_DIR}/RigPolygonTools-Test.cpp
|
||||
)
|
||||
|
||||
if(RESINSIGHT_ENABLE_GRPC)
|
||||
|
173
ApplicationLibCode/UnitTests/RigPolygonTools-Test.cpp
Normal file
173
ApplicationLibCode/UnitTests/RigPolygonTools-Test.cpp
Normal file
@ -0,0 +1,173 @@
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
//
|
||||
// Copyright (C) 2020- Equinor ASA
|
||||
//
|
||||
// ResInsight is free software: you can redistribute it and/or modify
|
||||
// it under the terms of the GNU General Public License as published by
|
||||
// the Free Software Foundation, either version 3 of the License, or
|
||||
// (at your option) any later version.
|
||||
//
|
||||
// ResInsight is distributed in the hope that it will be useful, but WITHOUT ANY
|
||||
// WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
// FITNESS FOR A PARTICULAR PURPOSE.
|
||||
//
|
||||
// See the GNU General Public License at <http://www.gnu.org/licenses/gpl.html>
|
||||
// for more details.
|
||||
//
|
||||
/////////////////////////////////////////////////////////////////////////////////
|
||||
|
||||
#include "RigPolygonTools.h"
|
||||
#include "gtest/gtest.h"
|
||||
|
||||
// Test for erode function
|
||||
TEST( RigPolygonToolsTest, ErodeTest )
|
||||
{
|
||||
// Arrange
|
||||
RigPolygonTools::IntegerImage image = { { 1, 1, 1 }, { 1, 1, 1 }, { 1, 1, 1 } };
|
||||
int kernelSize = 1;
|
||||
RigPolygonTools::IntegerImage expected = { { 1, 1, 1 }, { 1, 1, 1 }, { 1, 1, 1 } };
|
||||
|
||||
// Act
|
||||
RigPolygonTools::IntegerImage result = RigPolygonTools::erode( image, kernelSize );
|
||||
|
||||
// Assert
|
||||
EXPECT_EQ( result, expected );
|
||||
}
|
||||
|
||||
// Test for erode function with invalid input
|
||||
TEST( RigPolygonToolsTest, ErodeInvalidInputTest )
|
||||
{
|
||||
// Arrange
|
||||
RigPolygonTools::IntegerImage emptyImage = {};
|
||||
int negativeKernelSize = -1;
|
||||
|
||||
// Act & Assert
|
||||
EXPECT_EQ( emptyImage, RigPolygonTools::erode( emptyImage, 1 ) );
|
||||
EXPECT_EQ( emptyImage, RigPolygonTools::erode( { { 1, 1 }, { 1 } }, 1 ) ); // Inconsistent row sizes
|
||||
EXPECT_EQ( emptyImage, RigPolygonTools::erode( { { 1, 1 }, { 1, 1 } }, negativeKernelSize ) );
|
||||
}
|
||||
|
||||
// Test for dilate function
|
||||
TEST( RigPolygonToolsTest, DilateTest )
|
||||
{
|
||||
// Arrange
|
||||
RigPolygonTools::IntegerImage image = {
|
||||
{ 0, 0, 0, 0, 0 },
|
||||
{ 0, 0, 0, 0, 0 },
|
||||
{ 0, 0, 1, 0, 0 },
|
||||
{ 0, 0, 0, 0, 0 },
|
||||
{ 0, 0, 0, 0, 0 },
|
||||
};
|
||||
int kernelSize = 2;
|
||||
|
||||
RigPolygonTools::IntegerImage expected = {
|
||||
{ 0, 0, 0, 0, 0 },
|
||||
{ 0, 1, 1, 1, 0 },
|
||||
{ 0, 1, 1, 1, 0 },
|
||||
{ 0, 1, 1, 1, 0 },
|
||||
{ 0, 0, 0, 0, 0 },
|
||||
};
|
||||
|
||||
// Act
|
||||
RigPolygonTools::IntegerImage result = RigPolygonTools::dilate( image, kernelSize );
|
||||
|
||||
// Assert
|
||||
EXPECT_EQ( result, expected );
|
||||
}
|
||||
|
||||
// Test for dilate function with invalid input
|
||||
TEST( RigPolygonToolsTest, DilateInvalidInputTest )
|
||||
{
|
||||
// Arrange
|
||||
RigPolygonTools::IntegerImage emptyImage = {};
|
||||
int negativeKernelSize = -1;
|
||||
|
||||
// Act & Assert
|
||||
EXPECT_EQ( emptyImage, RigPolygonTools::dilate( emptyImage, 1 ) );
|
||||
EXPECT_EQ( emptyImage, RigPolygonTools::dilate( { { 1, 1 }, { 1 } }, 1 ) ); // Inconsistent row sizes
|
||||
EXPECT_EQ( emptyImage, RigPolygonTools::dilate( { { 1, 1 }, { 1, 1 } }, negativeKernelSize ) );
|
||||
}
|
||||
|
||||
// Test for fillInterior function
|
||||
TEST( RigPolygonToolsTest, FillInteriorTest )
|
||||
{
|
||||
// Arrange
|
||||
RigPolygonTools::IntegerImage image = { { 1, 1, 1 }, { 1, 0, 1 }, { 1, 1, 1 } };
|
||||
RigPolygonTools::IntegerImage expected = { { 1, 1, 1 }, { 1, 1, 1 }, { 1, 1, 1 } };
|
||||
|
||||
// Act
|
||||
RigPolygonTools::IntegerImage result = RigPolygonTools::fillInterior( image );
|
||||
|
||||
// Assert
|
||||
EXPECT_EQ( result, expected );
|
||||
}
|
||||
|
||||
// Test for fillInterior function with invalid input
|
||||
TEST( RigPolygonToolsTest, FillInteriorInvalidInputTest )
|
||||
{
|
||||
// Arrange
|
||||
RigPolygonTools::IntegerImage emptyImage = {};
|
||||
|
||||
// Act & Assert
|
||||
EXPECT_EQ( emptyImage, RigPolygonTools::fillInterior( emptyImage ) );
|
||||
EXPECT_EQ( emptyImage, RigPolygonTools::fillInterior( { { 1, 1 }, { 1 } } ) ); // Inconsistent row sizes
|
||||
}
|
||||
|
||||
// Test for boundary function
|
||||
TEST( RigPolygonToolsTest, BoundaryTest )
|
||||
{
|
||||
// Arrange
|
||||
RigPolygonTools::IntegerImage image = { { 1, 1, 1 }, { 1, 0, 1 }, { 1, 1, 1 } };
|
||||
std::vector<std::pair<int, int>> expected = { { 0, 0 }, { 0, 1 }, { 0, 2 }, { 1, 2 }, { 2, 2 }, { 2, 1 }, { 2, 0 }, { 1, 0 } };
|
||||
|
||||
// Act
|
||||
std::vector<std::pair<int, int>> result = RigPolygonTools::boundary( image );
|
||||
|
||||
// Assert
|
||||
EXPECT_EQ( result, expected );
|
||||
}
|
||||
|
||||
// Test for boundary function with invalid input
|
||||
TEST( RigPolygonToolsTest, BoundaryInvalidInputTest )
|
||||
{
|
||||
// Arrange
|
||||
RigPolygonTools::IntegerImage emptyImage = {};
|
||||
|
||||
// Act & Assert
|
||||
EXPECT_TRUE( RigPolygonTools::boundary( emptyImage ).empty() );
|
||||
EXPECT_TRUE( RigPolygonTools::boundary( { { 1, 1 }, { 1 } } ).empty() ); // Inconsistent row sizes
|
||||
}
|
||||
|
||||
// Test for simplifyPolygon function
|
||||
TEST( RigPolygonToolsTest, SimplifyPolygonTest )
|
||||
{
|
||||
// Arrange
|
||||
std::vector<cvf::Vec3d> vertices =
|
||||
{ { 0.0, 0.0, 0.0 }, { 1.0, 0.1, 0.0 }, { 1.5, -0.1, 0.0 }, { 3.0, 5.0, 0.0 }, { 5.0, 6.0, 0.0 }, { 7.0, 7.0, 0.0 }, { 8.0, 8.0, 0.0 } };
|
||||
double epsilon = 1;
|
||||
std::vector<cvf::Vec3d> expected = { { 0.0, 0.0, 0.0 }, { 1.5, -0.1, 0.0 }, { 3.0, 5.0, 0.0 }, { 8.0, 8.0, 0.0 } };
|
||||
|
||||
// Act
|
||||
RigPolygonTools::simplifyPolygon( vertices, epsilon );
|
||||
|
||||
// Assert
|
||||
EXPECT_EQ( vertices.size(), expected.size() );
|
||||
for ( size_t i = 0; i < vertices.size(); ++i )
|
||||
{
|
||||
EXPECT_DOUBLE_EQ( vertices[i].x(), expected[i].x() );
|
||||
EXPECT_DOUBLE_EQ( vertices[i].y(), expected[i].y() );
|
||||
EXPECT_DOUBLE_EQ( vertices[i].z(), expected[i].z() );
|
||||
}
|
||||
}
|
||||
|
||||
// Test for simplifyPolygon function with invalid input
|
||||
TEST( RigPolygonToolsTest, SimplifyPolygonInvalidInputTest )
|
||||
{
|
||||
// Arrange
|
||||
std::vector<cvf::Vec3d> emptyVertices;
|
||||
double epsilon = 1.0;
|
||||
|
||||
// Act & Assert
|
||||
EXPECT_NO_THROW( RigPolygonTools::simplifyPolygon( emptyVertices, epsilon ) );
|
||||
EXPECT_EQ( emptyVertices.size(), 0 );
|
||||
}
|
Loading…
Reference in New Issue
Block a user