mirror of
https://github.com/opencv/opencv.git
synced 2026-07-29 15:23:05 +04:00
Merge branch 4.x
This commit is contained in:
@@ -166,11 +166,11 @@ public:
|
||||
*/
|
||||
CV_WRAP std::vector<Point3f> getChessboardCorners() const;
|
||||
|
||||
/** @brief get CharucoBoard::nearestMarkerIdx
|
||||
/** @brief get CharucoBoard::nearestMarkerIdx, for each charuco corner, nearest marker index in ids array
|
||||
*/
|
||||
CV_PROP std::vector<std::vector<int> > getNearestMarkerIdx() const;
|
||||
|
||||
/** @brief get CharucoBoard::nearestMarkerCorners
|
||||
/** @brief get CharucoBoard::nearestMarkerCorners, for each charuco corner, nearest marker corner id of each marker
|
||||
*/
|
||||
CV_PROP std::vector<std::vector<int> > getNearestMarkerCorners() const;
|
||||
|
||||
|
||||
@@ -394,5 +394,69 @@ class aruco_objdetect_test(NewOpenCVTests):
|
||||
self.assertEqual(2, img_points.shape[1])
|
||||
np.testing.assert_array_equal(chessboard_corners, obj_points[:, :2].reshape(-1, 2))
|
||||
|
||||
def test_draw_detected_markers(self):
|
||||
detected_points = [[[10, 10], [50, 10], [50, 50], [10, 50]]]
|
||||
img = np.zeros((60, 60), dtype=np.uint8)
|
||||
|
||||
# add extra dimension in Python to create Nx4 Mat with 2 channels
|
||||
points1 = np.array(detected_points).reshape(-1, 4, 1, 2)
|
||||
img = cv.aruco.drawDetectedMarkers(img, points1, borderColor=255)
|
||||
|
||||
# check that the marker borders are painted
|
||||
contours, _ = cv.findContours(img, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)
|
||||
self.assertEqual(len(contours), 1)
|
||||
self.assertEqual(img[10, 10], 255)
|
||||
self.assertEqual(img[50, 10], 255)
|
||||
self.assertEqual(img[50, 50], 255)
|
||||
self.assertEqual(img[10, 50], 255)
|
||||
|
||||
# must throw Exception without extra dimension
|
||||
points2 = np.array(detected_points)
|
||||
with self.assertRaises(Exception):
|
||||
img = cv.aruco.drawDetectedMarkers(img, points2, borderColor=255)
|
||||
|
||||
def test_draw_detected_charuco(self):
|
||||
detected_points = [[[10, 10], [50, 10], [50, 50], [10, 50]]]
|
||||
img = np.zeros((60, 60), dtype=np.uint8)
|
||||
|
||||
# add extra dimension in Python to create Nx1 Mat with 2 channels
|
||||
points = np.array(detected_points).reshape(-1, 1, 2)
|
||||
img = cv.aruco.drawDetectedCornersCharuco(img, points, cornerColor=255)
|
||||
|
||||
# check that the 4 charuco corners are painted
|
||||
contours, _ = cv.findContours(img, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)
|
||||
self.assertEqual(len(contours), 4)
|
||||
for contour in contours:
|
||||
center_x = round(np.average(contour[:, 0, 0]))
|
||||
center_y = round(np.average(contour[:, 0, 1]))
|
||||
center = [center_x, center_y]
|
||||
self.assertTrue(center in detected_points[0])
|
||||
|
||||
# must throw Exception without extra dimension
|
||||
points2 = np.array(detected_points)
|
||||
with self.assertRaises(Exception):
|
||||
img = cv.aruco.drawDetectedCornersCharuco(img, points2, borderColor=255)
|
||||
|
||||
def test_draw_detected_diamonds(self):
|
||||
detected_points = [[[10, 10], [50, 10], [50, 50], [10, 50]]]
|
||||
img = np.zeros((60, 60), dtype=np.uint8)
|
||||
|
||||
# add extra dimension in Python to create Nx4 Mat with 2 channels
|
||||
points = np.array(detected_points).reshape(-1, 4, 1, 2)
|
||||
img = cv.aruco.drawDetectedDiamonds(img, points, borderColor=255)
|
||||
|
||||
# check that the diamonds borders are painted
|
||||
contours, _ = cv.findContours(img, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE)
|
||||
self.assertEqual(len(contours), 1)
|
||||
self.assertEqual(img[10, 10], 255)
|
||||
self.assertEqual(img[50, 10], 255)
|
||||
self.assertEqual(img[50, 50], 255)
|
||||
self.assertEqual(img[10, 50], 255)
|
||||
|
||||
# must throw Exception without extra dimension
|
||||
points2 = np.array(detected_points)
|
||||
with self.assertRaises(Exception):
|
||||
img = cv.aruco.drawDetectedDiamonds(img, points2, borderColor=255)
|
||||
|
||||
if __name__ == '__main__':
|
||||
NewOpenCVTests.bootstrap()
|
||||
|
||||
@@ -349,7 +349,7 @@ int quad_segment_maxima(const DetectorParameters &td, int sz, struct line_fit_pt
|
||||
}
|
||||
y[iy] = acc;
|
||||
}
|
||||
copy(y.begin(), y.end(), errs.begin());
|
||||
std::copy(y.begin(), y.end(), errs.begin());
|
||||
}
|
||||
|
||||
std::vector<int> maxima(sz);
|
||||
|
||||
@@ -319,8 +319,9 @@ struct CharucoBoardImpl : Board::Impl {
|
||||
// vector of chessboard 3D corners precalculated
|
||||
std::vector<Point3f> chessboardCorners;
|
||||
|
||||
// for each charuco corner, nearest marker id and nearest marker corner id of each marker
|
||||
// for each charuco corner, nearest marker index in ids array
|
||||
std::vector<std::vector<int> > nearestMarkerIdx;
|
||||
// for each charuco corner, nearest marker corner id of each marker
|
||||
std::vector<std::vector<int> > nearestMarkerCorners;
|
||||
|
||||
void createCharucoBoard();
|
||||
|
||||
@@ -1321,24 +1321,26 @@ void drawDetectedMarkers(InputOutputArray _image, InputArrayOfArrays _corners,
|
||||
int nMarkers = (int)_corners.total();
|
||||
for(int i = 0; i < nMarkers; i++) {
|
||||
Mat currentMarker = _corners.getMat(i);
|
||||
CV_Assert(currentMarker.total() == 4 && currentMarker.type() == CV_32FC2);
|
||||
CV_Assert(currentMarker.total() == 4 && currentMarker.channels() == 2);
|
||||
if (currentMarker.type() != CV_32SC2)
|
||||
currentMarker.convertTo(currentMarker, CV_32SC2);
|
||||
|
||||
// draw marker sides
|
||||
for(int j = 0; j < 4; j++) {
|
||||
Point2f p0, p1;
|
||||
p0 = currentMarker.ptr<Point2f>(0)[j];
|
||||
p1 = currentMarker.ptr<Point2f>(0)[(j + 1) % 4];
|
||||
Point p0, p1;
|
||||
p0 = currentMarker.ptr<Point>(0)[j];
|
||||
p1 = currentMarker.ptr<Point>(0)[(j + 1) % 4];
|
||||
line(_image, p0, p1, borderColor, 1);
|
||||
}
|
||||
// draw first corner mark
|
||||
rectangle(_image, currentMarker.ptr<Point2f>(0)[0] - Point2f(3, 3),
|
||||
currentMarker.ptr<Point2f>(0)[0] + Point2f(3, 3), cornerColor, 1, LINE_AA);
|
||||
rectangle(_image, currentMarker.ptr<Point>(0)[0] - Point(3, 3),
|
||||
currentMarker.ptr<Point>(0)[0] + Point(3, 3), cornerColor, 1, LINE_AA);
|
||||
|
||||
// draw ID
|
||||
if(_ids.total() != 0) {
|
||||
Point2f cent(0, 0);
|
||||
Point cent(0, 0);
|
||||
for(int p = 0; p < 4; p++)
|
||||
cent += currentMarker.ptr<Point2f>(0)[p];
|
||||
cent += currentMarker.ptr<Point>(0)[p];
|
||||
cent = cent / 4.;
|
||||
stringstream s;
|
||||
s << "id=" << _ids.getMat().ptr<int>(0)[i];
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "../precomp.hpp"
|
||||
|
||||
#include <opencv2/3d.hpp>
|
||||
#include <opencv2/core/utils/logger.hpp>
|
||||
#include "opencv2/objdetect/charuco_detector.hpp"
|
||||
#include "aruco_utils.hpp"
|
||||
|
||||
@@ -26,12 +27,13 @@ struct CharucoDetector::CharucoDetectorImpl {
|
||||
bool checkBoard(InputArrayOfArrays markerCorners, InputArray markerIds, InputArray charucoCorners, InputArray charucoIds) {
|
||||
vector<Mat> mCorners;
|
||||
markerCorners.getMatVector(mCorners);
|
||||
Mat mIds = markerIds.getMat();
|
||||
const Mat& mIds = markerIds.getMat();
|
||||
|
||||
Mat chCorners = charucoCorners.getMat();
|
||||
Mat chIds = charucoIds.getMat();
|
||||
const Mat& chCorners = charucoCorners.getMat();
|
||||
const Mat& chIds = charucoIds.getMat();
|
||||
const vector<int>& boardIds = board.getIds();
|
||||
|
||||
vector<vector<int> > nearestMarkerIdx = board.getNearestMarkerIdx();
|
||||
const vector<vector<int> >& nearestMarkerIdx = board.getNearestMarkerIdx();
|
||||
vector<Point2f> distance(board.getNearestMarkerIdx().size(), Point2f(0.f, std::numeric_limits<float>::max()));
|
||||
// distance[i].x: max distance from the i-th charuco corner to charuco corner-forming markers.
|
||||
// The two charuco corner-forming markers of i-th charuco corner are defined in getNearestMarkerIdx()[i]
|
||||
@@ -41,13 +43,19 @@ struct CharucoDetector::CharucoDetectorImpl {
|
||||
Point2f charucoCorner(chCorners.ptr<Point2f>(0)[i]);
|
||||
for (size_t j = 0ull; j < mIds.total(); j++) {
|
||||
int idMaker = mIds.ptr<int>(0)[j];
|
||||
// skip the check if the marker is not in the current board.
|
||||
if (find(boardIds.begin(), boardIds.end(), idMaker) == boardIds.end())
|
||||
continue;
|
||||
Point2f centerMarker((mCorners[j].ptr<Point2f>(0)[0] + mCorners[j].ptr<Point2f>(0)[1] +
|
||||
mCorners[j].ptr<Point2f>(0)[2] + mCorners[j].ptr<Point2f>(0)[3]) / 4.f);
|
||||
float dist = sqrt(normL2Sqr<float>(centerMarker - charucoCorner));
|
||||
// check distance from the charuco corner to charuco corner-forming markers
|
||||
if (nearestMarkerIdx[chId][0] == idMaker || nearestMarkerIdx[chId][1] == idMaker) {
|
||||
int nearestCornerId = nearestMarkerIdx[chId][0] == idMaker ? board.getNearestMarkerCorners()[chId][0] : board.getNearestMarkerCorners()[chId][1];
|
||||
// nearestMarkerIdx contains for each charuco corner, nearest marker index in ids array
|
||||
const int nearestMarkerId1 = boardIds[nearestMarkerIdx[chId][0]];
|
||||
const int nearestMarkerId2 = boardIds[nearestMarkerIdx[chId][1]];
|
||||
if (nearestMarkerId1 == idMaker || nearestMarkerId2 == idMaker) {
|
||||
int nearestCornerId = nearestMarkerId1 == idMaker ? board.getNearestMarkerCorners()[chId][0] : board.getNearestMarkerCorners()[chId][1];
|
||||
Point2f nearestCorner = mCorners[j].ptr<Point2f>(0)[nearestCornerId];
|
||||
// distToNearest: distance from the charuco corner to charuco corner-forming markers
|
||||
float distToNearest = sqrt(normL2Sqr<float>(nearestCorner - charucoCorner));
|
||||
distance[chId].x = max(distance[chId].x, distToNearest);
|
||||
// check that nearestCorner is nearest point
|
||||
@@ -363,6 +371,7 @@ void CharucoDetector::detectBoard(InputArray image, OutputArray charucoCorners,
|
||||
InputOutputArrayOfArrays markerCorners, InputOutputArray markerIds) const {
|
||||
charucoDetectorImpl->detectBoard(image, charucoCorners, charucoIds, markerCorners, markerIds);
|
||||
if (charucoDetectorImpl->checkBoard(markerCorners, markerIds, charucoCorners, charucoIds) == false) {
|
||||
CV_LOG_DEBUG(NULL, "ChArUco board is built incorrectly");
|
||||
charucoCorners.release();
|
||||
charucoIds.release();
|
||||
}
|
||||
@@ -511,20 +520,27 @@ void drawDetectedCornersCharuco(InputOutputArray _image, InputArray _charucoCorn
|
||||
InputArray _charucoIds, Scalar cornerColor) {
|
||||
CV_Assert(!_image.getMat().empty() &&
|
||||
(_image.getMat().channels() == 1 || _image.getMat().channels() == 3));
|
||||
CV_Assert((_charucoCorners.getMat().total() == _charucoIds.getMat().total()) ||
|
||||
_charucoIds.getMat().total() == 0);
|
||||
CV_Assert((_charucoCorners.total() == _charucoIds.total()) ||
|
||||
_charucoIds.total() == 0);
|
||||
CV_Assert(_charucoCorners.channels() == 2);
|
||||
|
||||
size_t nCorners = _charucoCorners.getMat().total();
|
||||
Mat charucoCorners = _charucoCorners.getMat();
|
||||
if (charucoCorners.type() != CV_32SC2)
|
||||
charucoCorners.convertTo(charucoCorners, CV_32SC2);
|
||||
Mat charucoIds;
|
||||
if (!_charucoIds.empty())
|
||||
charucoIds = _charucoIds.getMat();
|
||||
size_t nCorners = charucoCorners.total();
|
||||
for(size_t i = 0; i < nCorners; i++) {
|
||||
Point2f corner = _charucoCorners.getMat().at<Point2f>((int)i);
|
||||
Point corner = charucoCorners.at<Point>((int)i);
|
||||
// draw first corner mark
|
||||
rectangle(_image, corner - Point2f(3, 3), corner + Point2f(3, 3), cornerColor, 1, LINE_AA);
|
||||
rectangle(_image, corner - Point(3, 3), corner + Point(3, 3), cornerColor, 1, LINE_AA);
|
||||
// draw ID
|
||||
if(!_charucoIds.empty()) {
|
||||
int id = _charucoIds.getMat().at<int>((int)i);
|
||||
int id = charucoIds.at<int>((int)i);
|
||||
stringstream s;
|
||||
s << "id=" << id;
|
||||
putText(_image, s.str(), corner + Point2f(5, -5), FONT_HERSHEY_SIMPLEX, 0.5,
|
||||
putText(_image, s.str(), corner + Point(5, -5), FONT_HERSHEY_SIMPLEX, 0.5,
|
||||
cornerColor, 2);
|
||||
}
|
||||
}
|
||||
@@ -544,25 +560,27 @@ void drawDetectedDiamonds(InputOutputArray _image, InputArrayOfArrays _corners,
|
||||
int nMarkers = (int)_corners.total();
|
||||
for(int i = 0; i < nMarkers; i++) {
|
||||
Mat currentMarker = _corners.getMat(i);
|
||||
CV_Assert(currentMarker.total() == 4 && currentMarker.type() == CV_32FC2);
|
||||
CV_Assert(currentMarker.total() == 4 && currentMarker.channels() == 2);
|
||||
if (currentMarker.type() != CV_32SC2)
|
||||
currentMarker.convertTo(currentMarker, CV_32SC2);
|
||||
|
||||
// draw marker sides
|
||||
for(int j = 0; j < 4; j++) {
|
||||
Point2f p0, p1;
|
||||
p0 = currentMarker.at< Point2f >(j);
|
||||
p1 = currentMarker.at< Point2f >((j + 1) % 4);
|
||||
Point p0, p1;
|
||||
p0 = currentMarker.at<Point>(j);
|
||||
p1 = currentMarker.at<Point>((j + 1) % 4);
|
||||
line(_image, p0, p1, borderColor, 1);
|
||||
}
|
||||
|
||||
// draw first corner mark
|
||||
rectangle(_image, currentMarker.at< Point2f >(0) - Point2f(3, 3),
|
||||
currentMarker.at< Point2f >(0) + Point2f(3, 3), cornerColor, 1, LINE_AA);
|
||||
rectangle(_image, currentMarker.at<Point>(0) - Point(3, 3),
|
||||
currentMarker.at<Point>(0) + Point(3, 3), cornerColor, 1, LINE_AA);
|
||||
|
||||
// draw id composed by four numbers
|
||||
if(_ids.total() != 0) {
|
||||
Point2f cent(0, 0);
|
||||
Point cent(0, 0);
|
||||
for(int p = 0; p < 4; p++)
|
||||
cent += currentMarker.at< Point2f >(p);
|
||||
cent += currentMarker.at<Point>(p);
|
||||
cent = cent / 4.;
|
||||
stringstream s;
|
||||
s << "id=" << _ids.getMat().at< Vec4i >(i);
|
||||
|
||||
@@ -691,6 +691,58 @@ TEST(Charuco, testmatchImagePoints)
|
||||
}
|
||||
}
|
||||
|
||||
typedef testing::TestWithParam<int> CharucoDraw;
|
||||
INSTANTIATE_TEST_CASE_P(/**/, CharucoDraw, testing::Values(CV_8UC2, CV_8SC2, CV_16UC2, CV_16SC2, CV_32SC2, CV_32FC2, CV_64FC2));
|
||||
TEST_P(CharucoDraw, testDrawDetected) {
|
||||
vector<vector<Point>> detected_golds = {{Point(20, 20), Point(80, 20), Point(80, 80), Point2f(20, 80)}};
|
||||
Point center_gold = (detected_golds[0][0] + detected_golds[0][1] + detected_golds[0][2] + detected_golds[0][3]) / 4;
|
||||
int type = GetParam();
|
||||
vector<Mat> detected(detected_golds[0].size(), Mat(4, 1, type));
|
||||
// copy detected_golds to detected with any 2 channels type
|
||||
for (size_t i = 0ull; i < detected_golds[0].size(); i++) {
|
||||
detected[0].row((int)i) = Scalar(detected_golds[0][i].x, detected_golds[0][i].y);
|
||||
}
|
||||
vector<vector<Point>> contours;
|
||||
Point detectedCenter;
|
||||
Moments m;
|
||||
Mat img;
|
||||
|
||||
// check drawDetectedMarkers
|
||||
img = Mat::zeros(100, 100, CV_8UC1);
|
||||
ASSERT_NO_THROW(aruco::drawDetectedMarkers(img, detected, noArray(), Scalar(255, 255, 255)));
|
||||
// check that the marker borders are painted
|
||||
findContours(img, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
|
||||
ASSERT_EQ(contours.size(), 1ull);
|
||||
m = moments(contours[0]);
|
||||
detectedCenter = Point(cvRound(m.m10/m.m00), cvRound(m.m01/m.m00));
|
||||
ASSERT_EQ(detectedCenter, center_gold);
|
||||
|
||||
|
||||
// check drawDetectedCornersCharuco
|
||||
img = Mat::zeros(100, 100, CV_8UC1);
|
||||
ASSERT_NO_THROW(aruco::drawDetectedCornersCharuco(img, detected[0], noArray(), Scalar(255, 255, 255)));
|
||||
// check that the 4 charuco corners are painted
|
||||
findContours(img, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
|
||||
ASSERT_EQ(contours.size(), 4ull);
|
||||
for (size_t i = 0ull; i < 4ull; i++) {
|
||||
m = moments(contours[i]);
|
||||
detectedCenter = Point(cvRound(m.m10/m.m00), cvRound(m.m01/m.m00));
|
||||
// detectedCenter must be in detected_golds
|
||||
ASSERT_TRUE(find(detected_golds[0].begin(), detected_golds[0].end(), detectedCenter) != detected_golds[0].end());
|
||||
}
|
||||
|
||||
|
||||
// check drawDetectedDiamonds
|
||||
img = Mat::zeros(100, 100, CV_8UC1);
|
||||
ASSERT_NO_THROW(aruco::drawDetectedDiamonds(img, detected, noArray(), Scalar(255, 255, 255)));
|
||||
// check that the diamonds borders are painted
|
||||
findContours(img, contours, RETR_EXTERNAL, CHAIN_APPROX_SIMPLE);
|
||||
ASSERT_EQ(contours.size(), 1ull);
|
||||
m = moments(contours[0]);
|
||||
detectedCenter = Point(cvRound(m.m10/m.m00), cvRound(m.m01/m.m00));
|
||||
ASSERT_EQ(detectedCenter, center_gold);
|
||||
}
|
||||
|
||||
typedef testing::TestWithParam<cv::Size> CharucoBoard;
|
||||
INSTANTIATE_TEST_CASE_P(/**/, CharucoBoard, testing::Values(Size(3, 2), Size(3, 2), Size(6, 2), Size(2, 6),
|
||||
Size(3, 4), Size(4, 3), Size(7, 3), Size(3, 7)));
|
||||
@@ -719,4 +771,60 @@ TEST_P(CharucoBoard, testWrongSizeDetection)
|
||||
ASSERT_TRUE(detectedCharucoIds.empty());
|
||||
}
|
||||
|
||||
// Temporary disabled in https://github.com/opencv/opencv/pull/24338
|
||||
// 5.x version produces conrnes with different shape than 4.x (32F_C2 instead of 2x 32FC1)
|
||||
TEST(Charuco, DISABLED_testSeveralBoardsWithCustomIds)
|
||||
{
|
||||
Size res{500, 500};
|
||||
Mat K = (Mat_<double>(3,3) <<
|
||||
0.5*res.width, 0, 0.5*res.width,
|
||||
0, 0.5*res.height, 0.5*res.height,
|
||||
0, 0, 1);
|
||||
|
||||
Mat expected_corners = (Mat_<float>(9,2) <<
|
||||
200, 200,
|
||||
250, 200,
|
||||
300, 200,
|
||||
200, 250,
|
||||
250, 250,
|
||||
300, 250,
|
||||
200, 300,
|
||||
250, 300,
|
||||
300, 300
|
||||
);
|
||||
|
||||
|
||||
aruco::Dictionary dict = cv::aruco::getPredefinedDictionary(aruco::DICT_4X4_50);
|
||||
vector<int> ids1 = {0, 1, 33, 3, 4, 5, 6, 8}, ids2 = {7, 9, 44, 11, 12, 13, 14, 15};
|
||||
aruco::CharucoBoard board1(Size(4, 4), 1.f, .8f, dict, ids1), board2(Size(4, 4), 1.f, .8f, dict, ids2);
|
||||
|
||||
// generate ChArUco board
|
||||
Mat gray;
|
||||
{
|
||||
Mat gray1, gray2;
|
||||
board1.generateImage(Size(res.width, res.height), gray1, 150);
|
||||
board2.generateImage(Size(res.width, res.height), gray2, 150);
|
||||
hconcat(gray1, gray2, gray);
|
||||
}
|
||||
|
||||
aruco::CharucoParameters charucoParameters;
|
||||
charucoParameters.cameraMatrix = K;
|
||||
aruco::CharucoDetector detector1(board1, charucoParameters), detector2(board2, charucoParameters);
|
||||
|
||||
vector<int> ids;
|
||||
vector<Mat> corners;
|
||||
Mat c_ids1, c_ids2, c_corners1, c_corners2;
|
||||
|
||||
detector1.detectBoard(gray, c_corners1, c_ids1, corners, ids);
|
||||
detector2.detectBoard(gray, c_corners2, c_ids2, corners, ids);
|
||||
|
||||
ASSERT_EQ(ids.size(), size_t(16));
|
||||
ASSERT_EQ(c_corners1.rows, expected_corners.rows);
|
||||
EXPECT_NEAR(0, cvtest::norm(expected_corners, c_corners1.reshape(1), NORM_INF), 3e-1);
|
||||
|
||||
ASSERT_EQ(c_corners2.rows, expected_corners.rows);
|
||||
expected_corners.col(0) += 500;
|
||||
EXPECT_NEAR(0, cvtest::norm(expected_corners, c_corners2.reshape(1), NORM_INF), 3e-1);
|
||||
}
|
||||
|
||||
}} // namespace
|
||||
|
||||
Reference in New Issue
Block a user