Skip to content

Commit ed045de

Browse files
committed
added cacheable_coverage_progress.py
1 parent e5a8965 commit ed045de

File tree

1 file changed

+193
-0
lines changed

1 file changed

+193
-0
lines changed

nodes/cacheable_coverage_progress.py

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
#! /usr/bin/env python3
2+
3+
import rospy
4+
import tf2_ros
5+
from genpy import DeserializationError
6+
from nav_msgs.msg import OccupancyGrid
7+
from nav_msgs.srv import GetMap
8+
from numpy import ones, sum
9+
from std_msgs.msg import Float32, Header
10+
from std_srvs.srv import Trigger
11+
from tf.transformations import euler_from_quaternion
12+
13+
# Constants for more readable index lookup
14+
X, Y, Z, W = 0, 1, 2, 3
15+
16+
17+
class CoverageProgressNode(object):
18+
"""
19+
The CoverageProgressNode keeps track of coverage progress.
20+
It does this by periodically looking up the position of the coverage disk in an occupancy grid.
21+
Cells within a radius from this position are 'covered'
22+
23+
Cell values (0 ~ 100) are interpreted in this way: Lower is yet to be visited, Higher is covered
24+
- 0: uncovered (initial value)
25+
- > 0: covered
26+
27+
The node emits a coverage progress,
28+
which is the ratio of cells considered coverage
29+
(0.0 is everything uncovered, 1.0 is all covered)
30+
"""
31+
32+
DIRTY = 0
33+
CLEAN = 100
34+
35+
def __init__(self):
36+
# Transform Initialization
37+
self.tfBuffer = tf2_ros.Buffer()
38+
self.tfListener = tf2_ros.TransformListener(self.tfBuffer)
39+
40+
# ROS Stuff
41+
self.map_frame = rospy.get_param("~map_frame", default="map")
42+
self.coverage_frame = rospy.get_param("~coverage_frame", default="base_link")
43+
self.cache_dir = rospy.get_param("~cache_dir", default="progress_map_data.txt")
44+
45+
self.progress_pub = rospy.Publisher("coverage_progress", Float32, queue_size=1)
46+
self.grid_pub = rospy.Publisher("coverage_grid", OccupancyGrid, queue_size=1)
47+
48+
self.reset_srv = rospy.Service("reset", Trigger, self.reset)
49+
50+
# Receive Initial Static Map
51+
self.grid = OccupancyGrid()
52+
try:
53+
with open(self.cache_dir, "rb") as f:
54+
self.grid.deserialize(f.read())
55+
self.grid.data = list(self.grid.data)
56+
except DeserializationError:
57+
rospy.logerr(
58+
"Fail to load cached progress map. Used static map to initial a new progress map."
59+
)
60+
self._initialize_map()
61+
except FileNotFoundError:
62+
rospy.logwarn("Used static map to initial a new progress map.")
63+
self._initialize_map()
64+
65+
# Get coverage radius, which is the robot radius which counts as coverage
66+
try:
67+
self.coverage_radius_meters = rospy.get_param("~coverage_radius")
68+
except KeyError:
69+
try:
70+
self.coverage_radius_meters = rospy.get_param("~coverage_width") / 2.0
71+
except KeyError:
72+
rospy.logerr("Specify either coverage_width or coverage_radius")
73+
raise ValueError(
74+
"Neither ~coverage_radius nor ~coverage_width specified, "
75+
"one of these is required"
76+
)
77+
self.coverage_radius_meters += (
78+
2 * self.grid.info.resolution
79+
) # Compensate for discretization
80+
self.coverage_radius_cells = int(
81+
self.coverage_radius_meters / self.grid.info.resolution
82+
)
83+
84+
# How covered is a cell after it has been covered for 1 time step
85+
self.coverage_effectivity = rospy.get_param("~coverage_effectivity", default=5)
86+
87+
# Timer Callback
88+
self._rate = rospy.get_param("~rate", 10.0)
89+
self._update_timer = rospy.Timer(
90+
rospy.Duration(1.0 / self._rate), self._update_callback
91+
)
92+
self._cache_timer = rospy.Timer(
93+
rospy.Duration(secs=5), self._save_to_file
94+
)
95+
96+
def _initialize_map(self):
97+
print("Waiting for static map service...")
98+
rospy.wait_for_service("static_map")
99+
try:
100+
static_map_client = rospy.ServiceProxy("static_map", GetMap)
101+
response = static_map_client()
102+
self.grid.header = response.map.header
103+
self.grid.info = response.map.info
104+
self.grid.data = list(response.map.data)
105+
106+
print("Received static map!")
107+
except rospy.ServiceException as e:
108+
print(f"static map service call failed: {e}")
109+
110+
def _update_callback(self, event):
111+
# Get the position of point (0,0,0) the coverage_disk frame wrt.
112+
# the map frame (which can both be remapped if need be)
113+
114+
try:
115+
trans = self.tfBuffer.lookup_transform(
116+
self.map_frame, self.coverage_frame, rospy.Time()
117+
)
118+
except (
119+
tf2_ros.LookupException,
120+
tf2_ros.ConnectivityException,
121+
tf2_ros.ExtrapolationException,
122+
) as e:
123+
print(f"tf error: {e}")
124+
return
125+
126+
# Element of matrix corresponding to middle of coverage surface
127+
x_point = int(
128+
(trans.transform.translation.x - self.grid.info.origin.position.x)
129+
/ self.grid.info.resolution
130+
)
131+
y_point = int(
132+
(trans.transform.translation.y - self.grid.info.origin.position.y)
133+
/ self.grid.info.resolution
134+
)
135+
136+
# Initialize message
137+
self.grid.header = Header()
138+
self.grid.header.frame_id = self.map_frame
139+
140+
# Loop over amount of cells covered in x (j) and y (i) direction
141+
for i in range(2 * self.coverage_radius_cells):
142+
for j in range(2 * self.coverage_radius_cells):
143+
144+
# iterate from (-radius) to (+radius)
145+
x_index = j - self.coverage_radius_cells
146+
y_index = i - self.coverage_radius_cells
147+
148+
array_index = (
149+
x_point + x_index + self.grid.info.width * (y_point + y_index)
150+
)
151+
152+
# if cell is inside coverage circle:
153+
# determined using circle formula x^2+y^2 = (radius)^2
154+
cell_in_coverage_circle = (
155+
x_index**2 + y_index**2 < self.coverage_radius_cells**2
156+
)
157+
# if cell is within the grid
158+
cell_in_grid = 0 <= x_point + x_index < abs(
159+
int(self.grid.info.width / self.grid.info.resolution)
160+
) and 0 <= y_point + y_index < abs(
161+
int(self.grid.info.height / self.grid.info.resolution)
162+
)
163+
164+
if cell_in_coverage_circle and cell_in_grid:
165+
self.grid.data[array_index] = min(
166+
self.CLEAN,
167+
self.grid.data[array_index] + self.coverage_effectivity,
168+
)
169+
170+
coverage_progress = float(sum(self.grid.data)) / (
171+
self.grid.info.width * self.grid.info.height * 100.0
172+
)
173+
174+
self.progress_pub.publish(coverage_progress)
175+
self.grid_pub.publish(self.grid)
176+
177+
def reset(self, srv_request):
178+
rospy.loginfo("Reset coverage progress and grid")
179+
self._initialize_map()
180+
return True, "Reset coverage progress and grid"
181+
182+
def _save_to_file(self, event):
183+
with open(self.cache_dir, "wb") as f:
184+
self.grid.serialize(f)
185+
186+
187+
if __name__ == "__main__":
188+
rospy.init_node("coverage_progress")
189+
try:
190+
node = CoverageProgressNode()
191+
rospy.spin()
192+
except rospy.ROSInterruptException:
193+
pass

0 commit comments

Comments
 (0)