Сжать набор точек многоугольника в более короткий набор точек

У меня есть следующий многоугольник, который представляет собой просто набор 2D-точек следующим образом: -

poly0=[80    60
    90    60
   100    60
   110    60
   110    50
   120    50
   130    50
   140    50
   150    50
   160    50
   170    50
   180    50
   190    50
   200    50
   210    50
   210    60
   210    70
   210    80
   210    90
   220    90
   220   100
   210   100
   210   110
   200   110
   200   120
   190   120
   180   120
   180   130
   170   130
   160   130
   150   130
   140   130
   130   130
   130   120
   120   120
   110   120
   110   110
   100   110
   100   100
    90   100
    90    90
    90    80
    90    70
    80    70
    80    60];

Теперь я могу построить его, используя.

>> line(poly0(:,1), poly0(:,2),'Color','k','LineWidth',3,'LineStyle',':');

Это ясно показывает одну вещь: мой исходный набор точек многоугольника сильно избыточен. Обычно выше перечисляются несколько точек, лежащих на одной прямой, в чем нет необходимости. Я мог бы начать проверять каждую пару точек и, если они находятся на одной прямой, я мог бы их удалить. Но это означало бы использовать много циклов for. Я не могу придумать умный способ векторизации.

Как мне получить новый набор точек, размер которого намного меньше предыдущего, но который по-прежнему представляет собой тот же самый многоугольник? У меня должно быть столько точек, сколько вершин в многоугольнике. Другими словами, как быстро найти вершины из приведенного выше набора данных?

PS: здесь углы вершин равны 90 градусам, но если вы дадите решение, не пытайтесь использовать этот факт. Я хочу более общий ответ.


person user_1_1_1    schedule 09.04.2019    source источник
comment
Это всего лишь одна петля: для каждых 3 последовательных точек, если они находятся на прямой, удалите среднюю. Если вы удалите точку, проверьте еще раз с той же первой точкой, добавив новую третью точку. После цикла вам также необходимо проверить точки end-1, end и 1, а также end, 1 и 2.   -  person Cris Luengo    schedule 10.04.2019
comment
Возможный дубликат stackoverflow.com/questions/ 53839733 /   -  person Durkee    schedule 10.04.2019
comment
@Durkee Ha, работает! Но я думаю, что здесь используется тот факт, что углы многоугольника равны 90 градусам. см. PS выше.   -  person user_1_1_1    schedule 10.04.2019
comment
@CrisLuengo Думаю, ваш ответ - наиболее общий ответ, который работает. Я приму его, если он будет преобразован в ответ с некоторыми кодами.   -  person user_1_1_1    schedule 10.04.2019
comment
Принудительно ли начисление очков в последовательном порядке?   -  person ShadowMan    schedule 10.04.2019
comment
@ShadowMan Да, они идут подряд.   -  person user_1_1_1    schedule 10.04.2019


Ответы (3)


У двух существующих ответов есть большие недостатки:

  • Метод Дурки работает только в том случае, если расстояния между последующими точками точно идентичны. Точки должны иметь координаты, идеально представляемые в виде значений с плавающей запятой, чтобы расстояния между последующими точками на линии могли быть идентичными. Если точки не равноудалены, метод ничего не делает. Кроме того, начало и конец многоугольника не исследуются вместе, поэтому, если прямая линия образуется через начало / конец многоугольника, останется на одну точку слишком много.

  • Метод ShadowMan лучше тем, что расстояния не обязательно должны быть идентичными, а линия, пересекающая начало / конец многоугольника, является правильно обработан. Однако он также использует сравнения на равенство с плавающей запятой, что в общем случае не работает. Только с целочисленными координатами этот метод будет работать правильно. Кроме того, он использует vecnorm (который вычисляет квадратный корень) и деление, оба являются относительно дорогостоящими операциями (по сравнению с методом, показанным здесь).

Чтобы увидеть, образуют ли три точки прямую линию, можно использовать простое арифметическое правило. Допустим, у нас есть точки p0, p1 и p2. Вектор от p0 до p1 и вектор от p0 до p2 образуют основу параллелограмма, площадь которого можно вычислить с помощью перекрестное произведение двух векторов (в 2D подразумевается, что в перекрестном произведении используется z=0, с результирующим вектором, имеющим x=0 и y=0, полезно только значение z; таким образом, 2D Предполагается, что перекрестное произведение дает скалярное значение). Его можно вычислить следующим образом:

v1 = p1 - p0;
v2 = p2 - p0;
x = v1(1)*v2(2) - v1(2)*v2(1);

x, перекрестное произведение, будет равно нулю, если два вектора параллельны, что означает, что три точки лежат на одной прямой. Но проверка на равенство нулю должна иметь некоторый допуск, поскольку арифметика с плавающей запятой неточна. Здесь я использую 1e-6 как допуск. Используйте значение, которое на несколько порядков меньше расстояния между вашими точками.

Учитывая входной набор точек p, мы можем найти угловые точки с помощью:

p1 = p;                                  % point 1
p0 = circshift(p1,1);                    % point 0
v1 = p1 - p0;                            % vector from point 0 to 1
v2 = circshift(p1,-1) - p0;              % vector from point 0 to 2
x = v1(:,1).*v2(:,2) - v1(:,2).*v2(:,1); % cross product
idx = abs(x) > 1e-6;                     % comparison with tolerance
p = p(idx,:);                            % corner points

Обратите внимание, что этот тест перекрестного произведения не удастся, если две последовательные точки имеют одинаковые координаты (т.е. один из векторов имеет нулевую длину). Если в данных могут быть дублированные точки, потребуется дополнительный тест.

Вот результаты трех методов. Я создал многоугольник с нетривиальными координатами и неравномерно расположенными вершинами. Я также поставил зазор между началом и концом в середине прямой кромки. Эти характеристики призваны показать недостатки двух других методов.

сравнение трех методов

Это код, который я использовал для построения графика:

% Make a polygon that will be difficult for the other two methods
p = [0,0 ; 0.5,0 ; 1,0 ; 1,1 ; 0.5,1 ; 0,1];
p = p + rand(size(p))/3;
p(end+1,:) = p(1,:);
q = [];
for ii = 1:size(p,1)-1
   t = p(ii,:) + (p(ii+1,:) - p(ii,:)) .* [0;0.1;1/3;0.45;0.5897545;pi/4;exp(1)/3];
   q = [q;t];
end
q = circshift(q,3,1);

figure
subplot(2,2,1)
plot(q(:,1),q(:,2),'bo-')
axis equal
title('input')

subplot(2,2,2)
res1 = method1(q);
plot(res1(:,1),res1(:,2),'ro-')
axis equal
title('Durkee''s method')

subplot(2,2,3)
res2 = method2(q);
plot(res2(:,1),res2(:,2),'ro-')
axis equal
title('ShadowMan''s method')

subplot(2,2,4)
res3 = method3(q);
plot(res3(:,1),res3(:,2),'go-')
axis equal
title('correct method')

% Durkee's method: https://stackoverflow.com/a/55603145/7328782
function P = method1(P)
a = logical([1 diff(P(:,1),2)' 1]);
b = logical([1 diff(P(:,2),2)' 1]);
idx = or(a,b);
P = P(idx,:);
end

% ShadowMan's method: https://stackoverflow.com/a/55603040/7328782
function corners = method2(poly0)
poly0Z = circshift(poly0,1);
poly0I = circshift(poly0,-1);
unitVectIn =(poly0 - poly0I)./vecnorm((poly0 - poly0I),2,2);
unitVectOut =(poly0Z - poly0)./vecnorm((poly0Z - poly0),2,2);
cornerIndices = sum(unitVectIn == unitVectOut,2)==0;
corners = poly0(cornerIndices,:);
end
% vecnorm is new to R2017b, I'm still running R2017a.
function p = vecnorm(p,n,d)
% n is always 2
p = sqrt(sum(p.^2,d));
end

function p = method3(p1)
p0 = circshift(p1,1);
v1 = p1 - p0;
v2 = circshift(p1,-1) - p0;
x = v1(:,1).*v2(:,2) - v1(:,2).*v2(:,1);
idx = abs(x) > 1e-6;
p = p1(idx,:);
end
person Cris Luengo    schedule 10.04.2019
comment
Крис, спасибо за отзыв. Я попытался внести в свой сценарий толерантность, используя ваши нервные точки. (abs(unitVectIn-unitVectOut)<1e-6 это не совсем так, потому что оставляет мне вектор. Будет ли sum((abs(unitVectIn-unitVectOut)<1e-6) == 2 подходящим, потому что теперь вместо сравнения чисел с плавающей запятой я просто сравниваю целые числа, являющиеся результатом первого логического значения? Я хотел бы узнать больше о принципах, лежащих в основе вашей критики, если вы знаете какие-либо хорошие ресурсы. - person ShadowMan; 10.04.2019
comment
@ShadowMan: Да, я имел в виду именно это. Речь идет о замене a==b на abs(a-b)<tol. Это работа, на которую все всегда ссылаются при обсуждении вопросов о представлении с плавающей запятой. - person Cris Luengo; 10.04.2019
comment
есть ли причина, по которой вы решили рассчитывать кросс-произведение вручную вместо использования cross()? - person ShadowMan; 11.04.2019
comment
@ShadowMan: cross требует 3D-векторов. Мне пришлось бы добавить столбец нулей (для 3-го измерения), и затем он вычислил бы значения x и y, которые, как я уже знаю, будут равны нулю. Так что это будет менее эффективно. Я не знаю, есть ли в MATLAB функция для вычисления двухмерного перекрестного произведения. Кроме того, я изменил код из кода C ++, который у меня уже был, я приложил усилия, чтобы заменить цикл, используя circshift, который я видел в вашем ответе, но не смотрел, можно ли что-то еще упростить. Так что оставить это ручное кросс-произведение было простым решением. :) - person Cris Luengo; 11.04.2019
comment
Спасибо за советы, исправление сравнения чисел с плавающей запятой в моем скрипте приводит к хорошим результатам даже для «нестабильных» данных. С этого момента я всегда буду искать эту проблему в своем коде: D - person ShadowMan; 11.04.2019
comment
@ShadowMan: Если вы сделаете это, вы уже на шаг впереди! :) - Кстати: это наша стандартная двойная цель для ошибок сравнения с плавающей запятой. Посмотрите на связанные вопросы, вы увидите, насколько распространена эта ошибка. Это только те, что указаны в теге MATLAB, я уверен, что для других языков есть похожие цели для дублирования. - person Cris Luengo; 11.04.2019

Хорошо, я адаптировал это для работы с неквадратными углами.

Рассмотрим треугольник, обозначенный точками

P = [0 0; 1 0; 2 0; 1.5 1; 1 2; .5 1; 0 0];

Это массив 7x2, если мы затем определим 2 производных вектора, как определено в вопросе, который я упомянул в комментариях.

a = logical([1 diff(P(:,1),2)' 1]);
b = logical([1 diff(P(:,2),2)' 1]);

Оттуда мы можем объединить их, чтобы получить новую переменную индексации

idx = or(a,b);

Наконец, мы можем использовать это, чтобы сделать наш сюжет

line(P(idx,1), P(idx,2),'Color','k','LineWidth',3,'LineStyle',':');

Если вы делаете линейный график, я думаю, вам нужно установить для последней переменной значение false.

idx(end) = false;
person Durkee    schedule 10.04.2019
comment
Этот способ намного быстрее дает решение, чем мой, с точки зрения вычислений, а также времени для написания кода: D. - person ShadowMan; 10.04.2019
comment
Если первая и последняя точки образуют прямую линию, вы удалите недостаточно точек. Думаю, другой ответ делает то же самое с круговым сдвигом. - person Cris Luengo; 10.04.2019
comment
Правда, в зависимости от ситуации это могло не иметь значения. В качестве альтернативы это можно исправить с помощью оператора if. - person Durkee; 10.04.2019
comment
Я только что понял, что у метода есть еще один недостаток. Вы предполагаете, что вершины расположены на одинаковом расстоянии. Если точки на прямой линии не находятся на равных расстояниях, они останутся на выходе. - person Cris Luengo; 10.04.2019

«Векторный» способ можно сделать довольно изящно. Я тоже пробовал использовать цикл for, и вы можете сделать то же самое, но вы попросили вектор, так что вот мой способ сделать это.

Единственное изменение, которое я внес в ваши данные, - это удалить все реплики перед запуском этого скрипта. Кроме того, указанные точки должны быть расположены в последовательном порядке по часовой стрелке или против часовой стрелки.

    poly0Z = circshift(poly0,1);
    poly0I = circshift(poly0,-1); 
    unitVectIn =(poly0 - poly0I)./vecnorm((poly0 - poly0I),2,2);
    unitVectOut =(poly0Z - poly0)./vecnorm((poly0Z - poly0),2,2)  ;
    cornerIndices = sum(unitVectIn == unitVectOut,2)==0
    corners = poly0(cornerIndices,:)

    line(poly0(:,1), poly0(:,2),'Color','k','LineWidth',2,'LineStyle',':');
    hold on
    scatter(corners(:,1), corners(:,2),'filled')

В основе этого метода лежит переход к каждой точке, вычисление входящего единичного вектора и выходящего единичного вектора. Точки, в которых единичный вектор in не совпадает с единичным вектором out, являются углами.

person ShadowMan    schedule 10.04.2019
comment
Этот метод работает хорошо, за исключением сравнения на равенство. Вы должны учитывать допуск при сравнении нормализованных векторов (abs(unitVectIn-unitVectOut)<1e-6 или подобных). - person Cris Luengo; 10.04.2019