Есть ли способ гарантировать определенное количество строк, обнаруженных с помощью cv2.HoughLines()?

Этот вопрос является продолжением моего предыдущего вопроса вопрос о том, как определить углы бильярдного стола. Я нашел контур бильярдного стола, и мне удалось применить преобразование Хафа к контуру. Результат этого преобразования Хафа показан ниже:

Есть ли способ гарантировать определенное количество строк, обнаруженных с помощью cv2.HoughLines()?

К сожалению, преобразование Хафа возвращает несколько строк для одного края таблицы. Я хочу, чтобы преобразование Хафа возвращало четыре строки, каждая из которых соответствовала краю стола с любым изображением бильярдного стола. Я не хочу настраивать параметры метода преобразования Хафа вручную (поскольку контур бильярдного стола может различаться для каждого изображения бильярдного стола). Есть ли способ гарантировать, что четыре строки будут сгенерированы 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)

Нет, не то, чтобы я знал об этом каким-либо прямым образом.

fmw42 07.05.2022 03:48

Зачем вдруг использовать cv.HoughLines()? Если можно предположить, что контур в основном состоит из четырех линейных сегментов, сначала рассмотрите возможность разделения пикселей на контуре на 4 группы. например Рассмотрите возможность голосования только в краевом (градиентном) направлении. (Его также можно назвать одномерным преобразованием Хафа.) Другими словами, создайте гистограмму направления градиента. После разделения выполните линейную подгонку к ним соответственно.

fana 07.05.2022 04:05

Используя cv.HoughLines(), вы не можете получить доступ к пространству для голосования, а также получить информацию «Кто здесь проголосовал?». Это очень неудобно на практике. Поэтому, если вы используете метод «Преобразование Хафа», я рекомендую вам реализовать его самостоятельно.

fana 07.05.2022 04:22

@fana Не могли бы вы уточнить, что вы подразумеваете под «голосованием только в направлении края» / «созданием гистограммы направления градиента»?

Stack Overflow 07.05.2022 04:40

Пиксели, принадлежащие одной линии, имеют одинаковое направление (в идеале одинаковое). И пиксели, принадлежащие другим линиям, имеют другое направление. (Так выглядит на вашем изображении, представленном в предыдущем вопросе) Поэтому при построении гистограммы вы увидите четыре локальных максимума. Вы можете разделить пиксели на 4 группы в зависимости от того, «какой пиксель проголосовал за какую ячейку».

fana 07.05.2022 12:00

@fana Большое спасибо за быстрый ответ. Затем по этим четырем локальным максимумам мы разбиваем пиксель на группы и выполняем линейную подгонку, верно?

Stack Overflow 08.05.2022 01:07

Да, по моим оценкам, пиксели, проголосовавшие за локальный-максимальный-бин (и бины в диапазоне, достаточно близком к нему), будут на одной линии. Думаю, таких групп пикселей найдется 4. Таким образом, выполняя подгонку строк к каждой группе, количество строк, которые вы получаете, становится равным 4.

fana 08.05.2022 04:08

Заметим, что я не знаю, сможем ли мы получить удовлетворительную точность, но на момент группировки параметры каждой прямой уже получены. (Точная/надежная подгонка не является обязательной.)

fana 08.05.2022 04:20

Линия определяется направлением и 1 позицией на линии. У группы уже есть эти статистические значения: Направление является голосовой информацией, и, например, центр тяжести пикселей может использоваться для Позиции.

fana 08.05.2022 04:27

(Конечно, среднее значение можно использовать как для Направления, так и для Позиции.)

fana 08.05.2022 04:33

Спасибо за развернутый ответ! Думаю, теперь я понимаю общий процесс. Как мне начать реализацию этой гистограммы направления градиента? Я использовал cv2.Canny(), чтобы получить схему бильярдного стола. Я знаю, что cv2.Canny() не предоставляет информацию о направлении края, поэтому вместо этого я буду применять фильтры Собеля Gx и Gy и самостоятельно вычислять изображения величины и направления края. Но я не совсем уверен, куда идти оттуда.

Stack Overflow 08.05.2022 20:08

Вы можете рассчитать угол из Gx и Gy (например, с помощью cv.cartToPolar или арктангенса для каждого пикселя). Теперь пиксели могут голосовать за направление (угол).

fana 09.05.2022 03:14

Однако я не знаю, как сделать функцию, которая голосует в зависимости от направления, как мы описали выше. Знаете ли вы какие-нибудь простые реализации?

Stack Overflow 09.05.2022 03:34

Проще говоря, например. массив, содержащий 360 целочисленных элементов, может использоваться как пространство для голосования. Сначала инициализируйте все 0. Затем вычислите угол (в данном случае в градусах) для каждого пикселя контура и проголосуйте (увеличьте элемент массива, указанный вычисленным углом). Конечно, может быть лучше/удобнее реализация, но я рекомендую попробовать такую ​​простую реализацию и посмотреть результат голосования, как первый шаг ваших проб и ошибок.

fana 09.05.2022 09:52

@фана Большое спасибо. Я обновил вопрос, чтобы отразить внесенные мной изменения (я сделал гистограмму направления градиента), но я не уверен, как я могу разделить эту гистограмму на четыре (и использовать разделение, чтобы найти четыре линии) .

Stack Overflow 11.05.2022 00:09
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
0
15
72
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

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?

Stack Overflow 13.05.2022 02:15

Я реализовал adaptiveThreshold для своей гистограммы. (См. cv.adaptiveThreshold() для значения размытых данных. Использование размытых данных такое же, как и у них.)

fana 13.05.2022 02:54

Еще раз большое спасибо за вашу помощь. У меня есть несколько вопросов, если не возражаете: 1) Зачем добавлять 0,5 дюйма BlurredVotes[I] = (int)(0.5 + (double) Sum/(2*r + 1))? 2) Зачем выполнять адаптивную пороговую обработку голосов за направление края? Не следует ли использовать адаптивную пороговую обработку для разделения переднего плана и фона с помощью интенсивности пикселей? 3) Почему VotingSpace[k].size() >= BlurredVotes[k] + ThreshOffset (оператор else) означает, что пиксели проголосовали за одну и ту же строку? 4) Почему мы начинаем поиск групп пикселей по индексу с наименьшим количеством голосов?

Stack Overflow 13.05.2022 16:09

1) Округление. например если среднее значение равно 0,6, результат преобразования в целое число становится равным 1. Если без этого 0,5, он становится равным 0.

fana 13.05.2022 17:52

2) Я хотел бы найти пики (локальные максимумы) из гистограммы. «Пик» — это место, где голосов больше, чем в окрестностях. Здесь я использовал местное среднее значение в качестве голосов по окружающей области (и это становится таким же, как у адаптивного порога).

fana 13.05.2022 17:58

3) В этом коде пик (группа) ищется как «группа бункеров с большим количеством голосов, чем окружающие». например При поиске, когда обнаруживается состояние, что «Бан [5], Бин [6] и Бин [7] имеют больше голосов, чем их окрестности, но Бин [8] (и Бин [4] тоже) не так», найдено, Пик состоит из {Bin[5], Bin[6] и Bin[7]} .

fana 13.05.2022 18:24

4) В этом примере, если поиск начинается с Bin[6], потребуется некоторая изобретательность, чтобы получить результат «Пик состоит из {Bin[5], Bin[6] и Bin[7]}». Зачем начинать с корзины с наименьшим количеством голосов, чтобы избежать этой проблемы (как написано в комментарии в коде, если стартовая корзина не принадлежит ни к какому пику (группе), этого можно избежать).

fana 13.05.2022 18:24

1) это очень тривиальное дело. 2),3) и 4) мой эвристический способ. Все они являются лишь образцом реализации.

fana 13.05.2022 18:35

По какой-то причине я всегда получаю более четырех групп. Я следил за вашей реализацией без огромных изменений.

Stack Overflow 16.05.2022 01:14

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

Stack Overflow 16.05.2022 04:09

Я использовал значение 5 для ThreshOffset, но это не очень хорошо продуманное значение. Если это значение слишком мало для ваших реальных данных, многие нежелательные группы будут извлечены. Это может стать причиной. Проверьте, что произошло. например постройте свою гистограмму и размытую гистограмму на одной диаграмме, чтобы проверить, что произошло с диапазонами ячеек ваших групп.

fana 16.05.2022 04:55

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

fana 16.05.2022 05:00

Другие вопросы по теме