Phát hin chng vòng

Phát hin chng vòng

Phương pháp phát hiện và xác định vị trí chồng vòng bằng thị giác máy tính.

Phương pháp phát hiện và xác định vị trí chồng vòng bằng thị giác máy tính.

Level

Intermediate

Source

Source

Author

Author

FTC Lib

FTC Lib

Translator

Translator

FTC26749 aDudu

FTC26749 aDudu

Date Published

Date Published

Jan 18, 2026

Jan 18, 2026

UGRectRingPipeline

Trái tim của bộ phát hiện Ultimate Goal chính là pipeline. Pipeline chỉ là một cách gọi “hoa mỹ” để mô tả chuỗi các chỉ dẫn được thực thi liên tục nhằm thao tác trên hình ảnh (trong trường hợp này là hình ảnh mà camera nhìn thấy). Nếu bỏ qua phần code phức tạp, pipeline có thể được rút gọn thành các bước sau:

Nhận dữ liệu đầu vào (Receiving the Input)

public Mat processFrame(Mat input)
public Mat processFrame(Mat input)

Ở dòng này, những gì camera nhìn thấy được truyền vào và biểu diễn dưới dạng một ma trận (matrix).

Xử lý dữ liệu đầu vào (Manipulating the Input)

Imgproc.cvtColor(input, matYCrCb, Imgproc.COLOR_RGB2YCrCb);
Rect topRect = new Rect(
    (int) (matYCrCb.width() * topRectWidthPercentage),
    (int) (matYCrCb.height() * topRectHeightPercentage),
    rectangleWidth,
    rectangleHeight
);
Rect bottomRect = new Rect(
    (int) (matYCrCb.width() * bottomRectWidthPercentage),
    (int) (matYCrCb.height() * bottomRectHeightPercentage),
    rectangleWidth,
    rectangleHeight
);
//The rectangle is drawn into the mat
drawRectOnToMat(input, topRect, new Scalar(255, 0, 0));
drawRectOnToMat(input, bottomRect, new Scalar(0, 255, 0));
Imgproc.cvtColor(input, matYCrCb, Imgproc.COLOR_RGB2YCrCb);
Rect topRect = new Rect(
    (int) (matYCrCb.width() * topRectWidthPercentage),
    (int) (matYCrCb.height() * topRectHeightPercentage),
    rectangleWidth,
    rectangleHeight
);
Rect bottomRect = new Rect(
    (int) (matYCrCb.width() * bottomRectWidthPercentage),
    (int) (matYCrCb.height() * bottomRectHeightPercentage),
    rectangleWidth,
    rectangleHeight
);
//The rectangle is drawn into the mat
drawRectOnToMat(input, topRect, new Scalar(255, 0, 0));
drawRectOnToMat(input, bottomRect, new Scalar(0, 255, 0));

Dòng đầu tiên sẽ chuyển không gian màu của ma trận đầu vào từ RGB sang YCrCb.
Do cách YCrCb biểu diễn màu sắc bằng:

  • Y: độ chói (luminance)

  • Cr: sắc độ đỏ (chroma of red)

  • Cb: sắc độ xanh lam (chroma of blue)

nên các giá trị màu sẽ ổn định hơn dưới các điều kiện ánh sáng khác nhau.

Tiếp theo, ta vẽ 2 hình chữ nhật lên màn hình với vị trí được xác định trước:

  • Hình chữ nhật phía trên: nơi vòng thứ 4 sẽ xuất hiện

  • Hình chữ nhật phía dưới: nơi vòng đầu tiên sẽ xuất hiện

Sau đó, ta trích xuất giá trị Cb của từng vùng xác định để so sánh.

Tìm giá trị Cb (Finding CB Values)

topBlock = matYCrCb.submat(topRect);
bottomBlock = matYCrCb.submat(bottomRect);
Core.extractChannel(bottomBlock, matCbBottom, 2);
Core.extractChannel(topBlock, matCbTop, 2);
Scalar bottomMean = Core.mean(matCbBottom);
Scalar topMean = Core.mean(matCbTop);
bottomAverage = bottomMean.val[0];
topAverage = topMean.val[0];
topBlock = matYCrCb.submat(topRect);
bottomBlock = matYCrCb.submat(bottomRect);
Core.extractChannel(bottomBlock, matCbBottom, 2);
Core.extractChannel(topBlock, matCbTop, 2);
Scalar bottomMean = Core.mean(matCbBottom);
Scalar topMean = Core.mean(matCbTop);
bottomAverage = bottomMean.val[0];
topAverage = topMean.val[0];

Ở đây, ta cắt ma trận để chỉ giữ phần nằm bên trong hai hình chữ nhật.
Sau đó, ta tính giá trị trung bình của các pixel trong từng vùng và lưu chúng vào:

  • bottomAverage

  • topAverage

Tạo một instance của UGRectDetector

UGRectDetector là một class minh họa cách sử dụng pipeline.
Để có giải thích chi tiết hơn về từng thành phần hoặc các chức năng mở rộng, vui lòng truy cập tại đây.

Đầu tiên, tạo một instance của UGRectDetector. Constructor của detector được overload, bạn có thể chọn một trong hai:

public UGRectDetector(HardwareMap hMap)
public UGRectDetector(HardwareMap hMap, String webcamName)
public UGRectDetector(HardwareMap hMap)
public UGRectDetector(HardwareMap hMap, String webcamName)
  • hMap: một instance của HardwareMap

  • webcamName: tên của webcam

Nếu sử dụng constructor thứ nhất, detector sẽ dùng camera của điện thoại. Nếu sử dụng constructor thứ hai, detector sẽ dùng webcam.

Điều chỉnh cài đặt Detector (Manipulating Detector Settings)

Bạn có thể thay đổi hướng, chiều rộng và chiều cao của camera cho tất cả các instance (vì camera không thay đổi giữa các lần chạy).
Bạn cập nhật các cài đặt này bằng cách thay đổi các biến static:

// change the height and width of the camera
UGRectDetector.CAMERA_WIDTH = 320;
UGRectDetector.CAMERA_HEIGHT = 240;
// change the orientation of the camera
UGRectDetector.ORIENTATION = OpenCvCameraRotation.UPRIGHT;
// change the height and width of the camera
UGRectDetector.CAMERA_WIDTH = 320;
UGRectDetector.CAMERA_HEIGHT = 240;
// change the orientation of the camera
UGRectDetector.ORIENTATION = OpenCvCameraRotation.UPRIGHT;

Thiết lập vị trí hình chữ nhật (Setting Rectangle Positions)

public void setTopRectangle(double topRectHeightPercentage, double topRectWidthPercentage)
public void setTopRectangle(double topRectHeightPercentage, double topRectWidthPercentage)
  • topRectHeightPercentage: phần trăm chiều cao của ảnh đầu vào (dưới 1), dùng để tính tọa độ y đầu tiên của hình chữ nhật trên

  • topRectWidthPercentage: phần trăm chiều rộng của ảnh đầu vào (dưới 1), dùng để tính tọa độ x đầu tiên của hình chữ nhật trên

public void setBottomRectangle(double bottomRectHeightPercentage, double bottomRectWidthPercentage)
public void setBottomRectangle(double bottomRectHeightPercentage, double bottomRectWidthPercentage)
  • bottomRectHeightPercentage: phần trăm chiều cao của ảnh đầu vào (dưới 1), dùng để tính tọa độ y đầu tiên của hình chữ nhật dưới

  • bottomRectWidthPercentage: phần trăm chiều rộng của ảnh đầu vào (dưới 1), dùng để tính tọa độ x đầu tiên của hình chữ nhật dưới

public void setRectangleSize(int rectangleWidth, int rectangleHeight)
public void setRectangleSize(int rectangleWidth, int rectangleHeight)
  • rectangleWidth: chiều rộng hình chữ nhật (pixel)

  • rectangleHeight: chiều cao hình chữ nhật (pixel)

Sau khi tạo detector và thiết lập vị trí hình chữ nhật, hãy liên tục gọi:

DetectorInstance.getStack()
DetectorInstance.getStack()

để lấy số vòng trong chồng.

UGContourRingPipeline

Vision Pipelines là trái tim của mọi Ultimate Goal Detector. Pipeline là tập hợp các chỉ dẫn được áp dụng cho mỗi frame từ camera.

Pipeline này sử dụng Contours (đường bao)Aspect Ratio (tỉ lệ hình dạng) để xác định số vòng hiện có trong chồng vòng.

Sử dụng Detector (Using the Detector)

Contour pipeline đi kèm một detector dựng sẵn có thể dùng ngay trong opmode.
UGContourRingDetector là object chạy pipeline này.

Để tạo detector, có nhiều constructor khác nhau (một số có tham số tùy chọn):

// tạo detector dùng camera điện thoại
detector = new UGContourRingDetector(
    hardwareMap, OpenCvInternalCamera.CameraDirection.BACK,
    telemetry, true
);
// tạo detector dùng webcam
detector = new UGContourRingDetector(
    hardwareMap, "webcam"
);
// tạo detector dùng webcam có telemetry debug
detector = new UGContourRingDetector(
    hardwareMap, "webcam", telemetry, true
);
// tạo detector dùng camera điện thoại
detector = new UGContourRingDetector(
    hardwareMap, OpenCvInternalCamera.CameraDirection.BACK,
    telemetry, true
);
// tạo detector dùng webcam
detector = new UGContourRingDetector(
    hardwareMap, "webcam"
);
// tạo detector dùng webcam có telemetry debug
detector = new UGContourRingDetector(
    hardwareMap, "webcam", telemetry, true
);

Bạn có thể điều chỉnh các cài đặt của contour detector tương tự detector hình chữ nhật.

UGContourRingDetector.PipelineConfiguration
    .setCAMERA_WIDTH(320);
UGContourRingDetector.PipelineConfiguration
    .setCAMERA_HEIGHT(240);
    
UGContourRingDetector.PipelineConfiguration
    .setHORIZON(100);
    
UGContourRingDetector.PipelineConfiguration
    .setCAMERA_ORIENTATION(OpenCvCameraRotation.UPRIGHT);
UGContourRingDetector.PipelineConfiguration
    .setCAMERA_WIDTH(320);
UGContourRingDetector.PipelineConfiguration
    .setCAMERA_HEIGHT(240);
    
UGContourRingDetector.PipelineConfiguration
    .setHORIZON(100);
    
UGContourRingDetector.PipelineConfiguration
    .setCAMERA_ORIENTATION(OpenCvCameraRotation.UPRIGHT);

Khởi tạo detector

detector.init();
detector.init();

Lệnh này khởi tạo pipeline với các cài đặt đã cấu hình.
Để lấy chiều cao (số vòng) mà pipeline xác định, gọi:

DetectorInstance.getHeight()
DetectorInstance.getHeight()

Tinh chỉnh (Tuning)

Pipeline có nhiều giá trị có thể điều chỉnh để tăng hoặc giảm độ chính xác.

Tất cả các giá trị cấu hình được lưu trong companion object tên là Config.
Trong Config có 6 biến, trong đó 2 biến là hằng số và không thể thay đổi.

  • lowerOrange: ngưỡng cam dưới dùng khi tạo mask

  • upperOrange: ngưỡng cam trên dùng khi tạo mask

  • CAMERA_WIDTH: độ phân giải chiều rộng của camera

  • HORIZON: giá trị đường chân trời trên trục y, dùng cho kiểm tra horizon

  • MIN_WIDTH: giá trị chiều rộng tối thiểu sinh ra bằng thuật toán

  • BOUND_RATIO: giá trị tỉ lệ hình dùng để phân biệt 1 vòng và 4 vòng

HORIZON là giá trị bạn rất có thể phải tinh chỉnh. Giá trị mặc định có thể quá nghiêm ngặt hoặc không.

Giá trị này sẽ hiển thị trên ảnh dưới dạng đường màu đỏ. Mọi thứ nằm trên đường đỏ sẽ bị bỏ qua trong tính toán pipeline. Hãy đảm bảo rằng cạnh dưới của khung contour chồng vòng nằm dưới đường horizon.

LƯU Ý:
Ảnh trả về từ pipeline sẽ chủ yếu là màu đen để minh họa phần logic bị bỏ qua.

  • Tất cả contour được vẽ màu xanh lá

  • Hình chữ nhật bao quanh contour lớn nhất dưới horizon được vẽ màu xanh dương

Do Config là companion object, nên mọi instance của UGContourRingPipeline đều dùng chung cấu hình.

Các giá trị này có thể thay đổi trong các bản phát hành tương lai.

Giải thích thuật toán của Pipeline

Nhận dữ liệu đầu vào

public Mat processFrame(Mat input)
public Mat processFrame(Mat input)

Những gì camera nhìn thấy được truyền vào pipeline dưới dạng OpenCV Mat (viết tắt của matrix).

Xử lý dữ liệu đầu vào

Imgproc.cvtColor(input, mat, Imgproc.COLOR_RGB2YCrCb);
Imgproc.cvtColor(input, mat, Imgproc.COLOR_RGB2YCrCb);

Đầu tiên, ta chuyển Mat sang không gian màu YCrCb để hỗ trợ threshold tốt hơn.

Mat mask = new Mat(mat.rows(), mat.cols(), CvType.CV_8UC1);
Core.inRange(mat, lowerOrange, upperOrange, mask);
Mat mask = new Mat(mat.rows(), mat.cols(), CvType.CV_8UC1);
Core.inRange(mat, lowerOrange, upperOrange, mask);

Ta thực hiện phép inRange và lưu kết quả vào mask.
Mask là ảnh trắng–đen:

  • Pixel trắng: nằm trong ngưỡng màu cam

  • Pixel đen: không nằm trong ngưỡng màu cam

Imgproc.GaussianBlur(mask, mask, Size(5.0, 15.0), 0.00)
Imgproc.GaussianBlur(mask, mask, Size(5.0, 15.0), 0.00)

Dùng Gaussian Blur để loại bỏ nhiễu giữa các vòng trong chồng.
Do ánh sáng hoặc bóng, có thể xuất hiện khoảng trống không mong muốn.

Tìm Contours

Contour là đường cong nối các điểm liên tục có cùng màu hoặc cường độ.
Contour rất hữu ích trong phân tích hình dạng và nhận dạng vật thể.

List<MatOfPoint> contours = new ArrayList<>();
Mat hierarchy = new Mat();
Imgproc.findContours(mask, contours, hierarchy, Imgproc.RETR_TREE, Imgproc.CHAIN_APPROX_NONE);
List<MatOfPoint> contours = new ArrayList<>();
Mat hierarchy = new Mat();
Imgproc.findContours(mask, contours, hierarchy, Imgproc.RETR_TREE, Imgproc.CHAIN_APPROX_NONE);

Sau khi tìm contour, ta duyệt tuyến tính qua danh sách contour.
Với mỗi contour:

  • Tính bounding rectangle

  • Chọn rectangle có chiều rộng lớn nhất

Mục đích là tránh nhầm chồng vòng với các vật thể màu cam khác.
Chồng vòng thường là khối màu cam lớn nhất trong tầm nhìn.

int maxWidth = 0;
Rect maxRect = new Rect();
for (MatOfPoint c : contours) {
    MathOfPoint2f copy = new MatOfPoint2f(c.toArray());
    Rect rect = Imgproc.boundingRect(copy);
    int w = rect.width;
    // checking if the rectangle is below the horizon
    if (w > maxWidth && rect.y + rect.height > HORIZON) {
        maxWidth = w;
        maxRect = rect;
    }
    c.release();
    copy.release();
}
int maxWidth = 0;
Rect maxRect = new Rect();
for (MatOfPoint c : contours) {
    MathOfPoint2f copy = new MatOfPoint2f(c.toArray());
    Rect rect = Imgproc.boundingRect(copy);
    int w = rect.width;
    // checking if the rectangle is below the horizon
    if (w > maxWidth && rect.y + rect.height > HORIZON) {
        maxWidth = w;
        maxRect = rect;
    }
    c.release();
    copy.release();
}

Pipeline cũng có kiểm tra horizon:
Bất cứ thứ gì nằm trên horizon đều bị bỏ qua, kể cả khi có chiều rộng lớn nhất.
Điều này giúp tránh nhầm red goal với vòng cam, vì YCrCb rất kém trong việc phân biệt đỏ và cam.

Tính Aspect Ratio

double aspectRatio = maxRect.getHeight() / maxRect.getWidth();
height = maxWidth >= MIN_WIDTH ? aspectRatio > BOUND_RATIO ? FOUR : ONE : ZERO;
double aspectRatio = maxRect.getHeight() / maxRect.getWidth();
height = maxWidth >= MIN_WIDTH ? aspectRatio > BOUND_RATIO ? FOUR : ONE : ZERO;

Hoặc tương đương:

if (maxWidth >= MIN_WIDTH) {
    if (aspectRatio > BOUND_RATIO) {
        height = FOUR;
    } else {
        height = ONE;
    }
} else {
    height = ZERO;
}
if (maxWidth >= MIN_WIDTH) {
    if (aspectRatio > BOUND_RATIO) {
        height = FOUR;
    } else {
        height = ONE;
    }
} else {
    height = ZERO;
}

Sau khi xác định contour rộng nhất (giả định là chồng vòng), ta tính tỉ lệ chiều cao / chiều rộng của bounding rectangle.

Câu hỏi thường gặp

Vì sao không chỉ đo chiều cao của bounding rectangle?
→ Vì độ phân giải camera khác nhau sẽ cho chiều cao pixel khác nhau dù đều là 4 vòng.

Nhưng bạn vẫn dùng chiều rộng (pixel) mà?
→ Đúng, nhưng chiều rộng chồng vòng là hằng số (luôn chỉ rộng bằng 1 vòng).
Nhờ vậy ta có thể sinh ra MIN_WIDTH bằng thuật toán, còn chiều cao thì không.

ADUDU

A proud team of passionate Robotics Enthusiasts competing in nation-wide Technology competitions in Vietnam, the FIRST Tech Challenge and the FIRST Robotics Competition.

Copyright ©

, all rights reserved

Made by aDudu's Programming Department

made by aDudu

made by aDudu

Phát hiện chồng vòng