我们通常把模型的各个组成成分分成 6 种类型:

  • 编码器(encoder):包括 voxel encoder 和 middle encoder 等进入 backbone 前所使用的基于体素的方法,如 HardVFE 和 PointPillarsScatter

  • 骨干网络(backbone):通常采用 FCN 网络来提取特征图,如 ResNet 和 SECOND

  • 颈部网络(neck):位于 backbones 和 heads 之间的组成模块,如 FPN 和 SECONDFPN

  • 检测头(head):用于特定任务的组成模块,如检测框的预测掩码的预测

  • RoI 提取器(RoI extractor):用于从特征图中提取 RoI 特征的组成模块,如 H3DRoIHead 和 PartAggregationROIHead

  • 损失函数(loss):heads 中用于计算损失函数的组成模块,如 FocalLossL1Loss 和 GHMLoss

一、自定义模型

添加新的编码器

接下来我们以 HardVFE 为例展示如何开发新的组成模块。

1. 定义一个新的体素编码器(如 HardVFE:即 HV-SECOND 中使用的体素特征编码器)

创建一个新文件 mmdet3d/models/voxel_encoders/voxel_encoder.py

import torch.nn as nnfrom mmdet3d.registry import MODELS@MODELS.register_module()
class HardVFE(nn.Module):def __init__(self, arg1, arg2):passdef forward(self, x):  # 需要返回一个元组pass

2. 导入该模块

您可以在 mmdet3d/models/voxel_encoders/__init__.py 中添加以下代码:

from .voxel_encoder import HardVFE

或者在配置文件中添加以下代码,从而避免修改源码:

custom_imports = dict(imports=['mmdet3d.models.voxel_encoders.voxel_encoder'],allow_failed_imports=False)

3. 在配置文件中使用体素编码器

model = dict(...voxel_encoder=dict(type='HardVFE',arg1=xxx,arg2=yyy),...
)

添加新的骨干网络

接下来我们以 SECOND(Sparsely Embedded Convolutional Detection)为例展示如何开发新的组成模块。

1. 定义一个新的骨干网络(如 SECOND)

创建一个新文件 mmdet3d/models/backbones/second.py

from mmengine.model import BaseModulefrom mmdet3d.registry import MODELS@MODELS.register_module()
class SECOND(BaseModule):def __init__(self, arg1, arg2):passdef forward(self, x):  # 需要返回一个元组pass

2. 导入该模块

您可以在 mmdet3d/mod

model = dict(...backbone=dict(type='SECOND',arg1=xxx,arg2=yyy),...
)

els/backbones/__init__.py 中添加以下代码:

from .second import SECOND

或者在配置文件中添加以下代码,从而避免修改源码:

custom_imports = dict(imports=['mmdet3d.models.backbones.second'],allow_failed_imports=False)

3. 在配置文件中使用骨干网络

model = dict(...backbone=dict(type='SECOND',arg1=xxx,arg2=yyy),...
)

添加新的颈部网络

1. 定义一个新的颈部网络(如 SECONDFPN)

创建一个新文件 mmdet3d/models/necks/second_fpn.py

from mmengine.model import BaseModulefrom mmdet3d.registry import MODELS@MODELS.register_module()
class SECONDFPN(BaseModule):def __init__(self,in_channels=[128, 128, 256],out_channels=[256, 256, 256],upsample_strides=[1, 2, 4],norm_cfg=dict(type='BN', eps=1e-3, momentum=0.01),upsample_cfg=dict(type='deconv', bias=False),conv_cfg=dict(type='Conv2d', bias=False),use_conv_for_no_stride=False,init_cfg=None):passdef forward(self, x):# 具体实现忽略pass

2. 导入该模块

您可以在 mmdet3d/models/necks/__init__.py 中添加以下代码:

from .second_fpn import SECONDFPN

或者在配置文件中添加以下代码,从而避免修改源码:

custom_imports = dict(imports=['mmdet3d.models.necks.second_fpn'],allow_failed_imports=False)

3. 在配置文件中使用颈部网络

model = dict(...neck=dict(type='SECONDFPN',in_channels=[64, 128, 256],upsample_strides=[1, 2, 4],out_channels=[128, 128, 128]),...
)

添加新的检测头

接下来我们以 PartA2 Head 为例展示如何开发新的检测头。

注意:此处展示的 PartA2 RoI Head 将用于检测器的第二阶段。对于单阶段的检测头,请参考 mmdet3d/models/dense_heads/ 中的例子。由于其简单高效,它们更常用于自动驾驶场景下的 3D 检测中。

首先,在 mmdet3d/models/roi_heads/bbox_heads/parta2_bbox_head.py 中添加新的 bbox head。PartA2 RoI Head 为目标检测实现了一个新的 bbox head。为了实现一个 bbox head,我们通常需要在新模块中实现如下两个函数。有时还需要实现其他相关函数,如 loss 和 get_targets

from mmengine.model import BaseModulefrom mmdet3d.registry import MODELS@MODELS.register_module()
class PartA2BboxHead(BaseModule):"""PartA2 RoI head."""def __init__(self,num_classes,seg_in_channels,part_in_channels,seg_conv_channels=None,part_conv_channels=None,merge_conv_channels=None,down_conv_channels=None,shared_fc_channels=None,cls_channels=None,reg_channels=None,dropout_ratio=0.1,roi_feat_size=14,with_corner_loss=True,bbox_coder=dict(type='DeltaXYZWLHRBBoxCoder'),conv_cfg=dict(type='Conv1d'),norm_cfg=dict(type='BN1d', eps=1e-3, momentum=0.01),loss_bbox=dict(type='SmoothL1Loss', beta=1.0 / 9.0, loss_weight=2.0),loss_cls=dict(type='CrossEntropyLoss',use_sigmoid=True,reduction='none',loss_weight=1.0),init_cfg=None):super(PartA2BboxHead, self).__init__(init_cfg=init_cfg)def forward(self, seg_feats, part_feats):pass

其次,如果有必要的话需要实现一个新的 RoI Head。我们从 Base3DRoIHead 中继承得到新的 PartAggregationROIHead。我们可以发现 Base3DRoIHead 已经实现了如下函数。

from mmdet.models.roi_heads import BaseRoIHeadfrom mmdet3d.registry import MODELS, TASK_UTILSclass Base3DRoIHead(BaseRoIHead):"""Base class for 3d RoIHeads."""def __init__(self,bbox_head=None,bbox_roi_extractor=None,mask_head=None,mask_roi_extractor=None,train_cfg=None,test_cfg=None,init_cfg=None):super(Base3DRoIHead, self).__init__(bbox_head=bbox_head,bbox_roi_extractor=bbox_roi_extractor,mask_head=mask_head,mask_roi_extractor=mask_roi_extractor,train_cfg=train_cfg,test_cfg=test_cfg,init_cfg=init_cfg)def init_bbox_head(self, bbox_roi_extractor: dict,bbox_head: dict) -> None:"""Initialize box head and box roi extractor.Args:bbox_roi_extractor (dict or ConfigDict): Config of boxroi extractor.bbox_head (dict or ConfigDict): Config of box in box head."""self.bbox_roi_extractor = MODELS.build(bbox_roi_extractor)self.bbox_head = MODELS.build(bbox_head)def init_assigner_sampler(self):"""Initialize assigner and sampler."""self.bbox_assigner = Noneself.bbox_sampler = Noneif self.train_cfg:if isinstance(self.train_cfg.assigner, dict):self.bbox_assigner = TASK_UTILS.build(self.train_cfg.assigner)elif isinstance(self.train_cfg.assigner, list):self.bbox_assigner = [TASK_UTILS.build(res) for res in self.train_cfg.assigner]self.bbox_sampler = TASK_UTILS.build(self.train_cfg.sampler)def init_mask_head(self):"""Initialize mask head, skip since ``PartAggregationROIHead`` does nothave one."""pass

接下来主要对 bbox_forward 的逻辑进行修改,同时其继承了来自 Base3DRoIHead 的其它逻辑。在 mmdet3d/models/roi_heads/part_aggregation_roi_head.py 中,我们实现了新的 RoI Head,如下所示:

from typing import Dict, List, Tuplefrom mmdet.models.task_modules import AssignResult, SamplingResult
from mmengine import ConfigDict
from torch import Tensor
from torch.nn import functional as Ffrom mmdet3d.registry import MODELS
from mmdet3d.structures import bbox3d2roi
from mmdet3d.utils import InstanceList
from ...structures.det3d_data_sample import SampleList
from .base_3droi_head import Base3DRoIHead@MODELS.register_module()
class PartAggregationROIHead(Base3DRoIHead):"""Part aggregation roi head for PartA2.Args:semantic_head (ConfigDict): Config of semantic head.num_classes (int): The number of classes.seg_roi_extractor (ConfigDict): Config of seg_roi_extractor.bbox_roi_extractor (ConfigDict): Config of part_roi_extractor.bbox_head (ConfigDict): Config of bbox_head.train_cfg (ConfigDict): Training config.test_cfg (ConfigDict): Testing config."""def __init__(self,semantic_head: dict,num_classes: int = 3,seg_roi_extractor: dict = None,bbox_head: dict = None,bbox_roi_extractor: dict = None,train_cfg: dict = None,test_cfg: dict = None,init_cfg: dict = None) -> None:super(PartAggregationROIHead, self).__init__(bbox_head=bbox_head,bbox_roi_extractor=bbox_roi_extractor,train_cfg=train_cfg,test_cfg=test_cfg,init_cfg=init_cfg)self.num_classes = num_classesassert semantic_head is not Noneself.init_seg_head(seg_roi_extractor, semantic_head)def init_seg_head(self, seg_roi_extractor: dict,semantic_head: dict) -> None:"""Initialize semantic head and seg roi extractor.Args:seg_roi_extractor (dict): Config of segroi extractor.semantic_head (dict): Config of semantic head."""self.semantic_head = MODELS.build(semantic_head)self.seg_roi_extractor = MODELS.build(seg_roi_extractor)@propertydef with_semantic(self):"""bool: whether the head has semantic branch"""return hasattr(self,'semantic_head') and self.semantic_head is not Nonedef predict(self,feats_dict: Dict,rpn_results_list: InstanceList,batch_data_samples: SampleList,rescale: bool = False,**kwargs) -> InstanceList:"""Perform forward propagation of the roi head and predict detectionresults on the features of the upstream network.Args:feats_dict (dict): Contains features from the first stage.rpn_results_list (List[:obj:`InstanceData`]): Detection resultsof rpn head.batch_data_samples (List[:obj:`Det3DDataSample`]): The Datasamples. It usually includes information such as`gt_instance_3d`, `gt_panoptic_seg_3d` and `gt_sem_seg_3d`.rescale (bool): If True, return boxes in original image space.Defaults to False.Returns:list[:obj:`InstanceData`]: Detection results of each sampleafter the post process.Each item usually contains following keys.- scores_3d (Tensor): Classification scores, has a shape(num_instances, )- labels_3d (Tensor): Labels of bboxes, has a shape(num_instances, ).- bboxes_3d (BaseInstance3DBoxes): Prediction of bboxes,contains a tensor with shape (num_instances, C), whereC >= 7."""assert self.with_bbox, 'Bbox head must be implemented in PartA2.'assert self.with_semantic, 'Semantic head must be implemented' \' in PartA2.'batch_input_metas = [data_samples.metainfo for data_samples in batch_data_samples]voxels_dict = feats_dict.pop('voxels_dict')# TODO: Split predict semantic and bboxresults_list = self.predict_bbox(feats_dict, voxels_dict,batch_input_metas, rpn_results_list,self.test_cfg)return results_listdef predict_bbox(self, feats_dict: Dict, voxel_dict: Dict,batch_input_metas: List[dict],rpn_results_list: InstanceList,test_cfg: ConfigDict) -> InstanceList:"""Perform forward propagation of the bbox head and predict detectionresults on the features of the upstream network.Args:feats_dict (dict): Contains features from the first stage.voxel_dict (dict): Contains information of voxels.batch_input_metas (list[dict], Optional): Batch image meta info.Defaults to None.rpn_results_list (List[:obj:`InstanceData`]): Detection resultsof rpn head.test_cfg (Config): Test config.Returns:list[:obj:`InstanceData`]: Detection results of each sampleafter the post process.Each item usually contains following keys.- scores_3d (Tensor): Classification scores, has a shape(num_instances, )- labels_3d (Tensor): Labels of bboxes, has a shape(num_instances, ).- bboxes_3d (BaseInstance3DBoxes): Prediction of bboxes,contains a tensor with shape (num_instances, C), whereC >= 7."""...def loss(self, feats_dict: Dict, rpn_results_list: InstanceList,batch_data_samples: SampleList, **kwargs) -> dict:"""Perform forward propagation and loss calculation of the detectionroi on the features of the upstream network.Args:feats_dict (dict): Contains features from the first stage.rpn_results_list (List[:obj:`InstanceData`]): Detection resultsof rpn head.batch_data_samples (List[:obj:`Det3DDataSample`]): The Datasamples. It usually includes information such as`gt_instance_3d`, `gt_panoptic_seg_3d` and `gt_sem_seg_3d`.Returns:dict[str, Tensor]: A dictionary of loss components"""assert len(rpn_results_list) == len(batch_data_samples)losses = dict()batch_gt_instances_3d = []batch_gt_instances_ignore = []voxels_dict = feats_dict.pop('voxels_dict')for data_sample in batch_data_samples:batch_gt_instances_3d.append(data_sample.gt_instances_3d)if 'ignored_instances' in data_sample:batch_gt_instances_ignore.append(data_sample.ignored_instances)else:batch_gt_instances_ignore.append(None)if self.with_semantic:semantic_results = self._semantic_forward_train(feats_dict, voxels_dict, batch_gt_instances_3d)losses.update(semantic_results.pop('loss_semantic'))sample_results = self._assign_and_sample(rpn_results_list,batch_gt_instances_3d)if self.with_bbox:feats_dict.update(semantic_results)bbox_results = self._bbox_forward_train(feats_dict, voxels_dict,sample_results)losses.update(bbox_results['loss_bbox'])return losses

此处我们省略了相关函数的更多细节。更多细节请参考代码。

最后,用户需要在 mmdet3d/models/roi_heads/bbox_heads/__init__.py 和 mmdet3d/models/roi_heads/__init__.py 添加模块,从而能被相应的注册器找到并加载。

此外,用户也可以在配置文件中添加以下代码以达到相同的目的。

custom_imports=dict(imports=['mmdet3d.models.roi_heads.part_aggregation_roi_head', 'mmdet3d.models.roi_heads.bbox_heads.parta2_bbox_head'],allow_failed_imports=False)

PartAggregationROIHead 的配置文件如下所示:

model = dict(...roi_head=dict(type='PartAggregationROIHead',num_classes=3,semantic_head=dict(type='PointwiseSemanticHead',in_channels=16,extra_width=0.2,seg_score_thr=0.3,num_classes=3,loss_seg=dict(type='mmdet.FocalLoss',use_sigmoid=True,reduction='sum',gamma=2.0,alpha=0.25,loss_weight=1.0),loss_part=dict(type='mmdet.CrossEntropyLoss',use_sigmoid=True,loss_weight=1.0)),seg_roi_extractor=dict(type='Single3DRoIAwareExtractor',roi_layer=dict(type='RoIAwarePool3d',out_size=14,max_pts_per_voxel=128,mode='max')),bbox_roi_extractor=dict(type='Single3DRoIAwareExtractor',roi_layer=dict(type='RoIAwarePool3d',out_size=14,max_pts_per_voxel=128,mode='avg')),bbox_head=dict(type='PartA2BboxHead',num_classes=3,seg_in_channels=16,part_in_channels=4,seg_conv_channels=[64, 64],part_conv_channels=[64, 64],merge_conv_channels=[128, 128],down_conv_channels=[128, 256],bbox_coder=dict(type='DeltaXYZWLHRBBoxCoder'),shared_fc_channels=[256, 512, 512, 512],cls_channels=[256, 256],reg_channels=[256, 256],dropout_ratio=0.1,roi_feat_size=14,with_corner_loss=True,loss_bbox=dict(type='mmdet.SmoothL1Loss',beta=1.0 / 9.0,reduction='sum',loss_weight=1.0),loss_cls=dict(type='mmdet.CrossEntropyLoss',use_sigmoid=True,reduction='sum',loss_weight=1.0))),...
)

MMDetection 2.0 开始支持配置文件之间的继承,因此用户可以关注配置文件的修改。PartA2 Head 的第二阶段主要使用了新的 PartAggregationROIHead 和 PartA2BboxHead,需要根据对应模块的 __init__ 函数来设置参数。

二、自定义运行时配置

自定义优化器设置

优化器相关的配置是由 optim_wrapper 管理的,其通常有三个字段:optimizerparamwise_cfgclip_grad。更多细节请参考 OptimWrapper。如下所示,使用 AdamW 作为优化器,骨干网络的学习率降低 10 倍,并添加了梯度裁剪。

optim_wrapper = dict(type='OptimWrapper',# 优化器optimizer=dict(type='AdamW',lr=0.0001,weight_decay=0.05,eps=1e-8,betas=(0.9, 0.999)),# 参数级学习率及权重衰减系数设置paramwise_cfg=dict(custom_keys={'backbone': dict(lr_mult=0.1, decay_mult=1.0),},norm_decay_mult=0.0),# 梯度裁剪clip_grad=dict(max_norm=0.01, norm_type=2))

自定义 PyTorch 支持的优化器

我们已经支持使用所有 PyTorch 实现的优化器,且唯一需要修改的地方就是改变配置文件中的 optim_wrapper 字段中的 optimizer 字段。例如,如果您想使用 Adam(注意这样可能会使性能大幅下降),您可以这样修改:

optim_wrapper = dict(type='OptimWrapper',optimizer=dict(type='Adam', lr=0.0003, weight_decay=0.0001))

为了修改模型的学习率,用户只需要修改 optimizer 中的 lr 字段。用户可以根据 PyTorch 的 API 文档直接设置参数。

自定义训练调度

默认情况下我们使用阶梯式学习率衰减的 1 倍训练调度,这会调用 MMEngine 中的 MultiStepLR。我们在这里支持了很多其他学习率调度,比如余弦退火多项式衰减调度。下面是一些样例:

多项式衰减调度

param_scheduler = [dict(type='PolyLR',power=0.9,eta_min=1e-4,begin=0,end=8,by_epoch=True)]

余弦退火调度

param_scheduler = [dict(type='CosineAnnealingLR',T_max=8,eta_min=lr * 1e-5,begin=0,end=8,by_epoch=True)]

自定义训练循环控制器

默认情况下,我们在 train_cfg 中使用 EpochBasedTrainLoop,并在每一个训练 epoch 完成后进行一次验证,如下所示:

train_cfg = dict(type='EpochBasedTrainLoop', max_epochs=12, val_begin=1, val_interval=1)

三、3D检测

数据准备

首先,我们需要下载原始数据并按照数据准备文档中提供的标准方式重新组织数据。

由于不同数据集的原始数据有不同的组织方式,我们通常需要用 .pkl 文件收集有用的数据信息。因此,在准备好所有的原始数据之后,我们需要运行 create_data.py 中提供的脚本来为不同的数据集生成数据集信息。例如,对于 KITTI,我们需要运行如下命令:

python tools/create_data.py kitti --root-path ./data/kitti --out-dir ./data/kitti --extra-tag kitti

随后,相关的目录结构将如下所示:

mmdetection3d
├── mmdet3d
├── tools
├── configs
├── data
│   ├── kitti
│   │   ├── ImageSets
│   │   ├── testing
│   │   │   ├── calib
│   │   │   ├── image_2
│   │   │   ├── velodyne
│   │   │   ├── velodyne_reduced
│   │   ├── training
│   │   │   ├── calib
│   │   │   ├── image_2
│   │   │   ├── label_2
│   │   │   ├── velodyne
│   │   │   ├── velodyne_reduced
│   │   ├── kitti_gt_database
│   │   ├── kitti_infos_train.pkl
│   │   ├── kitti_infos_trainval.pkl
│   │   ├── kitti_infos_val.pkl
│   │   ├── kitti_infos_test.pkl
│   │   ├── kitti_dbinfos_train.pkl

训练

接着,我们将使用提供的配置文件训练 PointPillars。当您使用不同的 GPU 设置进行训练时,您可以按照这个教程的示例。假设我们在一台具有 8 块 GPU 的机器上使用分布式训练:

./tools/dist_train.sh configs/pointpillars/pointpillars_hv_secfpn_8xb6-160e_kitti-3d-3class.py 8

注意,配置文件名中的 8xb6 是指训练用了 8 块 GPU,每块 GPU 上有 6 个数据样本。如果您的自定义设置不同于此,那么有时候您需要相应地调整学习率。基本规则可以参考此处。我们已经支持了使用 --auto-scale-lr 来自动缩放学习率。

四、模型读取

通过配置类的 fromfile 接口读取配置文件:

test_int = 1
test_list = [1, 2, 3]
test_dict = dict(key1='value1', key2=0.1) 
from mmengine.config import Configcfg = Config.fromfile('learn_read_config.py')
print(cfg)

Config (path: learn_read_config.py): {'test_int': 1, 'test_list': [1, 2, 3], 'test_dict': {'key1': 'value1', 'key2': 0.1}}

配置文件的导出

在启动训练脚本时,用户可能通过传参的方式来修改配置文件的部分字段,为此我们提供了 dump 接口来导出更改后的配置文件。与读取配置文件类似,用户可以通过 cfg.dump('config.xxx') 来选择导出文件的格式。dump 同样可以导出有继承关系的配置文件,导出的文件可以被独立使用,不再依赖于 _base_ 中定义的文件。

基于继承一节定义的 resnet50.py,我们将其加载后导出:

cfg = Config.fromfile('resnet50.py')
cfg.dump('resnet50_dump.py')

resnet50_dump.py

optimizer = dict(type='SGD', lr=0.02, momentum=0.9, weight_decay=0.0001)
model = dict(type='ResNet', depth=50)

MMDetection3D简单学习相关推荐

  1. SQL Server中的锁的简单学习

    原文:SQL Server中的锁的简单学习 简介 在SQL Server中,每一个查询都会找到最短路径实现自己的目标.如果数据库只接受一个连接一次只执行一个查询.那么查询当然是要多快好省的完成工作.但 ...

  2. [Python学习]PycURL简单学习 - limodou的学习记录 - limodou是一个程序员,他关心的焦点是Python, DocBook, Open Source …...

    [Python学习]PycURL简单学习 - limodou的学习记录 - limodou是一个程序员,他关心的焦点是Python, DocBook, Open Source - [Python学习] ...

  3. JavaScript学习笔记04【高级——DOM和事件的简单学习、BOM对象】

    w3school 在线教程:https://www.w3school.com.cn JavaScript学习笔记01[基础--简介.基础语法.运算符.特殊语法.流程控制语句][day01] JavaS ...

  4. 表盘时针的html代码,html5画布操作的简单学习-简单时钟

    html5画布操作的简单学习-简单时针 效果图 一.什么是 Canvas? HTML5 的 canvas 元素使用 JavaScript 在网页上绘制图像. 画布是一个矩形区域,您可以控制其每一像素. ...

  5. QuickSkin简单学习--控制结构

    QuickSkin简单学习 3.控制结构 if if ... endif 结构帮助模板的条件选择. QuickSkin支持和PHP相同的操作符. 比较操作符, 作为名称暗示,允许你比较两个值. 可以是 ...

  6. PL/SQL编程的简单学习

    PL/SQL简单学习 1.PL/SQL块的组成部分: ① 声明部分:该部分包含了变量和常量的定义,以及变量和常量的初始值定义,这部分由关键字declare开始,如果PL/SQL块中不需要声明变量或常量 ...

  7. php get 传循环出来的参数_简单学习PHP中的反射

    和Java一样PHP中也提供了一套完整的反射API,何为反射?以前我们是先写类,再在类中添加各种方法和属性,最后实例化一个类对象调用属性和方法.那有我们没有办法只通过这个实例对象获取到关于这个类的全部 ...

  8. day006bootstrap的简单学习 + 轮播图

    任务1:bootstrap的简单学习 <!DOCTYPE html> <html lang="zh-CN"> <head><meta ch ...

  9. Android简单学习使用PictureSelector框架图片选取裁剪

    Android简单学习使用PictureSelector框架图片选取裁剪 关于 效果图 第一步,添加引用 第二步,新建activity_main.xml布局文件 第三步,修改MainActivity. ...

最新文章

  1. ATT与Intel汇编语言的比较
  2. jquery .parents(), .parent() 和 closest()方法
  3. vsftp配置日志及其启用本地时间
  4. Python内置函数(49)——isinstance
  5. Python字符串isdecimal()
  6. Dart之字符串(String)的相关方法总结
  7. 简单实现虚拟机备份上云
  8. Java基础-SSM之Spring的AOP编程
  9. 待嫁闺中:PPTV的辛酸史
  10. canvas 擦除动画_HTML5 实现橡皮擦的擦除效果
  11. 网页超链接:主页与子页的具体链接
  12. 【已解决】win10离线安装.net framework 3.5(错误:0x8024402c)
  13. 计算机科学与技术核心期刊和相关网站
  14. Mobvista通过聆讯:上半年净利千万美元 同比降49%
  15. 由“微博”的发展史预测“轻博客”的命途
  16. 【转载】SP的前途??
  17. 普通一本计算机科学,值得报考的22所“普通一本”,优势专业很不错!
  18. 信息系统合同管理的分类
  19. VS中使用 loadimage()函数载入图像报错与图像无法载入的解决办法
  20. 20230119英语学习

热门文章

  1. metasploit利用IE漏洞XSS挂马拿内网主机
  2. React Native Android 应用内存使用探究
  3. 就大学毕业典礼的演讲所感触的
  4. G7从入门到精通 =(新手实践成功 HBOOT-0.93.0001 )
  5. 命令行/Python使用pdf2htmlEX将PDF转HTML
  6. 什么是铠装电缆,什么是非铠装电缆,两者的区别?
  7. 如何用Java编写一个简单的服务器和客户机
  8. 3分钟了解 WebAssembly
  9. Vulkan与OpenGL对比——Vulkan的全新渲染架构
  10. 苹果CEO乔布斯(Steve Jobs)20050612在斯坦福大学的演讲