YOLO v8目标跟踪详细解读(一)

news2025/2/24 17:19:36

在此之前,我们已经对yolo系列做出了详细的探析,有兴趣的朋友可以参考yolov8等文章。YOLOV8对生态进行了优化,目前已经支持了分割,分类,跟踪等功能,这对于我们开发者来说,是十分便利。今天我们对YOLO v8中支持的目标跟踪进行详细解读。
在这里插入图片描述代码地址:yolov8

一、算法简介

目标跟踪现阶段是强烈依赖目标检测结果的,主流的目标跟踪pipeline,简单的总结一下:

首先,目标检测模型输出的检测框是我们跟踪的对象。假设我们在第一帧确定了跟踪对象,即给定检测框id,那当检测模型输出第2帧检测结果,我们需要将第2帧的检测框与第1帧检测框的id进行匹配。那么匹配的过程是怎样的呢?

有人说,直接用两帧检测框的IOU去匹配呗,将IOU看作cost_matrix,利用匈牙利算法去匹配两帧的id。理想情况下,是没问题的,但是,当我们处于crowd场景下,遮挡,光照等给检测带来误差,那么,IOU直接匹配就不那么靠谱了。

远古神器卡尔曼滤波器有神奇的疗效,我们将检测框的坐标看作状态变量,通过状态转移矩阵可以预测出下一帧检测框的位置。然后,下一帧的检测框坐标作为观测量,可以对预测量进行更新并矫正,从而更好的预测下下帧的检测框位置。

总结来说,就是利用卡尔曼滤波器预测检测框位置,构建合适的cost_matrix并利用匈牙利算法匹配轨迹与当前帧检测框的id,同时加入适当的逻辑,就能构建一个效果不错的跟踪器。

二、代码详解

YOLOv8采用2022年提出的跟踪算法BoT-SORT和ByteTrack两种算法实现目标跟踪。如果你想体验该算法的跟踪效果只需要执行以下代码。

from ultralytics import YOLO
	
model = YOLO('yolov8n.pt')
results = model.track(source=".avi", show=True,  save=True)

在Yolo V8中,实现对一个视频中运动目标跟踪的核心代码块为文件:.\ultralytics\tracker\trackers\byte_tracker.py 中类BYTETracker.update()。下面我们不赘述卡尔曼以及匈牙利算法的原理,仅从代码逻辑来拆解。

在这之前,需要了解一个重要的类class BaseTrack,该对象为跟踪的基类,用于处理基本的跟踪属性与操作。class STrack(BaseTrack),class BOTrack(STrack),BoT-SORT和ByteTrack的track都是继承于此。从代码中我们可以发现该类记录了跟踪id,is_activated 激活状态等等操作与属性。在跟踪过程中,每帧的检测框都会分配一个track

class BaseTrack:
    """Base class for object tracking, handling basic track attributes and operations."""

    _count = 0

    track_id = 0
    is_activated = False
    state = TrackState.New

    history = OrderedDict()
    features = []
    curr_feature = None
    score = 0
    start_frame = 0
    frame_id = 0
    time_since_update = 0

    # Multi-camera
    location = (np.inf, np.inf)

    @property
    def end_frame(self):
        """Return the last frame ID of the track."""
        return self.frame_id

    @staticmethod
    def next_id():
        """Increment and return the global track ID counter."""
        BaseTrack._count += 1
        return BaseTrack._count

    def activate(self, *args):
        """Activate the track with the provided arguments."""
        raise NotImplementedError
	 ## ........
   

在跟踪前,先初始化跟踪器,其中self.tracked_stracks列表保存了可跟踪的轨迹,self.lost_stracks保存丢失的轨迹,self.removed_stracks保存被移除的轨迹。当self.lost_stracks的成员在满足被移除的条件后则变为self.removed_stracks的成员,而过程中若被新的track匹配上,则变为self.tracked_stracks的成员。self.frame_id用来记录帧数,self.kalman_filter表示卡尔曼滤波器,后面称之为KF。

def __init__(self, args, frame_rate=30):
        """Initialize a YOLOv8 object to track objects with given arguments and frame rate."""
        self.tracked_stracks = []  # type: list[STrack]
        self.lost_stracks = []  # type: list[STrack]
        self.removed_stracks = []  # type: list[STrack]

        self.frame_id = 0
        self.args = args
        self.max_time_lost = int(frame_rate / 30.0 * args.track_buffer)
        self.kalman_filter = self.get_kalmanfilter()
        self.reset_id()

接下来正式进入update逻辑讲解。每检测一帧就会执行一次update,因此self.frame_id帧数+1,activated_starcks 用来保存该帧激活的轨迹,refind_stracks保存该帧重新激活的轨迹,lost_stracks 保存该帧丢失的轨迹,removed_stracks 保存该帧移除的轨迹。results为YOLOv8的检测结果,bboxes 是检测框坐标,后面接上了索引。

def update(self, results, img=None):
        """Updates object tracker with new detections and returns tracked object bounding boxes."""
        self.frame_id += 1
        activated_starcks = []
        refind_stracks = []
        lost_stracks = []
        removed_stracks = []

        scores = results.conf
        bboxes = results.xyxy
        cls = results.cls
        # Add index
        bboxes = np.concatenate([bboxes, np.arange(len(bboxes)).reshape(-1, 1)], axis=-1)

byte_tracker根据置信度将检测框分成两类,当置信度高于self.args.track_high_thresh(0.5)时,我们将其称为第一检测框,当置信度低于0.5且高于0.1时,称为第二检测框。

		remain_inds = scores > self.args.track_high_thresh
        inds_low = scores > self.args.track_low_thresh
        inds_high = scores < self.args.track_high_thresh

        inds_second = np.logical_and(inds_low, inds_high)
        dets_second = bboxes[inds_second]
        dets = bboxes[remain_inds]
        scores_keep = scores[remain_inds]
        scores_second = scores[inds_second]
        cls_keep = cls[remain_inds]
        cls_second = cls[inds_second]

为每个检测框分配track,上面讲到,track类提供了跟踪属性与操作。目前,YOLOV8中的self.args.with_reid=False,与特征相关的还未实现。

detections = self.init_track(dets, scores_keep, cls_keep, img) 
def init_track(self, dets, scores, cls, img=None):
        """Initialize track with detections, scores, and classes.为每一个det分配一个track"""
        if len(dets) == 0:
            return []
        if self.args.with_reid and self.encoder is not None:
            features_keep = self.encoder.inference(img, dets)
            return [BOTrack(xyxy, s, c, f) for (xyxy, s, c, f) in zip(dets, scores, cls, features_keep)]  # detections
        else:
            return [BOTrack(xyxy, s, c) for (xyxy, s, c) in zip(dets, scores, cls)]  # detections

步骤一:将可跟踪轨迹与第一检测框进行关联匹配。首先,遍历self.tracked_stracks可跟踪轨迹,将已激活的轨迹添加到tracked_stracks中,未被激活的轨迹加入unconfirmed未证实轨迹。将丢失轨迹与激活轨迹合并到strack_pool中,注意剔除重复track_id。重点来了,self.multi_predict(strack_pool),我们需要利用KF将strack_pool中前一帧轨迹通过状态转移矩阵预测出该帧最优估计以及状态协方差矩阵,并计算轨迹池中的轨迹与该帧检测出的bbox的距离作为cost_matrix,利用匈牙利算法进行关联匹配。

通过匈牙利算法,我们获得matches(关联成功后轨迹与检测框的索引),u_track是未关联上的轨迹,u_detection未关联上的检测框。

		unconfirmed = []
        tracked_stracks = []  # type: list[STrack]
        for track in self.tracked_stracks:
            if not track.is_activated:
                unconfirmed.append(track)
            else:
                tracked_stracks.append(track)
        # Step 2: First association, with high score detection boxes
        strack_pool = self.joint_stracks(tracked_stracks, self.lost_stracks) ##将丢失track与激活track合并到strack_pool中,注意剔除重复track_id。
        # Predict the current location with KF
        self.multi_predict(strack_pool) ## 第一帧step4已初始化KF,第二帧的track通过KF的状态转移方程预测最优估计以及状态协方差矩阵
        if hasattr(self, 'gmc') and img is not None:
            warp = self.gmc.apply(img, dets)
            STrack.multi_gmc(strack_pool, warp)
            STrack.multi_gmc(unconfirmed, warp)

        dists = self.get_dists(strack_pool, detections) ## 计算轨迹池中的轨迹与该帧检测出的bbox的距离,并利用匈牙利算法进行匹配。
        matches, u_track, u_detection = matching.linear_assignment(dists, thresh=self.args.match_thresh)

遍历matches,已激活的轨迹需要根据该帧检测框的信息去更新轨迹属性,KF根据测量值(该帧的检测框坐标)矫正最优估计,并更新状态协方差矩阵。而未激活的轨迹重新关联上检测框,需要重新激活,re_activate与update功能类似。

 		for itracked, idet in matches:
            track = strack_pool[itracked]
            det = detections[idet]
            if track.state == TrackState.Tracked: ## 已激活的track
                track.update(det, self.frame_id)  ## 确定匹配后更新track信息,KF根据测量值矫正最优估计,并更新状态协方差矩阵
                activated_starcks.append(track)
            else: ## 未激活的track重新匹配上需要重新激活
                track.re_activate(det, self.frame_id, new_id=False)
                refind_stracks.append(track)
                
     def update(self, new_track, frame_id):
        """
        Update a matched track
        :type new_track: STrack
        :type frame_id: int
        :return:
        """
        self.frame_id = frame_id
        self.tracklet_len += 1

        new_tlwh = new_track.tlwh
        self.mean, self.covariance = self.kalman_filter.update(self.mean, self.covariance,self.convert_coords(new_tlwh))
        self.state = TrackState.Tracked
        self.is_activated = True

        self.score = new_track.score
        self.cls = new_track.cls
        self.idx = new_track.idx
        
     def re_activate(self, new_track, frame_id, new_id=False):
        """Reactivates a previously lost track with a new detection."""
        self.mean, self.covariance = self.kalman_filter.update(self.mean, self.covariance, self.convert_coords(new_track.tlwh))
        self.tracklet_len = 0
        self.state = TrackState.Tracked
        self.is_activated = True
        self.frame_id = frame_id
        if new_id:
            self.track_id = self.next_id()
        self.score = new_track.score
        self.cls = new_track.cls
        self.idx = new_track.idx

步骤二: 将可跟踪轨迹与第二检测框进行关联匹配。首先,为第二检测框分配track,将轨迹池strack_pool中在步骤一中未关联上的轨迹存放在r_tracked_stracks中。计算r_tracked_stracks与第二检测框的IOU,将IOU作为cost_matric进行匈牙利匹配,注意与步骤一匹配过程相比,步骤二的匹配阈值从0.8降低至0.5。由于第二检测框置信度较低,坐标回归质量较差,为了捞回更多的轨迹,适当降低阈值是必要的。遍历matches,更新可激活轨迹,并重新激活未激活轨迹。若步骤二中仍存在未关联上的轨迹,需将其状态改成lost丢失

		detections_second = self.init_track(dets_second, scores_second, cls_second, img) ## 0.1<置信度<0.5的box分配track
        r_tracked_stracks = [strack_pool[i] for i in u_track if strack_pool[i].state == TrackState.Tracked] ## 第一次未匹配上的track
        # TODO
        dists = matching.iou_distance(r_tracked_stracks, detections_second)
        matches, u_track, u_detection_second = matching.linear_assignment(dists, thresh=0.5)
        for itracked, idet in matches:
            track = r_tracked_stracks[itracked]
            det = detections_second[idet]
            if track.state == TrackState.Tracked:
                track.update(det, self.frame_id)
                activated_starcks.append(track)
            else:
                track.re_activate(det, self.frame_id, new_id=False)
                refind_stracks.append(track)

        for it in u_track: ## 第2次还未匹配上的track,将其状态改成lost丢失
            track = r_tracked_stracks[it]
            if track.state != TrackState.Lost:
                track.mark_lost()
                lost_stracks.append(track)

u_detection是步骤一中没有关联上的track(即检测框),unconfirmed未证实的轨迹是可跟踪轨迹中is_tracked=False的轨迹,这里需要与u_detection进行匹配尝试。如若关联成功,则需要更新轨迹将is_tracked置为True,并更新其KF相关参数,否则该轨迹被移除

 # Deal with unconfirmed tracks, usually tracks with only one beginning frame
        detections = [detections[i] for i in u_detection]
        dists = self.get_dists(unconfirmed, detections) ## 这里的detections是第一检测框未匹配上的
        matches, u_unconfirmed, u_detection = matching.linear_assignment(dists, thresh=0.7) 
        for itracked, idet in matches: ## unconfirmed未证实的轨迹与步骤一未匹配上的检测框关联成功,更新轨迹并加入激活轨迹
            unconfirmed[itracked].update(detections[idet], self.frame_id)
            activated_starcks.append(unconfirmed[itracked])
        for it in u_unconfirmed: ## 未证实轨迹仍然关联失败,则移除未证实轨迹
            track = unconfirmed[it]
            track.mark_removed()
            removed_stracks.append(track)

步骤三:初始化新轨迹。当我们在处理第一帧时,因为可跟踪轨迹等列表均为空,无法匹配,因此直接进入步骤三,将检测框置信度高于self.args.new_track_thresh的track激活,is_tracked置为True,初始化KF,并将track存放于列表activated_starcks中,等待合并到self.tracked_stracks可跟踪轨迹中。当frame_id != 1时,activate不会将is_tracked置为True,未匹配上的track存于activated_starcks中,等待合并到self.tracked_stracks。注意由于is_tracked=False,这部分在步骤一中被归纳为unconfirmed未证实轨迹。

		## 处理第一帧时,因为无法匹配,直接进入Step 4,激活track,将is_tracked置True,初始化KF,并将track存放于列表activated_starcks中,等待合并到self.tracked_stracks可跟踪轨迹中。
        ## frame_id!=1时,activate不会将is_tracked置为True,未匹配上的track存于activated_starcks中,等待合并到self.tracked_stracks。注意这部分在220行被归纳到unconfirmed中。
        for inew in u_detection:
            track = detections[inew]
            if track.score < self.args.new_track_thresh:
                continue
            track.activate(self.kalman_filter, self.frame_id)
            activated_starcks.append(track)

步骤四: 可跟踪轨迹在经过步骤一与步骤二后,仍未与检测框关联成功,其状态会变为lost丢失,并从可跟踪轨迹中移除,并入self.lost_stracks丢失轨迹中。如果丢失轨迹中的track在30帧内仍未匹配上,则将其移除。

 # Step 5: Update state
        for track in self.lost_stracks: ## 如果超过30帧lost_track仍未匹配上,则移除
            if self.frame_id - track.end_frame > self.max_time_lost:
                track.mark_removed()
                removed_stracks.append(track)

步骤五: 更新self.tracked_stracks,self.lost_stracks,self.removed_stracks列表。将activated_starcks,refind_stracks合并到可跟踪轨迹中,继续在下一帧进行关联跟踪。将lost_stracks在self.tracked_stracks列表中出现的track剔除(丢失已找回),将lost_stracks在self.removed_stracks列表中出现的track剔除(丢失已移除)。remove_duplicate_stracks将self.tracked_stracks, self.lost_stracks列表中IOU接近的track去重,保留最新出现的track。最后return被激活的轨迹,其坐标是KF修正后的值。

		self.tracked_stracks = [t for t in self.tracked_stracks if t.state == TrackState.Tracked]
        self.tracked_stracks = self.joint_stracks(self.tracked_stracks, activated_starcks) ## 将激活的tracks合并到self.tracked_stracks列表中
        self.tracked_stracks = self.joint_stracks(self.tracked_stracks, refind_stracks)
        self.lost_stracks = self.sub_stracks(self.lost_stracks, self.tracked_stracks) ## 将lost_stracks在self.tracked_stracks列表中出现的track剔除(丢失已找回)
        self.lost_stracks.extend(lost_stracks)
        self.lost_stracks = self.sub_stracks(self.lost_stracks, self.removed_stracks) ## 将lost_stracks在self.removed_stracks列表中出现的track剔除(丢失已移除)
        self.tracked_stracks, self.lost_stracks = self.remove_duplicate_stracks(self.tracked_stracks, self.lost_stracks)
        self.removed_stracks.extend(removed_stracks)
        if len(self.removed_stracks) > 1000:
            self.removed_stracks = self.removed_stracks[-999:]  # clip remove stracks to 1000 maximum
        return np.asarray(
            [x.tlbr.tolist() + [x.track_id, x.score, x.cls, x.idx] for x in self.tracked_stracks if x.is_activated],
            dtype=np.float32)

代码详解过于冗长,文字单调,缺少精炼的图释,有不理解的或者讲解错误的地方还望指出,接下来会对目标跟踪中的卡尔曼滤波进行剖析,有兴趣的朋友可以关注留言。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/863297.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

沐渥六门氮气柜技术参数详解

氮气柜是用来存储电子元器件、芯片、半导体器件、金属材料、电路板、精密仪器等物品的设备&#xff0c;通过充入氮气降低柜内湿度&#xff0c;达到防潮、防氧化、防静电、防锈和防霉效果。 六门氮气柜参数 1、容积&#xff1a;约1380L&#xff1b;外尺寸&#xff1a;W1200*D700…

【人工智能前沿弄潮】—— SAM系列:SAM从提示生成物体mask

SAM从提示生成物体mask Segment Anything Model&#xff08;SAM&#xff09;根据指示所需的对象来预测对象掩码。该模型首先将图像转换为图像嵌入&#xff0c;从而可以从提示中高效地生成高质量的掩码。 SamPredictor类为模型提供了一个简单的接口来提示模型。用户可以首先使…

LeetCode 33题:搜索旋转排序数组

目录 题目 思路 代码 暴力解法 分方向法 二分法 题目 整数数组 nums 按升序排列&#xff0c;数组中的值 互不相同 。 在传递给函数之前&#xff0c;nums 在预先未知的某个下标 k&#xff08;0 < k < nums.length&#xff09;上进行了 旋转&#xff0c;使数组变为 …

Macbook pro、air、imac 在打字好卡,延迟特别严重,要怎么解决?

MacBook pro在打字好卡&#xff0c;延迟特别严重&#xff0c;到底是什么问题呢&#xff1f;卡的死&#xff0c;打个字要反应很久很久才能响应过来&#xff0c;这不是我理想中的macbook pro。以前也没有这样的问题&#xff0c;找找原因。 上网逛了一圈&#xff0c;发现大家都说…

自动切换HTTP爬虫ip助力Python数据采集

在Python的爬虫世界里&#xff0c;你是否也被网站的IP封锁问题困扰过&#xff1f;别担心&#xff0c;我来教你一个终极方案&#xff0c;让你的爬虫自动切换爬虫ip&#xff0c;轻松应对各种封锁和限制&#xff01;快来跟我学&#xff0c;让你的Python爬虫如虎添翼&#xff01; 首…

@Transactional 注解下,事务失效的七种场景

文章目录 1、异常被捕获后没有抛出2、抛出非运行时异常3、方法内部直接调用4、新开启一个线程5、注解到private方法上6、数据库本身不支持7、事务传播属性设置错误 Transactional是一种基于注解管理事务的方式&#xff0c;spring通过动态代理的方式为目标方法实现事务管理的增强…

腾讯云服务器镜像操作系统大全_Linux_Windows清单

腾讯云CVM服务器的公共镜像是由腾讯云官方提供的镜像&#xff0c;公共镜像包含基础操作系统和腾讯云提供的初始化组件&#xff0c;公共镜像分为Windows和Linux两大类操作系统&#xff0c;如TencentOS Server、Windows Server、OpenCloudOS、CentOS Stream、CentOS、Ubuntu、Deb…

Python基础小项目

今天给大家写一期特别基础的Python小项目&#xff0c;欢迎大家支持&#xff0c;并给出自己的完善修改 &#xff08;因为我写的都是很基础的&#xff0c;运行速率不是很好的 目录 1. 地铁票价题目程序源码运行截图 2. 购物车题目程序源码运行截图 3. 名片管理器题目程序源码运行…

应用程序运行报错:First section must be [net] or [network]:No such file or directory

应用程序报错环境&#xff1a; 在linux下&#xff0c;调用darknet训练的模型&#xff0c;报错&#xff1a;First section must be [net] or [network]:No such file or directory&#xff0c;并提示&#xff1a;"./src/utils.c:256: error: Assertion 0 failed." 如…

GAMES101:作业1记录

主要记录一下GAMES101作业的记录和思考。 1 总览2. 代码编写get_model_matrix(float rotation_angle)get_projection_matrix(float eye_fov,float aspect_ratio,float zNear,f1 oat zFar)进阶代码 Eigen::Matrix4f get_model_matrix_anyaxis(Vector3f axis, float angle) 3. 其…

如何学习嵌入式软件开发?

首先就是认知和基础阶段的学习。这部分一般都是要求学习一些行业认知类的课程&#xff0c;指导嵌入式未来的发展前景和就业趋势&#xff0c;C语言的入门&#xff0c;开发工具的使用&#xff0c;常见的命令&#xff0c;数据结构算法等内容。这一部分主要的就是要靠记忆力&#x…

odoo-034 float 浮点数比较

文章目录 前提问题解决总结 前提 odoo 版本&#xff1a;13 python&#xff1a;3.6.9 问题 比较销售订单行中已送货跟已开票&#xff0c;在 tree 视图显示搜索后的结果。发现搜索条件为已送货 > 已开票时&#xff0c;结果中会包含已送货已开票的。 解决 把这两个值打印出…

【ARM 调试】如何从 crash 信息找出问题原因

一、问题背景 粉丝在进行 ARM-A 系列软件编程时遇到以下问题&#xff0c;串口打印这段日志后就重启了&#xff0c;粉丝求助问是什么原因&#xff1f; Unhandled Exception in EL3. x30 0x0000000000b99b84 x0 0x00000000179a25b0 x1 …

【LeetCode】1572.矩阵对角线元素的和

题目 给你一个正方形矩阵 mat&#xff0c;请你返回矩阵对角线元素的和。 请你返回在矩阵主对角线上的元素和副对角线上且不在主对角线上元素的和。 示例 1&#xff1a; 输入&#xff1a;mat [[1,2,3],[4,5,6],[7,8,9]] 输出&#xff1a;25 解释&#xff1a;对角线的和为&a…

『向阳花赠书活动 | 第一期』《互联网广告系统:架构、算法与智能化》

大家好&#xff0c;我是向阳花&#xff0c;数据科学路上&#xff0c;与你同行。&#x1f680; 个人主页&#xff1a;向阳花个人主页 声明&#xff1a;赠书活动是博主与出版社达成合作&#xff0c;为粉丝专属福利&#xff0c;免费参与&#xff0c;非粉丝中奖不算&#xff01; 『…

Server - WandB 统计运行 Epoch 以及 手动上传日志

欢迎关注我的CSDN&#xff1a;https://spike.blog.csdn.net/ 本文地址&#xff1a;https://spike.blog.csdn.net/article/details/132227253 WandB (Weights & Biases) 是在线的模型训练可视化工具&#xff0c;可以帮助跟踪机器学习项目&#xff0c;记录运行中的超参数和输…

yum仓库简介

yum仓库网络服务 一.yum仓库简介二.yum配置文件2.1yum主配置文件2.2仓库设置2.3日志文件 三.yum命令详解3.1查询软件包命令3.2查询软件包组命令3.3yum安装升级3.4软件卸载 四.搭建yum仓库4.1 http方式搭建仓库 一.yum仓库简介 yum是基于RPM包构建的软件更新机制&#xff0c;能够…

从初学者到专家:Java运算符的完整指南

目录 1.算数运算符 2.增量运算符 2.1自增/自减运算符 4. 逻辑运算符 5.位运算符 6.移位运算符 7. 条件运算符 导言&#xff1a; Java作为一门广泛使用的编程语言&#xff0c;其运算符是编写代码时必不可少的一部分。本篇博客将为你详细介绍Java中的各种运算符&#xf…

【AndV】ant-design-vue中select使用mode=“combobox“无效:

文章目录 一、问题:二、解决: 一、问题: Warning: [antdv: Select] The combobox mode of Select is deprecated,it will be removed in next major version,please use AutoComplete instead 二、解决: 将mode"combobox"改为mode"SECRET_COMBOBOX_MODE_DO_NOT_…

Docker启动一个Centos镜像

搜索可用的centos的docker镜像 docker search <image>&#xff1a;在docker index中搜索imagedocker search centos 下载centos镜像&#xff08;拉取镜像&#xff09; docker pull centos:latest查看镜像docker images&#xff1a;列出imagesdocker images -a&#xff…