【AI】LoFTR图像匹配算法源码解析

news2025/6/7 23:44:33

0.LoFTR简介

Local Feature Transformers (LoFTR)是一种Detector-free的局部特征匹配方法,使用了具有自注意层和互注意层的Transformer模块来处理从卷积网络中提取的密集局部特征:首先在低特征分辨率(图像维度的1/8)上提取密集匹配,然后从这些匹配中选择具有高可信度的匹配,使用基于相关的方法将其细化到高分辨率的亚像素级别。这样,模型的大感受野使转换后的特征符能够体现出上下文和位置信息,通过多次自注意力和互注意层,LoFTR学习在GT中的匹配先验。另外,LOFTR还采用Linear Attention方法将计算复杂度降低到可接受的水平。

源码地址:https://github.com/zju3dv/LoFTR
论文下载地址:https://arxiv.org/pdf/2104.00680.pdf

在LoFTR之前的图像匹配算法中,都比较依赖于检测到的特征点,一旦点找不到,就没有办法完成匹配;对于位置不同的两个点,如果它们的背景特征相似或者说缺乏纹理特征,也会导致匹配失败。LoFTR的优势就是不需要先得到特征点,而且采用End2End的方式,用起来比较方便。
在这里插入图片描述

  1. 局部特征CNN从图像对中提取粗粒度特征图 F ~ A \tilde{F}^{A} F~A , F ~ B \tilde{F}^{B} F~B,以及细粒度特征图 F ^ A \hat{F}^{A} F^A , F ^ B \hat{F}^{B} F^B
  2. 粗粒度的特征经过flatten操作并添加位置编码,然后由LoFTR模块进行处理;该模块中包括自注意和互注意层,并重复
    Nc 次
  3. 使用可微匹配层来匹配LoFTR模块输出的特征 F ~ t r A \tilde{F}_{tr}^{A} F~trA , F ~ t r B \tilde{F}_{tr}^{B} F~trB,得到一个置信矩阵 P c P_c Pc ,然后根据置信阈值和最近邻算法(MNN)选择匹配对,得到粗粒度的匹配预测 M c M_c Mc
  4. 对于每个粗粒度匹配对 ,从细粒度级特征图中裁剪一个大小为w*w的局部窗口,粗粒度匹配将在此窗口内细化为亚像素级别,并作为最终的匹配预测 M f M_f Mf

1.源码解析

源码的结构层次比较鲜明,在模型文件loftr.py的forward函数中,可以清晰的看到整个模型的前向路径:

  1. Local Feature CNN
  2. coarse-level loftr module
  3. match coarse-level
  4. fine-level refinement
  5. match fine-level

基础特征提取模块Local Feature CNN

通过CNN提取特征图

# 1. Local Feature CNN
        data.update({
            'bs': data['image0'].size(0),
            'hw0_i': data['image0'].shape[2:], 'hw1_i': data['image1'].shape[2:]
        })

        if data['hw0_i'] == data['hw1_i']:  # faster & better BN convergence
            print(torch.cat([data['image0'], data['image1']], dim=0).shape)
            feats_c, feats_f = self.backbone(torch.cat([data['image0'], data['image1']], dim=0))
            print(feats_c.shape) # 1/8
            print(feats_f.shape) # 1/2
            (feat_c0, feat_c1), (feat_f0, feat_f1) = feats_c.split(data['bs']), feats_f.split(data['bs'])
            print(feat_c0.shape)
            print(feat_c1.shape)
            print(feat_f0.shape)
            print(feat_f1.shape)
        else:  # handle different input shapes
            (feat_c0, feat_f0), (feat_c1, feat_f1) = self.backbone(data['image0']), self.backbone(data['image1'])
            print(feat_c0.shape)
            print(feat_c1.shape)
            print(feat_f0.shape)
            print(feat_f1.shape)
        data.update({
            'hw0_c': feat_c0.shape[2:], 'hw1_c': feat_c1.shape[2:],
            'hw0_f': feat_f0.shape[2:], 'hw1_f': feat_f1.shape[2:]
        })

这里的backbone就是一系列的卷积和连接操作,可以跳入self.backbone中去:

    def forward(self, x):
        # ResNet Backbone
        x0 = self.relu(self.bn1(self.conv1(x)))
        x1 = self.layer1(x0)  # 1/2
        x2 = self.layer2(x1)  # 1/4
        x3 = self.layer3(x2)  # 1/8

        # FPN
        x3_out = self.layer3_outconv(x3)

        x3_out_2x = F.interpolate(x3_out, scale_factor=2., mode='bilinear', align_corners=True)
        x2_out = self.layer2_outconv(x2)
        x2_out = self.layer2_outconv2(x2_out+x3_out_2x)

        x2_out_2x = F.interpolate(x2_out, scale_factor=2., mode='bilinear', align_corners=True)
        x1_out = self.layer1_outconv(x1)
        x1_out = self.layer1_outconv2(x1_out+x2_out_2x)

        return [x3_out, x1_out]

注意力机制应用 coarse-level loftr module

        # 2. coarse-level loftr module
        # add featmap with positional encoding, then flatten it to sequence [N, HW, C]
        # 添加位置编码
        feat_c0 = rearrange(self.pos_encoding(feat_c0), 'n c h w -> n (h w) c')
        print(feat_c0.shape)
        feat_c1 = rearrange(self.pos_encoding(feat_c1), 'n c h w -> n (h w) c')
        print(feat_c1.shape)
        mask_c0 = mask_c1 = None  # mask is useful in training
        if 'mask0' in data:
            mask_c0, mask_c1 = data['mask0'].flatten(-2), data['mask1'].flatten(-2)
        # 进入transformer模块,这是论文的核心模块
        feat_c0, feat_c1 = self.loftr_coarse(feat_c0, feat_c1, mask_c0, mask_c1)
        print(feat_c0.shape)
        print(feat_c1.shape)

进入self.loftr_coarse模块,这里需要计算自身的attention注意力,还需要将两张图像计算cross attention,从代码中的循环中可以看到,self和cross两种操作分别是自己和自己计算注意力以及自己和其他特征图计算注意力,从layer的计算参数可以明确这一点。

    def forward(self, feat0, feat1, mask0=None, mask1=None):
        """
        Args:
            feat0 (torch.Tensor): [N, L, C]
            feat1 (torch.Tensor): [N, S, C]
            mask0 (torch.Tensor): [N, L] (optional)
            mask1 (torch.Tensor): [N, S] (optional)
        """

        assert self.d_model == feat0.size(2), "the feature number of src and transformer must be equal"

        for layer, name in zip(self.layers, self.layer_names):
            if name == 'self':
                feat0 = layer(feat0, feat0, mask0, mask0)
                print(feat0.shape)
                feat1 = layer(feat1, feat1, mask1, mask1)
                print(feat1.shape)
            elif name == 'cross':
                feat0 = layer(feat0, feat1, mask0, mask1)
                print(feat0.shape)
                feat1 = layer(feat1, feat0, mask1, mask0)
                print(feat1.shape)
            else:
                raise KeyError
        print(feat0.shape)
        print(feat1.shape)
        return feat0, feat1

具体的计算layer在LoFTREncoderLayer定义中,这里就是基本的attention计算方法,主要是QKV的计算和一些线性计算、连接操作。

    def forward(self, x, source, x_mask=None, source_mask=None):
        """
        Args:
            x (torch.Tensor): [N, L, C]
            source (torch.Tensor): [N, S, C]
            x_mask (torch.Tensor): [N, L] (optional)
            source_mask (torch.Tensor): [N, S] (optional)
        """
        bs = x.size(0)
        query, key, value = x, source, source

        # multi-head attention
        query = self.q_proj(query).view(bs, -1, self.nhead, self.dim)  # [N, L, (H, D)]
        key = self.k_proj(key).view(bs, -1, self.nhead, self.dim)  # [N, S, (H, D)]
        value = self.v_proj(value).view(bs, -1, self.nhead, self.dim)
        message = self.attention(query, key, value, q_mask=x_mask, kv_mask=source_mask)  # [N, L, (H, D)]
        message = self.merge(message.view(bs, -1, self.nhead*self.dim))  # [N, L, C]
        message = self.norm1(message)

        # feed-forward network
        message = self.mlp(torch.cat([x, message], dim=2))
        message = self.norm2(message)

        return x + message

粗粒度匹配模块 match coarse-level

        # 3. match coarse-level
        self.coarse_matching(feat_c0, feat_c1, data, mask_c0=mask_c0, mask_c1=mask_c1)

跟入coarse_matching模块
通过注意力机制获取到两个图像的特征图,进入到粗粒度匹配模块,粗粒度匹配是采用的内积的计算方式,即下面代码的torch.einsum计算,然后通过softmax转成概率值。

    def forward(self, feat_c0, feat_c1, data, mask_c0=None, mask_c1=None):
        """
        Args:
            feat0 (torch.Tensor): [N, L, C]
            feat1 (torch.Tensor): [N, S, C]
            data (dict)
            mask_c0 (torch.Tensor): [N, L] (optional)
            mask_c1 (torch.Tensor): [N, S] (optional)
        Update:
            data (dict): {
                'b_ids' (torch.Tensor): [M'],
                'i_ids' (torch.Tensor): [M'],
                'j_ids' (torch.Tensor): [M'],
                'gt_mask' (torch.Tensor): [M'],
                'mkpts0_c' (torch.Tensor): [M, 2],
                'mkpts1_c' (torch.Tensor): [M, 2],
                'mconf' (torch.Tensor): [M]}
            NOTE: M' != M during training.
        """
        N, L, S, C = feat_c0.size(0), feat_c0.size(1), feat_c1.size(1), feat_c0.size(2)

        # normalize
        feat_c0, feat_c1 = map(lambda feat: feat / feat.shape[-1]**.5,
                               [feat_c0, feat_c1])

        if self.match_type == 'dual_softmax':
            sim_matrix = torch.einsum("nlc,nsc->nls", feat_c0,
                                      feat_c1) / self.temperature
            print(sim_matrix.shape)
            if mask_c0 is not None:
                sim_matrix.masked_fill_(
                    ~(mask_c0[..., None] * mask_c1[:, None]).bool(),
                    -INF)
            conf_matrix = F.softmax(sim_matrix, 1) * F.softmax(sim_matrix, 2)
            print(conf_matrix.shape)

        elif self.match_type == 'sinkhorn':
            # sinkhorn, dustbin included
            sim_matrix = torch.einsum("nlc,nsc->nls", feat_c0, feat_c1)
            if mask_c0 is not None:
                sim_matrix[:, :L, :S].masked_fill_(
                    ~(mask_c0[..., None] * mask_c1[:, None]).bool(),
                    -INF)

            # build uniform prior & use sinkhorn
            log_assign_matrix = self.log_optimal_transport(
                sim_matrix, self.bin_score, self.skh_iters)
            assign_matrix = log_assign_matrix.exp()
            conf_matrix = assign_matrix[:, :-1, :-1]

            # filter prediction with dustbin score (only in evaluation mode)
            if not self.training and self.skh_prefilter:
                filter0 = (assign_matrix.max(dim=2)[1] == S)[:, :-1]  # [N, L]
                filter1 = (assign_matrix.max(dim=1)[1] == L)[:, :-1]  # [N, S]
                conf_matrix[filter0[..., None].repeat(1, 1, S)] = 0
                conf_matrix[filter1[:, None].repeat(1, L, 1)] = 0

            if self.config['sparse_spvs']:
                data.update({'conf_matrix_with_bin': assign_matrix.clone()})

        data.update({'conf_matrix': conf_matrix})

        # predict coarse matches from conf_matrix
        data.update(**self.get_coarse_match(conf_matrix, data))

跟入get_coarse_match模块
通过softmax获取到概率值之后,然后根据阈值使用mask矩阵过滤掉低于阈值的值,然后使用互最近邻来确定互相匹配关系。

    def get_coarse_match(self, conf_matrix, data):
        """
        Args:
            conf_matrix (torch.Tensor): [N, L, S]
            data (dict): with keys ['hw0_i', 'hw1_i', 'hw0_c', 'hw1_c']
        Returns:
            coarse_matches (dict): {
                'b_ids' (torch.Tensor): [M'],
                'i_ids' (torch.Tensor): [M'],
                'j_ids' (torch.Tensor): [M'],
                'gt_mask' (torch.Tensor): [M'],
                'm_bids' (torch.Tensor): [M],
                'mkpts0_c' (torch.Tensor): [M, 2],
                'mkpts1_c' (torch.Tensor): [M, 2],
                'mconf' (torch.Tensor): [M]}
        """
        axes_lengths = {
            'h0c': data['hw0_c'][0],
            'w0c': data['hw0_c'][1],
            'h1c': data['hw1_c'][0],
            'w1c': data['hw1_c'][1]
        }
        _device = conf_matrix.device
        # 1. confidence thresholding
        mask = conf_matrix > self.thr
        print(mask.shape)
        mask = rearrange(mask, 'b (h0c w0c) (h1c w1c) -> b h0c w0c h1c w1c',
                         **axes_lengths)
        print(mask.shape)
        if 'mask0' not in data:
            mask_border(mask, self.border_rm, False)
        else:
            mask_border_with_padding(mask, self.border_rm, False,
                                     data['mask0'], data['mask1'])
        mask = rearrange(mask, 'b h0c w0c h1c w1c -> b (h0c w0c) (h1c w1c)',
                         **axes_lengths)
        print(mask.shape)

        # 2. mutual nearest
        print(conf_matrix.max(dim=2, keepdim=True)[0].shape)
        mask = mask \
            * (conf_matrix == conf_matrix.max(dim=2, keepdim=True)[0]) \
            * (conf_matrix == conf_matrix.max(dim=1, keepdim=True)[0])
        print(mask.shape)
        # 3. find all valid coarse matches
        # this only works when at most one `True` in each row
        mask_v, all_j_ids = mask.max(dim=2)
        print(mask_v.shape)
        print(all_j_ids.shape)
        b_ids, i_ids = torch.where(mask_v)
        print(b_ids.shape)
        print(i_ids.shape)
        j_ids = all_j_ids[b_ids, i_ids]
        print(j_ids.shape)
        mconf = conf_matrix[b_ids, i_ids, j_ids]
        print(mconf.shape)

        # 4. Random sampling of training samples for fine-level LoFTR
        # (optional) pad samples with gt coarse-level matches
        if self.training:
            # NOTE:
            # The sampling is performed across all pairs in a batch without manually balancing
            # #samples for fine-level increases w.r.t. batch_size
            if 'mask0' not in data:
                num_candidates_max = mask.size(0) * max(
                    mask.size(1), mask.size(2))
            else:
                num_candidates_max = compute_max_candidates(
                    data['mask0'], data['mask1'])
            num_matches_train = int(num_candidates_max *
                                    self.train_coarse_percent)
            num_matches_pred = len(b_ids)
            assert self.train_pad_num_gt_min < num_matches_train, "min-num-gt-pad should be less than num-train-matches"

            # pred_indices is to select from prediction
            if num_matches_pred <= num_matches_train - self.train_pad_num_gt_min:
                pred_indices = torch.arange(num_matches_pred, device=_device)
            else:
                pred_indices = torch.randint(
                    num_matches_pred,
                    (num_matches_train - self.train_pad_num_gt_min, ),
                    device=_device)

            # gt_pad_indices is to select from gt padding. e.g. max(3787-4800, 200)
            gt_pad_indices = torch.randint(
                    len(data['spv_b_ids']),
                    (max(num_matches_train - num_matches_pred,
                        self.train_pad_num_gt_min), ),
                    device=_device)
            mconf_gt = torch.zeros(len(data['spv_b_ids']), device=_device)  # set conf of gt paddings to all zero

            b_ids, i_ids, j_ids, mconf = map(
                lambda x, y: torch.cat([x[pred_indices], y[gt_pad_indices]],
                                       dim=0),
                *zip([b_ids, data['spv_b_ids']], [i_ids, data['spv_i_ids']],
                     [j_ids, data['spv_j_ids']], [mconf, mconf_gt]))

        # These matches select patches that feed into fine-level network
        coarse_matches = {'b_ids': b_ids, 'i_ids': i_ids, 'j_ids': j_ids}

        # 4. Update with matches in original image resolution
        scale = data['hw0_i'][0] / data['hw0_c'][0]
        scale0 = scale * data['scale0'][b_ids] if 'scale0' in data else scale
        scale1 = scale * data['scale1'][b_ids] if 'scale1' in data else scale
        mkpts0_c = torch.stack(
            [i_ids % data['hw0_c'][1], i_ids // data['hw0_c'][1]],
            dim=1) * scale0
        mkpts1_c = torch.stack(
            [j_ids % data['hw1_c'][1], j_ids // data['hw1_c'][1]],
            dim=1) * scale1

        # These matches is the current prediction (for visualization)
        coarse_matches.update({
            'gt_mask': mconf == 0,
            'm_bids': b_ids[mconf != 0],  # mconf == 0 => gt matches
            'mkpts0_c': mkpts0_c[mconf != 0],
            'mkpts1_c': mkpts1_c[mconf != 0],
            'mconf': mconf[mconf != 0]
        })

        return coarse_matches

精细化调整 fine-level refinement

将特征图级别的匹配转换成像素点级别的匹配

        # 4. fine-level refinement
        # 预处理操作,拆解特征图,提取匹配到的候选点
        feat_f0_unfold, feat_f1_unfold = self.fine_preprocess(feat_f0, feat_f1, feat_c0, feat_c1, data)
        
        if feat_f0_unfold.size(0) != 0:  # at least one coarse level predicted
            feat_f0_unfold, feat_f1_unfold = self.loftr_fine(feat_f0_unfold, feat_f1_unfold)

这里self.loftr_fine也是进入到attention的计算,与粗粒度的计算时用的相同的算法

细粒度匹配 match fine-level

通过期望计算得到最终的输出结果

        # 5. match fine-level
        self.fine_matching(feat_f0_unfold, feat_f1_unfold, data)

跳入FineMatching的forward函数中
原文中使用55的矩阵进行细粒度匹配,这里匹配的时候会计算55矩阵中各个点与中间点的关系,生成热度图heatmap,然后计算这个热度图的期望dsnt.spatial_expectation2d,然后还原到图像中即可。

    def forward(self, feat_f0, feat_f1, data):
        """
        Args:
            feat0 (torch.Tensor): [M, WW, C]
            feat1 (torch.Tensor): [M, WW, C]
            data (dict)
        Update:
            data (dict):{
                'expec_f' (torch.Tensor): [M, 3],
                'mkpts0_f' (torch.Tensor): [M, 2],
                'mkpts1_f' (torch.Tensor): [M, 2]}
        """
        M, WW, C = feat_f0.shape
        W = int(math.sqrt(WW))
        scale = data['hw0_i'][0] / data['hw0_f'][0]
        self.M, self.W, self.WW, self.C, self.scale = M, W, WW, C, scale

        # corner case: if no coarse matches found
        if M == 0:
            assert self.training == False, "M is always >0, when training, see coarse_matching.py"
            # logger.warning('No matches found in coarse-level.')
            data.update({
                'expec_f': torch.empty(0, 3, device=feat_f0.device),
                'mkpts0_f': data['mkpts0_c'],
                'mkpts1_f': data['mkpts1_c'],
            })
            return

        feat_f0_picked = feat_f0_picked = feat_f0[:, WW//2, :]
        print(feat_f0_picked.shape)
        sim_matrix = torch.einsum('mc,mrc->mr', feat_f0_picked, feat_f1)
        print(sim_matrix.shape)
        softmax_temp = 1. / C**.5
        heatmap = torch.softmax(softmax_temp * sim_matrix, dim=1).view(-1, W, W)
        print(heatmap.shape)

        # compute coordinates from heatmap
        coords_normalized = dsnt.spatial_expectation2d(heatmap[None], True)[0]  # [M, 2]
        print(coords_normalized.shape)
        grid_normalized = create_meshgrid(W, W, True, heatmap.device).reshape(1, -1, 2)  # [1, WW, 2]
        print(grid_normalized.shape)
        # compute std over <x, y>
        var = torch.sum(grid_normalized**2 * heatmap.view(-1, WW, 1), dim=1) - coords_normalized**2  # [M, 2]
        std = torch.sum(torch.sqrt(torch.clamp(var, min=1e-10)), -1)  # [M]  clamp needed for numerical stability
        
        # for fine-level supervision
        data.update({'expec_f': torch.cat([coords_normalized, std.unsqueeze(1)], -1)})

        # compute absolute kpt coords
        self.get_fine_match(coords_normalized, data)

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

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

相关文章

mysql的读写分离

MySQL 读写分离原理 读写分离就是只在主服务器上写&#xff0c;只在从服务器上读。 主数据库处理事务性操作&#xff0c;而从数据库处理 select 查询。 数据库复制被用来把主数据库上事务性操作导致的变更同步到集群中的从数据库。 常见的mysql读写分离分为以下两种 1&…

网络安全—模拟IP代理隐藏身份

文章目录 网络拓扑安装使用代理服务器设置隐藏者设置 使用古老的ccproxy实现代理服务器&#xff0c;仅做实验用途&#xff0c;禁止做违法犯罪的事情&#xff0c;后果自负。 网络拓扑 均使用Windows Server 2003系统 Router 外网IP&#xff1a;使用NAT模式 IP DHCP自动分配或者…

【损失函数】Hinge Loss 合页损失

1、介绍 Hinge Loss&#xff08;合页损失&#xff09;通常用于支持向量机&#xff08;Support Vector Machine&#xff0c;SVM&#xff09;等模型中&#xff0c;特别是在二分类问题中。它的目标是使正确类别的分数与错误类别的最高分之间的差异达到一个固定的边界&#xff0c;从…

【代码随想录】刷题笔记Day45

前言 早上又赖了会床......早睡早起是奢望了现在&#xff0c;新一年不能这样&#xff01;支棱起来&#xff01; 377. 组合总和 Ⅳ - 力扣&#xff08;LeetCode&#xff09; 这一题用的就是完全背包排列数的遍历顺序&#xff1a;先背包再物品&#xff0c;从前往后求的也是有几…

RK3568驱动指南|第九篇 设备模型-第106章 为什么注册总线之前要先注册设备实例分析实验

瑞芯微RK3568芯片是一款定位中高端的通用型SOC&#xff0c;采用22nm制程工艺&#xff0c;搭载一颗四核Cortex-A55处理器和Mali G52 2EE 图形处理器。RK3568 支持4K 解码和 1080P 编码&#xff0c;支持SATA/PCIE/USB3.0 外围接口。RK3568内置独立NPU&#xff0c;可用于轻量级人工…

OpenAI API/Plus会员信用卡绑定付款方式经历

前言 9月25日起ChatGPT风控升级&#xff0c;428813的卡只可以支付Open ai API-key&#xff0c;直接订阅plus会被拒。部分用户将卡绑定美区Google pay使用app store进行订阅可以成功&#xff0c;如果您没有这两种支付方式&#xff0c;请您重新开一张534786的万事达美卡升级订阅…

专题一:递推与递归

递归 例题 递归实现指数型枚举 从 1∼n这 n个整数中随机选取任意多个&#xff0c;输出所有可能的选择方案。 输入格式 输入一个整数 n。 输出格式 每行输出一种方案。 同一行内的数必须升序排列&#xff0c;相邻两个数用恰好 1 个空格隔开。 对于没有选任何数的方案&#xff0c…

【ModelScope】部署一个属于自己的AI服务

前言 技术栈是Fastapi。 FastAPI 是一个现代、快速&#xff08;基于 Starlette 和 Pydantic&#xff09;、易于使用的 Python web 框架&#xff0c;主要用于构建 RESTful API。以下是 FastAPI 的一些优势&#xff1a; 性能卓越&#xff1a; FastAPI 基于 Starlette 框架&…

GC4938 三相无刷直流电机预驱动芯片可替代A4938/allegro,外置MOS,具有低速降噪,宽电压工作的功能

GC4938 是一款三相无刷直流电机预驱 动芯片。芯片可以驱动 N 型功率 MOSFET&#xff0c; 最高电源到 36V。芯片状态切换逻辑受三个 相位差为 120的霍尔输入确定。 芯片集成固定衰减时间的脉冲调制来控 制电流并且抑制励磁涌流&#xff1b;堵转保护时间可 调&#xff1b;过温保护…

AIOps探索 | 基于大模型构建高效的运维知识及智能问答平台(2)案例分享

原作者&#xff1a;擎创科技产品专家 布博士 案例分享 所需要的软件列表 本次案例的实现&#xff0c;全部采用开源或SAAS的产品来提供&#xff0c;并不涉及到私有化部署的软件产品。软件列表如下所示&#xff0c;如何申请apikey请自行研究&#xff0c;在这里不再详细说明&…

Linux第3步_安装Ubuntu操作系统

创建好虚拟机后&#xff0c;就可以安装Ubuntu操作系统了。 1、双击“VMware Workstation Pro”&#xff0c;得到下面的界面。 2、点击“编辑虚拟机设置”&#xff0c;见下图&#xff1a; 3、等几秒钟&#xff0c;得到下面的界面&#xff1a; 4、点击“CD/DVD”&#xff0c;得到…

opencv入门到精通——Canny边缘检测

目录 理论 OpenCV中的Canny Edge检测 附加资源 在本章中&#xff0c;我们将学习 Canny边缘检测的概念 OpenCV函数: cv.Canny() 理论 Canny Edge Detection是一种流行的边缘检测算法。它由John F. Canny发明 1.这是一个多阶段算法&#xff0c;我们将经历每个阶段。 2.降…

redis重启后数据丢失问题解决(亲测好用)

redis修改密码重启后发现redis中的数据丢失了 解决办法&#xff1a; 首先在redis的安装目录下查找重启之前的dump.rdb文件&#xff0c;发现只有当天的一个dump.rdb文件&#xff0c;确认不是重启备份的文件 然后我就全盘找一下dump.rdb的备份文件&#xff0c;找到前一天的备份…

鸿蒙应用中图片的显示(Image组件)

目录 1、加载图片资源 1.1、存档图类型数据源 a.本地资源 b.网络资源 c.Resource资源 d.媒体库file://data/storage e.base64 1.2、多媒体像素图片 2、显示矢量图 3、添加属性 3.1、设置图片缩放类型 3.2、设置图片重复样式 3.3、设置图片渲染模式 3.4、设置图…

工具篇--Spring-Cloud--feign 通过feign 接口完成文件的下载

文章目录 前言一、feign接口获取文件流程&#xff1a;二、文件获取实现2.1 引入jar&#xff1a;2.2 实现&#xff1a; 总结 前言 通常在spring-boot 项目中&#xff0c;对于文件的下载都是直接调用到对应的服务中&#xff0c;而不是通过feign 接口获取文件&#xff1b;有时我们…

珠海盈致浅析MES生产管理系统的优点

MES系统是用于管理和控制制造过程的信息化系统.它具有许多优点&#xff1a; 1. 生产过程可视化:MES系统提供实时的生产过程监控和数据收集,使管理人员能够清晰地了解生产线上的各个环节,包括设备状态、生产进度、质量指标等.这使得管理人员能够作出准确的决策并快速响应变化. 2…

APPnium 自动化实践 :第一步adb 连接手机

1. 下载安装 adb ,添加到环境变量。 ADB Download - Get the latest version of ADB and fastboot 2. 手机开启开发者模式 https://developer.huawei.com/consumer/cn/doc/quickApp-Guides/quickapp-open-developer-option-0000001137005543 3. adb 连接设备 【And…

【C语言】字符串 和 ctype.h 中的函数 练习

前面总结了有关字符串和ctype.h的文章&#xff0c;接下来就以几个例子来练习一下&#xff0c;以巩固之前的基础概念。注意&#xff1a;以下示例都有更简单更高效的解决方法&#xff0c;但本次仅以巩固基础为目的&#xff0c;所以方法可能稍作繁琐 Leetcode 344.反转字符串 编…

HUAWEI WATCH 系列 eSIM 全新开通指南来了

HUAWEI WATCH 系列手表提供了eSIM硬件能力&#xff0c;致力为用户提供更便捷、高效的通信体验。但eSIM 业务是由运营商管理并提供服务的&#xff0c;当前运营商eSIM业务集中全面恢复&#xff0c;电信已经全面恢复&#xff0c;移动大部分省份已经全面放开和多号App开通方式&…

自由DIY预约小程序源码系统:适用于任何行业+自由DIY你的界面布局+全新升级的UI+多用户系统 带安装部署教程

随着移动互联网的普及&#xff0c;预约服务逐渐成为人们日常生活的一部分。从家政服务、医疗挂号到汽车保养&#xff0c;预约已经渗透到各个行业。然而&#xff0c;市面上的预约小程序大多功能单一&#xff0c;界面老旧&#xff0c;无法满足商家和用户的个性化需求。今天来给大…