【图像配准】多图配准/不同特征提取算法/匹配器比较测试

news2024/11/27 1:42:39

前言

本文首先完成之前专栏前置博文未完成的多图配准拼接任务,其次对不同特征提取器/匹配器效率进行进一步实验探究。

各类算法原理简述

看到有博文[1]指出,在速度方面SIFT<SURF<BRISK<FREAK<ORB,在对有较大模糊的图像配准时,BRISK算法在其中表现最为出色,后面考虑选取其中SIFT、BRISK、ORB三种算法进行验证。
在此之前,先对后续算法的原理做一些初步了解。

SIFT算法

在前文【图像配准】SIFT算法原理及二图配准拼接已经对此做过分析,这里不作赘述。

BRISK算法

BRISK算法是2011年ICCV上《BRISK:Binary Robust Invariant Scalable Keypoints》文章中,提出来的一种特征提取算法。

BRISK算法通过利用简单的像素灰度值比较,进而得到一个级联的二进制比特串来描述每个特征点,之后采用了邻域采样模式,即以特征点为圆心,构建多个不同半径的离散化Bresenham同心圆,然后再每一个同心圆上获得具有相同间距的N个采样点。

在这里插入图片描述
更详细的内容可参考文献[3]对论文的解读。

ORB算法

ORB(Oriented FAST and rotated BRIEF)是OpenCV实验室开发的一种特征检测与特征描述算法,将 FAST 特征检测与 BRIEF 特征描述结合并进行了改进,具有尺度不变性和旋转不变性,对噪声有较强的抗干扰能力[4]。

ORB算法在图像金字塔中使用FAST算法检测关键点,通过一阶矩计算关键点的方向,使用方向校正的BRIEF生成特征描述符。

更详细的内容可参考文献[4]。

AKAZE算法

Alcantarilla等人提出了AKAZE(Accelerated-KAZE)算法,即加速KAZE算法,加速了非线性尺度空间的构造,效率较KAZE有所提升,以各向异性的非线性滤波来构造尺度空间,将整个尺度空间进行分割,利用局部自适应分级获得细节和噪声,保留较多的边缘细节信息,但该算法关键点检测能力不足,且鲁棒性不强[5]。

多图配准

无论何种算法,图像配准无非是这样几个步骤->图像灰度化->提取特征->构建匹配器->计算变换矩阵->图像合并。
那么多图配准,实际上可以分解为多个双图配准。
以下代码主要参考了这个仓库:https://github.com/799034552/concat_pic

下面按处理顺序对各部分内容进行分块拆解:

图像读取

首先是读取图像再进行灰度化转换。
这里进行了一个判断,判断传入的是否是图像的文本路径,这一步主要是为了后面多图拼接的便利性,因为后面多图拼接会把拼接好的部分图像直接放在内存中,这里若不是路径,就直接赋值给变量,相当于用整张大图去和另外一张小图去做拼接。

# 读取图像-转换灰度图用于检测
# 这里做一个文本判断是为了后面多图拼接处理
if isinstance(path2, str):
    imageA = cv2.imread(path2)
else:
    imageA = path2
if isinstance(path1, str):
    imageB = cv2.imread(path1)
else:
    imageB = path1
imageA_gray = cv2.cvtColor(imageA, cv2.COLOR_BGR2GRAY)
imageB_gray = cv2.cvtColor(imageB, cv2.COLOR_BGR2GRAY)

构建特征提取器

OpenCV对各种算法都进行了较好的封装,这里主要对比测试了sift,brisk,orb,akaze这几种算法,所用opencv-python版本为4.7.0,值得注意的是,OpenCV4以后的版本,cv2.SURF_create()无法使用,只能用老版本的cv2.xfeatures2d.SURF_create()来实现SURF,因此这里没有对SURF算法进行比较测试。

# 选择特征提取器函数
def detectAndDescribe(image, method=None):
    if method == 'sift':
        descriptor = cv2.SIFT_create()
    elif method == 'surf':
        descriptor = cv2.xfeatures2d.SURF_create()  # OpenCV4以上不可用
    elif method == 'brisk':
        descriptor = cv2.BRISK_create()
    elif method == 'orb':
        descriptor = cv2.ORB_create()
    elif method == 'akaze':
        descriptor = cv2.AKAZE_create()
    (kps, features) = descriptor.detectAndCompute(image, None)
    return kps, features

提取特征/特征匹配

# 提取两张图片的特征
kpsA, featuresA = detectAndDescribe(imageA_gray, method=feature_extractor)
kpsB, featuresB = detectAndDescribe(imageB_gray, method=feature_extractor)
# 进行特征匹配
if feature_matching == 'bf':
    matches = matchKeyPointsBF(featuresA, featuresB, method=feature_extractor)
elif feature_matching == 'knn':
    matches = matchKeyPointsKNN(featuresA, featuresB, ratio=0.75, method=feature_extractor)
    if len(matches) < 10:
        return None, None

这里比较了两种匹配器,一种是暴力匹配器(BFMatcher),函数接口为cv2.BFMatcher,主要有下面两个参数可以设置:

  • normType:距离类型,可选项,默认为欧式距离NORM_L2。
    • NORM_L1:L1范数,曼哈顿距离。
    • NORM_L2:L2范数,欧式距离。
    • NORM_HAMMING:汉明距离。
    • NORM_HAMMING2:汉明距离2,对每2个比特相加处理。
  • crossCheck:交叉匹配选项,可选项,默认为False,若为True,即两张图像中的特征点必须互相都是唯一选择

注:对于SIFT、SURF描述符,推荐选择欧氏距离L1和L2范数;对于ORB、BRISK、BRIEF描述符,推荐选择汉明距离NORM_HAMMING;对于ORB描述符,当WTA_K=3或4时,推荐使用汉明距离NORM_HAMMING2。

对于该函数更详细的内容,可参考博文[6]。

另一个是FLANN匹配器,Flann-based matcher 使用快速近似最近邻搜索算法寻找,FlannBasedMatcher接受两个参数:index_params和search_params:

  • index_params:可用不同的数值表示不同的算法,有下表这些可选项(表中数据来源文章[7])

在这里插入图片描述

  • search_params(int checks=32, float eps=0, bool sorted=true)
    checks为int类型,是遍历次数,一般只改变这个参数
# 创建匹配器
def createMatcher(method, crossCheck):
    """
    不同的方法创建不同的匹配器参数,参数释义
        BFMatcher:暴力匹配器
        NORM_L2-欧式距离
        NORM_HAMMING-汉明距离
        crossCheck-若为True,即两张图像中的特征点必须互相都是唯一选择
    """
    if method == 'sift' or method == 'surf':
        bf = cv2.BFMatcher(cv2.NORM_L2, crossCheck=crossCheck)
    elif method == 'orb' or method == 'brisk' or method == 'akaze':
        # 创建BF匹配器
        # bf = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=crossCheck)
        index_params = dict(algorithm=1, trees=5)
        search_params = dict(checks=50)
        # 创建Flann匹配器
        bf = cv2.FlannBasedMatcher(index_params, search_params)
    return bf

二者的区别在于BFMatcher总是尝试所有可能的匹配,从而使得它总能够找到最佳匹配,这也是Brute Force(暴力法)的原始含义。
而FlannBasedMatcher中FLANN的含义是Fast Library forApproximate Nearest Neighbors,从字面意思可知它是一种近似法,算法更快但是找到的是最近邻近似匹配,所以当我们需要找到一个相对好的匹配但是不需要最佳匹配的时候往往使用FlannBasedMatcher。当然也可以通过调整FlannBasedMatcher的参数来提高匹配的精度或者提高算法速度,但是相应地算法速度或者算法精度会受到影响[8]。

特征匹配也有两种方式,可以直接进行暴力检测,也可以采用KNN进行检测,不同检测方式的代码如下:

# 暴力检测函数
def matchKeyPointsBF(featuresA, featuresB, method):
    start_time = time.time()
    bf = createMatcher(method, crossCheck=True)
    best_matches = bf.match(featuresA, featuresB)
    rawMatches = sorted(best_matches, key=lambda x: x.distance)
    print("Raw matches (Brute force):", len(rawMatches))
    end_time = time.time()
    print("暴力检测共耗时" + str(end_time - start_time))
    return rawMatches


# 使用knn检测函数
def matchKeyPointsKNN(featuresA, featuresB, ratio, method):
    start_time = time.time()
    bf = createMatcher(method, crossCheck=False)
    # rawMatches = bf.knnMatch(featuresA, featuresB, k=2)
    # 上面这行在用Flann时会报错
    rawMatches = bf.knnMatch(np.asarray(featuresA, np.float32), np.asarray(featuresB, np.float32), k=2)
    matches = []
    for m, n in rawMatches:
        if m.distance < n.distance * ratio:
            matches.append(m)
    print(f"knn匹配的特征点数量:{len(matches)}")
    end_time = time.time()
    print("KNN检测共耗时" + str(end_time - start_time))
    return matches

计算视角变换矩阵/透视变换

匹配完关键点后,就可以计算视角变换矩阵,然后一幅图不动,另一幅图进行透视变换,这里的具体方式和前文较为类似。

# 计算视角变换矩阵
def getHomography(kpsA, kpsB, matches, reprojThresh):
    start_time = time.time()
    # 将各关键点保存为Array
    kpsA = np.float32([kp.pt for kp in kpsA])
    kpsB = np.float32([kp.pt for kp in kpsB])
    # 如果匹配点大于四个点,再进行计算
    if len(matches) > 4:
        # 构建出匹配的特征点Array
        ptsA = np.float32([kpsA[m.queryIdx] for m in matches])
        ptsB = np.float32([kpsB[m.trainIdx] for m in matches])

        # 计算视角变换矩阵
        (H, status) = cv2.findHomography(ptsA, ptsB, cv2.RANSAC, reprojThresh)

        end_time = time.time()
        print("透视关系计算共耗时" + str(end_time - start_time))

        return matches, H, status
    else:
        return None

M = getHomography(kpsA, kpsB, matches, reprojThresh=4)
   if M is None:
       print("Error!")
   (matches, H, status) = M
   # 将图片A进行透视变换
   result = cv2.warpPerspective(imageA, H, ((imageA.shape[1] + imageB.shape[1]) * 2, (imageA.shape[0] + imageB.shape[0]) * 2))
   resultAfterCut = cutBlack(result)

图片黑边裁剪

在做透视变换时,往往会采取一个比较大的背景,以确保图片能够不遗漏的拼接上去,比如这里图片的尺寸设定为(imageA.shape[1] + imageB.shape[1]) * 2, (imageA.shape[0] + imageB.shape[0]) * 2),这样会产生一些背景黑边,需要进行裁切。

之前的文章提到过一种通过膨胀方式来找到最大内接矩形,这里的代码处理方式更为巧妙,直接采用像素点搜索的方式,找到图像的最大外接矩形。

# 去除图像黑边
def cutBlack(pic):
    rows, cols = np.where(pic[:, :, 0] != 0)
    min_row, max_row = min(rows), max(rows) + 1
    min_col, max_col = min(cols), max(cols) + 1
    return pic[min_row:max_row, min_col:max_col, :]

图片位置检查

由于无法提前知道两张图片的位置关系,对于透视变换,可能图片会映射到整个选取区域的左边,这样的话,无法正常显示图片,因此,要对透视变换后的图片进行面积检查,如果比原来的图片面积小太多,就用另一张图片来进行透视变换[9]。

if np.size(resultAfterCut) < np.size(imageA) * 0.95:
    print("图片位置不对,将自动调换")
    # 调换图片
    kpsA, kpsB = swap(kpsA, kpsB)
    imageA, imageB = swap(imageA, imageB)
    if feature_matching == 'bf':
        matches = matchKeyPointsBF(featuresB, featuresA, method=feature_extractor)
    elif feature_matching == 'knn':
        matches = matchKeyPointsKNN(featuresB, featuresA, ratio=0.75, method=feature_extractor)
        if len(matches) < 10:
            return None, None
    matchCount = len(matches)
    M = getHomography(kpsA, kpsB, matches, reprojThresh=4)
    if M is None:
        print("Error!")
    (matches, H, status) = M
    result = cv2.warpPerspective(imageA, H,
                                 ((imageA.shape[1] + imageB.shape[1]) * 2, (imageA.shape[0] + imageB.shape[0]) * 2))

图像融合

图像融合这里处理得也比较巧妙,对图片接壤部分选取最大值,这样确保了色调的统一性。

# 合并图片-相同的区域选取最大值,从而实现融合
    result[0:imageB.shape[0], 0:imageB.shape[1]] = np.maximum(imageB, result[0:imageB.shape[0], 0:imageB.shape[1]])
    result = cutBlack(result)  # 结果去除黑边

多图拼接

最后是拼接多幅图像,反复调用拼接双图即可。

#  合并多张图
def handleMulti(*args):
    l = len(args)
    assert (l > 1)
    # isHandle用于标记图片是否参与合并
    isHandle = [0 for i in range(l - 1)]
    nowPic = args[0]
    args = args[1:]
    for j in range(l - 1):
        isHas = False  # 在一轮中是否找到
        matchCountList = []
        resultList = []
        indexList = []
        for i in range(l - 1):
            if isHandle[i] == 1:
                continue
            result, matchCount = handle(nowPic, args[i])
            if not result is None:
                matchCountList.append(matchCount)  # matchCountList存储两图匹配的特征点
                resultList.append(result)
                indexList.append(i)
                isHas = True
        if not isHas:  # 一轮找完都没有可以合并的
            return None
        else:
            index = matchCountList.index(max(matchCountList))
            nowPic = resultList[index]
            isHandle[indexList[index]] = 1
            print(f"合并第{indexList[index] + 2}个")
    return nowPic

完整代码

utils.py

import cv2
import numpy as np
import time


# 选择特征提取器函数
def detectAndDescribe(image, method=None):
    if method == 'sift':
        descriptor = cv2.SIFT_create()
    elif method == 'surf':
        descriptor = cv2.xfeatures2d.SURF_create()  # OpenCV4以上不可用
    elif method == 'brisk':
        descriptor = cv2.BRISK_create()
    elif method == 'orb':
        descriptor = cv2.ORB_create()
    elif method == 'akaze':
        descriptor = cv2.AKAZE_create()
    (kps, features) = descriptor.detectAndCompute(image, None)
    return kps, features


# 创建匹配器
def createMatcher(method, crossCheck):
    """
    不同的方法创建不同的匹配器参数,参数释义
        BFMatcher:暴力匹配器
        NORM_L2-欧式距离
        NORM_HAMMING-汉明距离
        crossCheck-若为True,即两张图像中的特征点必须互相都是唯一选择
    """
    if method == 'sift' or method == 'surf':
        bf = cv2.BFMatcher(cv2.NORM_L2, crossCheck=crossCheck)
    elif method == 'orb' or method == 'brisk' or method == 'akaze':
        # 创建BF匹配器
        # bf = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=crossCheck)
        index_params = dict(algorithm=1, trees=5)
        search_params = dict(checks=50)
        # 创建Flann匹配器
        bf = cv2.FlannBasedMatcher(index_params, search_params)
    return bf


# 暴力检测函数
def matchKeyPointsBF(featuresA, featuresB, method):
    start_time = time.time()
    bf = createMatcher(method, crossCheck=True)
    best_matches = bf.match(featuresA, featuresB)
    rawMatches = sorted(best_matches, key=lambda x: x.distance)
    print("Raw matches (Brute force):", len(rawMatches))
    end_time = time.time()
    print("暴力检测共耗时" + str(end_time - start_time))
    return rawMatches


# 使用knn检测函数
def matchKeyPointsKNN(featuresA, featuresB, ratio, method):
    start_time = time.time()
    bf = createMatcher(method, crossCheck=False)
    # rawMatches = bf.knnMatch(featuresA, featuresB, k=2)
    # 上面这行在用Flann时会报错
    rawMatches = bf.knnMatch(np.asarray(featuresA, np.float32), np.asarray(featuresB, np.float32), k=2)
    matches = []
    for m, n in rawMatches:
        if m.distance < n.distance * ratio:
            matches.append(m)
    print(f"knn匹配的特征点数量:{len(matches)}")
    end_time = time.time()
    print("KNN检测共耗时" + str(end_time - start_time))
    return matches


# 计算视角变换矩阵
def getHomography(kpsA, kpsB, matches, reprojThresh):
    start_time = time.time()
    # 将各关键点保存为Array
    kpsA = np.float32([kp.pt for kp in kpsA])
    kpsB = np.float32([kp.pt for kp in kpsB])
    # 如果匹配点大于四个点,再进行计算
    if len(matches) > 4:
        # 构建出匹配的特征点Array
        ptsA = np.float32([kpsA[m.queryIdx] for m in matches])
        ptsB = np.float32([kpsB[m.trainIdx] for m in matches])

        # 计算视角变换矩阵
        (H, status) = cv2.findHomography(ptsA, ptsB, cv2.RANSAC, reprojThresh)

        end_time = time.time()
        print("透视关系计算共耗时" + str(end_time - start_time))

        return matches, H, status
    else:
        return None


# 去除图像黑边
def cutBlack(pic):
    rows, cols = np.where(pic[:, :, 0] != 0)
    min_row, max_row = min(rows), max(rows) + 1
    min_col, max_col = min(cols), max(cols) + 1
    return pic[min_row:max_row, min_col:max_col, :]


# 交换
def swap(a, b):
    return b, a

main.py

from utils import *


# 合并两张图(合并多张图基于此函数)
def handle(path1, path2):
    # 超参数-选择具体算法
    feature_extractor = 'brisk'
    feature_matching = 'knn'
    # 读取图像-转换灰度图用于检测
    # 这里做一个文本判断是为了后面多图拼接处理
    if isinstance(path2, str):
        imageA = cv2.imread(path2)
    else:
        imageA = path2
    if isinstance(path1, str):
        imageB = cv2.imread(path1)
    else:
        imageB = path1
    imageA_gray = cv2.cvtColor(imageA, cv2.COLOR_BGR2GRAY)
    imageB_gray = cv2.cvtColor(imageB, cv2.COLOR_BGR2GRAY)
    # 提取两张图片的特征
    kpsA, featuresA = detectAndDescribe(imageA_gray, method=feature_extractor)
    kpsB, featuresB = detectAndDescribe(imageB_gray, method=feature_extractor)
    # 进行特征匹配
    if feature_matching == 'bf':
        matches = matchKeyPointsBF(featuresA, featuresB, method=feature_extractor)
    elif feature_matching == 'knn':
        matches = matchKeyPointsKNN(featuresA, featuresB, ratio=0.75, method=feature_extractor)
        if len(matches) < 10:
            return None, None
    # 计算视角变换矩阵
    matchCount = len(matches)
    M = getHomography(kpsA, kpsB, matches, reprojThresh=4)
    if M is None:
        print("Error!")
    (matches, H, status) = M
    # 将图片A进行透视变换
    result = cv2.warpPerspective(imageA, H, ((imageA.shape[1] + imageB.shape[1]) * 2, (imageA.shape[0] + imageB.shape[0]) * 2))
    resultAfterCut = cutBlack(result)
    # 查看裁剪完黑边后的图片
    # cv2.imshow("resultAfterCut", resultAfterCut)
    # cv2.waitKey(0)
    if np.size(resultAfterCut) < np.size(imageA) * 0.95:
        print("图片位置不对,将自动调换")
        # 调换图片
        kpsA, kpsB = swap(kpsA, kpsB)
        imageA, imageB = swap(imageA, imageB)
        if feature_matching == 'bf':
            matches = matchKeyPointsBF(featuresB, featuresA, method=feature_extractor)
        elif feature_matching == 'knn':
            matches = matchKeyPointsKNN(featuresB, featuresA, ratio=0.75, method=feature_extractor)
            if len(matches) < 10:
                return None, None
        matchCount = len(matches)
        M = getHomography(kpsA, kpsB, matches, reprojThresh=4)
        if M is None:
            print("Error!")
        (matches, H, status) = M
        result = cv2.warpPerspective(imageA, H,
                                     ((imageA.shape[1] + imageB.shape[1]) * 2, (imageA.shape[0] + imageB.shape[0]) * 2))
    # 合并图片-相同的区域选取最大值,从而实现融合
    result[0:imageB.shape[0], 0:imageB.shape[1]] = np.maximum(imageB, result[0:imageB.shape[0], 0:imageB.shape[1]])
    result = cutBlack(result)  # 结果去除黑边
    return result, matchCount


#  合并多张图
def handleMulti(*args):
    l = len(args)
    assert (l > 1)
    # isHandle用于标记图片是否参与合并
    isHandle = [0 for i in range(l - 1)]
    nowPic = args[0]
    args = args[1:]
    for j in range(l - 1):
        isHas = False  # 在一轮中是否找到
        matchCountList = []
        resultList = []
        indexList = []
        for i in range(l - 1):
            if isHandle[i] == 1:
                continue
            result, matchCount = handle(nowPic, args[i])
            if not result is None:
                matchCountList.append(matchCount)  # matchCountList存储两图匹配的特征点
                resultList.append(result)
                indexList.append(i)
                isHas = True
        if not isHas:  # 一轮找完都没有可以合并的
            return None
        else:
            index = matchCountList.index(max(matchCountList))
            nowPic = resultList[index]
            isHandle[indexList[index]] = 1
            print(f"合并第{indexList[index] + 2}个")
    return nowPic


if __name__ == "__main__":
    start_time_all = time.time()
    # 传入图片路径列表,既可以处理两张,也可以处理多张
    result = handleMulti("./input/foto2B.jpg", "./input/foto2A.jpg")
    if not result is None:
        cv2.imwrite("output.tif", result[:, :, [0, 1, 2]])
    else:
        print("没有找到对应特征点,无法合并")
    end_time_all = time.time()
    print("共耗时" + str(end_time_all - start_time_all))

实验结果

拿原仓库中的两张无人机图片进行了测试,拼接效果如下:

两张原图:

在这里插入图片描述

拼接后的图像:

在这里插入图片描述

此外,我选取了更大分辨率(4k x 7k)的图像进行拼接测试,比较不同算法的所用时间,结果如下表所示:

特征提取算法匹配器特征点个数时间(s)
siftbf14438463
briskbf964831.83
orbbf10920.57
akazebf487226.58
briskflann500024.71
orbflann5022.02

结论

经过此番实验,可以发现:

  1. 从速度上来说orb算法是最快的,比sift这种古老的算法快了一个数量级。但是通过观察生成的图像质量会发现,orb的图像会比较模糊,拼接质量不如其它算法高,增加速度的同时会牺牲部分质量。
  2. akaze算法速度和质量和brisk相差不大
  3. flann匹配器比bf匹配器通常情况下速度更快

因此,后续实验可以首选brisk算法+flann匹配器的组合方式。
另外说明,上面这些实验参数并没有针对性的进行调参,基本使用默认参数;若进行调优,可能会结果会发生一定变化。

Todo

  • 此示例中,默认图像位置是未知的,而在遥感图像中,可以通过gps坐标来确定图像的大致方位,后续考虑引进gps坐标,构建图像排布坐标系,从而加快配准速度。
  • 此示例中,多图拼接是直接用大图和小图去做配准,效率并不是太高。后续可能可以结合gps信息,从大图中挖出一部分小图来做配准。

参考文献

[1] BRISK特征点描述算法详解 https://blog.csdn.net/weixin_41063476/article/details/90407916
[2] 基于视觉的特征匹配算法(持续更新)https://zhuanlan.zhihu.com/p/147325381?ivk_sa=1024320u
[3] BRISK算法学习笔记 https://blog.csdn.net/weixin_40196271/article/details/84143545
[4] 【OpenCV 例程 300篇】246. 特征检测之ORB算法 https://blog.csdn.net/youcans/article/details/128033070
[5] 一种无人机滑坡遥感影像的快速匹配算法 https://mp.weixin.qq.com/s?__biz=MzIxOTY4NDQ1MA==&mid=2247493784&idx=1&sn=65676fc368e6b4fa62c965a996f956ef
[6]【OpenCV 例程 300篇】251. 特征匹配之暴力匹配 https://blog.csdn.net/youcans/article/details/128253435
[7] 以图搜图–基于FLANN特征匹配 https://zhuanlan.zhihu.com/p/520575652
[8] [opencv] BF匹配器和Flann匹配器 https://blog.csdn.net/simonyucsdy/article/details/112682566
[9] 基于opencv的图像拼接 https://blog.csdn.net/qq_30111427/article/details/121127233

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

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

相关文章

04 react css上下浮动动画效果

react css上下浮动动画效果html原生实现上下浮动react 实现上下浮动思路分析实现步骤1.引入useRef2.在所属组件内定义—个变量3.在按钮上添加事件4.定义点击事件对window.scrollTo()进行了解&#xff1a;在react中实现效果图&#xff1a;html原生实现上下浮动 我们有一个导向箭…

【分享】订阅金蝶KIS集简云连接器同步OA付款审批数据至金蝶KIS

方案简介 集简云基于钉钉连接平台完成与钉钉的深度融合&#xff0c;实现钉钉OA审批与数百款办公应用软件&#xff08;如金蝶KIS、用友等&#xff09;的数据互通&#xff0c;让钉钉的OA审批流程与企业内部应用软件的采购、付款、报销、收款、人事管理、售后工单、立项申请等环节…

【2023面试秘籍】 测试工程师的简历该怎么写?

作为软件测试的垂直领域深耕者&#xff0c;面试或者被面试都是常有的事&#xff0c;可是不管是啥&#xff0c;总和简历有着理不清的关系&#xff0c;面试官要通过简历了解面试者的基本信息、过往经历等&#xff0c;面试者希望通过简历把自己最好的一面体现给面试官&#xff0c;…

【Java 面试合集】重写以及重载有什么区别能简单说说嘛

重写以及重载有什么区别能简单说说嘛 前述 这是一道非常基础的面试题&#xff0c;我们在回答的过程中一定要逐一横向比较。 从方法的 修饰符&#xff0c;返回值&#xff0c;方法名&#xff0c;含义&#xff0c;参数等方面进行逐一分析来比较不同。 话不多话&#xff0c;看下…

什么样的台灯适合学生做作业的?开学季,适合孩子写作业的台灯

学生在做作业时&#xff0c;是离不开台灯的&#xff0c;在台灯下学习三四个小时&#xff0c;如果台灯质量不好&#xff0c;那对视力造成很大影响&#xff0c;研究表明&#xff0c;儿童在过亮或者过暗的环境中长时间学习&#xff0c;会导致视力下降等&#xff0c;那么什么样的台…

瀚博半导体载天VA1 加速卡安装过程

背景&#xff1a; 想用 瀚博半导体载天VA1 加速卡 代替 NVIDIA 显卡跑深度学习模型 感谢瀚博的周工帮助解答。 正文&#xff1a; 小心拔出 NVIDIA 显卡&#xff0c;在PCIe 接口插上瀚博半导体载天VA1加速卡&#xff0c;如图&#xff1a; 这时显示屏连接主板的集成显卡 卸载…

cookie和Session的作用和比较

目录 什么是cookie cookie的工作原理 什么是session Session的工作原理 为什么会有session和cookie cookie和session如何配合工作 cookie和Session作用 什么是会话 什么是cookie cookie是web服务器端向我们客户端发送的一块小文件&#xff0c;该文件是干嘛的呢&#xf…

Java基础知识疑难点

1. 基础 1.1. 正确使用 equals 方法1.2. 整型包装类值的比较1.3. BigDecimal 1.3.1. BigDecimal 的用处1.3.2. BigDecimal 的大小比较1.3.3. BigDecimal 保留几位小数1.3.4. BigDecimal 的使用注意事项1.3.5. 总结 1.4. 基本数据类型与包装数据类型的使用标准 2. 集合 2.1. Arr…

Docker-用Jenkins发版Java项目-(1)Docke安装Jenkins

文章目录前言环境背景操作流程docker安装及jenkins软件安装jenkins配置登录配置安装插件及创建账号前言 学海无涯&#xff0c;旅“途”漫漫&#xff0c;“途”中小记&#xff0c;如有错误&#xff0c;敬请指出&#xff0c;在此拜谢&#xff01; 最近新购得了M2的MAC&#xff0c…

LeetCode刷题--- 138. 复制带随机指针的链表(哈希表+迭代)

文章目录一、编程题&#xff1a;430. 扁平化多级双向链表&#xff08;双指针&#xff09;1.题目描述2.示例1&#xff1a;3.示例2&#xff1a;4.示例3&#xff1a;5.提示&#xff1a;二、解题思路1. 题目分析2. 方法1&#xff08;哈希表&#xff09;思路&#xff1a;复杂度分析&…

备考 PMP 考试时需要着重注意什么?

PMP考试难度并不是很大。科学备考一定没有问题的&#xff5e;这里在和大家说说2023年PMP的考试时间&#xff1a;3月、5月、8月、11月&#xff08;其中3月不开启新报名&#xff09;需要注意的地方还是蛮多的。我就根据自己考试的经验和大家分享一下在考试整个过程中注意啥&#…

2023年广西最新建筑施工焊工(建筑特种作业)模拟试题及答案

百分百题库提供特种工&#xff08;焊工&#xff09;考试试题、特种工&#xff08;焊工&#xff09;考试预测题、特种工&#xff08;焊工&#xff09;考试真题、特种工&#xff08;焊工&#xff09;证考试题库等,提供在线做题刷题&#xff0c;在线模拟考试&#xff0c;助你考试轻…

【nodejs】nodejs入门核心知识(命令行使用、内置模块、node 模块化开发)

&#x1f4bb; nodejs入门核心知识(命令行使用、内置模块、node 模块化开发) &#x1f3e0;专栏&#xff1a;JavaScript &#x1f440;个人主页&#xff1a;繁星学编程&#x1f341; &#x1f9d1;个人简介&#xff1a;一个不断提高自我的平凡人&#x1f680; &#x1f50a;分享…

(1分钟突击面试) 高斯牛顿、LM、Dogleg后端优化算法

高斯牛顿法 LM法 DogLeg方法编辑切换为居中添加图片注释&#xff0c;不超过 140 字&#xff08;可选&#xff09;知识点&#xff1a;高斯牛顿是线搜索方法 LM方法是信赖域方法。编辑切换为居中添加图片注释&#xff0c;不超过 140 字&#xff08;可选&#xff09;这个就是JTJ是…

React设计原理—1框架原理

阅读前须知 本文是笔者学习卡颂的《React设计原理》的读书笔记&#xff0c;对书中有价值内容以Q&A方式进行呈现&#xff0c;同时结合了自己的理解&#x1f914;阅读时推荐先看问题&#xff0c;想想自己的答案&#xff0c;再和答案比对一下本文属于前端框架科普&#xff0c;…

68. Python的相对路径

68. Python的相对路径 文章目录68. Python的相对路径1. 知识回顾2. 什么是相对路径3. 相对路径的语法4. 查看相对路径的方法5. 写出所有txt文件的相对路径5.1 同目录5.2 上级目录6. 用相对路径读取txt文件6.1 读取旅游.txt6.2 读取旅游经费.txt6.3 读取笔记.txt和new.txt6.4 读…

微服务调用组件Feign

目录 JAVA 项目中如何实现接口调用&#xff1f; Httpclient Okhttp HttpURLConnection RestTemplate WebClient 什么是Feign 优势 Spring Cloud Alibaba快速整合OpenFeign 引入依赖 编写调用接口FeignClient注解 调用端在启动类上添加EnableFeignClients注解 发起调…

【送书活动】学Vue核心技术和uni-app跨平台实战项目就来看看这本书

本节目录1、书籍介绍2、推荐理由2.1 有充足的配套资源图书页内展示。2.2 PPT示例代码讲解演示2.3 内容由浅入深&#xff0c;渐进式学习3、参与方式1、书籍介绍 本书共分为14个章节&#xff0c;包括Vue.js核心基础、Vue.js高级进阶、Axios发送HTTP请求&#xff0c;Vuex状态管理…

牛客 面试必刷TOP101 题解(3、二叉树)

23 二叉树的前序遍历 /*** struct TreeNode {* int val;* struct TreeNode *left;* struct TreeNode *right;* TreeNode(int x) : val(x), left(nullptr), right(nullptr) {}* };*/ #include <vector> class Solution { public:vector<int> ans;void show(TreeNode…

腾讯架构师极力推荐:Java多线程与Socket实战微服务框架

在这个技术不断更新的年代&#xff0c;跟不上时代变化的速度就会被刷掉&#xff0c;特别是咱们程序员这一群体&#xff0c;技术不断更新的同时也要同时进步&#xff0c;不然长江后浪推前浪&#xff0c;前浪… 一个程序员从一个什么都不懂的小白在学到有一定的Java基础的时候肯…