2021SC@SDUSC
首先,我们团队分析的是LVI-SAM两大模块中的lidar模块。如今,我们已经完成了lidar模块中/lvi_sam_imuPreintegration(IMU预积分节点),/lvi_sam_featureExtraction(激光雷达特征点提取节点) ,/lvi_sam_imageProjection(生成深度图),总共3个节点的分析。从代码开始,通过代码的阅读,不断补充相关的知识点,从ROS的语法到代码运用到的数学原理,我们都有所收获。
接下来,我们团队,将继续分析lidar模块中的/lvi_sam_mapOptmization(因子图优化节点),把lidar模块中的最后一个节点分析完,然后会串联lidar模块中的各个节点,并且和LIO-SAM进行比较,分析LVI-SAM中的优化的内容。最后,visual模块尝试进行分析,但不能保证在这学期结束能分析完。在其中一个队友继续分析/lvi_sam_mapOptmization的时候,其他队友会继续分析visual模块,等待完成/lvi_sam_mapOptmization的分析。
在构造函数中,主要订阅了三个话题;下面主要分析这三个话题的回调函数:
//订阅当前激光帧点云信息,来自featureExtraction
subLaserCloudInfo = nh.subscribe<lvi_sam::cloud_info>(PROJECT_NAME + "/lidar/feature/cloud_info", 5, &mapOptimization::laserCloudInfoHandler, this, ros::TransportHints().tcpNoDelay());
//订阅GPS里程计
subGPS = nh.subscribe<nav_msgs::Odometry>(gpsTopic,50, &mapOptimization::gpsHandler, this, ros::TransportHints().tcpNoDelay());
//订阅来自外部闭环检测程序提供的闭环数据,本程序没有提供,这里实际没用上
subLoopInfo = nh.subscribe<std_msgs::Float64MultiArray>(PROJECT_NAME + "/vins/loop/match_frame", 5, &mapOptimization::loopHandler, this, ros::TransportHints().tcpNoDelay());
订阅激光帧点云信息
void laserCloudInfoHandler(const lvi_sam::cloud_infoConstPtr& msgIn)
{
// extract time stamp 时间戳
timeLaserInfoStamp = msgIn->header.stamp;
timeLaserInfoCur = msgIn->header.stamp.toSec();
// extract info ana feature cloud 提取当前激光帧角点、平面点集合
cloudInfo = *msgIn;
//pcl::PointCloud<PointType>::Ptr
pcl::fromROSMsg(msgIn->cloud_corner, *laserCloudCornerLast);
pcl::fromROSMsg(msgIn->cloud_surface, *laserCloudSurfLast);
// 设置锁(同步)
std::lock_guard<std::mutex> lock(mtx);
//静态变量,第一次声明后就会一直存在
static double timeLastProcessing = -1;
//mapping执行频率控制
// 如果 当前激光帧 与 上一帧激光帧之间的差值 大于 某个值(暂时认为是执行时间间隔要大于这个时间)
//mappingProcessInterval 在utility.h中被初始化为0.15 or PROJECT_NAME + "/mappingProcessInterval"
if (timeLaserInfoCur - timeLastProcessing >= mappingProcessInterval) {
//更新时间戳
timeLastProcessing = timeLaserInfoCur;
//当前帧位姿初始化
//1.如果是第一帧,用原始imu数据的RPY初始化当前帧位姿(旋转部分)
//2.后续帧,用imu里程计计算两帧之间的增量位姿变换,作用于前一帧的激光位姿,得到当前帧激光位姿
updateInitialGuess();
//提取局部角点、平面点云集合,加入局部地图
//1.对最近的一帧关键帧,搜索时空维度上相邻的关键帧集合,降采样一下
//2.对关键帧集合中的每一帧,提取对应的角点、平面点,加入局部地图中
extractSurroundingKeyFrames();
/*
将一组类似的激光雷达位姿 获得的点云图 进行匹配
*/
//当前激光帧角点、平面点集合降采样
downsampleCurrentScan();
// 1、要求当前帧特征点数量足够多,且匹配的点数够多,才执行优化。
// 2、迭代30次(上限)优化。
// 1) 当前激光帧角点寻找局部map匹配点
// a.更新当前帧位姿,将当前帧角点坐标变换到map系下,在局部map中查找5个最近点,距离小于1m,且5个点构成直线(用距离中心点的协方差矩阵,特征值进行判断),则认为匹配上了;
// b.计算当前帧角点到直线的距离、垂线的单位向量,存储为角点参数
// 2) 当前激光帧平面点寻找局部map匹配点
// a.更新当前帧位姿,将当前帧平面点坐标变换到map系下,在局部map中查找5个最近点,距离小于1m,且5个点构成平面(最小二乘拟合平面),则认为匹配上了
// b.计算当前帧平面点到平面的距离、垂线的单位向量,存储为平面点参数
// 3) 提取当前帧中与局部map匹配上了的角点、平面点,加入同一集合
// 4) 对匹配特征点计算Jacobian矩阵,观测值为特征点到直线、平面的距离,构建高斯牛顿方程,迭代优化当前位姿,存transformTobeMapped
// 3、用imu原始RPY数据与scan-to-map优化后的位姿进行加权融合,更新当前帧位姿的roll、pitch,约束z坐标。
scan2MapOptimization();//最长(最坏情况下看到这个
//因子图优化,更新所有关键帧位姿
/**
* 设置当前帧为关键帧并执行因子图优化
* 1.计算当前帧与前一帧位姿变化,如果变化太小,不设为关键帧,反之设为关键帧
* 2.添加激光里程计因子、GPS因子、闭环因子
* 3.执行因子图优化
* 4.得到当前帧优化后位姿,位姿协方差
* 5.添加cloudKeyPoses3D,cloudKeyPoses6D,更新transformToboMapped,添加当前关键帧的角点、平面点集合
*
* 该优化是独立于scan-map的另一个优化
*/
saveKeyFramesAndFactor();
//更新因子图中所有变量节点的位姿,也就是所有历史关键帧的位姿,更新里程计轨迹
correctPoses();
//发布激光里程计
publishOdometry();
//发布里程计、点云、轨迹
//1.发布历史关键帧位姿集合
//2.发布局地图降采样平面点集合
//3.发布历史帧(累加的)的角点、平面点降采样集合
//4.发布里程计轨迹
publishFrames();
}
}
当前帧的位姿初始化
void updateInitialGuess()
{
// 前一帧的位姿,这里指 lidar 的位姿
//Affine3f 仿射变换矩阵(移向量+旋转变换组合而成,可以同时实现旋转,缩放,平移等空间变换。)
//https://blog.csdn.net/weixin_42503785/article/details/112552689
static Eigen::Affine3f lastImuTransformation;
// system initialization
// 如果 关键帧集合 为空,继续进行初始化
if (cloudKeyPoses3D->points.empty())
{
//大小为6的数组
//当前帧位姿的旋转部分,用激光帧信息中的RPY(来自IMU原始数据)初始化
transformTobeMapped[0] = cloudInfo.imuRollInit;
transformTobeMapped[1] = cloudInfo.imuPitchInit;
transformTobeMapped[2] = cloudInfo.imuYawInit;
//在utility.h中定义
if (!useImuHeadingInitialization)
transformTobeMapped[2] = 0;
//应该是坐标 + 旋转角???
lastImuTransformation = pcl::getTransformation(0, 0, 0, cloudInfo.imuRollInit, cloudInfo.imuPitchInit, cloudInfo.imuYawInit); // save imu before return;
return;
}
// use VINS odometry estimation for pose guess
// 用当前帧和前一帧对应的imu里程计计算相对位姿变换,再用前一帧的位姿与相对变换,计算当前帧的位姿,存 transformTobeMapped
static int odomResetId = 0;
static bool lastVinsTransAvailable = false;
static Eigen::Affine3f lastVinsTransformation;
//imageProjection.cpp: cloudInfo.odomResetId = (int)round(startOdomMsg.pose.covariance[0]);
//里程计
if (cloudInfo.odomAvailable == true && cloudInfo.odomResetId == odomResetId)
{
// ROS_INFO("Using VINS initial guess");
if (lastVinsTransAvailable == false)
{
// ROS_INFO("Initializing VINS initial guess");
// 如果是首次积分,则将 lastVinsTransformation 赋值为根据 odom 的 xyz,rpy 转换得到的 transform
lastVinsTransformation = pcl::getTransformation(cloudInfo.odomX, cloudInfo.odomY, cloudInfo.odomZ,
cloudInfo.odomRoll, cloudInfo.odomPitch, cloudInfo.odomYaw);
lastVinsTransAvailable = true;
} else {
// ROS_INFO("Obtaining VINS incremental guess");
//首先从 odom 转换成 transform,获得当前 transform 在 lastVinsTransformation 下的位置
// 当前帧的初始估计位姿(来自imu里程计),后面用来计算增量位姿变换
//
Eigen::Affine3f transBack = pcl::getTransformation(cloudInfo.odomX, cloudInfo.odomY, cloudInfo.odomZ,
cloudInfo.odomRoll, cloudInfo.odomPitch, cloudInfo.odomYaw);
// inverse 逆矩阵
//当前帧相对于前一帧的位姿变换---imu里程计计算得到
Eigen::Affine3f transIncre = lastVinsTransformation.inverse() * transBack;
//前一帧的位姿
Eigen::Affine3f transTobe = trans2Affine3f(transformTobeMapped);
//当前帧的位姿
Eigen::Affine3f transFinal = transTobe * transIncre;
//更新当前帧位姿transformTobeMapped
pcl::getTranslationAndEulerAngles(transFinal, transformTobeMapped[3], transformTobeMapped[4], transformTobeMapped[5],
transformTobeMapped[0], transformTobeMapped[1], transformTobeMapped[2]);
// 将视觉惯性里程计的 transform 赋值为 odom 的位姿
lastVinsTransformation = pcl::getTransformation(cloudInfo.odomX, cloudInfo.odomY, cloudInfo.odomZ,
cloudInfo.odomRoll, cloudInfo.odomPitch, cloudInfo.odomYaw);
// 保存当前时态的 imu 位姿
lastImuTransformation = pcl::getTransformation(0, 0, 0, cloudInfo.imuRollInit, cloudInfo.imuPitchInit, cloudInfo.imuYawInit); // save imu before return;
return;
/// transformTobeMapped存储激光雷达位姿
}
} else {
// ROS_WARN("VINS failure detected.");
// 没有 odom 信息,或者是第一帧进入的时候
lastVinsTransAvailable = false;
odomResetId = cloudInfo.odomResetId;
}
//当odo不可用 或者 lastVinsTransAvailable == false
// use imu incremental estimation for pose guess (only rotation)
// 只在第一帧调用(注意上面的return),用 imu 累计估计位姿(只估计旋转的)
//能进入这个if 说明是
if (cloudInfo.imuAvailable == true)
{
// ROS_INFO("Using IMU initial guess");
// 首先从 imu 转换成 transform,获得当前 transform 在 lastImuTransformation 下的位置
// 当前帧的姿态角(来自原始imu数据)
Eigen::Affine3f transBack = pcl::getTransformation(0, 0, 0, cloudInfo.imuRollInit, cloudInfo.imuPitchInit, cloudInfo.imuYawInit);
// 当前帧相对于前一帧的姿态变换
Eigen::Affine3f transIncre = lastImuTransformation.inverse() * transBack;
// 将上一时态的 imu 的 transform 点乘相邻两个 imu 时态间的位姿变换,将其赋值给 transformTobeMapped 数组
// 前一帧的位姿
Eigen::Affine3f transTobe = trans2Affine3f(transformTobeMapped);
Eigen::Affine3f transFinal = transTobe * transIncre;
pcl::getTranslationAndEulerAngles(transFinal, transformTobeMapped[3], transformTobeMapped[4], transformTobeMapped[5],
transformTobeMapped[0], transformTobeMapped[1], transformTobeMapped[2]);
lastImuTransformation = pcl::getTransformation(0, 0, 0, cloudInfo.imuRollInit, cloudInfo.imuPitchInit, cloudInfo.imuYawInit); // save imu before return;
return;
}
}
提取局部角点、平面点云集合,加入局部地图
void extractSurroundingKeyFrames()
{
//关键帧集合为空 直接返回
if (cloudKeyPoses3D->points.empty() == true)
return;
extractNearby();
}
void extractNearby()
{
pcl::PointCloud<PointType>::Ptr surroundingKeyPoses(new pcl::PointCloud<PointType>());
pcl::PointCloud<PointType>::Ptr surroundingKeyPosesDS(new pcl::PointCloud<PointType>());
std::vector<int> pointSearchInd;
std::vector<float> pointSearchSqDis;
// extract all the nearby key poses and downsample them 提取所有附近的关键点姿势并对其进行下采样
// kdtree的输入,全局关键帧位姿集合(历史所有关键帧集合)
//kdtree作为一个搜索的工具
kdtreeSurroundingKeyPoses->setInputCloud(cloudKeyPoses3D); // create kd-tree
// surroundingKeyframeSearchRadius默认值为 50.0,scan-to-map 优化的距离
// 对最近的一帧关键帧,在半径区域内搜索空间区域上相邻的关键帧集合
// 搜索的结果存储在:pointSearchInd和pointSearchSqDis中
/
//开始对back理解错了,back应该就这个点云里的最后一个点
//这个函数 的第一个参数 应该传入一个点 在树中,搜索这个所有到这个点的距离 < 某个值的点集
kdtreeSurroundingKeyPoses->radiusSearch(cloudKeyPoses3D->back(), (double)surroundingKeyframeSearchRadius, pointSearchInd, pointSearchSqDis);
for (int i = 0; i < (int)pointSearchInd.size(); ++i)
{
//将搜索到的每个点,通过id在cloudKeyPose3D中找到对应的关键帧(点云)并加入
int id = pointSearchInd[i];
//加入 相邻关键帧位姿集合 中
surroundingKeyPoses->push_back(cloudKeyPoses3D->points[id]);
}
//降采样
downSizeFilterSurroundingKeyPoses.setInputCloud(surroundingKeyPoses);
//降采样后的结果存储在DS中
downSizeFilterSurroundingKeyPoses.filter(*surroundingKeyPosesDS);
// also extract some latest key frames in case the robot rotates in one position 同时提取一些最新的关键帧,以防机器人在一个位置旋转
// 加入时间上相邻的一些关键帧,比如当载体在原地转圈,这些帧加进来是合理的
// 有个问题:原地旋转为什么不算做空间上相邻的关键帧??????
/*
暂时的想法是:
3D点云集合中只保存位置信息(xyz);6D点云中保存位置+旋转信息(xyz+rpy)
当原地旋转时,关键帧的xyz应该是保持不变的 --> 所以连续的若干帧会在3D点云集合中只保存一个坐标(是不是这样需要后续看cloudKeyPose3D是怎么添加帧的)
所以使用3D点云集合搜索空间邻域时,会丢失旋转前后的帧(xyz相同 rpy不同);所以需要重新对事件邻域进行添加(不过这里的添加是将时间段内的帧全部添加)
这也解释了:为什么楼下判断时,使用的时6D的时间戳(因为6D会保存旋转时的全部帧)
*/
int numPoses = cloudKeyPoses3D->size();
for (int i = numPoses-1; i >= 0; --i)
{
//当前帧时间戳
// 为啥用6D
if (timeLaserInfoCur - cloudKeyPoses6D->points[i].time < 10.0)
surroundingKeyPosesDS->push_back(cloudKeyPoses3D->points[i]);
else
break;
}
//将相邻关键帧集合对应的角点、平面点,加入到局部map中,作为scan-to-map匹配的局部点云地图
extractCloud(surroundingKeyPosesDS);
}
// 相邻关键帧集合对应的角点、平面点,加入到局部 map 中
// 称之为局部 map,后面进行 scan-to-map 匹配,所用到的 map 就是这里的相邻关键帧对应点云集合
void extractCloud(pcl::PointCloud<PointType>::Ptr cloudToExtract)
{
std::vector<pcl::PointCloud<PointType>> laserCloudCornerSurroundingVec;
std::vector<pcl::PointCloud<PointType>> laserCloudSurfSurroundingVec;
laserCloudCornerSurroundingVec.resize(cloudToExtract->size());
laserCloudSurfSurroundingVec.resize(cloudToExtract->size());
// extract surrounding map
// 遍历当前帧(实际是取最近的一个关键帧来找它相邻的关键帧集合)时空维度上相邻的关键帧集合
#pragma omp parallel for num_threads(numberOfCores)
for (int i = 0; i < (int)cloudToExtract->size(); ++i)
{
//相邻关键帧索引
//所以能理解成这玩意自动按照intensity排好序了?
int thisKeyInd = (int)cloudToExtract->points[i].intensity;
// 提取 50 米范围内的点,transformPointCloud 作用是返回输入点云乘以输入的 transform
//自定义函数 计算两个点云图之间的距离
if (pointDistance(cloudKeyPoses3D->points[thisKeyInd], cloudKeyPoses3D->back()) > surroundingKeyframeSearchRadius)
continue;
// 相邻关键帧对应的角点、平面点云,通过 6D 位姿变换到世界坐标系下
//transformPointCloud 自定义函数??????????????
laserCloudCornerSurroundingVec[i] = *transformPointCloud(cornerCloudKeyFrames[thisKeyInd], &cloudKeyPoses6D->points[thisKeyInd]);
laserCloudSurfSurroundingVec[i] = *transformPointCloud(surfCloudKeyFrames[thisKeyInd], &cloudKeyPoses6D->points[thisKeyInd]);
}
// fuse the map
// 赋值线和面特征点集,并且进行下采样
// laserCloudCornerFromMapDS 比较重要
laserCloudCornerFromMap->clear();
laserCloudSurfFromMap->clear();
for (int i = 0; i < (int)cloudToExtract->size(); ++i)
{
// 加入局部map
*laserCloudCornerFromMap += laserCloudCornerSurroundingVec[i];
*laserCloudSurfFromMap += laserCloudSurfSurroundingVec[i];
}
//降采样
// Downsample the surrounding corner key frames (or map)
downSizeFilterCorner.setInputCloud(laserCloudCornerFromMap);
downSizeFilterCorner.filter(*laserCloudCornerFromMapDS);
// Downsample the surrounding surf key frames (or map)
downSizeFilterSurf.setInputCloud(laserCloudSurfFromMap);
downSizeFilterSurf.filter(*laserCloudSurfFromMapDS);
}
当前激光帧角点、平面点集合降采样
void downsampleCurrentScan()
{
// Downsample cloud from current scan
//对第二部得到的角点和平面点集合进一步降采样
laserCloudCornerLastDS->clear();
downSizeFilterCorner.setInputCloud(laserCloudCornerLast);
downSizeFilterCorner.filter(*laserCloudCornerLastDS);
laserCloudCornerLastDSNum = laserCloudCornerLastDS->size();
laserCloudSurfLastDS->clear();
downSizeFilterSurf.setInputCloud(laserCloudSurfLast);
downSizeFilterSurf.filter(*laserCloudSurfLastDS);
laserCloudSurfLastDSNum = laserCloudSurfLastDS->size();
}
https://blog.csdn.net/qq_42145185/article/details/106860227
在utility.h文件中定义了很多变量,这些变量都是在paramServer函数中赋值的,使用了ros::NodeHandle.param<>("/一个路径",被赋值的变量,常量)
,当参数服务器中有参数1的路径里的值的时候,会将该值赋值给变量;找不到该路径的话,就将参数3赋值。这里探究一下参数服务器。
参数服务器是节点存储参数的地方、用于配置参数、全局共享参数。参数服务器使用互联网传输,在节点管理器中运行,实现整个通信过程。
包括以下类型:32位整数、布尔值、字符串、双精度浮点、ISO 8601日期、列表(List)、基于64位编码的二进制数据。
可以使用三种方式操作参数服务器:
ROS中关于参数服务器的工具是rosparam。其支持的参数如下所示:
命令 | 作用 |
---|---|
rosparam list | 列出了服务器中的所有参数 |
rosparam get [parameter] | 获取参数值 |
rosparam set [parameter] [value] | 设置参数值 |
rosparam delete [parameter] | 删除参数 |
rosparam dump [file] | 将参数服务器保存到一个文件 |
rosparam load [file] | 加载参数文件到参数服务器 |
launch文件中有两个标签<param/> <rosparam/>
用来给参数赋值。其中param用来给单个参数赋值;rosparam用于加载参数文件,参数文件中包含若干参数
lvi-sam项目中的例子:
modeule_sam.launch
<launch>
<arg name="project" default="lvi_sam"/>
<!-- Lidar odometry param -->
<rosparam file="$(find lvi_sam)/config/params_lidar.yaml" command="load" />
<!-- VINS config -->
<param name="vins_config_file" type="string" value="$(find lvi_sam)/config/params_camera.yaml" />
.
.
.
</launch>
/config/params_camera.yaml
# project name
PROJECT_NAME: "lvi_sam"
lvi_sam:
# Topics
pointCloudTopic: "/points_raw" # Point cloud data
imuTopic: "/imu_raw" # IMU data
# Heading
useImuHeadingInitialization: true # if using GPS data, set to "true"
.
.
.
$(find lvi_sam)
lvi_sam:(pkg_name) https://blog.csdn.net/qq_33835307/article/details/81533140ros::NodeHandle.param();
utility.h
nh.param<std::string>("/PROJECT_NAME", PROJECT_NAME, "sam");
nh.param<std::string>("/robot_id", robot_id, "roboat");
nh.param<std::string>(PROJECT_NAME + "/pointCloudTopic", pointCloudTopic, "points_raw");
nh.param<std::string>(PROJECT_NAME + "/imuTopic", imuTopic, "imu_correct");
nh.param<std::string>(PROJECT_NAME + "/odomTopic", odomTopic, "odometry/imu");
nh.param<std::string>(PROJECT_NAME + "/gpsTopic", gpsTopic, "odometry/gps");
文章浏览阅读6.2k次,点赞6次,收藏26次。我是一个深度学习代码小白,请你用中文写上注释,能让我能轻松理解下面这段代码。注意包含所有函数、调用和参数的注释。以同样的python代码块样式返回你写的代码给我。代码看累了,就看《动手学深度学习》文档:基于PyTorch框架,从底层函数实现基础功能,再到框架的高级功能。努力上路的小白一枚,麻烦路过的大佬指导一二,同时希望能和大家交流学习~争取更新学习这个文档的专栏,记录学习过程。量身定做了一套话术hhh,亲身测试还不错。这个感觉更浅一点儿,之后复习看吧。20天吃掉那只Pytorch。_深度学习程序怎么读
文章浏览阅读2.7w次,点赞126次,收藏1.2k次。耗废1024根秀发,Java学习路线图来了,整合了自己所学的所有技术整理出来的2022最新版Java学习路线图,适合于初、中级别的Java程序员。_java学习路线
文章浏览阅读4.4k次。1.7-savingPNG介绍代码详情函数详解savePNGFile()源码savePNGFile()源码提示savePNGFile()推荐用法处理结果代码链接介绍PCL提供了将点云的值保存到PNG图像文件的可能性。这只能用有有序的云来完成,因为结果图像的行和列将与云中的行和列完全对应。例如,如果您从类似Kinect或Xtion的传感器中获取了点云,则可以使用它来检索与该云匹配的640x480 RGB图像。代码详情#include <pcl / io / pcd_io.h>#incl_pcl::io:savepng
文章浏览阅读936次。吸引妹子的关键点不在于喝什么咖啡,主要在于竖立哪种男性人设。能把人设在几分钟内快速固定下来,也就不愁吸引对口的妹子了。我有几个备选方案,仅供参考。1. 运动型男生左手单手俯卧撑,右手在键盘上敲代码。你雄壮的腰腹肌肉群活灵活现,简直就是移动的春药。2.幽默男生花 20 块找一个托(最好是老同学 or 同事)坐你对面。每当你侃侃而谈,他便满面涨红、放声大笑、不能自已。他笑的越弱_咖啡厅写代码
文章浏览阅读1.2w次,点赞5次,收藏5次。今天 (应该是昨天了,昨晚太晚了没发出去)下午参加了腾讯WXG的面委会面试。前面在牛客上搜索了面委会相关的面经普遍反映面委会较难,因为都是微信的核心大佬,问的问题也会比较深。昨晚还蛮紧张的,晚上都没睡好。面试使用的是腾讯会议,时间到了面试官准时进入会议。照例是简单的自我介绍,然后是几个常见的基础问题:例如数据库索引,什么时候索引会失效、设计模式等。这部分比较普通,问的也不是很多,不再赘述。现在回想下,大部分还是简历上写的技能点。接下来面试官让打开项目的代码,对着代码讲解思路。我笔记本上没有这部分代码,所_腾讯面委会面试是什么
文章浏览阅读382次,点赞3次,收藏4次。AI绘画自动生成器是一种利用人工智能技术,特别是深度学习算法,来自动创建视觉艺术作品的软件工具。这些工具通常基于神经网络模型,如生成对抗网络(GANs),通过学习大量的图像数据来生成新的图像。AI绘画自动生成器作为艺术与科技结合的产物,正在开启艺术创作的新篇章。它们不仅为艺术家和设计师提供了新的工具,也为普通用户提供了探索艺术的机会。随着技术的不断进步,我们可以预见,AI绘画自动生成器将在未来的创意产业中发挥越来越重要的作用。
文章浏览阅读1.7k次。理解为ListView 的三种形式吧ListView 默认构造但是这种方式创建的列表存在一个问题:对于那些长列表或者需要较昂贵渲染开销的子组件,即使还没有出现在屏幕中但仍然会被ListView所创建,这将是一项较大的开销,使用不当可能引起性能问题甚至卡顿直接返回的是每一行的Widget,相当于ios的row。行高按Widget(cell)高设置ListView.build 就和io..._flutter listview.separated和listview.builder
文章浏览阅读1.4k次,点赞4次,收藏14次。废话不多说直接上干货1.js运行机制JavaScript单线程,任务需要排队执行同步任务进入主线程排队,异步任务进入事件队列排队等待被推入主线程执行定时器的延迟时间为0并不是立刻执行,只是代表相比于其他定时器更早的被执行以宏任务和微任务进一步理解js执行机制整段代码作为宏任务开始执行,执行过程中宏任务和微任务进入相应的队列中整段代码执行结束,看微任务队列中是否有任务等待执行,如果有则执行所有的微任务,直到微任务队列中的任务执行完毕,如果没有则继续执行新的宏任务执行新的宏任务,凡是在..._前端面试
文章浏览阅读1k次。(3)若没有查到,则将请求发给根域DNS服务器,并依序从根域查找顶级域,由顶级查找二级域,二级域查找三级,直至找到要解析的地址或名字,即向客户机所在网络的DNS服务器发出应答信息,DNS服务器收到应答后现在缓存中存储,然后,将解析结果发给客户机。(3)若没有查到,则将请求发给根域DNS服务器,并依序从根域查找顶级域,由顶级查找二级域,二级域查找三级,直至找到要解析的地址或名字,即向客户机所在网络的DNS服务器发出应答信息,DNS服务器收到应答后现在缓存中存储,然后,将解析结果发给客户机。_linux
文章浏览阅读7.9k次,点赞26次,收藏66次。HTML DOM——文档元素的操作1、通过id获取文档元素任务描述相关知识什么是DOM文档元素节点树通过id获取文档元素代码文件2、通过类名获取文档元素任务描述相关知识通过类名获取文档元素代码文件3、通过标签名获取文档元素任务描述相关知识通过标签名获取文档元素获取标签内部的子元素代码文件4、html5中获取元素的方法一任务描述相关知识css选择器querySelector的用法代码文件5、html5中获取元素的方法二任务描述相关知识querySelectorAll的用法代码文件6、节点树上的操作任务描述相关_javascript学习手册十三:html dom——文档元素的操作(一)
文章浏览阅读132次。《LeetCode学习》172. 阶乘后的零(java篇)_java 给定一个整数n,返回n!结果尾数中零的数量
文章浏览阅读426次。请注意,本文将要给大家分享的并不是开启公众号的安全操作风险提醒,而是当公众号粉丝给公众号发消息的时候,公众号的管理员和运营者如何能在手机上立即收到消息通知,以及在手机上回复粉丝消息。第一步:授权1、在微信中点击右上角+,然后选择“添加朋友”,然后选择“公众号”,然后输入“微小助”并关注该公众号。2、进入微小助公众号,然后点击底部菜单【新增授权】,如下图所示:3、然后会打开一个温馨提示页面。请一定要..._php微信公众号服务提示