| """ |
| 分块流式处理rosbag模块 |
| |
| 核心思路(参考Diffusion Policy的按需读取方式): |
| 1. 第一遍扫描:只读取时间戳,确定主时间线(不加载图像数据,内存占用极小) |
| 2. 第二遍扫描:按时间窗口分块读取,边读取边对齐边写入dataset |
| |
| 与原始方法的区别: |
| - 原始:一次性加载所有数据到内存 → 对齐 → 写入dataset(内存峰值巨大) |
| - 新方法:分块读取 → 即时对齐 → 即时写入 → 释放内存(内存占用可控) |
| """ |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| import numpy as np |
| import rosbag |
| from collections import defaultdict |
| from typing import Dict, List, Callable, Optional, Tuple |
| import logging |
| import bisect |
|
|
| logger = logging.getLogger(__name__) |
|
|
|
|
| class ChunkedRosbagProcessor: |
| """ |
| 分块流式处理rosbag,实现边读取边对齐边处理 |
| |
| 工作流程: |
| 1. scan_timestamps(): 第一遍扫描,只读取时间戳(内存占用极小) |
| 2. process_chunks(): 第二遍扫描,按时间窗口分块处理 |
| """ |
| |
| def __init__(self, msg_processer, topic_process_map: dict, |
| camera_names: list, train_hz: int, main_timeline: str, main_timeline_fps: int, |
| sample_drop: int): |
| self._msg_processer = msg_processer |
| self._topic_process_map = topic_process_map |
| self.camera_names = camera_names |
| self.train_hz = train_hz |
| self.main_timeline_fps = main_timeline_fps |
| self.sample_drop = sample_drop |
| self.main_timeline = main_timeline |
| |
| def scan_timestamps_only(self, bag_file: str) -> Tuple[str, List[float], Dict[str, List[float]]]: |
| """ |
| 第一遍扫描:只读取时间戳,不加载数据 |
| |
| 内存占用:只有时间戳列表(几MB),不包含图像数据 |
| |
| Returns: |
| main_timeline: 主时间线话题key |
| main_timestamps: 对齐后的主时间戳序列(降采样后) |
| all_timestamps: 每个话题的原始时间戳列表 |
| """ |
| bag = self._load_bag(bag_file) |
| |
| |
| all_timestamps = defaultdict(list) |
| topic_to_key = {} |
| for k, v in self._topic_process_map.items(): |
| if v["topic"] not in topic_to_key.keys(): |
| topic_to_key[v["topic"]] = [k] |
| else: |
| topic_to_key[v["topic"]].append(k) |
| |
| logger.info(f"[Phase 1] Scanning timestamps from {bag_file}...") |
| |
| |
| all_topics = [v["topic"] for v in self._topic_process_map.values()] |
| for topic, msg, t in bag.read_messages(topics=all_topics): |
| keys = topic_to_key.get(topic) |
| for key in keys: |
| all_timestamps[key].append(t.to_sec()) |
| |
| |
| |
| bag.close() |
| |
| |
| camera_counts = {k: len(all_timestamps.get(k, [])) for k in self.camera_names} |
| if not any(camera_counts.values()): |
| raise ValueError("No camera data found in rosbag, please check your camera topics or bag file.") |
| if self.main_timeline is None: |
| main_timeline = max(camera_counts, key=lambda k: camera_counts[k]) |
| else: |
| main_timeline = self.main_timeline |
| logger.info(f"Main timeline: {main_timeline} ({camera_counts[main_timeline]} frames)") |
| |
| |
| jump = self.main_timeline_fps // self.train_hz |
|
|
| |
| for cam, count in camera_counts.items(): |
| raw_duration = (count - 2 * self.sample_drop) / self.main_timeline_fps if count > 2 * self.sample_drop else 0 |
| out_frames = (count - 2 * self.sample_drop) // jump if self.sample_drop > 0 else count // jump |
| out_duration = out_frames / self.train_hz |
| logger.info(f" Camera {cam}: {count} raw frames (~{raw_duration:.1f}s raw, ~{out_duration:.1f}s at {self.train_hz}Hz after jump={jump})") |
|
|
| raw_timestamps = all_timestamps[main_timeline] |
|
|
| if len(raw_timestamps) < 2 * self.sample_drop + 1: |
| raise ValueError(f"Not enough frames: {len(raw_timestamps)}") |
|
|
| |
| |
| |
| if self.sample_drop > 0: |
| main_timestamps = raw_timestamps[self.sample_drop:-self.sample_drop][::jump] |
| else: |
| main_timestamps = raw_timestamps[::jump] |
|
|
| out_duration_sec = len(main_timestamps) / self.train_hz |
| |
| max_end = max( |
| ts_list[-1] |
| for ts_list in all_timestamps.values() |
| if len(ts_list) > 0 |
| ) |
|
|
| |
| before_len = len(main_timestamps) |
| main_timestamps = [t for t in main_timestamps if t < max_end] |
| after_len = len(main_timestamps) |
|
|
| logger.info( |
| f"Trim main timeline by max_end={max_end:.6f}, " |
| f"frames: {before_len} -> {after_len}" |
| ) |
| |
| logger.info(f"Generated {len(main_timestamps)} aligned timestamps (~{out_duration_sec:.1f}s at {self.train_hz}Hz) " |
| f"(from {len(raw_timestamps)} raw frames, " |
| f"dropped {self.sample_drop} frames at each end, jump={jump})") |
| logger.info(f"Main timeline time range: [{main_timestamps[0]:.3f}, {main_timestamps[-1]:.3f}]") |
| return main_timeline, main_timestamps, dict(all_timestamps) |
| |
| def process_in_chunks( |
| self, |
| bag_file: str, |
| main_timestamps: List[float], |
| all_timestamps: Dict[str, List[float]], |
| frame_callback: Callable[[dict, int], None], |
| chunk_size: int = 100, |
| save_callback: Optional[Callable[[], None]] = None |
| ) -> int: |
| """ |
| 第二遍扫描:按时间窗口分块处理 |
| |
| 策略: |
| 1. 将main_timestamps分成多个chunk |
| 2. 对于每个chunk,只读取该时间范围内的消息 |
| 3. 对齐后立即调用frame_callback |
| 4. 每个chunk处理完后调用save_callback释放内存 |
| |
| Args: |
| bag_file: rosbag文件路径 |
| main_timestamps: 对齐后的主时间戳序列 |
| all_timestamps: 每个话题的原始时间戳列表(用于快速查找) |
| frame_callback: 处理每帧的回调函数 (aligned_frame, frame_idx) -> None |
| chunk_size: 每个chunk包含的帧数 |
| save_callback: 每个chunk处理完后的回调(用于保存和释放内存) |
| |
| Returns: |
| 处理的总帧数 |
| """ |
| bag = self._load_bag(bag_file) |
| |
| |
| gripper_keys = [] |
| for k in self._topic_process_map.keys(): |
| k_lower = k.lower() |
| if 'action' in k_lower and any(kw in k_lower for kw in ['claw', 'qiangnao', 'rq2f85', 'gripper']): |
| gripper_keys.append(k) |
| |
| |
| last_known_state = {} |
| |
| |
| |
| if gripper_keys: |
| logger.info(f"Prefetching initial shapes for gripper topics: {gripper_keys}") |
| for key in gripper_keys: |
| topic = self._topic_process_map[key]["topic"] |
| try: |
| for _, msg, t in bag.read_messages(topics=[topic]): |
| msg_process_fn = self._topic_process_map[key]["msg_process_fn"] |
| msg_data = msg_process_fn(msg) |
| if "data" in msg_data: |
| |
| last_known_state[key] = msg_data.copy() |
| last_known_state[key]["data"] = np.zeros_like(msg_data["data"]) |
| last_known_state[key]["timestamp"] = 0.0 |
| break |
| except Exception as e: |
| logger.warning(f"Failed to prefetch initial state for {key}: {e}") |
|
|
| |
| timestamp_arrays = {k: np.array(v) for k, v in all_timestamps.items()} |
| |
| |
| alignment_indices = self._precompute_alignment_indices( |
| main_timestamps, timestamp_arrays |
| ) |
| |
| num_chunks = (len(main_timestamps) + chunk_size - 1) // chunk_size |
| total_frames = 0 |
| |
| logger.info(f"[Phase 2] Processing {len(main_timestamps)} frames in {num_chunks} chunks...") |
| |
| for chunk_idx in range(num_chunks): |
| start_idx = chunk_idx * chunk_size |
| end_idx = min((chunk_idx + 1) * chunk_size, len(main_timestamps)) |
| chunk_timestamps = main_timestamps[start_idx:end_idx] |
| |
| if not chunk_timestamps: |
| continue |
| |
| |
| time_margin = 1.0 / self.train_hz |
| chunk_start_time = chunk_timestamps[0] - time_margin |
| chunk_end_time = chunk_timestamps[-1] + time_margin |
| |
| logger.debug(f"Chunk {chunk_idx+1}/{num_chunks}: " |
| f"frames {start_idx}-{end_idx-1}, " |
| f"time range [{chunk_start_time:.3f}, {chunk_end_time:.3f}]") |
| |
| |
| chunk_data = self._read_chunk_data(bag, chunk_start_time, chunk_end_time) |
| |
| |
| for local_idx, (global_idx, main_stamp) in enumerate( |
| zip(range(start_idx, end_idx), chunk_timestamps) |
| ): |
| aligned_frame = self._align_single_frame( |
| main_stamp=main_stamp, |
| global_idx=global_idx, |
| chunk_data=chunk_data, |
| timestamp_arrays=timestamp_arrays, |
| alignment_indices=alignment_indices, |
| last_known_state=last_known_state, |
| gripper_keys=gripper_keys |
| ) |
| |
| frame_callback(aligned_frame, global_idx) |
| total_frames += 1 |
| |
| |
| del chunk_data |
| |
| |
| if save_callback: |
| save_callback() |
| logger.info(f"Chunk {chunk_idx+1}/{num_chunks} processed and saved. " |
| f"Frames: {start_idx}-{end_idx-1}") |
| |
| bag.close() |
| logger.info(f"Total frames processed: {total_frames}") |
| return total_frames |
| |
| def _precompute_alignment_indices( |
| self, |
| main_timestamps: List[float], |
| timestamp_arrays: Dict[str, np.ndarray] |
| ) -> Dict[str, List[int]]: |
| """ |
| 预计算每个主时间戳对应的各话题索引 |
| 使用二分查找,比每帧都查找快很多 |
| """ |
| alignment_indices = {} |
| |
| for key, ts_array in timestamp_arrays.items(): |
| if len(ts_array) == 0: |
| alignment_indices[key] = [] |
| continue |
| |
| indices = [] |
| for stamp in main_timestamps: |
| |
| idx = bisect.bisect_left(ts_array, stamp) |
| if idx == 0: |
| closest_idx = 0 |
| elif idx == len(ts_array): |
| closest_idx = len(ts_array) - 1 |
| else: |
| |
| if abs(ts_array[idx] - stamp) < abs(ts_array[idx-1] - stamp): |
| closest_idx = idx |
| else: |
| closest_idx = idx - 1 |
| indices.append(closest_idx) |
| |
| alignment_indices[key] = indices |
| |
| return alignment_indices |
| |
| def _read_chunk_data(self, bag: rosbag.Bag, start_time: float, end_time: float) -> Dict[str, Dict[float, dict]]: |
| """ |
| 读取指定时间范围内的消息数据 |
| |
| Returns: |
| {topic_key: {timestamp: msg_data}} |
| """ |
| import rospy |
| chunk_data = defaultdict(dict) |
|
|
| topic_to_key = {} |
| for k, v in self._topic_process_map.items(): |
| if v["topic"] not in topic_to_key.keys(): |
| topic_to_key[v["topic"]] = [k] |
| else: |
| topic_to_key[v["topic"]].append(k) |
| |
| |
| try: |
| start_ros_time = rospy.Time.from_sec(start_time) |
| end_ros_time = rospy.Time.from_sec(end_time) |
| |
| for topic, msg, t in bag.read_messages( |
| topics=list(topic_to_key.keys()), |
| start_time=start_ros_time, |
| end_time=end_ros_time |
| ): |
| keys = topic_to_key.get(topic) |
| for key in keys: |
| msg_process_fn = self._topic_process_map[key]["msg_process_fn"] |
| msg_data = msg_process_fn(msg) |
| msg_data["timestamp"] = t.to_sec() |
| chunk_data[key][t.to_sec()] = msg_data |
| |
|
|
| except Exception as e: |
| logger.warning(f"Time-range filtering failed: {e}, falling back to full scan") |
| |
| for topic, msg, t in bag.read_messages(topics=list(topic_to_key.keys())): |
| ts = t.to_sec() |
| |
| |
|
|
| if start_time <= ts <= end_time: |
| key = topic_to_key.get(topic) |
| if key: |
| msg_process_fn = self._topic_process_map[key]["msg_process_fn"] |
| msg_data = msg_process_fn(msg) |
| msg_data["timestamp"] = ts |
| chunk_data[key][ts] = msg_data |
| return dict(chunk_data) |
| |
| def _align_single_frame( |
| self, |
| main_stamp: float, |
| global_idx: int, |
| chunk_data: Dict[str, Dict[float, dict]], |
| timestamp_arrays: Dict[str, np.ndarray], |
| alignment_indices: Dict[str, List[int]], |
| last_known_state: Dict[str, dict], |
| gripper_keys: List[str] |
| ) -> dict: |
| """ |
| 对齐单帧数据 |
| """ |
| aligned_frame = {"timestamp": main_stamp} |
| |
| for key in self._topic_process_map.keys(): |
| is_gripper = key in gripper_keys |
|
|
| |
| if key not in timestamp_arrays or len(timestamp_arrays[key]) == 0: |
| if is_gripper and key in last_known_state: |
| aligned_frame[key] = last_known_state[key].copy() |
| aligned_frame[key]["timestamp"] = main_stamp |
| else: |
| aligned_frame[key] = None |
| continue |
|
|
| first_ts = timestamp_arrays[key][0] |
| |
| |
| |
| if main_stamp < first_ts: |
| |
| if is_gripper and key in last_known_state: |
| aligned_frame[key] = last_known_state[key].copy() |
| aligned_frame[key]["timestamp"] = main_stamp |
| continue |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| |
| |
| if key not in alignment_indices or global_idx >= len(alignment_indices[key]): |
| if is_gripper and key in last_known_state: |
| aligned_frame[key] = last_known_state[key].copy() |
| aligned_frame[key]["timestamp"] = main_stamp |
| else: |
| aligned_frame[key] = None |
| continue |
|
|
| closest_idx = alignment_indices[key][global_idx] |
| target_ts = timestamp_arrays[key][closest_idx] |
| |
| |
| if key in chunk_data: |
| |
| ts_list = list(chunk_data[key].keys()) |
| if ts_list: |
| closest_chunk_ts = min(ts_list, key=lambda x: abs(x - target_ts)) |
| aligned_frame[key] = chunk_data[key][closest_chunk_ts] |
| |
| |
| if is_gripper: |
| last_known_state[key] = aligned_frame[key] |
| else: |
| aligned_frame[key] = None |
| else: |
| aligned_frame[key] = None |
| |
| |
| |
| |
| if is_gripper and aligned_frame[key] is None: |
| if key in last_known_state: |
| aligned_frame[key] = last_known_state[key].copy() |
| aligned_frame[key]["timestamp"] = main_stamp |
| return aligned_frame |
| |
| def _load_bag(self, bag_file: str) -> rosbag.Bag: |
| """加载rosbag文件""" |
| try: |
| return rosbag.Bag(bag_file) |
| except rosbag.bag.ROSBagUnindexedException: |
| logger.warning(f"Bag file {bag_file} is unindexed, attempting to reindex...") |
| from .utils import reindex_rosbag |
| reindexed_file = reindex_rosbag(bag_file) |
| if reindexed_file: |
| return rosbag.Bag(reindexed_file) |
| else: |
| return rosbag.Bag(bag_file, 'r', allow_unindexed=True) |
|
|