Hello. I’m rotarymars.
First of all, I’d like to announce the update frequency of the blog again.
ALPAKA | The remainder when the month is divided by 3 is 0 or 1 |
K10-K10 | The remainder when the month is divided by 3 is 0 or 2 |
rotarymars | The remainder when the month is divided by 3 is 1 or 2 |
We will update the article once a month in these months, so please look forward to us.
About the content of this article
This time, I’d like to write about the line tracing program that we are currently considering.
The program is about this point.
First, what will we use to do line tracing?
We want to continue to use the camera for line tracing this year.
Specifically, we want to connect a camera like this to the Raspberrypi and get the image to do line tracing.
I just want to let you know that we haven’t been able to test the actual run because the robot hasn’t been completed yet.
Actual program
The program is quite long, so I’ll paste it step by step. Also, it’s very ugly, so it may be difficult to read (I want to make a class someday, but I haven’t done anything yet).
By the way, this time, we’ll look at modules/settings.py, which is particularly related to line tracing.
from libcamera import controls
from picamera2 import MappedArray
import cv2
import time
import numpy as np
import threading
# 途中略
def Linetrace_Camera_Pre_callback(request):
if DEBUG_MODE:
print("Linetrace precallback called", str(time.time()))
global lastblackline, slope
try:
with MappedArray(request, "lores") as m:
image = m.array
camera_x = Linetrace_Camera_lores_width
camera_y = Linetrace_Camera_lores_height
if DEBUG_MODE:
cv2.imwrite(f"bin/{str(time.time())}_original.jpg", image)
gray_image = cv2.cvtColor(image, cv2.COLOR_RGB2GRAY)
_, binary_image = cv2.threshold(gray_image, Black_White_Threshold, 255,
cv2.THRESH_BINARY_INV)
if DEBUG_MODE:
cv2.imwrite(f"bin/{str(time.time())}_binary.jpg", binary_image)
kernel = np.ones((3, 3), np.uint8)
binary_image = cv2.erode(binary_image, kernel, iterations=2)
binary_image = cv2.dilate(binary_image, kernel, iterations=3)
detect_green_marks(image, binary_image)
contours, _ = cv2.findContours(binary_image, cv2.RETR_TREE,
cv2.CHAIN_APPROX_NONE)
if not contours:
return
best_contour = find_best_contour(contours, camera_x, camera_y,
lastblackline)
if best_contour is None:
return
cx, cy = calculate_contour_center(best_contour)
with LASTBLACKLINE_LOCK:
lastblackline = cx
with SLOPE_LOCK:
slope = calculate_slope(best_contour, cx, cy)
if DEBUG_MODE:
debug_image = visualize_tracking(image, best_contour, cx, cy)
cv2.imwrite(f"bin/{str(time.time())}_tracking.jpg", debug_image)
except Exception as e:
if DEBUG_MODE:
print(f"Error in line tracing: {e}")
Let’s look at it step by step.
Line 1 ~ 6
As you can see, we’re including the necessary libraries (in Python, they’re called modules) and importing them.
Line 8 ~
It’s finally time for the explanation.
First, let’s explain what precallback is.
In Raspberrypi, you can preview the camera image by using the rpicam-hello
command (the command name may be different).
The command is a program that sends an image acquisition request and then displays the image.
The thing used at that time is the picamera2
library (module).
Specifically, you can specify a precallback function
when generating the camera object.
It is None
by default, and there is no hook when the image is acquired.
However, by registering a function here, the object is passed to the first argument of the function (in this case, request
).
By doing this, the image data stored in request
can be obtained as a numpy
array, lores
quality.
This way, we can avoid capturing the image every time it is needed, and we can get the image faster (the processing inside the function is heavy, so I don’t know how to say it, but if we don’t do anything, we can get it at about 20msec/frame).
However, if there is an operation that consumes a lot of CPU resources in that function, if the camera is not stopped when it is not in use, it will consume a lot of power.
Therefore, the camera should be started and stopped separately when it is in use and not in use.
Line 9 ~ 10
As you can see, when DEBUG_MODE is enabled, the UNIX TIME when the function is called is printed.
Line 12
Global variables are declared.
Let’s talk about the variables used here.
Variable name | Usage |
---|---|
lastblackline | The center coordinate of the previous black line |
slope | The slope of the black line |
By making these global variables, we can access them from the main process.
Line 14 ~ 22
Let’s ignore all exceptions for now.
The image is stored in the variable image
as a numpy array.
camera_x
and camera_y
store the vertical and horizontal number of pixels of the camera.
And when DEBUG_MODE is enabled, the original image is saved.
Line 24 ~ 30
The image is converted to grayscale. This is because we want to binarize the image later.
In line 26, the image binarized with Black_White_Threshold
as the threshold is stored in binary_image
. The ones that are close to black become white, and the ones that are close to white become black.
Here’s a side note, but the reason for converting to grayscale is that we can’t binarize a color image directly into black and white. If we apply it directly to the function, each color (r, g, b) will be binarized as each threshold, and a highly saturated (?) image will be created (I recommend actually seeing it for explanation).
And when DEBUG_MODE
is enabled, the binarized image is saved.
Line 32 ~ 35
Here, we remove the noise contained in the binarized image.
Let’s explain how each function is doing.
kernel = np.ones((3, 3), np.uint8)
This creates a 3x3 numpy
two-dimensional array. All elements are 1
.
binary_image = cv2.erode(binary_image, kernel, iterations=2)
Using the kernel
we just created, we remove the noise from binary_image
. Specifically, if one of the pixels in the range of kernel
is black (0), the center pixel is converted to black (0).
This way, the white blocks become smaller and the noise is removed.
By the way, the black and white determination is made by whether the number of each element of kernel
is below or above.
Since iterations=2
, this is executed twice.
binary_image = cv2.dilate(binary_image, kernel, iterations=3)
This is the opposite of erode
. That is, the black blocks become larger.
Specifically, if one of the pixels in the range of kernel
is white (1), the center pixel is converted to white (1).
Since iterations=3
, this is executed three times.
Line 36
Here, we recognize the green marks, and perform the relative position determination. I’ll explain the function later.
Line 37 ~ 45
contours
is recognized using the cv2 function.
Then, using the find_best_contour
function we created ourselves, we get the most appropriate contour and store it in best_contour
.
Line 47 ~ 50
Here, we get the center coordinates of best_contour
.
If best_contour
is not found, the process is ended as is.
Line 53 ~ 56
Here, we update lastblackline
and slope
.
lastblackline
represents the x-coordinate of blackline
, and is used to find best_contour
.
slope
is the slope of the black line.
Specifically, the slope from the center of the bottom of the image to the center of best_contour
is calculated.
By the way, with ..._LOCK:
is locking the ..._LOCK
object.
Locking is a mechanism to prevent other processes from using a variable when a certain process is using it.
This way, the writing to this variable becomes atomic (exclusive) (it is guaranteed that it will not become strange if it is edited at the same time. It is also necessary to set it when reading).
※I’m not very familiar with it, but I think it’s probably an OS-level operation, so it’s better not to use it too much to be able to process faster.
Line 57 ~
When DEBUG_MODE
is enabled, the image is saved.
And then, we’re handling exceptions. It’s simple, isn’t it?
Finally
I hope you understand how the program is working. I want to fix the part where the image passed as an argument to the function that detects the green mark is directly overwritten as soon as possible.
This time, I haven’t been able to explain all the functions (because I didn’t have much time), but I hope you understand what I’m doing.
I want the team members to read this blog and understand the contents of the program.
Thank you.
↑↑I also want to follow K10-K10, so I’ll add it at the end.↑↑