/* * Copyright (C) 2007 Austin Robot Technology, Patrick Beeson * Copyright (C) 2009, 2010, 2012 Austin Robot Technology, Jack O'Quin * Copyright (C) 2019, Kaarta Inc, Shawn Hanna * * License: Modified BSD Software License Agreement * * $Id$ */ /** * @file * * Velodyne 3D LIDAR data accessor class implementation. * * Class for unpacking raw Velodyne LIDAR packets into useful * formats. * * Derived classes accept raw Velodyne data for either single packets * or entire rotations, and provide it in various formats for either * on-line or off-line processing. * * @author Patrick Beeson * @author Jack O'Quin * @author Shawn Hanna * * HDL-64E S2 calibration support provided by Nick Hillier */ #include #include #include #include #include #include namespace velodyne_rawdata { inline float SQR(float val) { return val*val; } //////////////////////////////////////////////////////////////////////// // // RawData base class implementation // //////////////////////////////////////////////////////////////////////// RawData::RawData() {} /** Update parameters: conversions and update */ void RawData::setParameters(double min_range, double max_range, double view_direction, double view_width) { config_.min_range = min_range; config_.max_range = max_range; //converting angle parameters into the velodyne reference (rad) config_.tmp_min_angle = view_direction + view_width/2; config_.tmp_max_angle = view_direction - view_width/2; //computing positive modulo to keep theses angles into [0;2*M_PI] config_.tmp_min_angle = fmod(fmod(config_.tmp_min_angle,2*M_PI) + 2*M_PI,2*M_PI); config_.tmp_max_angle = fmod(fmod(config_.tmp_max_angle,2*M_PI) + 2*M_PI,2*M_PI); //converting into the hardware velodyne ref (negative yaml and degrees) //adding 0.5 perfomrs a centered double to int conversion config_.min_angle = 100 * (2*M_PI - config_.tmp_min_angle) * 180 / M_PI + 0.5; config_.max_angle = 100 * (2*M_PI - config_.tmp_max_angle) * 180 / M_PI + 0.5; if (config_.min_angle == config_.max_angle) { //avoid returning empty cloud if min_angle = max_angle config_.min_angle = 0; config_.max_angle = 36000; } } int RawData::scansPerPacket() const { if( calibration_.num_lasers == 16) { return BLOCKS_PER_PACKET * VLP16_FIRINGS_PER_BLOCK * VLP16_SCANS_PER_FIRING; } else{ return BLOCKS_PER_PACKET * SCANS_PER_BLOCK; } } /** * Build a timing table for each block/firing. Stores in timing_offsets vector */ bool RawData::buildTimings(){ // vlp16 if (config_.model == "VLP16"){ // timing table calculation, from velodyne user manual timing_offsets.resize(12); for (size_t i=0; i < timing_offsets.size(); ++i){ timing_offsets[i].resize(32); } // constants double full_firing_cycle = 55.296 * 1e-6; // seconds double single_firing = 2.304 * 1e-6; // seconds double dataBlockIndex, dataPointIndex; bool dual_mode = false; // compute timing offsets for (size_t x = 0; x < timing_offsets.size(); ++x){ for (size_t y = 0; y < timing_offsets[x].size(); ++y){ if (dual_mode){ dataBlockIndex = (x - (x % 2)) + (y / 16); } else{ dataBlockIndex = (x * 2) + (y / 16); } dataPointIndex = y % 16; //timing_offsets[block][firing] timing_offsets[x][y] = (full_firing_cycle * dataBlockIndex) + (single_firing * dataPointIndex); } } } // vlp32 else if (config_.model == "32C"){ // timing table calculation, from velodyne user manual timing_offsets.resize(12); for (size_t i=0; i < timing_offsets.size(); ++i){ timing_offsets[i].resize(32); } // constants double full_firing_cycle = 55.296 * 1e-6; // seconds double single_firing = 2.304 * 1e-6; // seconds double dataBlockIndex, dataPointIndex; bool dual_mode = false; // compute timing offsets for (size_t x = 0; x < timing_offsets.size(); ++x){ for (size_t y = 0; y < timing_offsets[x].size(); ++y){ if (dual_mode){ dataBlockIndex = x / 2; } else{ dataBlockIndex = x; } dataPointIndex = y / 2; timing_offsets[x][y] = (full_firing_cycle * dataBlockIndex) + (single_firing * dataPointIndex); } } } // hdl32 else if (config_.model == "32E"){ // timing table calculation, from velodyne user manual timing_offsets.resize(12); for (size_t i=0; i < timing_offsets.size(); ++i){ timing_offsets[i].resize(32); } // constants double full_firing_cycle = 46.080 * 1e-6; // seconds double single_firing = 1.152 * 1e-6; // seconds double dataBlockIndex, dataPointIndex; bool dual_mode = false; // compute timing offsets for (size_t x = 0; x < timing_offsets.size(); ++x){ for (size_t y = 0; y < timing_offsets[x].size(); ++y){ if (dual_mode){ dataBlockIndex = x / 2; } else{ dataBlockIndex = x; } dataPointIndex = y / 2; timing_offsets[x][y] = (full_firing_cycle * dataBlockIndex) + (single_firing * dataPointIndex); } } } else{ timing_offsets.clear(); ROS_WARN("Timings not supported for model %s", config_.model.c_str()); } if (timing_offsets.size()){ // ROS_INFO("VELODYNE TIMING TABLE:"); for (size_t x = 0; x < timing_offsets.size(); ++x){ for (size_t y = 0; y < timing_offsets[x].size(); ++y){ printf("%04.3f ", timing_offsets[x][y] * 1e6); } printf("\n"); } return true; } else{ ROS_WARN("NO TIMING OFFSETS CALCULATED. ARE YOU USING A SUPPORTED VELODYNE SENSOR?"); } return false; } /** Set up for on-line operation. */ boost::optional RawData::setup(ros::NodeHandle private_nh) { private_nh.param("model", config_.model, std::string("64E")); buildTimings(); // get path to angles.config file for this device if (!private_nh.getParam("calibration", config_.calibrationFile)) { ROS_ERROR_STREAM("No calibration angles specified! Using test values!"); // have to use something: grab unit test version as a default std::string pkgPath = ros::package::getPath("velodyne_pointcloud"); config_.calibrationFile = pkgPath + "/params/64e_utexas.yaml"; } ROS_INFO_STREAM("correction angles: " << config_.calibrationFile); calibration_.read(config_.calibrationFile); if (!calibration_.initialized) { ROS_ERROR_STREAM("Unable to open calibration file: " << config_.calibrationFile); return boost::none; } ROS_INFO_STREAM("Number of lasers: " << calibration_.num_lasers << "."); // Set up cached values for sin and cos of all the possible headings for (uint16_t rot_index = 0; rot_index < ROTATION_MAX_UNITS; ++rot_index) { float rotation = angles::from_degrees(ROTATION_RESOLUTION * rot_index); cos_rot_table_[rot_index] = cosf(rotation); sin_rot_table_[rot_index] = sinf(rotation); } return calibration_; } /** Set up for offline operation */ int RawData::setupOffline(std::string calibration_file, double max_range_, double min_range_) { config_.max_range = max_range_; config_.min_range = min_range_; ROS_INFO_STREAM("data ranges to publish: [" << config_.min_range << ", " << config_.max_range << "]"); config_.calibrationFile = calibration_file; ROS_INFO_STREAM("correction angles: " << config_.calibrationFile); calibration_.read(config_.calibrationFile); if (!calibration_.initialized) { ROS_ERROR_STREAM("Unable to open calibration file: " << config_.calibrationFile); return -1; } // Set up cached values for sin and cos of all the possible headings for (uint16_t rot_index = 0; rot_index < ROTATION_MAX_UNITS; ++rot_index) { float rotation = angles::from_degrees(ROTATION_RESOLUTION * rot_index); cos_rot_table_[rot_index] = cosf(rotation); sin_rot_table_[rot_index] = sinf(rotation); } return 0; } /** @brief convert raw packet to point cloud * * @param pkt raw packet to unpack * @param pc shared pointer to point cloud (points are appended) */ void RawData::unpack(const velodyne_msgs::VelodynePacket &pkt, DataContainerBase& data, const ros::Time& scan_start_time) { using velodyne_pointcloud::LaserCorrection; ROS_DEBUG_STREAM("Received packet, time: " << pkt.stamp); /** special parsing for the VLP16 **/ if (calibration_.num_lasers == 16) { unpack_vlp16(pkt, data, scan_start_time); return; } float time_diff_start_to_this_packet = (pkt.stamp - scan_start_time).toSec(); const raw_packet_t *raw = (const raw_packet_t *) &pkt.data[0]; for (int i = 0; i < BLOCKS_PER_PACKET; i++) { // upper bank lasers are numbered [0..31] // NOTE: this is a change from the old velodyne_common implementation int bank_origin = 0; if (raw->blocks[i].header == LOWER_BANK) { // lower bank lasers are [32..63] bank_origin = 32; } for (int j = 0, k = 0; j < SCANS_PER_BLOCK; j++, k += RAW_SCAN_SIZE) { float x, y, z; float intensity; const uint8_t laser_number = j + bank_origin; float time = 0; const LaserCorrection &corrections = calibration_.laser_corrections[laser_number]; /** Position Calculation */ const raw_block_t &block = raw->blocks[i]; union two_bytes tmp; tmp.bytes[0] = block.data[k]; tmp.bytes[1] = block.data[k+1]; /*condition added to avoid calculating points which are not in the interesting defined area (min_angle < area < max_angle)*/ if ((block.rotation >= config_.min_angle && block.rotation <= config_.max_angle && config_.min_angle < config_.max_angle) ||(config_.min_angle > config_.max_angle && (raw->blocks[i].rotation <= config_.max_angle || raw->blocks[i].rotation >= config_.min_angle))){ if (timing_offsets.size()) { time = timing_offsets[i][j] + time_diff_start_to_this_packet; } if (tmp.uint == 0) // no valid laser beam return { // call to addPoint is still required since output could be organized data.addPoint(nanf(""), nanf(""), nanf(""), corrections.laser_ring, raw->blocks[i].rotation, nanf(""), nanf(""), time); continue; } float distance = tmp.uint * calibration_.distance_resolution_m; distance += corrections.dist_correction; float cos_vert_angle = corrections.cos_vert_correction; float sin_vert_angle = corrections.sin_vert_correction; float cos_rot_correction = corrections.cos_rot_correction; float sin_rot_correction = corrections.sin_rot_correction; // cos(a-b) = cos(a)*cos(b) + sin(a)*sin(b) // sin(a-b) = sin(a)*cos(b) - cos(a)*sin(b) float cos_rot_angle = cos_rot_table_[block.rotation] * cos_rot_correction + sin_rot_table_[block.rotation] * sin_rot_correction; float sin_rot_angle = sin_rot_table_[block.rotation] * cos_rot_correction - cos_rot_table_[block.rotation] * sin_rot_correction; float horiz_offset = corrections.horiz_offset_correction; float vert_offset = corrections.vert_offset_correction; // Compute the distance in the xy plane (w/o accounting for rotation) /**the new term of 'vert_offset * sin_vert_angle' * was added to the expression due to the mathemathical * model we used. */ float xy_distance = distance * cos_vert_angle - vert_offset * sin_vert_angle; // Calculate temporal X, use absolute value. float xx = xy_distance * sin_rot_angle - horiz_offset * cos_rot_angle; // Calculate temporal Y, use absolute value float yy = xy_distance * cos_rot_angle + horiz_offset * sin_rot_angle; if (xx < 0) xx=-xx; if (yy < 0) yy=-yy; // Get 2points calibration values,Linear interpolation to get distance // correction for X and Y, that means distance correction use // different value at different distance float distance_corr_x = 0; float distance_corr_y = 0; if (corrections.two_pt_correction_available) { distance_corr_x = (corrections.dist_correction - corrections.dist_correction_x) * (xx - 2.4) / (25.04 - 2.4) + corrections.dist_correction_x; distance_corr_x -= corrections.dist_correction; distance_corr_y = (corrections.dist_correction - corrections.dist_correction_y) * (yy - 1.93) / (25.04 - 1.93) + corrections.dist_correction_y; distance_corr_y -= corrections.dist_correction; } float distance_x = distance + distance_corr_x; /**the new term of 'vert_offset * sin_vert_angle' * was added to the expression due to the mathemathical * model we used. */ xy_distance = distance_x * cos_vert_angle - vert_offset * sin_vert_angle ; ///the expression wiht '-' is proved to be better than the one with '+' x = xy_distance * sin_rot_angle - horiz_offset * cos_rot_angle; float distance_y = distance + distance_corr_y; xy_distance = distance_y * cos_vert_angle - vert_offset * sin_vert_angle ; /**the new term of 'vert_offset * sin_vert_angle' * was added to the expression due to the mathemathical * model we used. */ y = xy_distance * cos_rot_angle + horiz_offset * sin_rot_angle; // Using distance_y is not symmetric, but the velodyne manual // does this. /**the new term of 'vert_offset * cos_vert_angle' * was added to the expression due to the mathemathical * model we used. */ z = distance_y * sin_vert_angle + vert_offset*cos_vert_angle; /** Use standard ROS coordinate system (right-hand rule) */ float x_coord = y; float y_coord = -x; float z_coord = z; /** Intensity Calculation */ float min_intensity = corrections.min_intensity; float max_intensity = corrections.max_intensity; intensity = raw->blocks[i].data[k+2]; float focal_offset = 256 * (1 - corrections.focal_distance / 13100) * (1 - corrections.focal_distance / 13100); float focal_slope = corrections.focal_slope; intensity += focal_slope * (std::abs(focal_offset - 256 * SQR(1 - static_cast(tmp.uint)/65535))); intensity = (intensity < min_intensity) ? min_intensity : intensity; intensity = (intensity > max_intensity) ? max_intensity : intensity; data.addPoint(x_coord, y_coord, z_coord, corrections.laser_ring, raw->blocks[i].rotation, distance, intensity, time); } } data.newLine(); } } /** @brief convert raw VLP16 packet to point cloud * * @param pkt raw packet to unpack * @param pc shared pointer to point cloud (points are appended) */ void RawData::unpack_vlp16(const velodyne_msgs::VelodynePacket &pkt, DataContainerBase& data, const ros::Time& scan_start_time) { float azimuth; float azimuth_diff; int raw_azimuth_diff; float last_azimuth_diff=0; float azimuth_corrected_f; int azimuth_corrected; float x, y, z; float intensity; float time_diff_start_to_this_packet = (pkt.stamp - scan_start_time).toSec(); const raw_packet_t *raw = (const raw_packet_t *) &pkt.data[0]; for (int block = 0; block < BLOCKS_PER_PACKET; block++) { // ignore packets with mangled or otherwise different contents if (UPPER_BANK != raw->blocks[block].header) { // Do not flood the log with messages, only issue at most one // of these warnings per minute. ROS_WARN_STREAM_THROTTLE(60, "skipping invalid VLP-16 packet: block " << block << " header value is " << raw->blocks[block].header); return; // bad packet: skip the rest } // Calculate difference between current and next block's azimuth angle. azimuth = (float)(raw->blocks[block].rotation); if (block < (BLOCKS_PER_PACKET-1)){ raw_azimuth_diff = raw->blocks[block+1].rotation - raw->blocks[block].rotation; azimuth_diff = (float)((36000 + raw_azimuth_diff)%36000); // some packets contain an angle overflow where azimuth_diff < 0 if(raw_azimuth_diff < 0)//raw->blocks[block+1].rotation - raw->blocks[block].rotation < 0) { ROS_WARN_STREAM_THROTTLE(60, "Packet containing angle overflow, first angle: " << raw->blocks[block].rotation << " second angle: " << raw->blocks[block+1].rotation); // if last_azimuth_diff was not zero, we can assume that the velodyne's speed did not change very much and use the same difference if(last_azimuth_diff > 0){ azimuth_diff = last_azimuth_diff; } // otherwise we are not able to use this data // TODO: we might just not use the second 16 firings else{ continue; } } last_azimuth_diff = azimuth_diff; }else{ azimuth_diff = last_azimuth_diff; } for (int firing=0, k=0; firing < VLP16_FIRINGS_PER_BLOCK; firing++){ for (int dsr=0; dsr < VLP16_SCANS_PER_FIRING; dsr++, k+=RAW_SCAN_SIZE){ velodyne_pointcloud::LaserCorrection &corrections = calibration_.laser_corrections[dsr]; /** Position Calculation */ union two_bytes tmp; tmp.bytes[0] = raw->blocks[block].data[k]; tmp.bytes[1] = raw->blocks[block].data[k+1]; /** correct for the laser rotation as a function of timing during the firings **/ azimuth_corrected_f = azimuth + (azimuth_diff * ((dsr*VLP16_DSR_TOFFSET) + (firing*VLP16_FIRING_TOFFSET)) / VLP16_BLOCK_TDURATION); azimuth_corrected = ((int)round(azimuth_corrected_f)) % 36000; /*condition added to avoid calculating points which are not in the interesting defined area (min_angle < area < max_angle)*/ if ((azimuth_corrected >= config_.min_angle && azimuth_corrected <= config_.max_angle && config_.min_angle < config_.max_angle) ||(config_.min_angle > config_.max_angle && (azimuth_corrected <= config_.max_angle || azimuth_corrected >= config_.min_angle))){ // convert polar coordinates to Euclidean XYZ float distance = tmp.uint * calibration_.distance_resolution_m; distance += corrections.dist_correction; float cos_vert_angle = corrections.cos_vert_correction; float sin_vert_angle = corrections.sin_vert_correction; float cos_rot_correction = corrections.cos_rot_correction; float sin_rot_correction = corrections.sin_rot_correction; // cos(a-b) = cos(a)*cos(b) + sin(a)*sin(b) // sin(a-b) = sin(a)*cos(b) - cos(a)*sin(b) float cos_rot_angle = cos_rot_table_[azimuth_corrected] * cos_rot_correction + sin_rot_table_[azimuth_corrected] * sin_rot_correction; float sin_rot_angle = sin_rot_table_[azimuth_corrected] * cos_rot_correction - cos_rot_table_[azimuth_corrected] * sin_rot_correction; float horiz_offset = corrections.horiz_offset_correction; float vert_offset = corrections.vert_offset_correction; // Compute the distance in the xy plane (w/o accounting for rotation) /**the new term of 'vert_offset * sin_vert_angle' * was added to the expression due to the mathemathical * model we used. */ float xy_distance = distance * cos_vert_angle - vert_offset * sin_vert_angle; // Calculate temporal X, use absolute value. float xx = xy_distance * sin_rot_angle - horiz_offset * cos_rot_angle; // Calculate temporal Y, use absolute value float yy = xy_distance * cos_rot_angle + horiz_offset * sin_rot_angle; if (xx < 0) xx=-xx; if (yy < 0) yy=-yy; // Get 2points calibration values,Linear interpolation to get distance // correction for X and Y, that means distance correction use // different value at different distance float distance_corr_x = 0; float distance_corr_y = 0; if (corrections.two_pt_correction_available) { distance_corr_x = (corrections.dist_correction - corrections.dist_correction_x) * (xx - 2.4) / (25.04 - 2.4) + corrections.dist_correction_x; distance_corr_x -= corrections.dist_correction; distance_corr_y = (corrections.dist_correction - corrections.dist_correction_y) * (yy - 1.93) / (25.04 - 1.93) + corrections.dist_correction_y; distance_corr_y -= corrections.dist_correction; } float distance_x = distance + distance_corr_x; /**the new term of 'vert_offset * sin_vert_angle' * was added to the expression due to the mathemathical * model we used. */ xy_distance = distance_x * cos_vert_angle - vert_offset * sin_vert_angle ; x = xy_distance * sin_rot_angle - horiz_offset * cos_rot_angle; float distance_y = distance + distance_corr_y; /**the new term of 'vert_offset * sin_vert_angle' * was added to the expression due to the mathemathical * model we used. */ xy_distance = distance_y * cos_vert_angle - vert_offset * sin_vert_angle ; y = xy_distance * cos_rot_angle + horiz_offset * sin_rot_angle; // Using distance_y is not symmetric, but the velodyne manual // does this. /**the new term of 'vert_offset * cos_vert_angle' * was added to the expression due to the mathemathical * model we used. */ z = distance_y * sin_vert_angle + vert_offset*cos_vert_angle; /** Use standard ROS coordinate system (right-hand rule) */ float x_coord = y; float y_coord = -x; float z_coord = z; /** Intensity Calculation */ float min_intensity = corrections.min_intensity; float max_intensity = corrections.max_intensity; intensity = raw->blocks[block].data[k+2]; float focal_offset = 256 * SQR(1 - corrections.focal_distance / 13100); float focal_slope = corrections.focal_slope; intensity += focal_slope * (std::abs(focal_offset - 256 * SQR(1 - tmp.uint/65535))); intensity = (intensity < min_intensity) ? min_intensity : intensity; intensity = (intensity > max_intensity) ? max_intensity : intensity; float time = 0; if (timing_offsets.size()) time = timing_offsets[block][firing * 16 + dsr] + time_diff_start_to_this_packet; data.addPoint(x_coord, y_coord, z_coord, corrections.laser_ring, azimuth_corrected, distance, intensity, time); } } data.newLine(); } } } } // namespace velodyne_rawdata