Этот вопрос является продолжением моего предыдущего вопроса вопрос о том, как определить углы бильярдного стола. Я нашел контур бильярдного стола, и мне удалось применить преобразование Хафа к контуру. Результат этого преобразования Хафа показан ниже:
К сожалению, преобразование Хафа возвращает несколько строк для одного края таблицы. Я хочу, чтобы преобразование Хафа возвращало четыре строки, каждая из которых соответствовала краю стола с любым изображением бильярдного стола. Я не хочу настраивать параметры метода преобразования Хафа вручную (поскольку контур бильярдного стола может различаться для каждого изображения бильярдного стола). Есть ли способ гарантировать, что четыре строки будут сгенерированы cv2.HoughLines()?
Заранее спасибо.
РЕДАКТИРОВАТЬ
Используя комментарии @fana, я создал гистограмму направлений градиента с помощью приведенного ниже кода. Я все еще не совсем уверен, как получить четыре линии из этой гистограммы.
img = cv2.imread("Assets/Setup.jpg")
hsv_img = cv2.cvtColor(img, cv2.COLOR_BGR2HSV)
masked_img = cv2.inRange(hsv_img, (50, 40, 40), (70, 255, 255))
gaussian_blur_img = cv2.GaussianBlur(masked_img, (5, 5), 0)
sobel_x = np.asarray([[1, 0, -1], [2, 0, -2], [1, 0, -1]], dtype=np.int8)
sobel_y = np.asarray([[1, 2, 1], [0, 0, 0], [-1, -2, -1]], dtype=np.int8)
gradient_x = cv2.filter2D(gaussian_blur_img, cv2.CV_16S, cv2.flip(sobel_x, -1), borderType=cv2.BORDER_CONSTANT)
gradient_y = cv2.filter2D(gaussian_blur_img, cv2.CV_16S, cv2.flip(sobel_y, -1), borderType=cv2.BORDER_CONSTANT)
edges = cv2.normalize(np.hypot(gradient_x, gradient_y), None, 0, 255, cv2.NORM_MINMAX, cv2.CV_8U)
edge_direction = np.arctan2(gradient_y, gradient_x) * (180 / np.pi)
edge_direction[edge_direction < 0] += 360
np.around(edge_direction, 0, edge_direction)
edge_direction[edge_direction == 360] = 0
edge_direction = edge_direction.astype("uint16")
histogram, bins = np.histogram(edge_direction, 359)
Зачем вдруг использовать cv.HoughLines()? Если можно предположить, что контур в основном состоит из четырех линейных сегментов, сначала рассмотрите возможность разделения пикселей на контуре на 4 группы. например Рассмотрите возможность голосования только в краевом (градиентном) направлении. (Его также можно назвать одномерным преобразованием Хафа.) Другими словами, создайте гистограмму направления градиента. После разделения выполните линейную подгонку к ним соответственно.
Используя cv.HoughLines(), вы не можете получить доступ к пространству для голосования, а также получить информацию «Кто здесь проголосовал?». Это очень неудобно на практике. Поэтому, если вы используете метод «Преобразование Хафа», я рекомендую вам реализовать его самостоятельно.
@fana Не могли бы вы уточнить, что вы подразумеваете под «голосованием только в направлении края» / «созданием гистограммы направления градиента»?
Пиксели, принадлежащие одной линии, имеют одинаковое направление (в идеале одинаковое). И пиксели, принадлежащие другим линиям, имеют другое направление. (Так выглядит на вашем изображении, представленном в предыдущем вопросе) Поэтому при построении гистограммы вы увидите четыре локальных максимума. Вы можете разделить пиксели на 4 группы в зависимости от того, «какой пиксель проголосовал за какую ячейку».
@fana Большое спасибо за быстрый ответ. Затем по этим четырем локальным максимумам мы разбиваем пиксель на группы и выполняем линейную подгонку, верно?
Да, по моим оценкам, пиксели, проголосовавшие за локальный-максимальный-бин (и бины в диапазоне, достаточно близком к нему), будут на одной линии. Думаю, таких групп пикселей найдется 4. Таким образом, выполняя подгонку строк к каждой группе, количество строк, которые вы получаете, становится равным 4.
Заметим, что я не знаю, сможем ли мы получить удовлетворительную точность, но на момент группировки параметры каждой прямой уже получены. (Точная/надежная подгонка не является обязательной.)
Линия определяется направлением и 1 позицией на линии. У группы уже есть эти статистические значения: Направление является голосовой информацией, и, например, центр тяжести пикселей может использоваться для Позиции.
(Конечно, среднее значение можно использовать как для Направления, так и для Позиции.)
Спасибо за развернутый ответ! Думаю, теперь я понимаю общий процесс. Как мне начать реализацию этой гистограммы направления градиента? Я использовал cv2.Canny(), чтобы получить схему бильярдного стола. Я знаю, что cv2.Canny() не предоставляет информацию о направлении края, поэтому вместо этого я буду применять фильтры Собеля Gx и Gy и самостоятельно вычислять изображения величины и направления края. Но я не совсем уверен, куда идти оттуда.
Вы можете рассчитать угол из Gx и Gy (например, с помощью cv.cartToPolar или арктангенса для каждого пикселя). Теперь пиксели могут голосовать за направление (угол).
Однако я не знаю, как сделать функцию, которая голосует в зависимости от направления, как мы описали выше. Знаете ли вы какие-нибудь простые реализации?
Проще говоря, например. массив, содержащий 360 целочисленных элементов, может использоваться как пространство для голосования. Сначала инициализируйте все 0. Затем вычислите угол (в данном случае в градусах) для каждого пикселя контура и проголосуйте (увеличьте элемент массива, указанный вычисленным углом). Конечно, может быть лучше/удобнее реализация, но я рекомендую попробовать такую простую реализацию и посмотреть результат голосования, как первый шаг ваших проб и ошибок.
@фана Большое спасибо. Я обновил вопрос, чтобы отразить внесенные мной изменения (я сделал гистограмму направления градиента), но я не уверен, как я могу разделить эту гистограмму на четыре (и использовать разделение, чтобы найти четыре линии) .
Using @fana's comments, I have created a histogram of gradient directions with the code below. I'm still not entirely sure how to obtain four lines from this histogram.
Я немного попробовал.
Поскольку я не знаю Python, следующий пример кода — C++. Однако то, что сделано, написано в виде комментариев, так что я думаю, вы сможете понять.
Этот образец включает в себя следующее:
Этот пример не включает процесс подбора линии.
Глядя на результат группировки, кажется, что некоторые пиксели станут выбросами для подгонки линий. Поэтому, я думаю, лучше использовать какой-нибудь надежный метод подбора (например, М-оценку, RANSAC).
int main()
{
//I obtained this image from your previous question.
//However, I do not used as it is.
//This image "PoolTable.png" is 25% scale version.
//(Because your original image was too large for my monitor!)
cv::Mat SrcImg = cv::imread( "PoolTable.png" ); //Size is 393x524[pixel]
if ( SrcImg.empty() )return 0;
//Extract Outline Pixels
std::vector< cv::Point > OutlinePixels;
{
//Here, I adjusted a little.
// - Change argument value for inRange
// - Emplying morphologyEx() additionally.
cv::Mat HSVImg;
cv::cvtColor( SrcImg, HSVImg, cv::COLOR_BGR2HSV );
cv::Mat Mask;
cv::inRange( HSVImg, cv::Scalar(40,40,40), cv::Scalar(80,255,255), Mask );
cv::morphologyEx( Mask, Mask, cv::MORPH_OPEN, cv::Mat() );
//Here, outline is found as the contour which has max area.
std::vector< std::vector<cv::Point> > contours;
cv::findContours( Mask, contours, cv::RETR_EXTERNAL, cv::CHAIN_APPROX_NONE );
if ( contours.empty() )return 0;
int MaxAreaIndex = 0;
double MaxArea=0;
for( int iContour=0; iContour<contours.size(); ++iContour )
{
double Area = cv::contourArea( contours[iContour] );
if ( MaxArea < Area ){ MaxArea = Area; MaxAreaIndex = iContour; }
}
OutlinePixels = contours[MaxAreaIndex];
}
//Sobel
cv::Mat Gx,Gy;
{
const int KernelSize = 5;
cv::Mat GraySrc;
cv::cvtColor( SrcImg, GraySrc, cv::COLOR_BGR2GRAY );
cv::Sobel( GraySrc, Gx, CV_32F, 1,0, KernelSize );
cv::Sobel( GraySrc, Gy, CV_32F, 0,1, KernelSize );
}
//Voting
// Here, each element is the vector of index of point.
// (Make it possible to know which pixel voted where.)
std::vector<int> VotingSpace[360]; //360 Bins
for( int iPoint=0; iPoint<OutlinePixels.size(); ++iPoint ) //for all outline pixels
{
const cv::Point &P = OutlinePixels[iPoint];
float gx = Gx.at<float>(P);
float gy = Gy.at<float>(P);
//(Ignore this pixel if magnitude of gradient is weak.)
if ( gx*gx + gy*gy < 100*100 )continue;
//Determine the bin to vote based on the angle
double angle_rad = atan2( gy,gx );
double angle_deg = angle_rad * 180.0 / CV_PI;
int BinIndex = cvRound(angle_deg);
if ( BinIndex<0 )BinIndex += 360;
if ( BinIndex>=360 )BinIndex -= 360;
//Vote
VotingSpace[ BinIndex ].push_back( iPoint );
}
//Find Pixel-Groups Based on Voting Result.
std::vector< std::vector<cv::Point> > PixelGroups;
{
//- Create Blurred Vote count (used for threshold at next process)
//- Find the bin with the fewest votes (used for start bin of serching at next process)
unsigned int BlurredVotes[360];
int MinIndex = 0;
{
const int r = 10; //(blur-kernel-radius)
unsigned int MinVoteVal = VotingSpace[MinIndex].size();
for( int i=0; i<360; ++i )
{
//blur
unsigned int Sum = 0;
for( int k=i-r; k<=i+r; ++k ){ Sum += VotingSpace[ (k<0 ? k+360 : (k>=360 ? k-360 : k)) ].size(); }
BlurredVotes[i] = (int)( 0.5 + (double)Sum / (2*r+1) );
//find min
if ( MinVoteVal > VotingSpace[i].size() ){ MinVoteVal = VotingSpace[i].size(); MinIndex = i; }
}
}
//Find Pixel-Groups
// Search is started from the bin with the fewest votes.
// (Expect the starting bin to not belong to any group.)
std::vector<cv::Point> Pixels_Voted_to_SameLine;
const int ThreshOffset = 5;
for( int i=0; i<360; ++i )
{
int k = (MinIndex + i)%360;
if ( VotingSpace[k].size() <= BlurredVotes[k]+ThreshOffset )
{
if ( !Pixels_Voted_to_SameLine.empty() )
{//The end of the group was found
PixelGroups.push_back( Pixels_Voted_to_SameLine );
Pixels_Voted_to_SameLine.clear();
}
}
else
{//Add pixels which voted to Bin[k] to current group
for( int iPixel : VotingSpace[k] )
{ Pixels_Voted_to_SameLine.push_back( OutlinePixels[iPixel] ); }
}
}
if ( !Pixels_Voted_to_SameLine.empty() )
{ PixelGroups.push_back( Pixels_Voted_to_SameLine ); }
//This line is just show the number of groups.
//(When I execute this code, 4 groups found.)
std::cout << PixelGroups.size() << " groups found." << std::endl;
}
{//Draw Pixel Groups to check result
cv::Mat ShowImg = SrcImg * 0.2;
for( int iGroup=0; iGroup<PixelGroups.size(); ++iGroup )
{
const cv::Vec3b DrawColor{
unsigned char( ( (iGroup+1) & 0x4) ? 255 : 80 ),
unsigned char( ( (iGroup+1) & 0x2) ? 255 : 80 ),
unsigned char( ( (iGroup+1) & 0x1) ? 255 : 80 )
};
for( const auto &P : PixelGroups[iGroup] ){ ShowImg.at<cv::Vec3b>(P) = DrawColor; }
}
cv::imshow( "GroupResult", ShowImg );
if ( cv::waitKey() == 's' ){ cv::imwrite( "GroupResult.png", ShowImg ); }
}
return 0;
}
Изображение результата: Найдено 4 группы, и пиксели, принадлежащие одной группе, были нарисованы одним цветом. (красный, зеленый, синий и желтый)
Спасибо большое, за реализацию. Могу я спросить, что делает BlurredVotes?
Я реализовал adaptiveThreshold
для своей гистограммы. (См. cv.adaptiveThreshold() для значения размытых данных. Использование размытых данных такое же, как и у них.)
Еще раз большое спасибо за вашу помощь. У меня есть несколько вопросов, если не возражаете: 1) Зачем добавлять 0,5 дюйма BlurredVotes[I] = (int)(0.5 + (double) Sum/(2*r + 1))
? 2) Зачем выполнять адаптивную пороговую обработку голосов за направление края? Не следует ли использовать адаптивную пороговую обработку для разделения переднего плана и фона с помощью интенсивности пикселей? 3) Почему VotingSpace[k].size() >= BlurredVotes[k] + ThreshOffset
(оператор else) означает, что пиксели проголосовали за одну и ту же строку? 4) Почему мы начинаем поиск групп пикселей по индексу с наименьшим количеством голосов?
1) Округление. например если среднее значение равно 0,6, результат преобразования в целое число становится равным 1. Если без этого 0,5, он становится равным 0.
2) Я хотел бы найти пики (локальные максимумы) из гистограммы. «Пик» — это место, где голосов больше, чем в окрестностях. Здесь я использовал местное среднее значение в качестве голосов по окружающей области (и это становится таким же, как у адаптивного порога).
3) В этом коде пик (группа) ищется как «группа бункеров с большим количеством голосов, чем окружающие». например При поиске, когда обнаруживается состояние, что «Бан [5], Бин [6] и Бин [7] имеют больше голосов, чем их окрестности, но Бин [8] (и Бин [4] тоже) не так», найдено, Пик состоит из {Bin[5], Bin[6] и Bin[7]} .
4) В этом примере, если поиск начинается с Bin[6], потребуется некоторая изобретательность, чтобы получить результат «Пик состоит из {Bin[5], Bin[6] и Bin[7]}». Зачем начинать с корзины с наименьшим количеством голосов, чтобы избежать этой проблемы (как написано в комментарии в коде, если стартовая корзина не принадлежит ни к какому пику (группе), этого можно избежать).
1) это очень тривиальное дело. 2),3) и 4) мой эвристический способ. Все они являются лишь образцом реализации.
По какой-то причине я всегда получаю более четырех групп. Я следил за вашей реализацией без огромных изменений.
Я выполняю вычисление градиента на моем замаскированном изображении (не должно быть разницы, на каком изображении я это делаю, потому что направление края одинаковое), но у меня есть 24 группы. Я попытался выполнить расчет градиента на исходном изображении, и группы стали хуже.
Я использовал значение 5 для ThreshOffset
, но это не очень хорошо продуманное значение. Если это значение слишком мало для ваших реальных данных, многие нежелательные группы будут извлечены. Это может стать причиной. Проверьте, что произошло. например постройте свою гистограмму и размытую гистограмму на одной диаграмме, чтобы проверить, что произошло с диапазонами ячеек ваших групп.
Кстати, когда обнаруживается много групп, можно просто выбрать только четыре с наибольшим количеством голосов.
Нет, не то, чтобы я знал об этом каким-либо прямым образом.