val.py源码解读

1.导入需要的包和基本配置
import argparse # 解析命令行参数模块import json     # 字典列表和json字符串之间的相互解析模块import os       # 与操作系统进行交互的模块 包含文件路径操作和解析import sys      # sys系统模块 包含了与python解释器和它的环境有关的函数from pathlib import path  # path将str转换为path对象 使字符串路径易于操作的模块import numpy as np # numpy(numerical python)是python的一种开源的数值计算扩展import oneflow as flow # oneflow 深度学习框架from tqdm import tqdm # 进度条模块 from models.common import detectmultibackend # 下面都是 one-yolov5 定义的模块,在本系列的其它文章都有涉及from utils.callbacks import callbacksfrom utils.dataloaders import create_dataloaderfrom utils.general import (    logger,    check_dataset,    check_img_size,    check_requirements,    check_yaml,    coco80_to_coco91_class,    colorstr,    increment_path,    non_max_suppression,    print_args,    scale_coords,    xywh2xyxy,    xyxy2xywh,)from utils.metrics import confusionmatrix, ap_per_class, box_ioufrom utils.oneflow_utils import select_device, time_syncfrom utils.plots import output_to_target, plot_images, plot_val_study  
2.opt参数详解
参数 解析  
data dataset.yaml path 数据集配置文件地址 包含数据集的路径、类别个数、类名、下载地址等信息
weights model weights path(s) 模型的权重文件地址 weights/yolov5s
batch-size batch size 计算样本的批次大小 默认32
imgsz inference size (pixels) 输入网络的图片分辨率    默认640
conf-thres confidence threshold object置信度阈值 默认0.001
iou-thres nms iou threshold 进行nms时iou的阈值 默认0.6
task train, val, test, speed or study 设置测试的类型 有train, val, test, speed or study几种 默认val
device cuda device, i.e. 0 or 0,1,2,3 or cpu 测试的设备
workers max dataloader workers (per rank in ddp mode) 加载数据使用的 dataloader workers
single-cls treat as single-class dataset 数据集是否只用一个类别 默认false
augment augmented inference 测试是否使用tta test time augment 默认false
verbose report map by class 是否打印出每个类别的map 默认false
save-hybrid save label+prediction hybrid results to *.txt 保存label+prediction 杂交结果到对应.txt 默认false
save-conf save confidences in --save-txt labels  
save-json save a coco-json results file 是否按照coco的json格式保存结果       默认false
project save to project/name 测试保存的源文件 默认runs/val
name save to project/name 测试保存的文件地址名 默认exp  保存在runs/val/exp下
exist-ok existing project/name ok, do not increment 是否保存在当前文件,不新增 默认false
half use fp16 half-precision inference 是否使用半精度推理 默认false
dnn use opencv dnn for onnx inference 是否使用 opencv dnn 对 onnx 模型推理
3.main函数
根据解析的opt参数,调用run函数
def main(opt):    #  检测requirements文件中需要的包是否安装好了    check_requirements(requirements=root / requirements.txt, exclude=(tensorboard, thop))        if opt.task in (train, val, test):  # run normally        if opt.conf_thres > 0.001:  # 更多请见 https://github.com/ultralytics/yolov5/issues/1466            logger.info(fwarning: confidence threshold {opt.conf_thres} > 0.001 produces invalid results)        run(**vars(opt))    else:        weights = opt.weights if isinstance(opt.weights, list) else [opt.weights]        opt.half = true  # fp16 for fastest results        if opt.task == speed:  # speed benchmarks            # python val.py --task speed --data coco.yaml            #                --batch 1 --weights yolov5n/ yolov5s/ ...            opt.conf_thres, opt.iou_thres, opt.save_json = 0.25, 0.45, false            for opt.weights in weights:                run(**vars(opt), plots=false)        elif opt.task == study:  # speed vs map benchmarks            # python val.py --task study --data coco.yaml            #                --iou 0.7 --weights yolov5n/ yolov5s/...            for opt.weights in weights:                f = fstudy_{path(opt.data).stem}_{path(opt.weights).stem}.txt                x, y = (                    list(range(256, 1536 + 128, 128)),                    [],                )  # x axis (image sizes), y axis                # study: 模型在各个尺度下的指标并可视化,                # 上面list(range(256, 1536 + 128, 128)),代表 img-size 的各个尺度, 具体代码如下:                for opt.imgsz in x:  # img-size                    logger.info(frunning {f} --imgsz {opt.imgsz}...)                    r, _, t = run(**vars(opt), plots=false)                    y.append(r + t)  # results and times                np.savetxt(f, y, fmt=%10.4g)  # save            os.system(zip -r study.zip study_*.txt)            # 可视化各个指标            plot_val_study(x=x)  # plot  
3. run函数
3.1 载入参数
# 不参与反向传播@flow.no_grad() def run(    data, # 数据集配置文件地址 包含数据集的路径、类别个数、类名、下载地址等信息 train.py时传入data_dict    weights=none,  # 模型的权重文件地址 运行train.py=none 运行test.py=默认weights/yolov5s    batch_size=32,  # 前向传播的批次大小 运行test.py传入默认32 运行train.py则传入batch_size // world_size * 2    imgsz=640,  # 输入网络的图片分辨率 运行test.py传入默认640 运行train.py则传入imgsz_test    conf_thres=0.001,  # object置信度阈值 默认0.001    iou_thres=0.6,  # 进行nms时iou的阈值 默认0.6    task=val,  # 设置测试的类型 有train, val, test, speed or study几种 默认val    device=,  # 执行 val.py 所在的设备 cuda device, i.e. 0 or 0,1,2,3 or cpu    workers=8,  # dataloader中的最大 worker 数(线程个数)    single_cls=false,  # 数据集是否只有一个类别 默认false    augment=false,  # 测试时增强,详细请看我们的教程:https://start.oneflow.org/oneflow-yolo-doc/tutorials/03_chapter/tta.html    verbose=false,  # 是否打印出每个类别的map 运行test.py传入默认fasle 运行train.py则传入nc < 50 and final_epoch    save_txt=false,  # 是否以txt文件的形式保存模型预测框的坐标 默认true    save_hybrid=false,  # 是否save label+prediction hybrid results to *.txt  默认false    save_conf=false,  # 是否保存预测每个目标的置信度到预测txt文件中 默认true    save_json=false,  # 是否按照coco的json格式保存预测框,并且使用cocoapi做评估(需要同样coco的json格式的标签),                      #运行test.py传入默认fasle 运行train.py则传入is_coco and final_epoch(一般也是false)    project=root / runs/val,  # 验证结果保存的根目录 默认是 runs/val    name=exp,   # 验证结果保存的目录 默认是exp  最终: runs/val/exp    exist_ok=false,  # 如果文件存在就increment name,不存在就新建  默认false(默认文件都是不存在的)    half=true,    # 使用 fp16 的半精度推理    dnn=false,    # 在 onnx 推理时使用 opencv dnn 后段端    model=none,   # 如果执行val.py就为none 如果执行train.py就会传入( model=attempt_load(f, device).half() )    dataloader=none,   # 数据加载器 如果执行val.py就为none 如果执行train.py就会传入testloader    save_dir=path(), # 文件保存路径 如果执行val.py就为‘’ , 如果执行train.py就会传入save_dir(runs/train/expn)    plots=true,  # 是否可视化 运行val.py传入,默认true     callbacks=callbacks(),     compute_loss=none, # 损失函数 运行val.py传入默认none 运行train.py则传入compute_loss(train)):  
3.2 initialize/load model and set device(初始化/加载模型以及设置设备)
if training:  # 通过 train.py 调用的run函数        device, of, engine = (            next(model.parameters()).device,            true,            false,        )  # get model device, oneflow model        half &= device.type != cpu  # half precision only supported on cuda        model.half() if half else model.float()    else:  # 直接通过 val.py 调用 run 函数        device = select_device(device, batch_size=batch_size)        # directories  生成 save_dir 文件路径  run/val/expn        save_dir = increment_path(path(project) / name, exist_ok=exist_ok)  # increment run        (save_dir / labels if save_txt else save_dir).mkdir(parents=true, exist_ok=true)  # make dir        # 加载模型 只在运行 val.py 才需要自己加载model        model = detectmultibackend(weights, device=device, dnn=dnn, data=data, fp16=half)                stride, of, engine = model.stride, model.of, model.engine        # 检测输入图片的分辨率 imgsz 是否能被 stride 整除         imgsz = check_img_size(imgsz, s=stride)  # check image size        half = model.fp16  # fp16 supported on limited backends with cuda        if engine:            batch_size = model.batch_size        else:            device = model.device            if not of:                batch_size = 1  # export.py models default to batch-size 1                logger.info(fforcing --batch-size 1 inference (1,3,{imgsz},{imgsz}) for non-oneflow models)                # data        data = check_dataset(data)  # check  
3.3 configure
# 配置model.eval() # 启动模型验证模式cuda = device.type != cpuis_coco = isinstance(data.get(val), str) and data[val].endswith(fcoco{os.sep}val2017.txt)  # 通过 coco 数据集的文件夹组织结构判断当前数据集是否为 coco 数据集nc = 1 if single_cls else int(data[nc])  # number of classes# 设置iou阈值 从0.5-0.95取10个(0.05间隔)   iou vector for map@0.5:0.95# iouv: [0.50000, 0.55000, 0.60000, 0.65000, 0.70000, 0.75000, 0.80000, 0.85000, 0.90000, 0.95000]iouv = flow.linspace(0.5, 0.95, 10, device=device)  # iou vector for map@0.5:0.95niou = iouv.numel() # 示例 map@0.5:0.95 iou阈值个数=10个,计算 map 的详细教程可以在 https://start.oneflow.org/oneflow-yolo-doc/tutorials/05_chapter/map_analysis.html 这里查看  
3.4 dataloader
通过 train.py 调用 run 函数会传入一个 dataloader,而通过 val.py 需要加载测试数据集
# dataloader# 如果不是训练(执行val.py脚本调用run函数)就调用create_dataloader生成dataloader# 如果是训练(执行train.py调用run函数)就不需要生成dataloader 可以直接从参数中传过来testloaderif not training: # 加载val数据集    if of and not single_cls:  # check --weights are trained on --data        ncm = model.model.nc        assert ncm == nc, (            f{weights} ({ncm} classes) trained on different --data than what you passed ({nc}  fclasses). pass correct combination of f --weights and --data that are trained together.        )    model.warmup(imgsz=(1 if of else batch_size, 3, imgsz, imgsz))  # warmup    pad = 0.0 if task in (speed, benchmark) else 0.5    rect = false if task == benchmark else of  # square inference for benchmarks    task = task if task in (train, val, test) else val  # path to train/val/test images    # 创建dataloader 这里的rect默认为true 矩形推理用于测试集 在不影响map的情况下可以大大提升推理速度    dataloader = create_dataloader(        data[task],        imgsz,        batch_size,        stride,        single_cls,        pad=pad,        rect=rect,        workers=workers,        prefix=colorstr(f{task}: ),    )[0]  
3.5 初始化
# 初始化验证的图片的数量seen = 0# 初始化混淆矩阵confusion_matrix = confusionmatrix(nc=nc)#  获取数据集所有目标类别的类名names = dict(enumerate(model.names if hasattr(model, names) else model.module.names))# coco80_to_coco91_class :  converts 80-index (val2014) to 91-index (paper) # https://tech.amikelive.com/node-718/what-object-categories-labels-are-in-coco-dataset/class_map = coco80_to_coco91_class() if is_coco else list(range(1000))# 设置进度条模块显示信息s = (%20s + %11s * 6) % (    class,    images,    labels,    p,    r,    map@.5,    map@.5:.95,)# 初始化时间 dt[t0(预处理的时间), t1(推理的时间), t2(后处理的时间)] 和 p, r, f1, mp, mr, map50, map指标dt, p, r, f1, mp, mr, map50, map = (    [0.0, 0.0, 0.0],    0.0,    0.0,    0.0,    0.0,    0.0,    0.0,    0.0,)#  初始化验证集的损失loss = flow.zeros(3, device=device)#  初始化 json 文件中的字典, 统计信息, ap, ap_class jdict, stats, ap, ap_class = [], [], [], []callbacks.run(on_val_start)# 初始化 tqdm 进度条模块pbar = tqdm(dataloader, desc=s, bar_format={l_bar}{bar:10}{r_bar}{bar:-10b})示例输出val: data=data/coco.yaml, weights=['yolov5x'], batch_size=32, imgsz=640, conf_thres=0.001, iou_thres=0.6, task=val,     device=, workers=8, single_cls=false, augment=false, verbose=false, save_txt=false, save_hybrid=false,     save_conf=false, save_json=true, project=runs/val, name=exp, exist_ok=false, half=true, dnn=falseyolov5  v1.0-8-g94ec5c4 python-3.8.13 oneflow-0.8.1.dev20221018+cu112 fusing layers... model summary: 322 layers, 86705005 parameters, 571965 gradientsval: scanning '/data/dataset/fengwen/coco/val2017.cache' images and labels... 4952 found, 48 missing, 0 empty, 0 corrupt: 100%|████████               class     images     labels          p          r     map@.5 map@.5:.95: 100%|██████████| 157/157 [01:55<00:00,  1.36it/                 all       5000      36335      0.743      0.627      0.685      0.503speed: 0.1ms pre-process, 7.5ms inference, 2.1ms nms per image at shape (32, 3, 640, 640)  # <--- baseline speedevaluating pycocotools map... saving runs/val/exp3/yolov5x_predictions.json......average precision  (ap) @[ iou=0.50:0.95 | area=   all | maxdets=100 ] = 0.505 # <--- baseline map average precision  (ap) @[ iou=0.50      | area=   all | maxdets=100 ] = 0.689 average precision  (ap) @[ iou=0.75      | area=   all | maxdets=100 ] = 0.545 average precision  (ap) @[ iou=0.50:0.95 | area= small | maxdets=100 ] = 0.339 average precision  (ap) @[ iou=0.50:0.95 | area=medium | maxdets=100 ] = 0.557 average precision  (ap) @[ iou=0.50:0.95 | area= large | maxdets=100 ] = 0.650 average recall     (ar) @[ iou=0.50:0.95 | area=   all | maxdets=  1 ] = 0.382 average recall     (ar) @[ iou=0.50:0.95 | area=   all | maxdets= 10 ] = 0.628 average recall     (ar) @[ iou=0.50:0.95 | area=   all | maxdets=100 ] = 0.677  # <--- baseline mar average recall     (ar) @[ iou=0.50:0.95 | area= small | maxdets=100 ] = 0.523 average recall     (ar) @[ iou=0.50:0.95 | area=medium | maxdets=100 ] = 0.730 average recall     (ar) @[ iou=0.50:0.95 | area= large | maxdets=100 ] = 0.826  
3.6 开始验证
for batch_i, (im, targets, paths, shapes) in enumerate(pbar): https://github.com/oneflow-inc/one-yolov5/blob/bf8c66e011fcf5b8885068074ffc6b56c113a20c/utils/dataloaders.py#l735im :  flow.from_numpy(img);targets : labels_out paths: self.im_files[index] shapes : shapes  
3.6.1 验证开始前的预处理
callbacks.run(on_val_batch_start)t1 = time_sync()if cuda:    im = im.to(device)    targets = targets.to(device)im = im.half() if half else im.float()  # uint8 to fp16/32im /= 255  # 0 - 255 to 0.0 - 1.0nb, _, height, width = im.shape  # batch size, channels, height, widtht2 = time_sync()dt[0] += t2 - t1  
3.6.2 推理
# inferenceout, train_out = model(im) if training else model(im, augment=augment, val=true)  # 输出为:推理结果、损失值dt[1] += time_sync() - t2  
3.6.3 计算损失
# loss分类损失(cls_loss):该损失用于判断模型是否能够准确地识别出图像中的对象,并将其分类到正确的类别中。置信度损失(obj_loss):该损失用于衡量模型预测的框(即包含对象的矩形)与真实框之间的差异。边界框损失(box_loss):该损失用于衡量模型预测的边界框与真实边界框之间的差异,这有助于确保模型能够准确地定位对象。if compute_loss:    loss += compute_loss([x.float() for x in train_out], targets)[1]  # box, obj, cls  
3.6.4 run nms
# nms# 将真实框 target的 xywh (因为 target 是在 labelimg 中做了归一化的)映射到真实的图像 (test) 尺寸targets[:, 2:] *= flow.tensor((width, height, width, height), device=device)  # to pixels# 在 nms 之前将数据集标签 targets 添加到模型预测中,这允许在数据集中自动标记(for autolabelling)其它对象(在pred中混入gt) 并且map反映了新的混合标签# targets: [num_target, img_index+class_index+xywh] = [31, 6]# lb: {list: bs} 第一张图片的target[17, 5] 第二张[1, 5] 第三张[7, 5] 第四张[6, 5]lb = [targets[targets[:, 0] == i, 1:] for i in range(nb)] if save_hybrid else []  # for autolabellingt3 = time_sync()non_max_suppression (非最大值抑制)non-maximum suppression (nms) on inference results to reject overlapping bounding boxes该算法的原理:先假设有6个矩形框,根据分类器的类别分类概率大小排序,假设从小到大属于车辆(被检测的目标)的概率分别为:a、b、c、d、e、f(1)从最大概率 矩形框f开始,分别判断a~e与f的重叠度iou是否大于某个指定的阀值;(2)假设b、d与f的重叠度大于指定的阀值,则丢弃b、d,并标记第一个矩形框 f,是我们要保留的(3)从剩下的矩形框a、c、e中,选择最大概率,假设为e,然后判断a、c与e的重叠度是否大于指定的阀值,     假如大于就丢弃a、c,并标记e,是我们保留下来的第二个矩形框一直重复上述过程,找到所有被保留的矩形框returns:     list of detections, on (n,6) tensor per image [xyxy, conf, cls]out = non_max_suppression(out, conf_thres, iou_thres, labels=lb, multi_label=true, agnostic=single_cls)#  获取nms时间dt[2] += time_sync() - t3  
3.6.5 统计每张图片的真实框、预测框信息
# 为每张图片做统计,写入预测信息到txt文件,生成json文件字典,统计tp等# out: list{bs}  [300, 6] [42, 6] [300, 6] [300, 6]  [:, image_index+class+xywh]for si, pred in enumerate(out):    # 获取第 si 张图片的 gt 标签信息 包括 class, x, y, w, h    target[:, 0]为标签属于哪张图片的编号    labels = targets[targets[:, 0] == si, 1:] # [:, class+xywh]    nl, npr = labels.shape[0], pred.shape[0]  # number of labels, predictions    path, shape = path(paths[si]), shapes[si][0]    correct = flow.zeros(npr, niou, dtype=flow.bool, device=device)  # init    seen += 1 # 统计测试图片数量 +1    if npr == 0:# 如果预测为空,则添加空的信息到stats里        if nl:            stats.append((correct, *flow.zeros((2, 0), device=device), labels[:, 0]))            if plots:                confusion_matrix.process_batch(detections=none, labels=labels[:, 0])        continue        # predictions        if single_cls:            pred[:, 5] = 0        predn = pred.clone()        # 将预测坐标映射到原图img中        scale_coords(im[si].shape[1:], predn[:, :4], shape, shapes[si][1])  # native-space pred        # evaluate        if nl:            tbox = xywh2xyxy(labels[:, 1:5])  # target boxes            scale_coords(im[si].shape[1:], tbox, shape, shapes[si][1])  # native-space labels            labelsn = flow.cat((labels[:, 0:1], tbox), 1)  # native-space labels            correct = process_batch(predn, labelsn, iouv)            if plots:                confusion_matrix.process_batch(predn, labelsn)        stats.append((correct, pred[:, 4], pred[:, 5], labels[:, 0]))  # (correct, conf, pcls, tcls)        # save/log        # 保存预测信息到txt文件  runsvalexp7labelsimage_name.txt        if save_txt:            save_one_txt(                predn,                save_conf,                shape,                file=save_dir / labels / f{path.stem}.txt,            )        if save_json:            save_one_json(predn, jdict, path, class_map)  # append to coco-json dictionary        callbacks.run(on_val_image_end, pred, predn, path, names, im[si])  
3.6.6 画出前三个batch图片的 gt 和 pred 框
gt : 真实框,ground truth box, 是人工标注的位置,存放在标注文件中
pred : 预测框,prediction box, 是由目标检测模型计算输出的框
# plot imagesif plots and batch_i < 3:    plot_images(im, targets, paths, save_dir / fval_batch{batch_i}_labels.jpg, names)  # labels    plot_images(        im,        output_to_target(out),        paths,        save_dir / fval_batch{batch_i}_pred.jpg,        names,    )  # predcallbacks.run(on_val_batch_end)  
3.7 计算指标
指标名字在代码中体现
# compute metricsstats = [flow.cat(x, 0).cpu().numpy() for x in zip(*stats)]  # to numpyif len(stats) and stats[0].any():    tp, fp, p, r, f1, ap, ap_class = ap_per_class(*stats, plot=plots, save_dir=save_dir, names=names)    ap50, ap = ap[:, 0], ap.mean(1)  # ap@0.5, ap@0.5:0.95    mp, mr, map50, map = p.mean(), r.mean(), ap50.mean(), ap.mean()nt = np.bincount(stats[3].astype(int), minlength=nc)  # number of targets per class  
3.8 打印日志
# print results per classif (verbose or (nc  1 and len(stats):for i, c in enumerate(ap_class):    logger.info(pf % (names[c], seen, nt[c], p[i], r[i], ap50[i], ap[i]))# print speedst = tuple(x / seen * 1e3 for x in dt)  # speeds per imageif not training:shape = (batch_size, 3, imgsz, imgsz)logger.info(fspeed: %.1fms pre-process, %.1fms inference, %.1fms nms per image at shape {shape} % t)  
3.9 保存验证结果
# plotsif plots:    confusion_matrix.plot(save_dir=save_dir, names=list(names.values()))    callbacks.run(on_val_end)# save jsonif save_json and len(jdict):    w = path(weights[0] if isinstance(weights, list) else weights).stem if weights is not none else   # weights    anno_json = str(path(data.get(path, ../coco)) / annotations/instances_val2017.json)  # annotations json    pred_json = str(save_dir / f{w}_predictions.json)  # predictions json    logger.info(fevaluating pycocotools map... saving {pred_json}...)    with open(pred_json, w) as f:        json.dump(jdict, f)    # try-catch,会有哪些error        pycocotools介绍:        https://github.com/cocodataset/cocoapi/blob/master/pythonapi/pycocoevaldemo.ipynb    尝试:        使用pycocotools工具计算loss        coco api - http://cocodataset.org/    失败error:        直接打印抛出的异常        1. 可能没有安装 pycocotools,但是网络有问题,无法实现自动下载。        2. pycocotools包版本有问题        try:  # https://github.com/cocodataset/cocoapi/blob/master/pythonapi/pycocoevaldemo.ipynb        check_requirements([pycocotools])        from pycocotools.coco import coco        from pycocotools.cocoeval import cocoeval        anno = coco(anno_json)  # init annotations api        pred = anno.loadres(pred_json)  # init predictions api        eval = cocoeval(anno, pred, bbox)        if is_coco:            eval.params.imgids = [int(path(x).stem) for x in dataloader.dataset.im_files]  # image ids to evaluate        eval.evaluate()        eval.accumulate()        eval.summarize()        map, map50 = eval.stats[:2]  # update results (map@0.5:0.95, map@0.5)    except exception as e:        logger.info(fpycocotools unable to run: {e})  
3.10 返回结果
# return resultsmodel.float()  # for trainingif not training:    s = f{len(list(save_dir.glob('labels/*.txt')))} labels saved to {save_dir / 'labels'} if save_txt else     logger.info(fresults saved to {colorstr('bold', save_dir)}{s})maps = np.zeros(nc) + mapfor i, c in enumerate(ap_class):    maps[c] = ap[i]return (mp, mr, map50, map, *(loss.cpu() / len(dataloader)).tolist()), maps, t


灵动微电子获得数千万元C轮融资
CITE2018开幕 长虹AI电视将带来什么样的震撼
星云医学影像人工智能平台落地,飞利浦新产品如何开启AI医疗时代?
插座冒烟的原因和解决方式
车身电子产品的趋势及行业前景
val.py源码解读
联华电子正面临12吋成熟制程需求量的激增
匹敌7nm GPU,燧原科技第二代云端AI推理卡发布,落地服务器、身份识别、车路协同等应用
前11月中国燃料电池汽车产销分别完成935辆和948辆
电源监控器IMP809/IMP810及其应用
硬件算法协同设计
ASML发布2023年第四季度及全年财报 净赚608亿元
博通物联网平台助力海尔新智能家电
如何让多个智能体学会一起完成同一个任务,学会彼此合作和相互竞争
住友增加对SINTAVIA的投资,加速高性能航空部件增材制造服务
360N5评测:高通骁龙653+大内存6GB+支持应用分身多开
智能锁厂家坤坤浅析全自动智能锁的选择标准
与小米6同行?miui9也要来了?更多黑科技,米粉们期待吗?
鸿蒙生态中华为并未“以权谋私” 鸿蒙生态的参与者需要这些人
管窥一下汽车这样的“自控力”是如何实现的