计算机视觉——OpenCV实现Lucas-Kanade 光流追踪

news2024/10/5 20:26:49

1.光流

光流法是计算机视觉中用于估计图像序列中物体运动的关键技术。它类似于观察夜空中的彗星,通过其在天空中的运动轨迹来追踪它的路径。在图像处理中,光流帮助我们理解像素点如何在连续的帧之间移动。

1.1 稀疏光流法

稀疏光流法关注于图像中的关键点(通常是角点或显著的特征点),并计算这些点在连续帧中的运动。Lucas-Kanade算法是这种方法的一个经典例子,它通过比较特征点在连续两帧中的灰度值变化来估计这些点的运动。Lucas-Kanade方法适用于跟踪图像序列中的局部运动,尤其是当特征点清晰且显著时。

1.2 稠密光流法

与稀疏光流法不同,稠密光流法计算图像中每个像素的运动,生成一个速度场,其中每个像素都有一个对应的运动向量。Horn-Schunck算法是稠密光流法的一个代表,它通过平滑约束来优化光流场,假设图像亮度在物体运动的方向上变化不大。

1.3 Lucas-Kanade光流

在Lucas-Kanade光流中,图像的灰度值被视为位置和时间的函数。对于一个固定空间点,尽管其在世界坐标系中的位置是固定的,但在图像平面上的像素坐标会随着相机的运动会发生变化。Lucas-Kanade算法通过最小化重投影误差来估计这些像素坐标的变化,即:

I ( x , y , t ) = I ( x + Δ x , y + Δ y , t + Δ t ) I(x, y, t) = I(x + \Delta x, y + \Delta y, t + \Delta t) I(x,y,t)=I(x+Δx,y+Δy,t+Δt)

其中, I ( x , y , t ) I(x, y, t) I(x,y,t)是在时间 t t t的图像中的灰度值, ( d x , d y ) (dx, dy) (dx,dy)是像素在图像平面上的位移,而 d t dt dt是时间间隔。通过建立一个关于位移 ( d x , d y ) (dx, dy) (dx,dy)的方程组,Lucas-Kanade算法可以估计出特征点的运动。

2. 光流基本假设推导过程

2.1. 光流法的基本假设
光流法的基本假设是同一个空间点的像素灰度值,在各个图像中的是固定不变的。公式描述为:
I ( x , y , t ) = I ( x + Δ x , y + Δ y , t + Δ t ) I(x, y, t) = I(x + \Delta x, y + \Delta y, t + \Delta t) I(x,y,t)=I(x+Δx,y+Δy,t+Δt)

2.2. 泰勒展开
对上式右侧进行泰勒展开,得到:
I ( x + Δ x , y + Δ y , t + Δ t ) ≈ I ( x , y , t ) + ∂ I ∂ x Δ x + ∂ I ∂ y Δ y + ∂ I ∂ t Δ t I(x + \Delta x, y + \Delta y, t + \Delta t) \approx I(x, y, t) + \frac{\partial I}{\partial x}\Delta x + \frac{\partial I}{\partial y}\Delta y + \frac{\partial I}{\partial t}\Delta t I(x+Δx,y+Δy,t+Δt)I(x,y,t)+xIΔx+yIΔy+tIΔt

2.3. 光流约束方程
因为假设了灰度不变,即 I ( x , y , t ) I(x, y, t) I(x,y,t)不随 Δ x , Δ y , Δ t \Delta x, \Delta y, \Delta t Δx,Δy,Δt变化,因此有:
∂ I ∂ x Δ x + ∂ I ∂ y Δ y + ∂ I ∂ t Δ t = 0 \frac{\partial I}{\partial x}\Delta x + \frac{\partial I}{\partial y}\Delta y + \frac{\partial I}{\partial t}\Delta t = 0 xIΔx+yIΔy+tIΔt=0

2.4. 速度和梯度
将像素在 x 轴上的速度记为 u u u,在 y 轴上的速度记为 v v v,上述方程可以写为:
∂ I ∂ x u Δ t + ∂ I ∂ y v Δ t = − ∂ I ∂ t \frac{\partial I}{\partial x}u\Delta t + \frac{\partial I}{\partial y}v\Delta t = -\frac{\partial I}{\partial t} xIuΔt+yIvΔt=tI

2.5. 矩阵形式
写成矩阵形式有:
[ I x I y ] [ u v ] = − I t \begin{bmatrix} I_x & I_y \end{bmatrix} \begin{bmatrix} u \\ v \end{bmatrix} = -I_t [IxIy][uv]=It

2.6. 超定线性方程组
在 Lucas-Kanade 光流中,引入了新的假设作为约束,即某一个窗口内的像素具有相同的运动。考虑一个大小为 w × h w \times h w×h 的窗口,其包含 k k k个像素。因为假设该窗口内像素具有相同的运动,因此可以得到 k k k个方程:
[ I x 1 I y 1 I x 2 I y 2 ⋮ ⋮ I x k I y k ] [ u v ] = − [ I t 1 I t 2 ⋮ I t k ] \begin{bmatrix} I_x^1 & I_y^1 \\ I_x^2 & I_y^2 \\ \vdots & \vdots \\ I_x^k & I_y^k \end{bmatrix} \begin{bmatrix} u \\ v \end{bmatrix} = -\begin{bmatrix} I_t^1 \\ I_t^2 \\ \vdots \\ I_t^k \end{bmatrix} Ix1Ix2IxkIy1Iy2Iyk [uv]= It1It2Itk

A = [ I x 1 I y 1 ⋮ I x k I y k ] A = \begin{bmatrix} I_x^1 & I_y^1 \\ \vdots \\ I_x^k & I_y^k \end{bmatrix} A= Ix1IxkIy1Iyk b = [ I t 1 ⋮ I t k ] b = \begin{bmatrix} I_t^1 \\ \vdots \\ I_t^k \end{bmatrix} b= It1Itk ,则方程组可以写为:
A [ u v ] = b A\begin{bmatrix} u \\ v \end{bmatrix} = b A[uv]=b

2.7. 最小二乘解
该方程是关于 u u u v v v 的超定线性方程组,可以使用最小二乘法求解:
[ u v ] ∗ = − ( A T A ) − 1 A T b \begin{bmatrix} u \\ v \end{bmatrix}^* = -(A^TA)^{-1}A^Tb [uv]=(ATA)1ATb

值得注意的是,上述公式中的 I x , I y , I t I_x, I_y,I_t Ix,Iy,It 分别表示图像在该点的 x 方向梯度、y 方向梯度和时间方向的梯度。 Δ x , Δ y \Delta x,\Delta y Δx,Δy是像素点在图像平面上的位移,而 Δ t \Delta t Δt 是时间间隔。 u u u v v v是像素点在 x 和 y 方向上的速度。

3.OpenCV中calcOpticalFlowPyrLK函数

OpenCV中calcOpticalFlowPyrLK方法使用迭代Lucas-Kanade算法计算稀疏特征点的光流,用来做特征点跟踪,该方法使用了金字塔,因此具有一定的尺度不变性。
函数原型:

void cv::calcOpticalFlowPyrLK(	
    InputArray 	        prevImg,
    InputArray 	        nextImg,
    InputArray 	        prevPts,
    InputOutputArray 	nextPts,
    OutputArray 	    status,
    OutputArray 	    err,
    Size 	            winSize = Size(21, 21),
    int 	            maxLevel = 3,
    TermCriteria 	    criteria = TermCriteria(TermCriteria::COUNT+TermCriteria::EPS, 30, 0.01),
    int 	            flags = 0,
    double          	minEigThreshold = 1e-4 
);
  • prevImg:上一帧图像
  • nextImg:下一帧图像
  • prevPts:上一帧图像中关键点
  • nextPts:根据光流计算的上一帧关键点在当前帧中的位置
  • status:关键点的跟踪状态,vector,1表示OK,0表示LOST
  • err:每个特征点的跟踪误差vector<float>len(status)==len(nextPts)==len(prePts)==len(err)
  • winSize:在每层金字塔中,LK算法中用来求解计算像素运动而假设具有相同运动的窗口大小。
  • maxLevel:金字塔的层数,层数多,尺度不变性能更好,运算时间更久
  • criteria:迭代搜索算法的终止条件,默认值表示在指定的最大迭代次数criteria.maxCount(30)之后或当搜索窗口移动小于criteria.epsilon(0.01)时终止迭代
  • flags:设置误差或者初始值参数,可选下面两个值:
    • OPTFLOW_USE_INITIAL_FLOW设置使用nextPts中的值作为迭代的初始值,如果不设置为OPTFLOW_USE_INITIAL_FLOW,初始状态就使用prevPts中的值,直接从prevPts复制到nextPts,OpenCV源码中对OPTFLOW_USE_INITIAL_FLOW的使用方式为:

      if( flags & OPTFLOW_USE_INITIAL_FLOW )
          nextPt = nextPts[ptidx]*(float)(1./(1 << level));
      else
          nextPt = prevPt;
      
      • OPTFLOW_LK_GET_MIN_EIGENVALS,flags设置为这个值时使用光流运动方程2x2的正规矩阵,也即空间梯度矩阵的最小特征值作为误差项。如果不设置成OPTFLOW_LK_GET_MIN_EIGENVALS,将原始点和移动点周围像素的距离除以窗口中的像素作为误差项。
  • minEigThreshold:迭代LK算法会计算光流运动方程2x2的正规矩阵,也即空间梯度矩阵的最小特征值,然后再除以运动不变窗口中的像素总数作为一个误差评价标准,当其小于minEigThreshold时,说明这个点已经追踪不到了,会将其从追踪特征点中移除,避免其对应相素运动的计算,可提升性能。

calcOpticalFlowPyrLK通常和goodFeatureToTrack方法一起使用,先使用GFTTDetector提取特征点的位置,再使用calcOpticalFlowPyrLK追踪其在连续视频流中的位置,避免了特征描述子的计算和特征点的匹配,可以极大的提升追踪的性能。

c++实现:

#include <memory> 
#include <vector>
#include <cstdlib>

#include <opencv2/features2d.hpp>
#include <opencv2/opencv.hpp>

// 定义一个名为TestOpticalFlowLK的类,用于处理光流跟踪
class TestOpticalFlowLK {
    public:
        // 使用std::shared_ptr来定义智能指针,便于管理类的实例
        typedef std::shared_ptr<TestOpticalFlowLK> Ptr;

        // 构造函数
        TestOpticalFlowLK();
        // 默认的析构函数

        // track函数用于处理特征点的跟踪
        void track(std::vector<cv::String> &filenames) const;

    private:
        // 使用cv::Ptr来定义一个智能指针,指向GFTTDetector对象
        cv::Ptr<cv::GFTTDetector> gftt_ptr_;
};

// 实现TestOpticalFlowLK类的构造函数
TestOpticalFlowLK::TestOpticalFlowLK()
{
    // 初始化GFTTDetector对象,用于特征点检测
    gftt_ptr_ = cv::GFTTDetector::create(500, 0.2, 50);
}

// 实现track函数,用于处理特征点的跟踪
void TestOpticalFlowLK::track(std::vector<cv::String> &filenames) const
{
    // 确保filenames至少有一个元素
    assert(filenames.size() > 1);

    // 存储检测到的特征点
    std::vector<cv::KeyPoint> kps1;
    // 存储上一帧和当前帧中特征点的位置
    std::vector<cv::Point2f> pts1, pts2;
    // 存储绘制特征点时使用的颜色
    std::vector<cv::Scalar> colors;

    // 读取第一帧图像,并初始化last_img
    cv::Mat last_img = cv::imread(filenames[0], 0), cur_img;
    // 创建一个掩码,用于绘制特征点
    cv::Mat mask(last_img.size(), CV_8UC1, 255);

    // 使用GFTTDetector检测第一帧图像中的特征点
    gftt_ptr_->detect(last_img, kps1, mask);

    // 遍历所有检测到的特征点
    for(auto &kp : kps1) {
        // 为每个特征点生成一个随机颜色
        int r = (int)(255. * rand() / (RAND_MAX + 1.f));
        int g = (int)(255. * rand() / (RAND_MAX + 1.f));
        int b = (int)(255. * rand() / (RAND_MAX + 1.f));
        // 输出随机颜色的值
        std::cout << "r:" << r << "g:" << g << "b:" << b << std::endl;
        // 将颜色添加到colors数组中
        colors.emplace_back(r, g, b);
        // 将特征点的位置添加到pts1和pts2数组中
        pts1.push_back(kp.pt);
        pts2.push_back(kp.pt);
    }

    // 存储特征点的跟踪状态
    std::vector<uchar> status;
    // 存储每个特征点的跟踪误差
    std::vector<float> err;

    // 将掩码转换为BGR格式,以便绘制
    cv::cvtColor(mask, mask, cv::COLOR_GRAY2BGR);

    // 创建一个Mat对象,用于存储绘制特征点后的图像
    cv::Mat frame;

    // 遍历所有提供的图像文件名
    for (auto &filename : filenames)
    {
        // 输出当前处理的文件名
        std::cout << "filename: " << filename << std::endl;

        // 读取当前帧图像
        cur_img = cv::imread(filename, 0);

        // 使用calcOpticalFlowPyrLK计算特征点的光流
        cv::calcOpticalFlowPyrLK(last_img,
                                 cur_img,
                                 pts1,
                                 pts2,
                                 status,
                                 err,
                                 cv::Size(13, 13),
                                 3,
                                 cv::TermCriteria(cv::TermCriteria::COUNT + cv::TermCriteria::EPS, 30, 0.01),
                                 cv::OPTFLOW_USE_INITIAL_FLOW
                                 );

        // 将当前帧图像转换为BGR格式
        cv::cvtColor(cur_img, cur_img, cv::COLOR_GRAY2BGR);

        int cnt = 0; // 用于计数成功跟踪的特征点数量
        // 遍历所有特征点
        for(size_t i = 0; i < status.size(); i++) {
            // 输出当前特征点的跟踪误差
            std::cout << " " << err[i];
            // 如果特征点未成功跟踪,则跳过
            if(!status[i]) continue;

            // 如果特征点的移动距离超过80像素,则跳过
            if(abs((pts1[i].x - pts2[i].x)) > 80 || 
               abs((pts1[i].y - pts2[i].y)) > 80) continue;

            // 在掩码上绘制特征点之间的连线
            cv::line(mask, pts1[i], pts2[i], colors[i], 2);
            // 在当前帧图像上绘制特征点
            cv::circle(cur_img, pts2[i], 10, colors[i], 1);

            // 更新pts1中的坐标为pts2中的坐标
            pts1[i].x = pts2[i].x;
            pts1[i].y = pts2[i].y;

            // 增加成功跟踪的特征点计数
            cnt += 1;
        }
        // 输出每帧成功跟踪的特征点数量
        std::cout << std::endl;

        // 将掩码和当前帧图像混合,以便同时显示原始图像和特征点
        cv::addWeighted(mask, 0.5, cur_img, 0.5, -65, frame);

        // 显示混合后的图像
        cv::imshow("frame", frame);

        // 等待用户按键,0表示无限等待
        cv::waitKey(0);

        // 更新last_img为当前帧图像
        cv::cvtColor(cur_img, cur_img, cv::COLOR_BGR2GRAY);
        last_img = cur_img;

        // 保存混合后的图像到文件
        cv::imwrite("frame.png", frame);
    }
}

在这里插入图片描述

Python 代码:

import cv2
import numpy as np
import random

class TestOpticalFlowLK:
    def __init__(self):
        # 初始化GFTTDetector对象,用于特征点检测
        self.gftt_ptr_ = cv2.GFTTDetector_create(500, 0.2, 50)


    def track(self, filenames):
        assert len(filenames) > 1, "At least one filename is required"

        last_img = cv2.imread(filenames[0], 0)
        kps1 = self.gftt_ptr_.detect(last_img)
        pts1 = np.array([kp.pt for kp in kps1], dtype='float32')
        pts2 = np.zeros((len(pts1), 2), dtype='float32')

        colors = [((random.randint(0, 255), random.randint(0, 255), random.randint(0, 255)) for _ in range(len(kps1)))]

        status = np.zeros((len(pts1),), dtype='uint8')
        err = np.zeros((len(pts1),), dtype='float32')

        for filename in filenames[1:]:
            print("Processing file:", filename)
        cur_img = cv2.imread(filename, 0)
        pts2, status, err = cv2.calcOpticalFlowPyrLK(last_img, cur_img, pts1, pts2, None, winSize=(13, 13), maxLevel=3,
                                                     criteria=(cv2.TERM_CRITERIA_COUNT | cv2.TERM_CRITERIA_EPS, 30, 0.01),
                                                     flags=cv2.OPTFLOW_USE_INITIAL_FLOW)

        img = cv2.cvtColor(cur_img, cv2.COLOR_GRAY2BGR)

        for i, (pt1, pt2, stat) in enumerate(zip(pts1, pts2, status)):
            if stat:
        # 确保坐标是一维数组
                pt1 = pt1.reshape(-1)
                pt2 = pt2.reshape(-1)
                cv2.line(img, (int(pt1[0]), int(pt1[1])), (int(pt2[0]), int(pt2[1])), (255), 2)
                cv2.circle(img, (int(pt2[0]), int(pt2[1])), 10, (127), 1)

                pts1 = pts2.copy()
                last_img = cur_img

        cv2.imshow("frame", img)
        cv2.waitKey(0)

        cv2.destroyAllWindows()


if __name__ == "__main__":
    # 假设我们有一个包含图像文件路径的列表
    filenames = ['data/00001.jpg', 'data/00001.jpg', 'data/00002.jpg','data/00003.jpg']  # 以此类推
    flow_tracker = TestOpticalFlowLK()
    flow_tracker.track(filenames)

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

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

相关文章

js实现复制功能

/*** 复制* param {*} val 要复制的内容* returns*/ export const copyToClipboard async val > {try {// 使用现代 API 尝试复制if (navigator.clipboard && navigator.permissions) {await navigator.clipboard.writeText(val)return // 如果成功&#xff0c;直接…

pikachu靶场-全套学习

文章目录 配置pikachu靶场浏览器访问过程burpsuite配置代理hackbar安装使用kali安装中国蚁剑暴力破解cookie简化场景解释各部分含义如何工作 基于表单的暴力破解验证码绕过(On server)验证码绕过(on client)token防爆破? XSS&#xff08;Cross-Site Scripting跨站脚本攻击 &am…

Linux下安装mysql8.0(以tar.xz包安装--编译安装)

前言&#xff1a;原文在我的博客网站中&#xff0c;持续更新数通、系统方面的知识&#xff0c;欢迎来访&#xff01; Linux下安装mysql8.0&#xff08;以tar.xz包安装--编译安装&#xff09;https://myweb.myskillstree.cn/126.html 目录 一、下载对应自己glic版本的MySQL …

Linux x86_64 dump_stack()函数基于FP栈回溯

文章目录 前言一、dump_stack函数使用二、dump_stack函数源码解析2.1 show_stack2.2 show_stack_log_lvl2.3 show_trace_log_lvl2.4 dump_trace2.5 print_context_stack 参考资料 前言 Linux x86_64 centos7 Linux&#xff1a;3.10.0 一、dump_stack函数使用 dump_stack函数…

【Python基础】装饰器(3848字)

文章目录 [toc]闭包什么是装饰器装饰器示例不使用装饰器语法使用装饰器语法 装饰器传参带参数的装饰器类装饰器魔术方法\__call__()类装饰器示例带参数类装饰器property装饰器分页操作商品价格操作 个人主页&#xff1a;丷从心 系列专栏&#xff1a;Python基础 学习指南&…

Redis不同数据类型value存储

一、Strings redis中String的底层没有用c的char来实现&#xff0c;而是使用SDS数据结构( char buf[])。 缺点:浪费空间 优势: 1.c字符串不记录自身的长度&#xff0c;所以获取一个字符串长度的复杂度是O(N),但是SDS记录分配的长度alloc,已使用长度len&#xff0c;获取长度的…

资深测试必备技能!TestNG自动化测试框架实战详解

1、TestNG导言 在软件测试工作中&#xff0c;自动测试框架是不可或缺的&#xff0c;之前有Junit和Nunit框架&#xff0c;后有TestNG。TestNG不但吸取了Junit和Nunit框架的思想&#xff0c;而且创造了更强大的功能&#xff0c;它不但是单元测试框架&#xff0c;同时也是集成自动…

Qt Tab键切换焦点顺序:setTabOrder()

使用这个方法setTabOrder()&#xff0c;设置使得焦点的顺序从前到后依次是&#xff1a; ui->lineEdit》 ui->lineEdit_2》ui->lineEdit_3 》ui->lineEdit_4 焦点先在ui->lineEdit上&#xff0c;当按下Tab键时&#xff0c;焦点跑到ui->lineEdit_2上。。。按…

VS远程调试

因为是做工厂应用的客制化项目&#xff0c;在客户现场出现异常&#xff0c;本地又很难复现&#xff0c;而且重启软件可能又自动恢复了&#xff0c;此时可以用VisualStudio自带的远程调试功能进行调试&#xff0c;不需要重启软件&#xff0c;能较好的定位问题。客户电脑上不需要…

VBA信息获取与处理第四节:获取唯一非重复值返回数组的代码

《VBA信息获取与处理》教程(版权10178984)是我推出第六套教程&#xff0c;目前已经是第一版修订了。这套教程定位于最高级&#xff0c;是学完初级&#xff0c;中级后的教程。这部教程给大家讲解的内容有&#xff1a;跨应用程序信息获得、随机信息的利用、电子邮件的发送、VBA互…

k8s部署skywalking(helm)

官方文档 官方文档说明&#xff1a;Backend setup | Apache SkyWalking官方helm源码&#xff1a;apache/skywalking-helm官方下载&#xff08;包括agent、apm&#xff09;:Downloads | Apache SkyWalking 部署 根据官方helm提示&#xff0c;选择你自己部署的方式&#xff0c…

企业网站从传统服务器迁移到弹性云有什么优势呢?

现代企业对于网站和应用程序的可用性和性能要求越来越高&#xff0c;传统基础设施可能无法满足这些需求。弹性云作为一种新兴的云计算服务模式&#xff0c;对于企业网站的运行和管理带来了许多优势。下面是企业网站从传统服务器迁移到弹性云的五大优势&#xff1a; 灵活弹性&a…

【Qt 学习笔记】Qt常用控件 | 多元素控件 | Tree Widget的说明及介绍

博客主页&#xff1a;Duck Bro 博客主页系列专栏&#xff1a;Qt 专栏关注博主&#xff0c;后期持续更新系列文章如果有错误感谢请大家批评指出&#xff0c;及时修改感谢大家点赞&#x1f44d;收藏⭐评论✍ Qt常用控件 | 多元素控件 | Tree Widget的说明及介绍 文章编号&#x…

Unable to locate the .NET SDK

问题描述&#xff1a; vs2019 加载项目时&#xff0c;提示如下&#xff1a; Unable to locate the .NET SDK as specified by global.json, please check that the specified version is installed. 项目中没有globan找al.json 文件 先使用&#xff1a; dotnet --list-sdks 命…

使用 docker-compose 搭建个人博客 Halo

说明 我这里使用的是 Halo 作为博客的工具&#xff0c;毕竟是开源了&#xff0c;也是使用 Java 写的嘛&#xff0c;另外一点就是使用 docker 来安装&#xff08;自动挡&#xff0c;不用自己考虑太多的环境因素&#xff09;&#xff0c;这样子搭建起来更快一点&#xff0c;我们…

SpringCloudAlibaba:5.1Sentinel的基本使用

概述 简介 Sentinel是阿里开源的项目&#xff0c;提供了流量控制、熔断降级、系统负载保护等多个维度来保障服务之间的稳定性。 官网 https://sentinelguard.io/zh-cn/ Sentinel的历史 2012 年&#xff0c;Sentinel 诞生&#xff0c;主要功能为入口流量控制。 2013-2017 年…

[ue5]编译报错:使用未定义的 struct“FPointDamageEvent“

编译报错&#xff0c;错误很多&#xff0c;但很明显核心问题是第一个&#xff1a;使用未定义的 struct“FPointDamageEvent“&#xff1a; 程序没有找到FPointDamageEvent的定义。 解决办法&#xff1a; 处理这类未定义都可以先F12&#xff0c;找到它的库位置&#xff0c;之后…

数据结构——快速排序

基本思想&#xff1a; 快速排序是Hoare于1962年提出的一种二叉树结构的交换排序方法&#xff0c;其基本思想为&#xff1a;任取待排序元素序列中的某元素作为基准值&#xff0c;按照该排序码将待排序集合分割成两子序列&#xff0c;左子序列中所有元素均小于基准值&#xff0c;…

BMJ英国医学杂志文献去哪里下载

《柳叶刀》The Lancet、《新英格兰医学期刊》NEJM、《美国医学会杂志》JAMA、《英国医学期刊》BMJ是世界四大医学顶尖期刊&#xff0c;今天有位医学同学求助一篇BMJ英国医学杂志文献&#xff0c;下面就用这篇文献演示一下在家获取BMJ文献的方法及过程。 文献名&#xff1a;Sur…

Multitouch for Mac:手势自定义,提升工作效率

Multitouch for Mac作为一款触控板手势增强软件&#xff0c;其核心功能在于手势的自定义和与Mac系统的深度整合。通过Multitouch&#xff0c;用户可以轻松设置各种手势&#xff0c;如三指轻点、四指左右滑动等&#xff0c;来执行常见的任务&#xff0c;如打开应用、切换窗口、滚…