paper_finder.py
Go to the documentation of this file.
1 #!/usr/bin/env python
2 # -*- coding:utf-8 -*-
3 
4 import cv2
5 import cv_bridge
7 from image_geometry.cameramodels import PinholeCameraModel
8 from jsk_recognition_msgs.msg import BoundingBox
9 from jsk_recognition_msgs.msg import BoundingBoxArray
10 from jsk_topic_tools import ConnectionBasedTransport
11 import message_filters
12 import numpy as np
13 import rospy
14 import sensor_msgs.msg
15 import std_msgs.msg
16 from tf.transformations import quaternion_from_matrix
17 from tf.transformations import unit_vector as normalize_vector
18 
19 
21  return np.array([[0, -v[2], v[1]],
22  [v[2], 0, -v[0]],
23  [-v[1], v[0], 0]])
24 
25 
26 def cross_product(a, b):
27  return np.dot(outer_product_matrix(a), b)
28 
29 
31  first_axis=(1, 0, 0), second_axis=(0, 1, 0), axes='xy'):
32  if axes not in ['xy', 'yx', 'xz', 'zx', 'yz', 'zy']:
33  raise ValueError("Valid axes are 'xy', 'yx', 'xz', 'zx', 'yz', 'zy'.")
34  e1 = normalize_vector(first_axis)
35  e2 = normalize_vector(second_axis - np.dot(second_axis, e1) * e1)
36  if axes in ['xy', 'zx', 'yz']:
37  third_axis = cross_product(e1, e2)
38  else:
39  third_axis = cross_product(e2, e1)
40  e3 = normalize_vector(
41  third_axis - np.dot(third_axis, e1) * e1 - np.dot(third_axis, e2) * e2)
42  first_index = ord(axes[0]) - ord('x')
43  second_index = ord(axes[1]) - ord('x')
44  third_index = ((first_index + 1) ^ (second_index + 1)) - 1
45  indices = [first_index, second_index, third_index]
46  return np.vstack([e1, e2, e3])[np.argsort(indices)].T
47 
48 
49 def area(poly):
50  if len(poly) < 3: # not a plane - no area
51  return 0
52 
53  total = [0, 0, 0]
54  for i in range(len(poly)):
55  vi1 = poly[i]
56  if i is len(poly) - 1:
57  vi2 = poly[0]
58  else:
59  vi2 = poly[i + 1]
60  prod = np.cross(vi1, vi2)
61  total[0] += prod[0]
62  total[1] += prod[1]
63  total[2] += prod[2]
64  result = np.dot(total, unit_normal(poly[0], poly[1], poly[2]))
65  return abs(result / 2)
66 
67 
68 def unit_normal(a, b, c):
69  x = np.linalg.det([[1, a[1], a[2]],
70  [1, b[1], b[2]],
71  [1, c[1], c[2]]])
72  y = np.linalg.det([[a[0], 1, a[2]],
73  [b[0], 1, b[2]],
74  [c[0], 1, c[2]]])
75  z = np.linalg.det([[a[0], a[1], 1],
76  [b[0], b[1], 1],
77  [c[0], c[1], 1]])
78  magnitude = (x**2 + y**2 + z**2)**.5
79  return (x / magnitude, y / magnitude, z / magnitude)
80 
81 
82 def angle(pt1, pt2, pt0):
83  pt0 = np.array(pt0, dtype=np.float32)
84  pt1 = np.array(pt1, dtype=np.float32)
85  pt2 = np.array(pt2, dtype=np.float32)
86  dx1 = pt1[0][0] - pt0[0][0]
87  dy1 = pt1[0][1] - pt0[0][1]
88  dx2 = pt2[0][0] - pt0[0][0]
89  dy2 = pt2[0][1] - pt0[0][1]
90  return (dx1 * dx2 + dy1 * dy2) / np.sqrt(
91  (dx1 * dx1 + dy1 * dy1) * (dx2 * dx2 + dy2 * dy2) + 1e-10)
92 
93 
95 
96  def __init__(self,
97  length_threshold=10,
98  distance_threshold=1.41421356,
99  canny_th1=50.0,
100  canny_th2=50.0,
101  canny_aperture_size=3,
102  do_merge=False):
103  self.lsd = cv2.ximgproc.createFastLineDetector(
104  length_threshold,
105  distance_threshold,
106  canny_th1,
107  canny_th2,
108  canny_aperture_size,
109  do_merge)
110 
111  def find_squares(self, img):
112  gray = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
113  line_segments = self.lsd.detect(gray)
114 
115  if line_segments is None:
116  line_segments = []
117  edge_img = np.zeros((img.shape[0], img.shape[1]), dtype=np.uint8)
118  for line in line_segments:
119  x1, y1, x2, y2 = map(int, line[0][:4])
120  cv2.line(edge_img, (x1, y1), (x2, y2), (255, 255, 255), 10)
121 
122  _, contours, _ = cv2.findContours(
123  edge_img,
124  cv2.RETR_CCOMP,
125  cv2.CHAIN_APPROX_SIMPLE)
126 
127  squares = []
128  count = 0
129  for contour in contours:
130  arclen = cv2.arcLength(contour, True)
131  approx = cv2.approxPolyDP(contour,
132  arclen * 0.02,
133  True)
134  if len(approx) == 4:
135  count += 1
136  if (len(approx) == 4 and
137  abs(cv2.contourArea(approx)) > 1000
138  and cv2.isContourConvex(approx)):
139  maxCosine = 0
140  for j in range(2, 5):
141  # find the maximum cosine of the angle between joint edges
142  cosine = abs(
143  angle(approx[j % 4], approx[j - 2], approx[j - 1]))
144  maxCosine = max(maxCosine, cosine)
145 
146  # if cosines of all angles are small
147  # (all angles are ~90 degree) then write quandrange
148  # vertices to resultant sequence
149  if maxCosine < 0.3:
150  squares.append(approx)
151  approx = np.array(approx).reshape(-1, 2)
152  cv2.polylines(img,
153  [approx],
154  True,
155  (0, 0, 255),
156  thickness=3)
157  cv2.line(img, tuple(approx[0]), tuple(approx[2]),
158  (0, 0, 255), 1)
159  cv2.line(img, tuple(approx[1]), tuple(approx[3]),
160  (0, 0, 255), 1)
161  return squares
162 
163 
164 def draw_squares(image, squares):
165  for i in range(len(squares)):
166  p = squares[i]
167  p = np.array(p).reshape(-1, 2)
168  image = cv2.polylines(image, [p], True, (0, 255, 0), thickness=3)
169 
170 
171 class PaperFinder(ConnectionBasedTransport):
172 
173  def __init__(self):
174  super(PaperFinder, self).__init__()
176  self.angle_tolerance = rospy.get_param(
177  '~angle_tolerance', np.rad2deg(5.0))
178  # 210mm * 297mm = 62370mm^2
179  self.area_tolerance = rospy.get_param(
180  '~area_tolerance', 0.1)
181  self.rect_x = rospy.get_param(
182  '~rect_x', 0.210)
183  self.rect_y = rospy.get_param(
184  '~rect_y', 0.297)
185  self.area_size = self.rect_x * self.rect_y
186  self.length_tolerance = rospy.get_param(
187  '~length_tolerance', 0.04)
188  self.image_pub = self.advertise('~output/viz',
189  sensor_msgs.msg.Image,
190  queue_size=1)
191  self.pose_array_pub = self.advertise('~output/pose',
192  geometry_msgs.msg.PoseArray,
193  queue_size=1)
194  self.bounding_box_array_pub = self.advertise(
195  '~output/boxes', BoundingBoxArray, queue_size=1)
196  self.length_array_pub = self.advertise(
197  '~output/length', std_msgs.msg.Float32MultiArray, queue_size=1)
198  self.bridge = cv_bridge.CvBridge()
199  self.camera_info_msg = None
200  self.cameramodel = None
201 
202  def subscribe(self):
203  queue_size = rospy.get_param('~queue_size', 10)
204  if rospy.get_param('~with_depth', True):
205  sub_img = message_filters.Subscriber(
206  '~input',
207  sensor_msgs.msg.Image,
208  queue_size=1,
209  buff_size=2**24)
210  sub_depth = message_filters.Subscriber(
211  '~input/depth',
212  sensor_msgs.msg.Image,
213  queue_size=1,
214  buff_size=2**24)
215  self.subs = [sub_img, sub_depth]
216  self.sub_info = rospy.Subscriber(
217  '~input/camera_info',
218  sensor_msgs.msg.CameraInfo, self._cb_cam_info)
219 
220  if rospy.get_param('~approximate_sync', True):
221  slop = rospy.get_param('~slop', 0.1)
222  sync = message_filters.ApproximateTimeSynchronizer(
223  fs=self.subs, queue_size=queue_size, slop=slop)
224  else:
226  fs=self.subs, queue_size=queue_size)
227  sync.registerCallback(self._cb_with_depth)
228  else:
229  sub = rospy.Subscriber(
230  '~input',
231  sensor_msgs.msg.Image,
232  callback=self._cb,
233  queue_size=queue_size)
234  self.subs = [sub]
235 
236  def unsubscribe(self):
237  for s in self.subs:
238  s.unregister()
239 
240  def _cb_cam_info(self, msg):
241  self.camera_info_msg = msg
242  self.cameramodel = PinholeCameraModel()
243  self.cameramodel.fromCameraInfo(msg)
244  self.sub_info.unregister()
245  self.sub_info = None
246  rospy.loginfo("Received camera info")
247 
248  def _cb(self, msg):
249  bridge = self.bridge
250  try:
251  cv_image = bridge.imgmsg_to_cv2(
252  msg, 'bgr8')
253  except cv_bridge.CvBridgeError as e:
254  rospy.logerr('{}'.format(e))
255  return
256  squares = self.rectangle_detector.find_squares(cv_image)
257 
258  if self.visualize:
259  draw_squares(cv_image, squares)
260  vis_msg = bridge.cv2_to_imgmsg(cv_image, encoding='bgr8')
261  vis_msg.header.stamp = msg.header.stamp
262  self.image_pub.publish(vis_msg)
263 
264  def _cb_with_depth(self, img_msg, depth_msg):
265  if self.camera_info_msg is None or self.cameramodel is None:
266  rospy.loginfo("Waiting camera info ...")
267  return
268  bridge = self.bridge
269  try:
270  cv_image = bridge.imgmsg_to_cv2(img_msg, 'bgr8')
271  depth_img = bridge.imgmsg_to_cv2(depth_msg, 'passthrough')
272 
273  if depth_msg.encoding == '16UC1':
274  depth_img = np.asarray(depth_img, dtype=np.float32)
275  depth_img /= 1000.0 # convert metric: mm -> m
276  elif depth_msg.encoding != '32FC1':
277  rospy.logerr('Unsupported depth encoding: %s' %
278  depth_msg.encoding)
279  except cv_bridge.CvBridgeError as e:
280  rospy.logerr('{}'.format(e))
281  return
282  squares = self.rectangle_detector.find_squares(cv_image)
283 
284  np_squares = np.array(squares, dtype=np.int32).reshape(-1, 2)
285 
286  height, width, _ = cv_image.shape
287  # calculate xyz-position
288  cameramodel = self.cameramodel
289  x = (np_squares[:, 0] - cameramodel.cx()) / cameramodel.fx()
290  y = (np_squares[:, 1] - cameramodel.cy()) / cameramodel.fy()
291  z = depth_img.reshape(-1)[np_squares[:, 1] * width + np_squares[:, 0]]
292 
293  x *= z
294  y *= z
295  x = x.reshape(-1, 4, 1, 1)
296  y = y.reshape(-1, 4, 1, 1)
297  z = z.reshape(-1, 4, 1, 1)
298  xyzs = np.concatenate([x, y, z], axis=3)
299  new_squares = []
300  valid_xyz_corners = []
301  length_array_for_publish = std_msgs.msg.Float32MultiArray()
302  for si, xyz in enumerate(xyzs):
303  xyz_org = xyz.reshape(4, 3)
304  xyz = np.concatenate([xyz, xyz], axis=0)
305  valid = True
306  for i in range(4):
307  vec_a = xyz[i] - xyz[i + 1]
308  vec_b = xyz[i + 2] - xyz[i + 1]
309  zzz = np.inner(vec_a, vec_b)
310  nnn = np.linalg.norm(vec_a) * np.linalg.norm(vec_b)
311  if nnn == 0.0:
312  valid = False
313  break
314  ccc = zzz / nnn
315  calc_axis = np.arccos(np.clip(ccc, -1.0, 1.0))
316  if abs(np.pi / 2.0 - calc_axis) > self.angle_tolerance:
317  valid = False
318  break
319  if valid is False:
320  break
321  _a = (np.sqrt(np.sum((xyz_org[0] - xyz_org[1]) ** 2)))
322  _b = (np.sqrt(np.sum((xyz_org[1] - xyz_org[2]) ** 2)))
323  _c = (np.sqrt(np.sum((xyz_org[2] - xyz_org[3]) ** 2)))
324  _d = (np.sqrt(np.sum((xyz_org[3] - xyz_org[0]) ** 2)))
325  tmp = sorted([_a, _b, _c, _d])
326  length_array = []
327  length_array.append(_a)
328  length_array.append(_b)
329  length_array.append(_c)
330  length_array.append(_d)
331  length_array_for_publish = std_msgs.msg.Float32MultiArray(
332  data=length_array)
333  if abs(tmp[0] - self.rect_x) > self.length_tolerance or \
334  abs(tmp[1] - self.rect_x) > self.length_tolerance or \
335  abs(tmp[2] - self.rect_y) > self.length_tolerance or \
336  abs(tmp[3] - self.rect_y) > self.length_tolerance:
337  break
338  area_value = area(xyz_org)
339  if abs(self.area_size - area_value) < self.area_tolerance:
340  new_squares.append(squares[si])
341  valid_xyz_corners.append(xyz_org)
342  self.length_array_pub.publish(length_array_for_publish)
343 
344  pose_array_msg = geometry_msgs.msg.PoseArray(header=img_msg.header)
345  bounding_box_array_msg = BoundingBoxArray(header=img_msg.header)
346  for xyz in valid_xyz_corners:
347  center = np.mean(xyz, axis=0)
348 
349  vec_a = xyz[2] - xyz[1]
350  vec_b = xyz[0] - xyz[1]
351  if np.linalg.norm(vec_a) > np.linalg.norm(vec_b):
352  vec_a, vec_b = vec_b, vec_a
353  normal = np.cross(vec_a, vec_b)
354  if normal[2] < 0:
355  normal *= -1.0
356  normal = normal / np.linalg.norm(normal)
357  M = np.eye(4)
358  M[:3, :3] = rotation_matrix_from_axis(
359  normal, vec_a)
360  q_xyzw = quaternion_from_matrix(M)
361  _a = (np.sqrt(np.sum((xyz[0] - xyz[1]) ** 2)))
362  _b = (np.sqrt(np.sum((xyz[1] - xyz[2]) ** 2)))
363  _c = (np.sqrt(np.sum((xyz[2] - xyz[3]) ** 2)))
364  _d = (np.sqrt(np.sum((xyz[3] - xyz[0]) ** 2)))
365  lengths = np.array([_a, _b, _c, _d])
366  indices = np.argsort(lengths)
367 
368  pose = geometry_msgs.msg.Pose()
369  pose.position.x = center[0]
370  pose.position.y = center[1]
371  pose.position.z = center[2]
372  pose.orientation.x = q_xyzw[0]
373  pose.orientation.y = q_xyzw[1]
374  pose.orientation.z = q_xyzw[2]
375  pose.orientation.w = q_xyzw[3]
376  pose_array_msg.poses.append(pose)
377  bounding_box_array = BoundingBox(header=img_msg.header)
378  bounding_box_array.pose.position.x = center[0]
379  bounding_box_array.pose.position.y = center[1]
380  bounding_box_array.pose.position.z = center[2]
381  bounding_box_array.pose.orientation.x = q_xyzw[0]
382  bounding_box_array.pose.orientation.y = q_xyzw[1]
383  bounding_box_array.pose.orientation.z = q_xyzw[2]
384  bounding_box_array.pose.orientation.w = q_xyzw[3]
385  bounding_box_array.dimensions.x = 0.01
386  bounding_box_array.dimensions.y = (
387  lengths[indices[0]] + lengths[indices[1]]) / 2.0
388  bounding_box_array.dimensions.z = (
389  lengths[indices[2]] + lengths[indices[3]]) / 2.0
390  bounding_box_array_msg.boxes.append(bounding_box_array)
391  self.bounding_box_array_pub.publish(bounding_box_array_msg)
392  self.pose_array_pub.publish(pose_array_msg)
393 
394  if self.visualize:
395  draw_squares(cv_image, new_squares)
396  vis_msg = bridge.cv2_to_imgmsg(cv_image, encoding='bgr8')
397  vis_msg.header.stamp = img_msg.header.stamp
398  self.image_pub.publish(vis_msg)
399 
400  @property
401  def visualize(self):
402  return self.image_pub.get_num_connections() > 0
403 
404 
405 if __name__ == '__main__':
406  rospy.init_node('paper_finder')
407  act = PaperFinder()
408  rospy.spin()
object
node_scripts.paper_finder.cross_product
def cross_product(a, b)
Definition: paper_finder.py:26
node_scripts.paper_finder.PaperFinder.visualize
def visualize(self)
Definition: paper_finder.py:401
node_scripts.paper_finder.PaperFinder.bridge
bridge
Definition: paper_finder.py:198
node_scripts.paper_finder.angle
def angle(pt1, pt2, pt0)
Definition: paper_finder.py:82
node_scripts.paper_finder.unit_normal
def unit_normal(a, b, c)
Definition: paper_finder.py:68
node_scripts.paper_finder.PaperFinder.length_tolerance
length_tolerance
Definition: paper_finder.py:186
node_scripts.paper_finder.PaperFinder.rectangle_detector
rectangle_detector
Definition: paper_finder.py:175
node_scripts.paper_finder.PaperFinder.area_size
area_size
Definition: paper_finder.py:185
msg
node_scripts.paper_finder.RectangleDetector
Definition: paper_finder.py:94
node_scripts.paper_finder.draw_squares
def draw_squares(image, squares)
Definition: paper_finder.py:164
node_scripts.paper_finder.PaperFinder.bounding_box_array_pub
bounding_box_array_pub
Definition: paper_finder.py:194
node_scripts.paper_finder.RectangleDetector.__init__
def __init__(self, length_threshold=10, distance_threshold=1.41421356, canny_th1=50.0, canny_th2=50.0, canny_aperture_size=3, do_merge=False)
Definition: paper_finder.py:96
node_scripts.paper_finder.PaperFinder.unsubscribe
def unsubscribe(self)
Definition: paper_finder.py:236
node_scripts.paper_finder.RectangleDetector.lsd
lsd
Definition: paper_finder.py:97
node_scripts.paper_finder.PaperFinder.angle_tolerance
angle_tolerance
Definition: paper_finder.py:176
message_filters::Subscriber
node_scripts.paper_finder.PaperFinder.camera_info_msg
camera_info_msg
Definition: paper_finder.py:199
node_scripts.paper_finder.PaperFinder.rect_y
rect_y
Definition: paper_finder.py:183
node_scripts.paper_finder.PaperFinder._cb_cam_info
def _cb_cam_info(self, msg)
Definition: paper_finder.py:240
node_scripts.paper_finder.PaperFinder.subs
subs
Definition: paper_finder.py:215
node_scripts.paper_finder.rotation_matrix_from_axis
def rotation_matrix_from_axis(first_axis=(1, 0, 0), second_axis=(0, 1, 0), axes='xy')
Definition: paper_finder.py:30
node_scripts.paper_finder.PaperFinder.cameramodel
cameramodel
Definition: paper_finder.py:200
node_scripts.paper_finder.outer_product_matrix
def outer_product_matrix(v)
Definition: paper_finder.py:20
node_scripts.paper_finder.PaperFinder.subscribe
def subscribe(self)
Definition: paper_finder.py:202
node_scripts.paper_finder.area
def area(poly)
Definition: paper_finder.py:49
node_scripts.paper_finder.PaperFinder.__init__
def __init__(self)
Definition: paper_finder.py:173
node_scripts.paper_finder.PaperFinder.length_array_pub
length_array_pub
Definition: paper_finder.py:196
node_scripts.paper_finder.PaperFinder.pose_array_pub
pose_array_pub
Definition: paper_finder.py:191
node_scripts.paper_finder.PaperFinder.area_tolerance
area_tolerance
Definition: paper_finder.py:179
node_scripts.paper_finder.PaperFinder.sub_info
sub_info
Definition: paper_finder.py:216
message_filters::TimeSynchronizer
node_scripts.paper_finder.PaperFinder
Definition: paper_finder.py:171
node_scripts.paper_finder.PaperFinder.rect_x
rect_x
Definition: paper_finder.py:181
node_scripts.paper_finder.PaperFinder._cb_with_depth
def _cb_with_depth(self, img_msg, depth_msg)
Definition: paper_finder.py:264
node_scripts.paper_finder.PaperFinder.image_pub
image_pub
Definition: paper_finder.py:188
node_scripts.paper_finder.PaperFinder._cb
def _cb(self, msg)
Definition: paper_finder.py:248
node_scripts.paper_finder.RectangleDetector.find_squares
def find_squares(self, img)
Definition: paper_finder.py:111


jsk_perception
Author(s): Manabu Saito, Ryohei Ueda
autogenerated on Fri May 16 2025 03:11:17