В одном проекте потребовалось сделать добавление текста (cv::putText) на изображение через OpenCV, да не просто горизонтально, а с поворотом на произвольный угол.
Как оказалось, штатного способа вывести текст с поворотом нет.
Найденные решения предлагали использовать «маску» — но она даёт артефакты по контуру, особенно если применяется сглаживание при выводе текста.
Решение, в принципе, простое.
Так как использовать маску мы не можем, то будем использовать как подложку кусок фона, на который текст должен был быть в итоге выведен, но перед выводом текста, повернём этот кусок фона в противоположном направлении и тогда сможем вывести текст горизонтально, потом вернём этот кусок фона с наложенным текстом на место. А так как мы можем взять часть фона только соосно, то придётся найти ограничительную рамку повёрнутого текст.
То есть план таков:
- Узнаём размер ограничительной рамки самого текста
- Узнаём размеры и положение ограничительной рамки при повороте на заданный угол, то есть фактически 4 точки — углы.
- Узнаём размер и положение ограничительной рамки, в которую входит рамка после поворота.
- Берём по этой рамке кусок с фона
- Поворачиваем ограничительную рамку в противоположную сторону, что бы потом текст вывести горизонтально
- Найдём размеры ограничительной рамку после поворота, тем самым мы узнаём размер холста, в который поместим промежуточный результат.
- Поворачиваем кусок фона.
- Выводим текст горизонтально
- Поворачиваем кусок фона с уже наложенным текстом на прежний угол
- Выводим на исходной изображение
- Всё.
Разумеется, нужно предусмотреть нюансы, например что бы не бороться с ошибкой округления при поворотах (у нас ведь минимальная единица это пиксели), то нужно взять от фона кусок с небольшим запасом, так же нужно учитывать когда текст может быть обрезан краем изображение и т.д.
rotatedtext.cpp/** * @brief Выводим текст с поворотом * @details Принцип прост - от исходной картинки вырезаем кусок по размеру рамки в которой будет повёрнутый текст, * так как текст мы можем выводить только горизонтально, то поворачиваем этот кусок фона на противоположный угол * пишем текст, * поворачиваем обратно и возвращаем кусок на своё место на исходной картинке * @param img - картинка, на которую выводить * @param text - сам текст надписи * @param textOrg - положение текста (левый верхний угол) * @param angle - на сколько градусов поворачивать * @param fontFace - стиль начертания * @param fontScale - маштаб * @param color - цвет */ static void putRotatedText(cv::Mat img, const cv::String &text, const cv::Point& textOrg, double angle, int fontFace, double fontScale, const cv::Scalar& color, int thickness, int lineType) { if ( text.length()==0 ) { return; } //-- Узнаём размер ограничительной рамки исходного текста const uint8_t margin =1; //-- Запас вокруг рамки текста, что бы не геммороиться с учётом погрешности округлений и т.д. int baseLine =0; cv::Size txtBB =cv::getTextSize(text, fontFace, fontScale, thickness, &baseLine); txtBB +=cv::Size(0, baseLine); //-- Нужно учитывать baseLine //-- Создаём матрицу вращения с поворотом на нужный угол относительно исходной точки cv::Mat rM =cv::getRotationMatrix2D(textOrg, angle, 1.0); //-- Узнаём размеры и положение ограничительной рамки при повороте std::vector<cv::Point> txtBBPoints ={cv::Point(-margin, -margin)+textOrg, cv::Point(txtBB.width+margin, -margin)+textOrg, cv::Point(txtBB.width+margin, txtBB.height+margin)+textOrg, cv::Point(-margin, txtBB.height+margin)+textOrg}; cv::transform(txtBBPoints, txtBBPoints, rM); //-- Узнаём ограничительную рамку, в которую входит рамка после поворота - что бы взять по ней кусок фона, т.к. мы копировать участок можем только соосно cv::Rect bgBB =cv::boundingRect(txtBBPoints); //-- Убеждаемся, что вообще будет что показать if ( bgBB.br().x<margin || bgBB.br().y<margin || bgBB.tl().x>=img.cols || bgBB.tl().y>=img.rows ) { return; } //-- Берём с исходной картинки фон cv::Mat bgImg =img(bgBB); //-- Поворачиваем ограничительную рамку в противоположную сторону, что бы потом текст вывести горизонтально std::vector<cv::Point> bgBBRevPoints ={bgBB.tl(), bgBB.tl()+cv::Point(bgBB.width, 0), bgBB.br(), bgBB.tl()+cv::Point(0, bgBB.height), textOrg}; cv::Mat rMRev =cv::getRotationMatrix2D(txtBBPoints[0], -angle, 1.0); cv::transform(bgBBRevPoints, bgBBRevPoints, rMRev); //-- Найдём ограничительную рамку после поворота предыдущей cv::Rect bgBBRev =cv::boundingRect(bgBBRevPoints); //-- Относительно левого верхнего угла поворачиваем, при этом уберём из минусов, что бы часть картинки не потерять cv::Point refP =cv::Point(bgBBRevPoints[0].x-bgBBRev.tl().x, bgBBRevPoints[0].y-bgBBRev.tl().y); rMRev.at<double>(0, 2) =refP.x; rMRev.at<double>(1, 2) =refP.y; cv::Mat ortImg; cv::warpAffine(bgImg, ortImg, rMRev, bgBBRev.size()); //-- Выводим текст cv::putText(ortImg, text, bgBBRevPoints[4]-bgBBRev.tl()+cv::Point(0, txtBB.height-baseLine), fontFace, fontScale, color, thickness, lineType); //-- Поворачиваем на прежний угол. Матрица для целевого угла у нас есть, осталось выяснить куда переносить картинку - вращаем относительно точки с предыдущего шага и переносим из минусов ей же const double rMA =rM.at<double>(0, 0), rMB =rM.at<double>(0, 1); rM.at<double>(0, 2) =(1-rMA)*refP.x - rMB*refP.y - refP.x; rM.at<double>(1, 2) =rMB*refP.x + (1-rMA)*refP.y - refP.y; cv::warpAffine(ortImg, ortImg, rM, bgBB.size()); //-- Копируем в исходну картинку, но только отрезаем на margin по сторонам из-за округлений что бы чёрной рамки не было ortImg(cv::Rect(margin, margin, bgBB.width-margin*2, bgBB.height-margin*2)).copyTo(img(cv::Rect(bgBB.x+margin, bgBB.y+margin, bgBB.width-margin*2, bgBB.height-margin*2))); }
Актуальную реализацию в коде можно взять на GitHub.
Вот как-то так.