Properly generate convex hull and get plane equations - computational-geometry

I am completely new to Computational Geometry. I want to generate convex hull of a set of points and then get plane equations for the generated convex polyhedron so that I can check inclusion/exclusion of points. I have followed the docs and tried the whole procedure probably a dozen times but there is always some issue. Maybe I'm missing some subtle point here. The whole procedure is as follows. I have the following plot, generated in Mathematica.
I want to include every point that is on the plot inside a convex hull. So I take all the points lying on all corners of both planes and the origin (Maybe that's the problem. Maybe there is a way to properly choose points so that all points on the plot are covered). The points for this specific plot are as follows. Note that the points are generated using infinite precision so they are exact values.
pts = {
{-24298771/25000000000,-223461425901/50000000000,0},
{11285077/10000000000,-223461425901/50000000000,0},
{-24298771/25000000000,0,0},
{-24298771/25000000000,-11285077/10000000000,0},
{-24298771/25000000000,120551411529/25000000000,-24298771/25000000000},
{11285077/10000000000,120551411529/25000000000,11285077/10000000000},
{11285077/10000000000,0,11285077/10000000000},
{-24298771/25000000000,24298771/25000000000,-24298771/25000000000},
{0,0,0}
};
Then, I use the following CGAL program to generate the convex hull and plane equations. Again, trying to keep things in infinite precision.
#include <CGAL/Exact_predicates_exact_constructions_kernel.h>
#include <CGAL/GMP/Gmpq_type.h>
#include <CGAL/Polyhedron_3.h>
#include <CGAL/convex_hull_3.h>
#include <CGAL/Side_of_triangle_mesh.h>
#include <CGAL/number_utils.h>
#include <unistd.h>
#include <iomanip>
typedef CGAL::Exact_predicates_exact_constructions_kernel Kernel;
typedef CGAL::Polyhedron_3<Kernel> Polyhedron_3;
typedef Kernel::Point_3 Point_3;
typedef Kernel::Plane_3 Plane_3;
typedef Kernel::Vector_3 Vector_3;
typedef CGAL::Side_of_triangle_mesh<Polyhedron_3, Kernel> Point_inside;
struct Plane_equation {
template <class Facet>
typename Facet::Plane_3 operator()( Facet& f) {
typename Facet::Halfedge_handle h = f.halfedge();
typedef typename Facet::Plane_3 Plane;
return Plane( h->vertex()->point(),
h->next()->vertex()->point(),
h->next()->next()->vertex()->point());
}
};
Point_3 create_point(std::vector<std::string> points) {
auto x = points[0], y = points[1], z = points[2];
Point_3 p;
std::istringstream input(x + " " + y + " " + z);
input >> p;
return p;
}
std::vector<std::string> create_coords_from_line(std::string line) {
std::vector<std::string> points;
std::istringstream stream(line);
std::string pt;
getline(stream, pt, ' ');
points.push_back(pt);
getline(stream, pt, ' ');
points.push_back(pt);
getline(stream, pt);
points.push_back(pt);
return points;
}
int main() {
std::vector<Point_3> points;
std::string line;
for (auto i = 0; i < 9; ++i) {
getline(std::cin, line);
points.push_back(create_point(create_coords_from_line(line)));
}
Polyhedron_3 poly;
CGAL::convex_hull_3(points.begin(), points.end(), poly);
// CGAL::draw(poly);
std::transform(poly.facets_begin(), poly.facets_end(), poly.planes_begin(), Plane_equation());
CGAL::set_pretty_mode(std::cout);
for (auto it = poly.planes_begin(); it != poly.planes_end(); ++it) {
if (isatty(fileno(stdin))) {
std::cout << "A = " << it->a().exact() << "\n";
std::cout << "B = " << it->b().exact() << "\n";
std::cout << "C = " << it->c().exact() << "\n";
std::cout << "D = " << it->d().exact() << "\n";
std::cout << "\n";
} else {
std::cout << it->a().exact() << " " << it->b().exact() << " "
<< it->c().exact() << " " << it->d().exact() << "\n";
}
}
return EXIT_SUCCESS;
}
Now, in order to make sure that the generated equations are correct and cover all the points, I create a Z3py script. In that, f is the function used to generate the plot, g is the conjunction of all plane equations with proper inequality (<, =, >). Then I check if f ---> g. I use the theory of reals for infinite precision. But it always comes up with a counter-example. And these counter examples are always on some edge of the plane. Here are a couple of pictures where red circle indicates the location of counterexample. This is a different plot from the one above but the process is the same. Just the input values to f is different.
Now, I don't really need infinite precision for my problem. But I would like to make sure that the procedure works with infinite precision so that I can be confident about correctness. But then I tried with CPLEX, which uses only 64-bits, and with that too, counterexamples were generated, in similar fashion to Z3. Here's an example
Now I have no idea in which step of the process am I making a mistake. My suspicion is the selection of initial points for convex hull. It would be great if someone can help me find the convex hull properly. As far as I have read, the convex hull algorithms are exact if infinite precision is used. That's why I didn't use Mathematica's convex hull feature, because it is not using infinite precision.
EDIT: There are two smaller planes which Mathematica is failing to show, as shown below. I want all points on those planes to be selected too. But the endpoints of the smaller planes coincide with the endpoints of larger planes. That's why I have taken only the corners of larger planes.
EDIT 2: Due to the range of y being so large compared to the other two variables, the convex hull generated for the above specified points looks just like a single line.
However, after diving the y values by 1000, we can see a clearer picture.

Related

Override floating point tolerance with boost intersection

Is it possible to introduce some tolerance with the intersection algorithm, such that close points or almost colinear lines are considered parallel?
To be concrete:
I have two line segments that should be considered parallel, however, due to some accuracy problems while doing floating point calculations, the line segments are not entirely parallel, the error is 3.78e-14 which should - by all means - be considered parallel in my case: so boost's intersection should give me two points.
However this is not the case and intersection regards those lines as not parallel. Example:
This is very similar to this question. This post is old and doesn't seem to satisfy me needs though. I'm confused as to how the intersection algorithm works in boost, too. I tried to find the code in boosts library but without success. Boosts code base is terrifying.
struct Point {
double x, y;
Point(double x_, double y_) : x(x_), y(y_) {};
}
BOOST_GEOMETRY_REGISTER_POINT_2D(Point, double, boost::geometry::cs::cartesian, x, y);
typedef boost::geometry::model::segment<Point> Segment;
Segment seg1({ -1012600, 9641189 }, { -935132, 9595186.14285714179277420043945 });
Segment seg2({ -1012600, 9641189 }, { -877031, 9560684 });
std::vector<Point> out;
boost::geometry::intersection(seg1, seg2, out);
Since I consider both segments as parallel, the expected output should be:
{ -1012600, 9641189 }, { -935132, 9595186.14285714179277420043945 }
Intersection indeed gives two points in a parallel case:
See for example:
Segment seg1({0, 0}, {6, 6});
Segment seg2({2, 2}, {3, 3});
boost::geometry::intersection(seg1, seg2, out);
Will give:
{2, 2}, {3, 3}
What do you expect the outcome to be? The segments still start in the same point.
And the resulting intersection is that point when I test it:
Live On Coliru
#include <boost/geometry.hpp>
#include <iostream>
#include <boost/geometry/geometries/register/point.hpp>
struct Point {
double x, y;
Point(double x_= 0, double y_= 0) : x(x_), y(y_) {};
};
BOOST_GEOMETRY_REGISTER_POINT_2D(Point, double, boost::geometry::cs::cartesian,
x, y)
typedef boost::geometry::model::segment<Point> Segment;
int main() {
Point P1{-1012600, 9641189};
std::cout << std::fixed;
Segment seg1(P1, {-935132, 9595186.14285714179277420043945});
Segment seg2(P1, {-877031, 9560684});
std::vector<Point> out;
boost::geometry::intersection(seg1, seg2, out);
for (auto& p : out) {
std::cout << boost::geometry::wkt(p) << "\n";
}
}
Print
POINT(-1012600.000000 9641189.000000)
On Precision
Otherwise, I've had success replacing double with long double or Boost Multiprecision types: https://stackoverflow.com/search?tab=newest&q=user%3a85371%20geometry%20multiprecision

Boost Geometry: segments intersection not yet implemented?

I am trying a simple test: compute the intersection of 2 segments with Boost Geometry. It does not compile. I also tried with some variations (int points instead of float points, 2D instead of 3D) with no improvement.
Is it really possible that boost doesn't implement segment intersection ? Or what did I do wrong ? Missing some hpp ? Confusion between algorithms "intersects" & "intersection" ?
The code is very basic:
#include <boost/geometry.hpp>
#include <boost/geometry/geometries/point.hpp>
#include <boost/geometry/geometries/segment.hpp>
#include <boost/geometry/algorithms/intersection.hpp>
typedef boost::geometry::model::point<float, 3, boost::geometry::cs::cartesian> testPoint;
typedef boost::geometry::model::segment<testPoint> testSegment;
testSegment s1(
testPoint(-1.f, 0.f, 0.f),
testPoint(1.f, 0.f, 0.f)
);
testSegment s2(
testPoint(0.f, -1.f, 0.f),
testPoint(0.f, 1.f, 0.f)
);
std::vector<testPoint> output;
bool intersectionExists = boost::geometry::intersects(s1, s2, output);
But I got the following errors at compile time by Visual:
- Error C2039 'apply' n'est pas membre de 'boost::geometry::dispatch::disjoint<Geometry1,Geometry2,3,boost::geometry::segment_tag,boost::geometry::segment_tag,false>' CDCadwork C:\Program Files\Boost\boost_1_75_0\boost\geometry\algorithms\detail\disjoint\interface.hpp 54
- Error C2338 This operation is not or not yet implemented. CDCadwork C:\Program Files\Boost\boost_1_75_0\boost\geometry\algorithms\not_implemented.hpp 47
There are indeed two problems:
you're intersecting 3D geometries. That's not implemented
Instead you can do the same operation on a projection.
you're passing an "output" geometry to intersects (which indeed only returns the true/false value as your chosen name intersectionExists suggested). In the presence of a third parameter, it would be used as a Strategy - a concept for which output obviously doesn't satisfy.
Note intersection always returns true: What does boost::geometry::intersection return - although that's not part of the documented interface
Since your geometries are trivially projected onto 2d plane Z=0:
Live On Coliru
#include <boost/geometry.hpp>
#include <boost/geometry/geometries/point.hpp>
#include <boost/geometry/geometries/segment.hpp>
#include <iostream>
namespace bg = boost::geometry;
namespace bgm = bg::model;
using Point = bgm::point<float, 2, bg::cs::cartesian>;
using Segment = bgm::segment<Point>;
int main() {
Segment s1{{-1, 0}, {1, 0}};
Segment s2{{0, -1}, {0, 1}};
bool exists = bg::intersects(s1, s2);
std::vector<Point> output;
/*bool alwaysTrue = */ bg::intersection(s1, s2, output);
std::cout << bg::wkt(s1) << "\n";
std::cout << bg::wkt(s2) << "\n";
for (auto& p : output) {
std::cout << bg::wkt(p) << "\n";
}
return exists? 0:1;
}
Prints
LINESTRING(-1 0,1 0)
LINESTRING(0 -1,0 1)
POINT(0 0)

Boost R tree node remove

I want to remove the nearest point node. and that should be satisfied the limit of distance.
but I think my code is not efficient.
How can I modify this?
for (int j = 0; j < 3; j++) {
bgi::rtree< value, bgi::quadratic<16> > nextRT;
// search for nearest neighbours
std::vector<value> matchPoints;
vector<pair<float, float>> pointList;
for (unsigned i = 0; i < keypoints[j + 1].size(); ++i) {
point p = point(keypoints[j + 1][i].pt.x, keypoints[j + 1][i].pt.y);
nextRT.insert(std::make_pair(p, i));
RT.query(bgi::nearest(p, 1), std::back_inserter(matchPoints));
if (bg::distance(p, matchPoints.back().first) > 3) matchPoints.pop_back();
else {
pointList.push_back(make_pair(keypoints[j + 1][i].pt.x, keypoints[j + 1][i].pt.y));
RT.remove(matchPoints.back());
}
}
and I also curious about result of matchPoints.
After query function works, there are values in matchPoints.
first one is point, and second one looks like some indexing number.
I don't know what second one means.
Q. and I also curious about result of matchPoints. After query function works, there are values in matchPoints. first one is point, and second one looks like some indexing number. I don't know what second one means.
Well, that's got to be a data member in your value type. What is in it depends solely on what you inserted into the rtree. it wouldn't surprise me if it was an ID that describes the geometry.
Since you do not even show the type of RT, we can only assume it is the same as nextRT. If so, we can assume that value is likely a pair like pair<box, unsigned> (because of what you insert). So, look at what got inserted for the unsigned value of the pair in RT...
Q.
if (bg::distance(p, matchPoints.back().first) > 3) matchPoints.pop_back();
else {
pointList.push_back(make_pair(keypoints[j + 1][i].pt.x, keypoints[j + 1][i].pt.y));
rtree.remove(matchPoints.back());
}
Simplify your code! Distilling the requirements:
It looks to me that for 4 sets of "key points", you want to create 4 rtrees containing all those key points with sequentially increasing ids.
Also for those 4 sets of "key points", you want to create a list of key points for which a geometry can be found with a radius of 3.
As a side-effect, remove those closely-matching geometries from the original rtree RT.
DECISION: Because these tasks are independent, let's do them separate:
// making up types that match the usage in your code:
struct keypoint_t { point pt; };
std::array<std::vector<keypoint_t>, 4> keypoints;
Now, let's do the tasks:
Note how RT is not used here:
for (auto const& current_key_set : keypoints) {
bgi::rtree< value, bgi::quadratic<16> > nextRT; // use a better name...
int i = 0;
for (auto const& kpd : current_key_set)
nextRT.insert(std::make_pair(kpd.pt, i++));
}
Creating the vector containing matched key-points (those with near geometries in RT):
for (auto const& current_key_set : keypoints) {
std::vector<point> matched_key_points;
for (auto const& kpd : current_key_set) {
point p = kpd.pt;
value match;
if (!RT.query(bgi::nearest(p, 1), &match))
continue;
if (bg::distance(p, match.first) <= 3) {
matched_key_points.push_back(p);
RT.remove(match);
}
}
}
Ironically, removing the matching geometries from RT became a bit of a minor issue in this: you can either delete by iterator or by a value. In this case, we use the overload that takes a value.
Summary
It was hard to understand the code enough to see what it did. I have shown how to clean up the code, and make it work. Maybe these aren't the things you need, but hopefully using the better separated code, you should be able to get further.
Note that the algorithms have side effects. This makes it hard to understand what really will happen. E.g.:
removing points from the original RT affects what the subsequent key points (even from subsequent sets (next j)) can match with
if you have the same key point multiple times, they may match more than 1 source RT point (because after removal of the first match, there might be a second match within radius 3)
key points are checked strictly sequentially. This means that if the first keypoint roughly matches a point X, this might cause a later keypoint to fail to match, even though the point X might be closer to that keypoint...
I'd suggest you THINK about the requirements really hard before implementing things with these side-effects. **Study the sample cases in the live demo below. If all these side-effects are exactly what you wanted, be sure to use much better naming and proper comments to describe what the code is doing.
Live Demo
Live On Coliru
#include <boost/geometry.hpp>
#include <boost/geometry/io/io.hpp>
#include <boost/geometry/index/rtree.hpp>
#include <iostream>
namespace bg = boost::geometry;
namespace bgi = bg::index;
typedef bg::model::point<float, 2, bg::cs::cartesian> point;
typedef std::pair<point, unsigned> pvalue;
typedef pvalue value;
int main() {
bgi::rtree< value, bgi::quadratic<16> > RT;
{
int i = 0;
for (auto p : { point(2.0f, 2.0f), point(2.5f, 2.5f) })
RT.insert(std::make_pair(p, i++));
}
struct keypoint_t { point pt; };
using keypoints_t = std::vector<keypoint_t>;
keypoints_t const keypoints[] = {
keypoints_t{ keypoint_t { point(-2, 2) } }, // should not match anything
keypoints_t{ keypoint_t { point(-1, 2) } }, // should match (2,2)
keypoints_t{ keypoint_t { point(2.0, 2.0) }, // matches (2.5,2.5)
{ point(2.5, 2.5) }, // nothing anymore...
},
};
for (auto const& current_key_set : keypoints) {
bgi::rtree< pvalue, bgi::quadratic<16> > nextRT; // use a better name...
int i = 0;
for (auto const& kpd : current_key_set)
nextRT.insert(std::make_pair(kpd.pt, i++));
}
for (auto const& current_key_set : keypoints) {
std::cout << "-----------\n";
std::vector<point> matched_key_points;
for (auto const& kpd : current_key_set) {
point p = kpd.pt;
std::cout << "Key: " << bg::wkt(p) << "\n";
value match;
if (!RT.query(bgi::nearest(p, 1), &match))
continue;
if (bg::distance(p, match.first) <= 3) {
matched_key_points.push_back(p);
std::cout << "\tRemoving close point: " << bg::wkt(match.first) << "\n";
RT.remove(match);
}
}
std::cout << "\nMatched keys: ";
for (auto& p : matched_key_points)
std::cout << bg::wkt(p) << " ";
std::cout << "\n\tElements remaining: " << RT.size() << "\n";
}
}
Prints
-----------
Key: POINT(-2 2)
Matched keys:
Elements remaining: 2
-----------
Key: POINT(-1 2)
Removing close point: POINT(2 2)
Matched keys: POINT(-1 2)
Elements remaining: 1
-----------
Key: POINT(2 2)
Removing close point: POINT(2.5 2.5)
Key: POINT(2.5 2.5)
Matched keys: POINT(2 2)
Elements remaining: 0

Eigen Library Euler Order Sequencing

I'm trying to understand the Eigen library's "eulerAngles" function, and I have some test code that doesn't make sense.
My testing includes determining if the Euler function call "eulerAngles(0, 1, 2)" corresponds to a "XYZ" Euler sequence of the static frame. For Eigen's column major matrix multiplication, it should correspond to "Z * Y * X". I have confirmed that with some simple tests, but for negative angles around the X axis I don't understand the results. Instead of using a Euler 3 Angle constructor, I'm using the "AngleAxisd" function:
rot3x3 = AngleAxisd( -M_PI, Vector3d::UnitX() );
Vector3d vec = rot3x3.eulerAngles(0, 1, 2);
For positive angles around the X axis, it works the way I expect. For the -M_PI angle (equivalent to 180 degrees), I'm seeing the following:
Euler angles: -0, 3.14159, -3.14159
I expect the first element in the array to be near -π, and the other two to be near zero. I expect the range of the first and last angles to be ±π, and the middle angle to be ±π/2. The middle angle is out of range.
The program is below:
#include "stdafx.h"
#include <iostream>
#include "<Eigen\Dense"
#include "<unsupported\Eigen\MatrixFunctions"
using namespace std;
using namespace Eigen;
int main(int argc, char* argv[])
{
Matrix3d rot3x3;
rot3x3 = AngleAxisd( -M_PI, Vector3d::UnitX() );
cout << "Here is rot3x3:" << endl << rot3x3 << endl << endl;
Vector3d vec = rot3x3.eulerAngles(0, 1, 2); // => 1-2-3 => XYZ => Z*Y*X
cout << "Euler angles: " << vec.x() << ", "
<< vec.y() << ", "
<< vec.z() << endl << endl;
system("pause");
return 0;
}
The output is:
Here is rot3x3:
1 0 0
0 -1 1.22465e-016
0 -1.22465e-016 -1
Euler angles: -0, 3.14159, -3.14159
This result is equivalent to a -π around the X axis (and it is the same as a positive π rotation around the X axis as well). Shouldn't the middle angle though be in the range that is equivalent to ±90 degrees?
Euler angles are not uniquely defined. In case of ambiguity, eulerAngles always pick the solution that minimizes the first angles. See also the following bug entries for related discussions: 609, 801, 947.

Detect if two rectangles can be combined into a single rectangle

I'm looking for an algorithm that takes two rectangles defined by (xa1,ya1,xa2,ya2) and (xb1,yb1,xb2,yb2), checks if they can be combined into a single rectangle and if they can, returns the new rectangle. An example:
xa1=0,ya1=0,xa2=320,ya2=119
xb1=0,yb1=120,xb2=320,yb2=239
These two rectangles can be combined into the following rectangle:
xc1=0,yc1=0,xc2=320,yc2=240
How would you implement such an algorithm? Thanks!
I'd draw the following pictures and would write it down as algorithm:
...xxxxxxx xxxxxxx....
. x . x x . x .
. x . x x . x .
...xxxxxxx xxxxxxx....
xxxxxxx .......
x x . .
x.....x xxxxxxx
xxxxxxx x.....x
. . x x
....... xxxxxxx
..........
. .
. xxxx .
. x x .
. x x .
. xxxx .
..........
xxxxxxxxxxxxxx
x x
x ....... x
x . . x
x . . x
x ....... x
x x
xxxxxxxxxxxxxx
Check out for corner cases!
After much fiddling I kind of worked out what you want. Note that there is still some contention to what you mean by 'strict bounding box': the sample in you original question does not satisfy the description you gave:
But the rectangles should only be combined if the bounding box is exactly the size of the two rectangles combined, i.e. the area of the bounding rectangle must be exactly the same as the size of the areas of the two source rectangles. If the area of rect 1 is a1, and the area of rect2 is a2, and the area of the bounding rect is a3, then a1+a2=a3.
This implementation should give you plenty of ideas, and I'm sure you know how to write
r.area() == a.area() + b.area()
if you really wanted that.
Codepad code:
// Final proposal: combine adjacent rectangles,
// if they are 'flush': almost touching
#include <iostream>
struct R
{
int x1,y1,x2,y2;
int height() const { return y2-y1; }
int width() const { return y2-y1; }
void normalize()
{
if (x1>x2) std::swap(x1,x2);
if (y1>y2) std::swap(y1,y2);
}
/*
* adjacent: return whether two rectangles
* are adjacent; the tolerance in pixels
* allow you to specify the gap:
* tolerance = 0: require at least one pixel overlap
* tolerance = 1: accepts 'flush' adjacent neighbours
* Negative tolerance require more overlap;
* tolerance > 1 allows gaps between rects;
*/
bool adjacent(R const& other, int tolerance=1) const
{
return !( (other.x1 - x2) > tolerance
|| (x1 - other.x2) > tolerance
|| (other.y1 - y2) > tolerance
|| (y1 - other.y2) > tolerance);
}
};
/*
* tolerance: see R::adjacent()
*
* strict: only allow strict ('pure') combinations of rects
*/
R combined(R const& a, R const& b, int tolerance=1, bool strict=false)
{
if (!a.adjacent(b, tolerance))
throw "combined(a,b): a and b don't satisfy adjacency requirements (are the coords normalized?)";
R r = { min(a.x1, b.x1), 1,1,1};
r.x1 = min(a.x1, b.x1);
r.y1 = min(a.y1, b.y1);
r.x2 = max(a.x2, b.x2);
r.y2 = max(a.y2, b.y2);
if (!strict)
return r;
if ( (r.height() <= max(a.height(), b.height()))
&& (r.width () <= max(a.width (), b.width ())) )
return r;
else
throw "combined(a,b): strict combination not available";
}
std::ostream& operator<<(std::ostream &os, R const& r)
{
return os << '(' << r.x1 << "," << r.y1 << ")-(" << r.x2 << ',' << r.y2 << ')';
}
int main()
{
const int tolerance = 1;
{
std::cout << "sample from original question" << std::endl;
R a = { 0, 0, 320, 119 }; /* a.normalize(); */
R b = { 0, 120, 320, 239 }; /* b.normalize(); */
std::cout << "a: " << a << "\t b: " << b << std::endl;
std::cout << "r: " << combined(a,b, tolerance) << std::endl;
}
{
std::cout << "sample from the comment" << std::endl;
R a = { 0, 0, 1, 320 }; /* a.normalize(); */
R b = { 0, 0, 2, 320 }; /* b.normalize(); */
std::cout << "a: " << a << "\t b: " << b << std::endl;
// NOTE: strict mode
std::cout << "r: " << combined(a,b, tolerance, true) << std::endl;
}
}
Output:
sample from original question
a: (0,0)-(320,119) b: (0,120)-(320,239)
r: (0,0)-(320,239)
sample from the comment
a: (0,0)-(1,320) b: (0,0)-(2,320)
r: (0,0)-(2,320)
They can be combined if and only if one pair of opposite sides of one rectangle overlaps one pair of opposite sides of the other rectangle. By overlap, I mean if they are parallel and contain at least one point in common.
You should be able to figure out the code ;)
EDIT: Oh I forgot to mention the case where the two rectangles are completely overlapping. That shouldn't be too hard to check either.
The two rectangles must intersect. The corners of the bounding rectangle must all land on existing corners.
Those two conditions are necessary and sufficient. Obviously the rectangles have to intersect, and, because you can't create a non-corner empty area using only 2 intersecting rectangles, the bounding corners must land on existing corners.
return r1.Intersects(r2) and r1.BoundingRectangleWith(r2).Corners.IsSubsetOf(r1.Corners.Union(r2.Corners))
Implementing Intersects, BoundingRectangleWith, Corners, and IsSubsetOf is straightforward. You can then inline them for better performance, but it will be a mess of unreadable comparisons.
Edit
One of your comments suggests you don't want the rectangles to overlap, only to touch. In that case you only need to check that on one axis (i.e. X or Y) the rectangle's ranges are equal and on the other axis the ranges touch. Two ranges touch if the median of their bounds has 2 occurrences. Note that if you want right=1 to touch left=2, you need to add 1 to the ceiling bounds.

Resources