Skip to content

Commit f775d8c

Browse files
committed
Add full-length map support to masked_replace_tool
When --full-length-x or --full-length-y is specified, the tool sends a full-size GridMap with the patch embedded at the specified position. This allows testing masked_replace with maps that match the node's full grid dimensions. New arguments: - --full-length-x: Total GridMap length in X - --full-length-y: Total GridMap length in Y - --full-center-x: GridMap center X for full-size map - --full-center-y: GridMap center Y for full-size map
1 parent 984a3c1 commit f775d8c

File tree

1 file changed

+131
-22
lines changed

1 file changed

+131
-22
lines changed

scripts/masked_replace_tool.py

Lines changed: 131 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,30 @@ def build_parser() -> argparse.ArgumentParser:
4646
parser.add_argument("--center-z", type=float, default=0.0, help="Patch center Z coordinate (meters).")
4747
parser.add_argument("--size-x", type=positive_float, default=1.0, help="Patch length in X (meters).")
4848
parser.add_argument("--size-y", type=positive_float, default=1.0, help="Patch length in Y (meters).")
49+
parser.add_argument(
50+
"--full-length-x",
51+
type=positive_float,
52+
default=None,
53+
help="Optional total GridMap length in X (meters). If set, a full-size map is sent and only the patch region is marked in the mask."
54+
)
55+
parser.add_argument(
56+
"--full-length-y",
57+
type=positive_float,
58+
default=None,
59+
help="Optional total GridMap length in Y (meters). If set, a full-size map is sent and only the patch region is marked in the mask."
60+
)
61+
parser.add_argument(
62+
"--full-center-x",
63+
type=float,
64+
default=0.0,
65+
help="GridMap center X (meters) to use when sending a full-size map. Defaults to 0."
66+
)
67+
parser.add_argument(
68+
"--full-center-y",
69+
type=float,
70+
default=0.0,
71+
help="GridMap center Y (meters) to use when sending a full-size map. Defaults to 0."
72+
)
4973
parser.add_argument("--resolution", type=positive_float, default=0.1, help="Grid resolution (meters per cell).")
5074
parser.add_argument("--elevation", type=float, default=0.1, help="Elevation value to set (meters).")
5175
parser.add_argument("--variance", type=non_negative_float, default=0.05, help="Variance value to set.")
@@ -84,6 +108,10 @@ class PatchConfig:
84108
mask_value: float
85109
add_valid_layer: bool
86110
invalidate_first: bool
111+
full_length_x: Optional[float] = None
112+
full_length_y: Optional[float] = None
113+
full_center_x: float = 0.0
114+
full_center_y: float = 0.0
87115

88116
@property
89117
def shape(self) -> Dict[str, int]:
@@ -139,10 +167,17 @@ def _base_grid_map(self) -> GridMap:
139167
gm.header.frame_id = cfg.frame_id
140168
gm.header.stamp = self.get_clock().now().to_msg()
141169
gm.info.resolution = cfg.resolution
142-
gm.info.length_x = cfg.actual_length_x
143-
gm.info.length_y = cfg.actual_length_y
144-
gm.info.pose.position.x = cfg.center_x
145-
gm.info.pose.position.y = cfg.center_y
170+
# If full map was requested, use the full lengths and center the GridMap at the full-map center.
171+
if cfg.full_length_x or cfg.full_length_y:
172+
gm.info.length_x = cfg.full_length_x or cfg.actual_length_x
173+
gm.info.length_y = cfg.full_length_y or cfg.actual_length_y
174+
gm.info.pose.position.x = cfg.full_center_x
175+
gm.info.pose.position.y = cfg.full_center_y
176+
else:
177+
gm.info.length_x = cfg.actual_length_x
178+
gm.info.length_y = cfg.actual_length_y
179+
gm.info.pose.position.x = cfg.center_x
180+
gm.info.pose.position.y = cfg.center_y
146181
gm.info.pose.position.z = cfg.center_z
147182
gm.info.pose.orientation.w = 1.0
148183
gm.basic_layers = ["elevation"]
@@ -157,32 +192,102 @@ def _mask_array(self, force_value: Optional[float] = None) -> np.ndarray:
157192
mask_value = 1.0
158193
return np.full((rows, cols), mask_value, dtype=np.float32)
159194

195+
def _make_full_arrays(self) -> Dict[str, np.ndarray]:
196+
"""Create full-size arrays (possibly larger than the patch) and place the patch in them."""
197+
cfg = self._config
198+
length_x = cfg.full_length_x or cfg.length_x
199+
length_y = cfg.full_length_y or cfg.length_y
200+
cols_full = max(1, ceil(length_x / cfg.resolution))
201+
rows_full = max(1, ceil(length_y / cfg.resolution))
202+
203+
# Base arrays filled with NaN (masked out)
204+
mask_full = np.full((rows_full, cols_full), np.nan, dtype=np.float32)
205+
elev_full = np.full((rows_full, cols_full), np.nan, dtype=np.float32)
206+
var_full = np.full((rows_full, cols_full), np.nan, dtype=np.float32)
207+
valid_full = np.zeros((rows_full, cols_full), dtype=np.float32)
208+
209+
# Patch dimensions and offset within the full map
210+
patch_rows = cfg.shape["rows"]
211+
patch_cols = cfg.shape["cols"]
212+
row_offset = int(round(cfg.center_y / cfg.resolution))
213+
col_offset = int(round(cfg.center_x / cfg.resolution))
214+
row_start = rows_full // 2 + row_offset - patch_rows // 2
215+
col_start = cols_full // 2 + col_offset - patch_cols // 2
216+
row_end = row_start + patch_rows
217+
col_end = col_start + patch_cols
218+
219+
# Clamp if window would exceed bounds
220+
if row_start < 0 or col_start < 0 or row_end > rows_full or col_end > cols_full:
221+
raise ValueError("Patch exceeds full map bounds; adjust center/size or full map length.")
222+
223+
mask_val = cfg.mask_value
224+
if np.isnan(mask_val):
225+
mask_val = 1.0
226+
mask_full[row_start:row_end, col_start:col_end] = mask_val
227+
elev_full[row_start:row_end, col_start:col_end] = cfg.elevation
228+
var_full[row_start:row_end, col_start:col_end] = cfg.variance
229+
if cfg.add_valid_layer:
230+
valid_full[row_start:row_end, col_start:col_end] = 1.0
231+
232+
return {
233+
"mask": mask_full,
234+
"elevation": elev_full,
235+
"variance": var_full,
236+
"is_valid": valid_full,
237+
"rows_full": rows_full,
238+
"cols_full": cols_full,
239+
}
240+
160241
def _build_validity_message(self, value: float) -> GridMap:
161242
gm = self._base_grid_map()
162-
mask = self._mask_array()
163-
rows, cols = mask.shape
164-
gm.layers = [self._config.mask_layer, "is_valid"]
165-
arrays = {
166-
self._config.mask_layer: mask,
167-
"is_valid": np.full((rows, cols), value, dtype=np.float32),
168-
}
243+
cfg = self._config
244+
if cfg.full_length_x or cfg.full_length_y:
245+
arrays_full = self._make_full_arrays()
246+
rows_full = arrays_full["rows_full"]
247+
cols_full = arrays_full["cols_full"]
248+
gm.layers = [cfg.mask_layer, "is_valid"]
249+
arrays = {
250+
cfg.mask_layer: arrays_full["mask"],
251+
"is_valid": np.full((rows_full, cols_full), value, dtype=np.float32),
252+
}
253+
else:
254+
mask = self._mask_array()
255+
rows, cols = mask.shape
256+
gm.layers = [cfg.mask_layer, "is_valid"]
257+
arrays = {
258+
cfg.mask_layer: mask,
259+
"is_valid": np.full((rows, cols), value, dtype=np.float32),
260+
}
169261
for layer in gm.layers:
170262
gm.data.append(self._numpy_to_multiarray(arrays[layer]))
171263
return gm
172264

173265
def _build_data_message(self, valid_value: Optional[float]) -> GridMap:
174266
gm = self._base_grid_map()
175-
mask = self._mask_array()
176-
rows, cols = mask.shape
177-
gm.layers = [self._config.mask_layer, "elevation", "variance"]
178-
arrays = {
179-
self._config.mask_layer: mask,
180-
"elevation": np.full((rows, cols), self._config.elevation, dtype=np.float32),
181-
"variance": np.full((rows, cols), self._config.variance, dtype=np.float32),
182-
}
183-
if valid_value is not None:
184-
gm.layers.append("is_valid")
185-
arrays["is_valid"] = np.full((rows, cols), valid_value, dtype=np.float32)
267+
cfg = self._config
268+
if cfg.full_length_x or cfg.full_length_y:
269+
arrays_full = self._make_full_arrays()
270+
gm.layers = [cfg.mask_layer, "elevation", "variance"]
271+
arrays = {
272+
cfg.mask_layer: arrays_full["mask"],
273+
"elevation": arrays_full["elevation"],
274+
"variance": arrays_full["variance"],
275+
}
276+
if valid_value is not None:
277+
gm.layers.append("is_valid")
278+
arrays["is_valid"] = arrays_full["is_valid"]
279+
else:
280+
mask = self._mask_array()
281+
rows, cols = mask.shape
282+
gm.layers = [cfg.mask_layer, "elevation", "variance"]
283+
arrays = {
284+
cfg.mask_layer: mask,
285+
"elevation": np.full((rows, cols), cfg.elevation, dtype=np.float32),
286+
"variance": np.full((rows, cols), cfg.variance, dtype=np.float32),
287+
}
288+
if valid_value is not None:
289+
gm.layers.append("is_valid")
290+
arrays["is_valid"] = np.full((rows, cols), valid_value, dtype=np.float32)
186291
for layer in gm.layers:
187292
gm.data.append(self._numpy_to_multiarray(arrays[layer]))
188293
return gm
@@ -216,6 +321,10 @@ def main() -> None:
216321
mask_value=args.mask_value,
217322
add_valid_layer=args.valid_layer,
218323
invalidate_first=args.invalidate_first,
324+
full_length_x=args.full_length_x,
325+
full_length_y=args.full_length_y,
326+
full_center_x=args.full_center_x,
327+
full_center_y=args.full_center_y,
219328
)
220329

221330
rclpy.init()

0 commit comments

Comments
 (0)