JeVoisBase  1.21
JeVois Smart Embedded Machine Vision Toolkit Base Modules
Share this page:
Loading...
Searching...
No Matches
PythonObject6D.py
Go to the documentation of this file.
1######################################################################################################################
2#
3# JeVois Smart Embedded Machine Vision Toolkit - Copyright (C) 2018 by Laurent Itti, the University of Southern
4# California (USC), and iLab at USC. See http://iLab.usc.edu and http://jevois.org for information about this project.
5#
6# This file is part of the JeVois Smart Embedded Machine Vision Toolkit. This program is free software; you can
7# redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software
8# Foundation, version 2. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY;
9# without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public
10# License for more details. You should have received a copy of the GNU General Public License along with this program;
11# if not, write to the Free Software Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA.
12#
13# Contact information: Laurent Itti - 3641 Watt Way, HNB-07A - Los Angeles, CA 90089-2520 - USA.
14# Tel: +1 213 740 3527 - itti@pollux.usc.edu - http://iLab.usc.edu - http://jevois.org
15######################################################################################################################
16
17import pyjevois
18if pyjevois.pro: import libjevoispro as jevois
19else: import libjevois as jevois
20import cv2
21import numpy as np
22import math # for cos, sin, etc
23
24## Simple example of object detection using ORB keypoints followed by 6D pose estimation in Python
25#
26# This module implements an object detector using ORB keypoints using OpenCV in Python. Its main goal is to also
27# demonstrate full 6D pose recovery of the detected object, in Python, as well as locating in 3D a sub-element of the
28# detected object (here, a window within a larger textured wall). See \jvmod{ObjectDetect} for more info about object
29# detection using keypoints. This module is available with \jvversion{1.6.3} and later.
30#
31# The algorithm consists of 5 phases:
32# - detect keypoint locations, typically corners or other distinctive texture elements or markings;
33# - compute keypoint descriptors, which are summary representations of the image neighborhood around each keypoint;
34# - match descriptors from current image to descriptors previously extracted from training images;
35# - if enough matches are found between the current image and a given training image, and they are of good enough
36# quality, compute the homography (geometric transformation) between keypoint locations in that training image and
37# locations of the matching keypoints in the current image. If it is well conditioned (i.e., a 3D viewpoint change
38# could well explain how the keypoints moved between the training and current images), declare that a match was
39# found, and draw a pink rectangle around the detected whole object.
40# - finally perform 6D pose estimation (3D translation + 3D rotation), here for a window located at a specific position
41# within the whole object, given the known physical sizes of both the whole object and the window within. A green
42# parallelepiped is drawn at that window's location, sinking into the whole object (as it is representing a tunnel
43# or port into the object).
44#
45# For more information about ORB keypoint detection and matching in OpenCV, see, e.g.,
46# https://docs.opencv.org/3.4.0/d1/d89/tutorial_py_orb.html
47#
48# This module is provided for inspiration. It has no pretension of actually solving the FIRST Robotics Power Up (sm)
49# vision problem in a complete and reliable way. It is released in the hope that FRC teams will try it out and get
50# inspired to develop something much better for their own robot.
51#
52# Note how, contrary to \jvmod{FirstVision}, \jvmod{DemoArUco}, etc, the green parallelepiped is drawn going into the
53# object instead of sticking out of it, as it is depicting a tunnel at the window location.
54#
55# Using this module
56# -----------------
57#
58# This module is for now specific to the "exchange" of the FIRST Robotics 2018 Power Up (sm) challenge. See
59# https://www.firstinspires.org/resource-library/frc/competition-manual-qa-system
60#
61# The exchange is a large textured structure with a window at the bottom into which robots should deliver foam cubes.
62#
63# A reference picture of the whole exchange (taken from the official rules) is in
64# <b>JEVOIS:/modules/JeVois/PythonObject6D/images/reference.png</b> on your JeVois microSD card. It will be processed
65# when the module starts. No additional training procedure is needed.
66#
67# If you change the reference image, you should also edit:
68# - values of \p self.owm and \p self.ohm to the width ahd height, in meters, of the actual physical object in your
69# picture. Square pixels are assumed, so make sure the aspect ratio of your PNG image matches the aspect ratio in
70# meters given by variables \p self.owm and \p self.ohm in the code.
71# - values of \p self.wintop, \p self.winleft, \p self.winw, \p self.winh to the location of the top-left corner, in
72# meters and relative to the top-left corner of the whole reference object, of a window of interest (the tunnel into
73# which the cubes should be delivered), and width and height, in meters, of the window.
74#
75# \b TODO: Add support for multiple images and online training as in \jvmod{ObjectDetect}
76#
77# Things to tinker with
78# ---------------------
79#
80# There are a number of limitations and caveats to this module:
81#
82# - It does not use color, the input image is converted to grayscale before processing. One could use a different
83# approach to object detection that would make use of color.
84# - Results are often quite noisy. Maybe using another detector, like SIFT which provides subpixel accuracy, and better
85# pruning of false matches (e.g., David Lowe's ratio of the best to second-best match scores) would help.
86# - This algorithm is slow in this single-threaded Python example, and frame rate depends on image complexity (it gets
87# slower when more keypoints are detected). One should explore parallelization, as was done in C++ for the
88# \jvmod{ObjectDetect} module. One could also alternate between full detection using this algorithm once in a while,
89# and much faster tracking of previous detections at a higher framerate (e.g., using the very robust TLD tracker
90# (track-learn-detect), also supported in OpenCV).
91# - If you want to detect smaller objects or pieces of objects, and you do not need 6D pose, you may want to use modules
92# \jvmod{ObjectDetect} or \jvmod{SaliencySURF} as done, for example, by JeVois user Bill Kendall at
93# https://www.youtube.com/watch?v=8wYhOnsNZcc
94#
95#
96# @author Laurent Itti
97#
98# @displayname Python Object 6D
99# @videomapping YUYV 320 262 15.0 YUYV 320 240 15.0 JeVois PythonObject6D
100# @email itti\@usc.edu
101# @address University of Southern California, HNB-07A, 3641 Watt Way, Los Angeles, CA 90089-2520, USA
102# @copyright Copyright (C) 2018 by Laurent Itti, iLab and the University of Southern California
103# @mainurl http://jevois.org
104# @supporturl http://jevois.org/doc
105# @otherurl http://iLab.usc.edu
106# @license GPL v3
107# @distribution Unrestricted
108# @restrictions None
109# @ingroup modules
111 # ###################################################################################################
112 ## Constructor
113 def __init__(self):
114 # Full file name of the training image:
115 self.fname = "/jevois/modules/JeVois/PythonObject6D/images/reference.png"
116
117 # Measure your object (in meters) and set its size here:
118 self.owm = 48 * 0.0254 # width in meters (specs call for 48 inches)
119 self.ohm = 77.75 * 0.0254 # height in meters (specs call for 77.75 inches)
120
121 # Window within the object for which we will compute 3D pose: top-left corner in meters relative to the top-left
122 # corner of the full reference object, and window width and height in meters:
123 self.wintop = (77.75 - 18) * 0.0254 # top of exchange window is 18in from ground
124 self.winleft = 6.88 * 0.0254 # left of exchange window is 6.88in from left edge
125 self.winw = (12 + 9) * 0.0254 # exchange window is 1ft 9in wide
126 self.winh = (12 + 4.25) * 0.0254 # exchange window is 1ft 4-1/4in tall
127
128 # Other parameters:
129 self.distth = 50.0 # Descriptor distance threshold (lower is stricter for exact matches)
130
131 # Instantiate a JeVois Timer to measure our processing framerate:
132 self.timer = jevois.Timer("PythonObject6D", 100, jevois.LOG_INFO)
133
134 # ###################################################################################################
135 ## Load camera calibration from JeVois share directory
136 def loadCameraCalibration(self, w, h):
137 try:
138 self.camMatrix, self.distCoeffs = jevois.loadCameraCalibration("calibration", True)
139 jevois.LINFO("Loaded camera calibration")
140 except:
141 jevois.LERROR("Failed to load camera calibration for {}x{} -- IGNORED".format(w,h))
142 self.camMatrix = np.eye(3, 3, dtype=np.double)
143 self.distCoeffs = np.zeros(5, 1, dtype=np.double)
144
145 # ###################################################################################################
146 ## Detect objects using keypoints
147 def detect(self, imggray, outimg = None):
148 h, w = imggray.shape
149 hlist = []
150
151 # Create a keypoint detector if needed:
152 if not hasattr(self, 'detector'):
153 self.detector = cv2.ORB_create()
154
155 # Load training image and detect keypoints on it if needed:
156 if not hasattr(self, 'refkp'):
157 refimg = cv2.imread(self.fname, 0)
158 self.refkp, self.refdes = self.detector.detectAndCompute(refimg, None)
159
160 # Also store corners of reference image and of window for homography mapping:
161 refh, refw = refimg.shape
162 self.refcorners = np.float32([ [ 0.0, 0.0 ], [ 0.0, refh ], [refw, refh ], [ refw, 0.0 ] ]).reshape(-1,1,2)
163 self.wincorners = np.float32([
164 [ self.winleft * refw / self.owm, self.wintop * refh / self.ohm ],
165 [ self.winleft * refw / self.owm, (self.wintop + self.winh) * refh / self.ohm ],
166 [ (self.winleft + self.winw) * refw / self.owm, (self.wintop + self.winh) * refh / self.ohm ],
167 [ (self.winleft + self.winw) * refw / self.owm, self.wintop * refh / self.ohm ] ]).reshape(-1,1,2)
168 jevois.LINFO("Extracted {} keypoints and descriptors from {}".format(len(self.refkp), self.fname))
169
170 # Compute keypoints and descriptors:
171 kp, des = self.detector.detectAndCompute(imggray, None)
172 str = "{} keypoints".format(len(kp))
173
174 # Create a matcher if needed:
175 if not hasattr(self, 'matcher'):
176 self.matcher = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck = True)
177
178 # Compute matches between reference image and camera image, then sort them by distance:
179 matches = self.matcher.match(des, self.refdes)
180 matches = sorted(matches, key = lambda x:x.distance)
181 str += ", {} matches".format(len(matches))
182
183 # Keep only good matches:
184 lastidx = 0
185 for m in matches:
186 if m.distance < self.distth: lastidx += 1
187 else: break
188 matches = matches[0:lastidx]
189 str += ", {} good".format(len(matches))
190
191 # If we have enough matches, compute homography:
192 corners = []
193 wincorners = []
194 if len(matches) >= 10:
195 obj = []
196 scene = []
197
198 # Localize the object (see JeVois C++ class ObjectMatcher for details):
199 for m in matches:
200 obj.append(self.refkp[m.trainIdx].pt)
201 scene.append(kp[m.queryIdx].pt)
202
203 # compute the homography
204 hmg, mask = cv2.findHomography(np.array(obj), np.array(scene), cv2.RANSAC, 5.0)
205
206 # Check homography conditioning using SVD:
207 u, s, v = np.linalg.svd(hmg, full_matrices = False)
208
209 # We need the smallest eigenvalue to not be too small, and the ratio of largest to smallest eigenvalue to be
210 # quite large for our homography to be declared good here. Note that linalg.svd returns the eigenvalues in
211 # descending order already:
212 if s[-1] > 0.001 and s[0] / s[-1] > 100:
213 # Project the reference image corners to the camera image:
214 corners = cv2.perspectiveTransform(self.refcorners, hmg)
215 wincorners = cv2.perspectiveTransform(self.wincorners, hmg)
216
217 # Display any results requested by the users:
218 if outimg is not None and outimg.valid():
219 if len(corners) == 4:
220 jevois.drawLine(outimg, int(corners[0][0,0] + 0.5), int(corners[0][0,1] + 0.5),
221 int(corners[1][0,0] + 0.5), int(corners[1][0,1] + 0.5),
222 2, jevois.YUYV.LightPink)
223 jevois.drawLine(outimg, int(corners[1][0,0] + 0.5), int(corners[1][0,1] + 0.5),
224 int(corners[2][0,0] + 0.5), int(corners[2][0,1] + 0.5),
225 2, jevois.YUYV.LightPink)
226 jevois.drawLine(outimg, int(corners[2][0,0] + 0.5), int(corners[2][0,1] + 0.5),
227 int(corners[3][0,0] + 0.5), int(corners[3][0,1] + 0.5),
228 2, jevois.YUYV.LightPink)
229 jevois.drawLine(outimg, int(corners[3][0,0] + 0.5), int(corners[3][0,1] + 0.5),
230 int(corners[0][0,0] + 0.5), int(corners[0][0,1] + 0.5),
231 2, jevois.YUYV.LightPink)
232 jevois.writeText(outimg, str, 3, h+4, jevois.YUYV.White, jevois.Font.Font6x10)
233
234 # Return window corners if we did indeed detect the object:
235 hlist = []
236 if len(wincorners) == 4: hlist.append(wincorners)
237
238 return hlist
239
240 # ###################################################################################################
241 ## Estimate 6D pose of each of the quadrilateral objects in hlist:
242 def estimatePose(self, hlist):
243 rvecs = []
244 tvecs = []
245
246 # set coordinate system in the middle of the window, with Z pointing out
247 objPoints = np.array([ ( -self.winw * 0.5, -self.winh * 0.5, 0 ),
248 ( -self.winw * 0.5, self.winh * 0.5, 0 ),
249 ( self.winw * 0.5, self.winh * 0.5, 0 ),
250 ( self.winw * 0.5, -self.winh * 0.5, 0 ) ])
251
252 for detection in hlist:
253 det = np.array(detection, dtype=np.float).reshape(4,2,1)
254 (ok, rv, tv) = cv2.solvePnP(objPoints, det, self.camMatrix, self.distCoeffs)
255 if ok:
256 rvecs.append(rv)
257 tvecs.append(tv)
258 else:
259 rvecs.append(np.array([ (0.0), (0.0), (0.0) ]))
260 tvecs.append(np.array([ (0.0), (0.0), (0.0) ]))
261
262 return (rvecs, tvecs)
263
264 # ###################################################################################################
265 ## Send serial messages, one per object
266 def sendAllSerial(self, w, h, hlist, rvecs, tvecs):
267 idx = 0
268 for c in hlist:
269 # Compute quaternion: FIXME need to check!
270 tv = tvecs[idx]
271 axis = rvecs[idx]
272 angle = (axis[0] * axis[0] + axis[1] * axis[1] + axis[2] * axis[2]) ** 0.5
273
274 # This code lifted from pyquaternion from_axis_angle:
275 mag_sq = axis[0] * axis[0] + axis[1] * axis[1] + axis[2] * axis[2]
276 if (abs(1.0 - mag_sq) > 1e-12): axis = axis / (mag_sq ** 0.5)
277 theta = angle / 2.0
278 r = math.cos(theta)
279 i = axis * math.sin(theta)
280 q = (r, i[0], i[1], i[2])
281
282 jevois.sendSerial("D3 {} {} {} {} {} {} {} {} {} {} OBJ6D".
283 format(np.asscalar(tv[0]), np.asscalar(tv[1]), np.asscalar(tv[2]), # position
284 self.owm, self.ohm, 1.0, # size
285 r, np.asscalar(i[0]), np.asscalar(i[1]), np.asscalar(i[2]))) # pose
286 idx += 1
287
288 # ###################################################################################################
289 ## Draw all detected objects in 3D
290 def drawDetections(self, outimg, hlist, rvecs = None, tvecs = None):
291 # Show trihedron and parallelepiped centered on object:
292 hw = self.winw * 0.5
293 hh = self.winh * 0.5
294 dd = -max(hw, hh)
295 i = 0
296 empty = np.array([ (0.0), (0.0), (0.0) ])
297
298 # NOTE: this code similar to FirstVision, but in the present module we only have at most one object in the list
299 # (the window, if detected):
300 for obj in hlist:
301 # skip those for which solvePnP failed:
302 if np.array_equal(rvecs[i], empty):
303 i += 1
304 continue
305 # This could throw some overflow errors as we convert the coordinates to int, if the projection gets
306 # singular because of noisy detection:
307 try:
308 # Project axis points:
309 axisPoints = np.array([ (0.0, 0.0, 0.0), (hw, 0.0, 0.0), (0.0, hh, 0.0), (0.0, 0.0, dd) ])
310 imagePoints, jac = cv2.projectPoints(axisPoints, rvecs[i], tvecs[i], self.camMatrix, self.distCoeffs)
311
312 # Draw axis lines:
313 jevois.drawLine(outimg, int(imagePoints[0][0,0] + 0.5), int(imagePoints[0][0,1] + 0.5),
314 int(imagePoints[1][0,0] + 0.5), int(imagePoints[1][0,1] + 0.5),
315 2, jevois.YUYV.MedPurple)
316 jevois.drawLine(outimg, int(imagePoints[0][0,0] + 0.5), int(imagePoints[0][0,1] + 0.5),
317 int(imagePoints[2][0,0] + 0.5), int(imagePoints[2][0,1] + 0.5),
318 2, jevois.YUYV.MedGreen)
319 jevois.drawLine(outimg, int(imagePoints[0][0,0] + 0.5), int(imagePoints[0][0,1] + 0.5),
320 int(imagePoints[3][0,0] + 0.5), int(imagePoints[3][0,1] + 0.5),
321 2, jevois.YUYV.MedGrey)
322
323 # Also draw a parallelepiped: NOTE: contrary to FirstVision, here we draw it going into the object, as
324 # opposed to sticking out of it (we just negate Z for that):
325 cubePoints = np.array([ (-hw, -hh, 0.0), (hw, -hh, 0.0), (hw, hh, 0.0), (-hw, hh, 0.0),
326 (-hw, -hh, -dd), (hw, -hh, -dd), (hw, hh, -dd), (-hw, hh, -dd) ])
327 cu, jac2 = cv2.projectPoints(cubePoints, rvecs[i], tvecs[i], self.camMatrix, self.distCoeffs)
328
329 # Round all the coordinates and cast to int for drawing:
330 cu = np.rint(cu)
331
332 # Draw parallelepiped lines:
333 jevois.drawLine(outimg, int(cu[0][0,0]), int(cu[0][0,1]), int(cu[1][0,0]), int(cu[1][0,1]),
334 1, jevois.YUYV.LightGreen)
335 jevois.drawLine(outimg, int(cu[1][0,0]), int(cu[1][0,1]), int(cu[2][0,0]), int(cu[2][0,1]),
336 1, jevois.YUYV.LightGreen)
337 jevois.drawLine(outimg, int(cu[2][0,0]), int(cu[2][0,1]), int(cu[3][0,0]), int(cu[3][0,1]),
338 1, jevois.YUYV.LightGreen)
339 jevois.drawLine(outimg, int(cu[3][0,0]), int(cu[3][0,1]), int(cu[0][0,0]), int(cu[0][0,1]),
340 1, jevois.YUYV.LightGreen)
341 jevois.drawLine(outimg, int(cu[4][0,0]), int(cu[4][0,1]), int(cu[5][0,0]), int(cu[5][0,1]),
342 1, jevois.YUYV.LightGreen)
343 jevois.drawLine(outimg, int(cu[5][0,0]), int(cu[5][0,1]), int(cu[6][0,0]), int(cu[6][0,1]),
344 1, jevois.YUYV.LightGreen)
345 jevois.drawLine(outimg, int(cu[6][0,0]), int(cu[6][0,1]), int(cu[7][0,0]), int(cu[7][0,1]),
346 1, jevois.YUYV.LightGreen)
347 jevois.drawLine(outimg, int(cu[7][0,0]), int(cu[7][0,1]), int(cu[4][0,0]), int(cu[4][0,1]),
348 1, jevois.YUYV.LightGreen)
349 jevois.drawLine(outimg, int(cu[0][0,0]), int(cu[0][0,1]), int(cu[4][0,0]), int(cu[4][0,1]),
350 1, jevois.YUYV.LightGreen)
351 jevois.drawLine(outimg, int(cu[1][0,0]), int(cu[1][0,1]), int(cu[5][0,0]), int(cu[5][0,1]),
352 1, jevois.YUYV.LightGreen)
353 jevois.drawLine(outimg, int(cu[2][0,0]), int(cu[2][0,1]), int(cu[6][0,0]), int(cu[6][0,1]),
354 1, jevois.YUYV.LightGreen)
355 jevois.drawLine(outimg, int(cu[3][0,0]), int(cu[3][0,1]), int(cu[7][0,0]), int(cu[7][0,1]),
356 1, jevois.YUYV.LightGreen)
357 except:
358 pass
359
360 i += 1
361
362 # ###################################################################################################
363 ## Process function with no USB output
364 def processNoUSB(self, inframe):
365 # Get the next camera image (may block until it is captured) as OpenCV GRAY:
366 imggray = inframe.getCvGRAY()
367 h, w = imggray.shape
368
369 # Start measuring image processing time:
370 self.timer.start()
371
372 # Get a list of quadrilateral convex hulls for all good objects:
373 hlist = self.detect(imggray)
374
375 # Load camera calibration if needed:
376 if not hasattr(self, 'camMatrix'): self.loadCameraCalibration(w, h)
377
378 # Map to 6D (inverse perspective):
379 (rvecs, tvecs) = self.estimatePose(hlist)
380
381 # Send all serial messages:
382 self.sendAllSerial(w, h, hlist, rvecs, tvecs)
383
384 # Log frames/s info (will go to serlog serial port, default is None):
385 self.timer.stop()
386
387 # ###################################################################################################
388 ## Process function with USB output
389 def process(self, inframe, outframe):
390 # Get the next camera image (may block until it is captured). To avoid wasting much time assembling a composite
391 # output image with multiple panels by concatenating numpy arrays, in this module we use raw YUYV images and
392 # fast paste and draw operations provided by JeVois on those images:
393 inimg = inframe.get()
394
395 # Start measuring image processing time:
396 self.timer.start()
397
398 # Convert input image to GRAY:
399 imggray = jevois.convertToCvGray(inimg)
400 h, w = imggray.shape
401
402 # Get pre-allocated but blank output image which we will send over USB:
403 outimg = outframe.get()
404 outimg.require("output", w, h + 22, jevois.V4L2_PIX_FMT_YUYV)
405 jevois.paste(inimg, outimg, 0, 0)
406 jevois.drawFilledRect(outimg, 0, h, outimg.width, outimg.height-h, jevois.YUYV.Black)
407
408 # Let camera know we are done using the input image:
409 inframe.done()
410
411 # Get a list of quadrilateral convex hulls for all good objects:
412 hlist = self.detect(imggray, outimg)
413
414 # Load camera calibration if needed:
415 if not hasattr(self, 'camMatrix'): self.loadCameraCalibration(w, h)
416
417 # Map to 6D (inverse perspective):
418 (rvecs, tvecs) = self.estimatePose(hlist)
419
420 # Send all serial messages:
421 self.sendAllSerial(w, h, hlist, rvecs, tvecs)
422
423 # Draw all detections in 3D:
424 self.drawDetections(outimg, hlist, rvecs, tvecs)
425
426 # Write frames/s info from our timer into the edge map (NOTE: does not account for output conversion time):
427 fps = self.timer.stop()
428 jevois.writeText(outimg, fps, 3, h-10, jevois.YUYV.White, jevois.Font.Font6x10)
429
430 # We are done with the output, ready to send it to host over USB:
431 outframe.send()
432
Simple example of object detection using ORB keypoints followed by 6D pose estimation in Python.
process(self, inframe, outframe)
Process function with USB output.
sendAllSerial(self, w, h, hlist, rvecs, tvecs)
Send serial messages, one per object.
estimatePose(self, hlist)
Estimate 6D pose of each of the quadrilateral objects in hlist:
detect(self, imggray, outimg=None)
Detect objects using keypoints.
drawDetections(self, outimg, hlist, rvecs=None, tvecs=None)
Draw all detected objects in 3D.
processNoUSB(self, inframe)
Process function with no USB output.
loadCameraCalibration(self, w, h)
Load camera calibration from JeVois share directory.