Closed MohammadKhan-3 closed 2 years ago
Flip it about about both horizontal axis to try to get the shape in the correct orientation.
The correct orientation is below the x-axis.
Tried resizing the image after the convolution to back to its original size which resulted in the heat map in the correct location but still flipped about the x-axis.
I then tried the cv2.flip() function about both the x & y axis which pushes the heat map to the correct location.
Changed kernel to 5x5
Plan of Action: (In progress as of 11/8)
- Find centers of contours
Current Error:
~~`raise Exception(("Contours tuple must have length 2 or 3, "
Exception: Contours tuple must have length 2 or 3, otherwise OpenCV changed their cv2.findContours return signature yet again. Refer to Open CV's documentation in that case'~~
- need a way to filter out other blue, red, green detected from the actual square
Used cv2.applyColorMap() on the frame to create a heatmap from that to get a gradient of value
The image on the left is the filter2D output while the image on the right is the colorMap output
The colorMap provides a range of values while the filte2D just gives [255,255,255] (white) or [0,255,255] (Cyan)
Below is applying Kernel (5x5 [0,0,255]) to the color map
Found the first issue. I think the kernel was being assembled incorrectly.
Original errored kernel creation
kernel_size = 25
kernel_blue = np.zeros((kernel_size,kernel_size)) # creating an empty array
kernel_blue = np.append(kernel_blue,[0,0,255])
kernel_blue = np.asanyarray(kernel_blue,np.float32)
This creates a float32
array of shape {628,1} containing values of either 0 or 255. Not the desired kernel.
Suggested correction:
Note, I'm using uint8
data type to agree with the imported image.
# Create kernel (format - "BGR")
kernel_size = 5
kernel_b = 255 * np.ones((kernel_size, kernel_size, 1), dtype='uint8')
kernel_gr = np.zeros((kernel_size, kernel_size, 2), dtype='uint8')
kernel = np.dstack((kernel_b,kernel_gr))
Still working on the cv2.filter2D
image parsing.
Taking some notes here from experimenting with cv2.filter2D
Shape: 400x400 pixel. 8-bit. | RGB Image Stack | Grayscale Slices ( R - G - B ) |
---|---|---|
The cv2.filter2D
filter accepts depth arrays, but the source and kernel frames must depth (as expected).
Thus, an m x n x d
source image must have a kernel of equal depth o x p x d
.
Using ImageJ / Fiji to inspect produced images.
Kernel sizes tested as ratio of image coverage. ex: 50x50 px kernel on 400x400 px image is (2500/160000)=1.56% coverage Documentation on pixel size (link)
The function uses the DFT-based algorithm in case of sufficiently large kernels (~11 x 11 or larger) and the direct algorithm for small kernels.
Settings: ddepth=-1 |
1.56% = 50x50 | 0.39% = 25x25 | 0.14% = 15x15 | 0.09% = 12x12 |
---|---|---|---|---|
ddepth
SettingDocumentation Link
Kernel Size = 15x15 |
-1 (Match Source Depth) |
0 | 1 | 2 | 3 |
---|---|---|---|---|---|
x | x |
Alternative Technique using cv2.matchTemplate
Documentation Link
Kernel Size: 15 x 15 px
Usage:
image = cv2.imread("test_image_RGB.tiff")
image = image[:, :, 0] # cv2 uses BGR format. Thus, blue is channel 0
kernel_size = 15
kernel = 255 * np.ones((kernel_size, kernel_size), dtype='uint8')
res = cv2.matchTemplate(image=image, templ=kernel, method=0)
Option | Filter Result | Option | Filter Result |
---|---|---|---|
0 | 1 | ||
2 | 3 | ||
4 | 5 |
Out of curiosity and the potential for progress using cv2.matchTemplate
, I plugged a sample image into each method of the filter to obtain the below results.
Original Image is RGB
Size: 640 x 360 px
Only the BLUE channel is passed into the below filter.
image = cv2.imread("sample_image.png")
image = image[:, :, 0] # cv2 uses BGR format. Thus blue is channel 0
kernel_size = 15
kernel = 255 * np.ones((kernel_size, kernel_size), dtype='uint8')
res = cv2.matchTemplate(image=image, templ=kernel, method=0)
Option | Filter Result | Option | Filter Result |
---|---|---|---|
0 | 1 | ||
2 | 3 | ||
4 | 5 | Did not capture |
!! Important: Results from Methods 0 and 2 are misleading as to the magnitude of each cell's values. Their grayscale brightness is scaled by ImageJ for readability, but their real value is in the range of 10^-7 or approx zero.
In regards to cv2.matchTemplate
, method 1
seems to produce the most useful results with a clear gradient across color regions of interest.
If the above works continue to large image sizes
Added convolution testing sample images and test script @ 46aec0aae91f4c821586a45990c2ef51da2f3087
Input Image: (originally .tiff file but GitHub doesn't support it)
Output Image Array from filter2D: (originally .tiff file but GitHub doesn't support it)
I used Scott Lab computers for this and was not able to check pixel values to see if a gradient was created. Below is the code to create a kernel with 3 channels
# Create kernel (format - "BGR")
kernel_size = 3
kernel = np.dstack((255 * np.ones((kernel_size, kernel_size, 1), dtype='uint8'),np.zeros((kernel_size, kernel_size, 3), dtype='uint8'))) # format: BGR
# numpy dstack docs: https://numpy.org/doc/stable/reference/generated/numpy.dstack.html
kernel_b = 255 * np.ones((kernel_size, kernel_size), dtype='uint8')
dst = image.copy()
output = cv2.filter2D(src=image, dst=dst, ddepth=-1, kernel=kernel_b)
cv2.imshow('Heatmap',output)
cv2.imshow('Output Array dst', dst)
cv2.imwrite("dst.tiff", dst)
cv2.imwrite("result.tff", output)
# # Method: Template Matching
image = image[:, :, 0]
# img_gray = cv2.cvtColor(image,cv2.COLOR_BGR2GRAY)
# resb = cv2.matchTemplate(image=image[:, :, 0], templ=kernel_b, method=1)
# resg = cv2.matchTemplate(image=image[:, :, 1], templ=kernel_b, method=1)
# resr = cv2.matchTemplate(image=image[:, :, 2], templ=kernel_b, method=1)
# cv2.imwrite('resb.tiff',resb)
# cv2.imwrite('resg.tiff',resg)
# cv2.imwrite('resr.tiff',resr)
# template = kernel_b[1,:,:]
res = cv2.matchTemplate(image=image,templ=kernel_b,method=1)
cv2.imwrite('res.tiff',res)
Drew a bounding box on where the function thinks the square is:
kernel_b=cv2.flip(kernel_b,-1)
output = cv2.filter2D(src=image, ddepth=-1, kernel=kernel_b)
cv2.imwrite('output_filter2D.tif', output)
As expected, the MatchTemplate is sensitive to subtle changes of the object. It works well if the object is in the same orientation and size but if it isn't, the detection breaks down quickly.
img_gray = cv2.cvtColor(image,cv2.COLOR_BGR2GRAY)
res = cv2.matchTemplate(image=img_gray,templ=kernel_b,method=3)
cv2.imwrite('res_match_template.tiff',res)
min_val, max_val, min_loc, max_loc = cv2.minMaxLoc(res)
# print values from above
# Drawing Bounding Box around detected shape
# determine the starting and ending (x, y)-coordinates of the bounding box
# From: https://www.pyimagesearch.com/2021/03/22/opencv-template-matching-cv2-matchtemplate/
(startX, startY) = max_loc
endX = startX + kernel_b.shape[1]
endY = startY + kernel_b.shape[0]
# draw the bounding box on the image
b_box_image = cv2.rectangle(image, (startX, startY), (endX, endY), (0, 255, 0), 1)
e
# Reading image
image = cv2.imread("tic_tac_toe_images/twistCorrectedColoredSquares_Color.tiff")
# For straight orientation use: image = cv2.imread("tic_tac_toe_images/CorrectedColoredSquares_Color.tiff")
# creating blue square crop as kernel
kernel_b = cv2.imread('tic_tac_toe_images/blue_square_crop.tiff')
res = cv2.matchTemplate(image=image,templ=kernel_b,method=3)
cv2.imwrite('res_match_template.tiff',res)
cv::TemplateMatchModes cv::TM_SQDIFF = 0, cv::TM_SQDIFF_NORMED = 1, cv::TM_CCORR = 2, cv::TM_CCORR_NORMED = 3, cv::TM_CCOEFF = 4, cv::TM_CCOEFF_NORMED = 5
# Read Image into script
image = cv2.imread("tic_tac_toe_images/twistCorrectedColoredSquares_Color.tiff")
# Creating Kernels from cropped images
kernel_b = cv2.imread('tic_tac_toe_images/blue_square_crop.tiff') # blue square detection
kernel_r = cv2.imread('tic_tac_toe_images/red_square_crop.tiff') # red square detection
kernel_g = cv2.imread('tic_tac_toe_images/green_square_crop.tiff') # green square detection
# MatchTemplate()
res_B = cv2.matchTemplate(image=image,templ=kernel_b,method=5)
cv2.imwrite('res_match_template_B.tiff',res_B)
min_val_B, max_val_B, min_loc_B, max_loc_B = cv2.minMaxLoc(res_B)
print('min_loc_B')
print(min_loc_B)
print('max_loc_B')
print(max_loc_B)
# Drawing Bounding Box around detected shape
# determine the starting and ending (x, y)-coordinates of the bounding box
# From: https://www.pyimagesearch.com/2021/03/22/opencv-template-matching-cv2-matchtemplate/
(startX_B, startY_B) = max_loc_B
endX_B = startX_B + kernel_b.shape[1]
endY_B = startY_B + kernel_b.shape[0]
# draw the bounding box on the image (same process for green & red boxes)
b_box_image = cv2.rectangle(image, (startX_B, startY_B), (endX_B, endY_B), (255, 0, 0), 4) # BGR for openCV
# show the output image
# cv2.imshow("Output based on matchTemplate", b_box_image)
cv2.imwrite('res_match_template_Blue_BoundingBox.tiff', b_box_image)
we use 4.2, so need to test on that version of opencv
Is there any reason we can't use the newer version of opencv? My understanding of the OpenCV version choice was arbitrary.
ImageJ 1.53a; Java 1.8.0_112 [64-bit]; Windows 10 10.0; 40MB of 24483MB (<1%)
java.lang.NegativeArraySizeException at ij.io.ImageReader.readCompressed32bitImage(ImageReader.java:271) at ij.io.ImageReader.read32bitImage(ImageReader.java:203) at ij.io.ImageReader.readPixels(ImageReader.java:788) at ij.io.FileOpener.readPixels(FileOpener.java:541) at ij.io.FileOpener.open(FileOpener.java:96) at ij.io.FileOpener.openImage(FileOpener.java:53) at ij.io.Opener.openTiff2(Opener.java:1039) at ij.io.Opener.openTiff(Opener.java:842) at ij.io.Opener.openImage(Opener.java:317) at ij.io.Opener.openImage(Opener.java:243) at ij.io.Opener.open(Opener.java:109) at ij.io.Opener.open(Opener.java:72) at ij.plugin.Commands.run(Commands.java:27) at ij.IJ.runPlugIn(IJ.java:204) at ij.Executer.runCommand(Executer.java:150) at ij.Executer.run(Executer.java:68) at java.lang.Thread.run(Thread.java:745)
- I think the error lies in creating a 3 depth array to use as a kernel. Here is the current method of making the 3 depth array:
kernel_size = 5 print('Shape of Input image') print(np.shape(image))
ch1 = 255*np.ones((kernel_size, kernel_size), dtype='uint8') ch2 = np.zeros((kernel_size, kernel_size), dtype='uint8') kernel_b = np.array([ch1, ch2, ch2], ndmin=3, dtype='uint8')
print('Kernel Matrix: should be 3x3x3') print(np.shape(kernel_b)) # returns 3x3x3 print(kernel_b)
**Output:**
Shape of image (50, 50, 3) Kernel Matrix: should be 5x5x3 (3, 5, 5) [[[255 255 255 255 255] [255 255 255 255 255] [255 255 255 255 255] [255 255 255 255 255] [255 255 255 255 255]]
[[ 0 0 0 0 0] [ 0 0 0 0 0] [ 0 0 0 0 0] [ 0 0 0 0 0] [ 0 0 0 0 0]]
[[ 0 0 0 0 0] [ 0 0 0 0 0] [ 0 0 0 0 0] [ 0 0 0 0 0] [ 0 0 0 0 0]]]
## Next Steps:
- Look into Numpy Array docs for creating a 3D Array
- https://www.kite.com/python/answers/how-to-create-a-3d-numpy-array-in-python
- https://numpy.org/doc/stable/reference/generated/numpy.array.html
-
np.shape(array) outputs a tuple value of (depth, rows, columns)
import numpy as np
kernel_size = 5
ch1 = 255*np.ones((kernel_size, kernel_size), dtype='uint8')
ch2 = np.zeros((kernel_size, kernel_size), dtype='uint8')
a = np.array([ch1,ch1,ch2])
print(a)
print(a.shape)
print(a.ndim)
[[[255 255 255 255 255]
[255 255 255 255 255]
[255 255 255 255 255]
[255 255 255 255 255]
[255 255 255 255 255]]
[[255 255 255 255 255]
[255 255 255 255 255]
[255 255 255 255 255]
[255 255 255 255 255]
[255 255 255 255 255]]
[[ 0 0 0 0 0]
[ 0 0 0 0 0]
[ 0 0 0 0 0]
[ 0 0 0 0 0]
[ 0 0 0 0 0]]]
(3, 5, 5)
3
Process for using matchTemplate():
img_gray = cv.cvtColor(img_rgb, cv.COLOR_BGR2GRAY)
template = cv.imread('mario_coin.png',0)
res = cv.matchTemplate(img_gray,template,cv.TM_CCOEFF_NORMED)
MatchTemplate Docs: https://docs.opencv.org/4.2.0/de/da9/tutorial_template_matching.html
Uses older version of Python (2.x) with Raspberry Pi but inputs camera feed without converting file types: https://stackoverflow.com/questions/42559985/python-opencv-template-matching-using-the-live-camera-feed-frame-as-input
The above link imports
from picamera.array import PiRGBArray
from picamera import PiCamera
This is a Raspberry Pi specific import. See here for more info: https://picamera.readthedocs.io/en/release-1.13/
This example is in C++ and imports a video into matchTemplate() not a camera feed. Not exactly what I need but the discussion of drift in video tracking is interesting: https://stackoverflow.com/questions/20180073/real-time-template-matching-opencv-c
More importantly, the use a live camera feed:
#Camera
cap = cv2.VideoCapture(0)
#symbool inladen
symbool = cv2.imread('klaver.jpg',0)
w, h = symbool.shape[::-1]
while(1):
res, frame = cap.read()
In my case, I am subscribing to a rostopic.
image_sub = rospy.Subscriber("/camera/color/image_raw", Image, runner)
However, I could forgo subscribing to the topic and access the camera feed directly since the script does not need to pull from the image topic. The purpose of the script is to recognize where the squares are and output the centers and orientation so the robot can make the appropriate movements to play the game.
However, I could forgo subscribing to the topic and access the camera feed directly since the script does not need to pull from the image topic.
Heads up, I don't think it's possible to have both a ROS-based camera feed AND a python-based feed open at the same time. I don't remember ever testing this specifically, but I assume due to the handshake between the camera and computer that it might not let you initiate the second stream.
However, I could forgo subscribing to the topic and access the camera feed directly since the script does not need to pull from the image topic.
Heads up, I don't think it's possible to have both a ROS-based camera feed AND a python-based feed open at the same time. I don't remember ever testing this specifically, but I assume due to the handshake between the camera and computer that it might not let you initiate the second stream.
Ok. I'll see if it can be done. Just to clarify I was going to remove the ros topic and ros imports and use just cv2.VideoCapture() for accessing the camera feed.
Just to clarify I was going to remove the ros topic and ros imports and use just cv2.VideoCapture() for accessing the camera feed.
Gotcha. No worries, just wanted to help catch anything before we got there.
This Stack Overflow post helped solve it: https://stackoverflow.com/questions/52029233/how-to-make-usb-camera-work-with-opencv
TLDR: Plug in the camera, go to your terminal home
When I plugged in iRS#2 camera and cd /dev
with the above steps, I had video0, video1, video2, video3, video4, video5
appear.
I found that cv2.VideoCapture(4) worked.
Tried adding a time.sleep() but even at time.sleep(30), the image remains dark. Image is shown below
Here is the code I am using:
cap = cv2.VideoCapture(4)
# refer to this github issue for why I used VideoCapture(4)
# https://github.com/OSU-AIMS/tic-tac-toe/issues/10#issuecomment-1016505927
# allowing the camera time to boot up and auto set exposure
time.sleep(30) # seconds
while(1):
res,frame = cap.read()
frame = cv2.cvtColor(frame,cv2.COLOR_BGR2RGB)
kernel_runner(frame)
cap.release()
Opening the camera in Intel Realsense viewer is fine. The video feed brightens up within 1 sec.
To make MatchTemplate() more robust detection will need to look into keypoint detectors, local invariant descriptors and keypoint matching to detect changes in scale, rotation, lighting, etc in the image.
Potential Local Invariant Descriptors & Keypoint Matching methods to look into: SIFT, SURF, FREAK, RANSAC
@acbuynak if you've heard of any these before or any suggestions on which to start with, let me know.
Using the kernels for the static image will not work in the above dynamic environment because the distance from the board to the camera is different which results in the colored squares being different sizes in the camera feed.
Understood. let's assume a sufficiently static environment for now where the board will always be a fixed z-distance from the camera. Before we jump onto making this more adjustable, let's run the techniques past some vision experts to get ideas/guidance first. See last note.
MatchTemplate() is sensitive to rotation, scaling, lighting, camera focal length. What works in one setting, may not work in another if the environment are different.
Makes sense, our first draft implementation is going to be rough.
@acbuynak if you've heard of any these before or any suggestions on which to start with, let me know.
Nope. These are all new to me, but let's pause on robustness if it's working even slightly at this point. We could run them past mgroeber to see if he recognizes them later.
Let's hold off for now. I'd rather have you help Luis to get the rest of the structure setup. Even if the vision is only working sometimes.. that's okay. We just want a basic prototype before proceeding.
Using MatchTemplate(), able to recognize 3 colors with a decent accuracy. Not perfect --> lighting, rotation, and other dynamic conditions still affect detection
For more info on Object detection Methods and making MatchTemplate() more robust. See the Object Detection Wiki Page in the right panel: https://github.com/OSU-AIMS/tic-tac-toe/wiki
Goal:
Create an Image Kernel to pass over a frame from Intel Realse d435i camera to detect a blue, red, and green square.
Computer Environment:
Current State:
Kernel:
Issue:
Code: