<?xml version="1.0" encoding="utf-8"?>
<feed xmlns="http://www.w3.org/2005/Atom">
  <author>
    <name>guoben</name>
  </author>
  <generator uri="https://hexo.io/">Hexo</generator>
  <id>https://grobenis.github.io/</id>
  <link href="https://grobenis.github.io/" rel="alternate"/>
  <link href="https://grobenis.github.io/atom.xml" rel="self"/>
  <rights>All rights reserved 2026, guoben</rights>
  <subtitle>曾梦想仗剑走天涯</subtitle>
  <title>曾是少年</title>
  <updated>2026-08-25T13:18:00.941Z</updated>
  <entry>
    <author>
      <name>guoben</name>
    </author>
    <content>
      <![CDATA[<p>本文是论文《基于车道宽度先验与车道边界观测的时间一致 IPM 在线外部相机校准》的翻译与学习笔记。论文提出利用消失点估计俯仰角与偏航角，再通过最小化车道宽度观测值与先验的差异求解横滚角与相机高度，并由扩展卡尔曼滤波顺序更新参数，从而生成时间一致的 IPM 鸟瞰图。</p><blockquote><p><strong>稳定的外参，才有稳定一致的俯视图</strong></p></blockquote><span id="more"></span><h1 id="Online-Extrinsic-Camera-Calibration-for-Temporally-Consistent-IPM-Using-Lane-Boundary-Observations-with-a-Lane-Width-Prior"><a href="#Online-Extrinsic-Camera-Calibration-for-Temporally-Consistent-IPM-Using-Lane-Boundary-Observations-with-a-Lane-Width-Prior" class="headerlink" title="Online Extrinsic Camera Calibration for Temporally Consistent IPM Using Lane Boundary Observations with a Lane Width Prior"></a>Online Extrinsic Camera Calibration for Temporally Consistent IPM Using Lane Boundary Observations with a Lane Width Prior</h1><p><img src="/2022/10/22/2022-10-20-Online_Extrinsic_Camera_Calibration/image-20221021163302120.png" alt="image-20221021163302120" loading="lazy"></p><p>标题：基于车道线等宽的先验进行车道边界观测的时间一致 IPM 的在线外部相机校准</p><h2 id="摘要"><a href="#摘要" class="headerlink" title="摘要"></a>摘要</h2><p>​ 在本文中，我们提出了一种在线外部相机校准方法，即在连续驾驶场景图像中从路面估计俯仰角、偏航角、侧倾角和相机高度。 所提出的方法通过两个步骤估计外部摄像机参数：</p><ol><li>使用从一组车道边界观测计算的消失点同时估计俯仰角和偏航角</li><li>通过最小化车道之间的差异来计算滚动角和摄像机高度 宽度观察和车道宽度先验。</li></ol><p>​ 外部相机参数使用扩展卡尔曼滤波 (EKF) 顺序更新，并最终用于通过逆透视映射 (IPM) 生成时间一致的鸟瞰 (BEV) 图像。 我们证明了所提出的方法在合成和现实世界数据集中的优越性。</p><h2 id="I-引言"><a href="#I-引言" class="headerlink" title="I 引言"></a>I 引言</h2><p>近年来，针对ADAS（高级驾驶辅助系统）和AD（自动驾驶）应用的摄像头视觉感知研究已经深入开展。许多研究集中在相邻物体和驾驶环境的检测上，例如车道边界检测[1]、[2]、交通标志检测[3]、[4]、物体检测和跟踪[5]、[6]等，来自前置摄像头捕获的输入图像。特别是对于路面标记，主要使用逆透视映射（IPM）[7]，因为它们受摄像机透视失真的影响更大。给定相机与路面的几何关系，即相机外参，可将输入图像转换为鸟瞰（BEV）图像，从而保留路面标记的实际形状并提高检测性能。此外，外参被广泛用于估计单目相机系统[8]、[9]中物体的距离，并通过生成增强特征[10]来提高物体检测的性能。</p><p>外参通常在驾驶前通过使用矩形&#x2F;梯形&#x2F;平行四边形的各种图案[11]、[12]或车道标记上的手动注释线&#x2F;顶点[13]、[14]、[15]来计算]。然而，由于出厂默认标定的不持久性，摄像头与路面的几何关系会逐渐发生变化，并且随着摄像头在行驶中的晃动而变化很大。因此，IPM 无法产生如图 1(b) 所示的良好 BEV 图像，因此在驾驶时应反复补偿外部相机参数，但人们不能在驾驶时干预校准过程，尤其是出于安全原因。因此，外部相机校准应该使用来自驾驶场景图像的观察来自动执行。</p><p>有几项研究 [16]、[17] 使用具有相机运动变化的驾驶环境的图像序列进行在线外部相机校准。他们使用从视觉里程计或车道边界的消失点 (VP) 估计的摄像机运动来更新外部摄像机参数，并生成显示平行车道边界的 BEV 图像。然而，这些研究仅更新俯仰角和偏航角，没有纠正所有外部相机参数。因此，当滚动角和相机高度发生变化时，它们可能会产生路面波动和比例（例如车道宽度和物体之间的距离）不一致的 BEV 图像。</p><p><img src="/2022/10/22/2022-10-20-Online_Extrinsic_Camera_Calibration/image-20221020152655478.png" alt="image-20221020152655478" loading="lazy"></p><p>​ 在本文中，我们提出了一种在线外在相机校准方法，该方法可以估计连续驾驶场景图像中与路面的几何关系，如俯仰角、偏航角、滚动角和相机高度。据我们所知，这是第一个以在线方式同时计算所有四个外部相机参数的工作。所提出的方法分两个阶段估计外部摄像机参数，估计 1）俯仰角和偏航角，然后 2）滚动角和摄像机高度。使用从一组车道边界观测值计算的 VP 同时估计俯仰角和偏航角。然后，给定车道宽度作为先验，通过最小化陆地宽度观测值和车道宽度先验之间的差异来计算滚动角和相机高度。所提出的方法使用扩展卡尔曼滤波 (EKF) [18] 在序列图像中更新外部相机参数，因此可以产生时间一致的 IPM 结果，如图 1(c) 所示。</p><h2 id="II-OVERVIEW"><a href="#II-OVERVIEW" class="headerlink" title="II OVERVIEW"></a>II OVERVIEW</h2><p>我们提出了一种在线外部相机校准方法，即从路面估计俯仰角、偏航角、滚动角和相机高度，从而产生时间上一致的 IPM 结果。我们假设相机经过校准，路面平坦，路面上所有车道边界相互平行，车道宽度与之前的车道宽度相同。</p><p><img src="/2022/10/22/2022-10-20-Online_Extrinsic_Camera_Calibration/image-20221020155507999.png" alt="image-20221020155507999" loading="lazy"></p><p>图 2 显示了所提出方法的总体过程。首先，我们使用基于全卷积网络的分割模型从输入图像中提取车道边界观测值 [19]。由于平行车道边界的 VP 仅取决于俯仰角和偏航角，并且不受滚动角和相机高度的变化，我们从一组平行车道边界中找到一个 VP，并使用 VP 估计俯仰角和偏航角。三）。然后我们计算滚动角和相机高度，以最小化车道宽度观测值与作为先验给出的实际车道宽度之间的差异（第 IV 节）。最后，使用更新的外部相机参数（第 V 节）计算 IPM。</p><h2 id="III-Pitch角和Yaw角的估计"><a href="#III-Pitch角和Yaw角的估计" class="headerlink" title="III Pitch角和Yaw角的估计"></a>III Pitch角和Yaw角的估计</h2><p>如在[13]、[15]、[17]中，我们将俯仰角和偏航角估计转换为找到相机和路面上平行车道边界的VP之间的旋转关系，如图3所示。C和W表示分别是相机和世界坐标系。让我们将 W 的 z 轴定义为 VP 的方向，即 VD（消失方向）。</p><p><img src="/2022/10/22/2022-10-20-Online_Extrinsic_Camera_Calibration/image-20221020155538760.png" alt="image-20221020155538760" loading="lazy"></p><p>然后俯仰角和偏航角可以定义为摄像机前向和VD之间的角度，如图3（b）和图3（c）所示。</p><p>我们采用基于高斯球体理论和 RANSAC [21] 的 [20] 的稳健 VP 估计方法，因为车道边界观测可能是嘈杂的。在使用 VP 初始化俯仰角和偏航角之后，它们在序列图像中由 EKF 估计。</p><h3 id="A-高斯球"><a href="#A-高斯球" class="headerlink" title="A. 高斯球"></a>A. 高斯球</h3><p>在针孔相机模型中，以相机主点为中心的单位球体称为高斯球。如图 4 所示，大圆是高斯球面与图像上的一条线与主点确定的平面的交点。由于平行线投影到像平面上时在VP处相交，平行线对应的大圆在高斯球面上有一个交点，主点到交点的方向为VD。 VD是由大圆（NGC）的所有法线确定的平面的法向量，我们称之为NGC-VD正交性。正交性与像平面中的线-VP入射相同，即像平面中的平行线入射到VP。</p><p><img src="/2022/10/22/2022-10-20-Online_Extrinsic_Camera_Calibration/image-20221020161949374.png" alt="image-20221020161949374" loading="lazy"></p><h3 id="B-灭点估计"><a href="#B-灭点估计" class="headerlink" title="B. 灭点估计"></a>B. 灭点估计</h3><p>我们假设给出了一组表示车道边界的线。 该集合通常包括一些噪声线或异常值，因此我们使用 RANSAC [21] 过滤掉异常值，然后估计对噪声线具有鲁棒性的 VP。 当给定一组线段 L 时，RANSAC 过程可以描述为算法 1。</p><p><img src="/2022/10/22/2022-10-20-Online_Extrinsic_Camera_Calibration/image-20221020162925514.png" alt="image-20221020162925514" loading="lazy"></p><p>在算法 1 中，VP 假设$v_i$由两个随机采样的线段 ${l_j , l_k} ⊂ L$ 计算得出，如下所示：</p><p>$$v_i = l_j \times l_k$$</p><p>然后，使用 Rother [22] 的评分函数计算 VP 假设 $v_i$ 的评分值 $s_i$。</p><p><img src="/2022/10/22/2022-10-20-Online_Extrinsic_Camera_Calibration/image-20221021161843215.png" alt="image-20221021161843215" loading="lazy"></p><p>图 5 显示了计算 $s_i$ 时要考虑的两个约束：对于每条线段 $l ∈ L，1) $ 和包括 vi 和 l 中心点的假想线之间的角度 $θ(v_i, l)$，以及 2) l 的长度，ll。 得分函数定义为</p><p>$$S\left(\mathbf{v}_i, \mathcal{L}\right)=\sum_{\mathbf{l} \in \mathcal{L}}\left[\lambda_1\left(1-\frac{\theta\left(\mathbf{v}_i, \mathbf{l}\right)}{\theta_{t h}}\right)+\lambda_2 \frac{l_1}{l_m}\right]$$</p><p>其中，$\lambda=0.8$，$\lambda=0.2$。表示权重，$\theta_{th}=0.7°$。$l_m$是$\mathcal{L}$最长的分割线长度。当$\theta(v_i,l)$不超过$\theta_{th}$，$l$不会包含在分数计算中。</p><p>因此，具有最高分数的一组线段 Lc 被 RANSAC 过程聚类。 聚类线用于计算最佳 VP。 为了获得最佳 VP，我们使用了 NGC-VD 正交性，如第 1 节所述。 III-A。 每行 l 的 NGC n 由下式计算</p><p>$$\mathbf{n}=\left(\mathbf{K}^{-1} \mathbf{p}_1\right) \times\left(\mathbf{K}^{-1} \mathbf{p}_2\right),$$</p><p>其中，K是相机的内参矩阵，$p_1$和$p_2$是l的端点。 那么v和NGCs的正交方程如下：</p><p>$$\mathbf{A v}=0 \text {, where } \mathbf{A}=[\cdots, \mathbf{n}, \cdots]^{\top} \text {. }$$</p><p>线性方程组的超定系统可以通过奇异值分解 (SVD) 轻松求解。 实际上，(4)中计算的v是一个VD向量，它被K投影为成像平面上一个VP，即$v_d = K^{−1}v_p$ 其中$v_d$和$v_p$分别是VD和VP，但是它们是相同的，所以VD 从现在开始将被写为VP。</p><h3 id="C-Pitch和Yaw角初始化"><a href="#C-Pitch和Yaw角初始化" class="headerlink" title="C Pitch和Yaw角初始化"></a>C Pitch和Yaw角初始化</h3><p>俯仰角和偏航角分别用 θ 和 φ 表示。 由pitch和yaw角计算的旋转矩阵，即从世界坐标到相机坐标的变换矩阵，用RCW(θ, φ)表示，世界坐标系W中z轴的方向向量用dWZ表示 &#x3D; [0, 0, 1]。 那么dWZ和v有如下关系，</p><p>$$\mathbf{v}=\mathbf{R}_{C W}(\theta, \phi) \mathbf{d}_{W_Z} .$$</p><p>We can decompose the rotation matrix into two rotation matrices of $\theta$ and $\phi$ as follows.</p><p>$$\begin{aligned}\mathbf{R}_{C W}(\theta, \phi) &amp;=\mathbf{R}(\theta) \mathbf{R}(\phi) \\&amp;=\left[\begin{array}{ccc}1 &amp; 0 &amp; 0 \\0 &amp; c_\theta &amp; -s_\theta \\0 &amp; s_\theta &amp; c_\theta\end{array}\right]\left[\begin{array}{ccc}c_\phi &amp; 0 &amp; s_\phi \\0 &amp; 1 &amp; 0 \\-s_\phi &amp; 0 &amp; c_\phi\end{array}\right](6)\end{aligned}$$</p><p>其中，$\theta$和$\phi$通过v来初始化：</p><p>$$\begin{aligned}\theta &amp;=\operatorname{atan2}\left(-v_y, v_z\right) \text { and } \\\phi &amp;=\operatorname{atan} 2\left(v_x, v_z\right)\end{aligned}$$</p><p>Wwhere $\mathbf{v}=\left[v_x, v_y, v_z\right]^{\top}$ and $\operatorname{atan} 2(y, x)$ is the 2 -argument arctangent function.</p><h3 id="D-EKF"><a href="#D-EKF" class="headerlink" title="D EKF"></a>D EKF</h3><p>我们使用 EKF [18] 来估计图像序列中的俯仰角和偏航角。 采用恒定角速度模型来模拟驾驶时的俯仰和偏航角变化。 因此，用于俯仰角和偏航角估计的状态向量 $x_{PY}$ 和系统模型$f_{PY}$ 被定义为：</p><p>$$\begin{aligned}\mathrm{x}_{P Y} &amp;=\left[\theta, \phi, \omega_\theta, \omega_\phi\right]^{\top} \text { and } \\\mathbf{f}_{P Y}\left(\mathbf{x}_{P Y}\right) &amp;=\left[\begin{array}{c}\theta+\omega_\theta \Delta t \\\phi+\omega_\phi \Delta t \\\omega_\theta \\\omega_\phi\end{array}\right]+\mathbf{w}_{P Y},\end{aligned}$$</p><p>where $\omega_\theta$ and $\omega_\phi$ are the angular velocity of pitch and yaw angles and $\mathbf{w}_{P Y}=\left[0,0, w_\theta, w_\phi\right]^{\top}$ is a noise variable of the system model with the normal distribution $\mathcal{N}\left(0, \mathbf{W}_{P Y}\right)$.</p><p>Using the NGC-VD orthogonality, a measurement model $h_{P Y}$ for the pitch and yaw angle estimation is defined as</p><p>$$h_{P Y}^{\mathbf{n}}\left(\mathbf{x}_{P Y}\right)=\mathbf{d}_{W_Z}^{\top} \mathbf{R}_{C W}(\theta, \phi)^{\top} \mathbf{n}+q_{P Y},$$</p><p>where $\mathbf{n}$ is an $\mathrm{NGC}$ of $\mathrm{l} \in \mathcal{L}_c$ and $q_{P Y}$ is a noise variable of the measurement model with the normal distribution $\mathcal{N}\left(0, Q_{P Y}\right)$.</p><p>The state vector $\mathrm{x}_{P Y}$ are estimated by $\mathrm{EKF}$ as follows: For simplicity, $\mathrm{x}_{P Y}$ is abbreviated as $\mathrm{x}$. Then let $\mathrm{x}_{t-1}$ and $\mathbf{P}_{t-1}$ denote the state estimate and its covariance at time $t-1$. The prediction of the state $\hat{\mathbf{x}}_t$ and its covariance $\hat{\mathbf{P}}_t$ is calculated as follows.</p><p>$$\begin{aligned}\hat{\mathbf{x}}_t &amp;=\mathbf{f}_{P Y}\left(\mathrm{x}_{t-1}\right) \text { and } \\\hat{\mathbf{P}}_t &amp;=\mathbf{F}_t \mathbf{P}_{t-1} \mathbf{F}_t^{\top}+\mathbf{W}_t,\end{aligned}$$</p><p>where $\mathbf{F}_t$ is a Jacobian of the system model. For all the inliers in $\mathcal{L}_c$, the residuals of the measurements are computed using (11) and concatenated as follows.</p><p>$$\mathrm{y}_t=-\left[\cdots, h_{P Y}^{\mathrm{n}}\left(\hat{\mathbf{x}}_t\right), \cdots\right]^{\top}$$</p><p>The Kalman gain $\mathrm{G}_t$ for update is computed as follows.</p><p>$\mathbf{G}_t=\hat{\mathbf{P}}_t \mathbf{H}_t^{\top} \mathbf{S}_t^{-1}$, where $\mathbf{S}_t=\mathbf{H}_t \hat{\mathbf{P}}_t \mathbf{H}_t^{\top}+\mathbf{Q}_t$.<br>Here, $\mathrm{H}_t$ and $\mathrm{Q}_t$ denote a Jacobian of the measurement model, i.e., $\mathbf{H}_t=\left[\cdots,\left.\frac{\partial h_{P Y}^{\mathrm{n}}}{\partial \mathbf{x}}\right|_{\hat{\mathbf{x}}_t} ^{\top}, \cdots\right]^{\top}$, and a measurement noise covariance at time $t$, respectively. From the residuals and the Kalman gain, the predicted state and covariance are updated as follows.</p><p>$$\begin{aligned}\mathrm{x}_t &amp;=\hat{\mathbf{x}}_t+\mathbf{G}_t \mathbf{y}_t \text { and } \\\mathbf{P}_t &amp;=\left(\mathbf{I}-\mathbf{G}_t \mathbf{H}_t\right) \hat{\mathbf{P}}_t\end{aligned}$$</p><p>where $\mathrm{I}$ is an identity matrix.</p><h2 id="IV-roll角和相机高度估计"><a href="#IV-roll角和相机高度估计" class="headerlink" title="IV roll角和相机高度估计"></a>IV roll角和相机高度估计</h2><p>与 [14] 使用车道宽度先验和道路表面上手动注释的 3D 顶点作为观测值相比。</p><p>由于缺乏几何图形以及投影属性和观察到的车道边界与外参之间的非线性几何关系产生的信息，以车道线的2D投影作为观测值来校准roll角和相机高度变得更加复杂。为简化问题，假设俯仰角和偏航角在第三部分中已得到校正。这样在图 6 所示，roll角和相机高度估计可以被认为是计算它们在 x-y 平面上的近似值。</p><p>通过将路面和线投影到 x-y 平面上，我们可以估计滚动角和相机高度值，其中路面与线的交点之间的距离应等于车道宽度先验$w_p$。</p><h3 id="A-Roll角和相机高度初始化"><a href="#A-Roll角和相机高度初始化" class="headerlink" title="A. Roll角和相机高度初始化"></a>A. Roll角和相机高度初始化</h3><p><img src="/2022/10/22/2022-10-20-Online_Extrinsic_Camera_Calibration/image-20221020173426411.png" alt="image-20221020173426411" loading="lazy"></p><p>（斜率可能出错，$\alpha$表示车道线与相机y轴的夹角）</p><p>$$w_{1_L, l_R}(\psi, h)=h\left(\tan \left(\alpha_L-\psi\right)-\tan \left(\alpha_R-\psi\right)\right)$$</p><p>An energy function for the roll angle and camera height estimation is defined as</p><p>$$E_{R H}(\psi, h)=\sum_{\left(1_L, 1_R\right) \in \mathcal{L}_c} C_{\mathbf{l}_L, 1_R}(\psi, h),$$</p><p>其中，</p><p>$$C_{1_L, 1_R}(\psi, h)=w_p-w_{1_L, 1_R}(\psi, h)$$</p><p>$\omega$ 和 $h$ 通过在有限范围内的穷举搜索进行初始化，然后使用 Gauss-Newton 方法通过最小化 (19) 的能量函数来优化。 值得注意的是，至少应考虑两条车道，因为侧倾角是根据一对车道宽度观测值的相等性估计的。</p><h3 id="B-基于EKF-估计滚动角和相机高度"><a href="#B-基于EKF-估计滚动角和相机高度" class="headerlink" title="B. 基于EKF 估计滚动角和相机高度"></a>B. 基于EKF 估计滚动角和相机高度</h3><p>与第三部分D类似，本文使用恒定角和线速度模型来对滚动角和相机高度进行时间一致的估计。 因此，用于滚动角和相机高度估计的状态向量 $X_{RH}$ 和系统模型 $f_{RH}$ 定义为</p><p>$$\begin{aligned}\mathbf{x}_{R H}=&amp; {\left[\psi, h, \omega_\psi, v_h\right]^{\top} \text { and } } \\\mathbf{f}_{R H}\left(\mathbf{x}_{R H}\right)=&amp; {\left[\begin{array}{c}\psi+\omega_\psi \Delta t \\h+v_h \Delta t \\\omega_\psi \\v_h\end{array}\right]+\mathbf{w}_{R H} }\end{aligned}$$</p><p>式中，$ω_ψ$和$v_h$分别为横滚角的角速度和相机高度的线速度，$w_{RH}=[0, 0, w_ψ, v_h]$是系统模型的噪声变量，服从正态分布$N(0, W_{RH})$。 滚动角和相机高度估计的测量模型 $h_{RH}(ψ, h)$ 与 (20) 相同。 最后，滚动角和相机高度由 EKF 使用方程(12)-(17)估计 。</p><h2 id="V-IPM"><a href="#V-IPM" class="headerlink" title="V IPM"></a>V IPM</h2><p>Finally, temporally consistent IPM is possible with the extrinsic camera parameter estimates $\theta, \phi, \psi$, and $h$. A homography matrix $\mathbf{H}_{W C}$ from the camera coordinates to the world (or ground) coordinates is calculated as follows.</p><p>$$\mathbf{H}_{W C}=\left[\begin{array}{ccc}a_X &amp; 0 &amp; \frac{b_X a_X}{2} \\0 &amp; -a_Z &amp; b_Z a_Z \\0 &amp; 0 &amp; 1\end{array}\right]\left[\begin{array}{c}\mathbf{R}_{r_1} \\\mathbf{R}_{r_3} \\\frac{1}{h} \mathbf{R}_{r_2}\end{array}\right] \mathbf{K}^{-1}$$</p><p>where $a_X$ and $a_Z$ are the scale parameters that determine the resolution of a BEV image, $b_X$ and $b_Z$ are the size parameters of $X$-axis and $Z$-axis in the world coordinate system, and $\mathbf{R}_{r_1}$ is the $i$-th row of the matrix $\mathbf{R}(\theta, \phi, \psi)$ which is computed by</p><p>$$\mathbf{R}(\theta, \phi, \psi)=\left[\begin{array}{ccc}\cos \psi &amp; \sin \psi &amp; 0 \\-\sin \psi &amp; \cos \psi &amp; 0 \\0 &amp; 0 &amp; 1\end{array}\right] \mathbf{R}_{C W}(\theta, \phi)^{\top}$$</p><p>Using (23), an input image can be converted into a BEV image as shown in Fig. 11(c).</p><h2 id="实验结果"><a href="#实验结果" class="headerlink" title="实验结果"></a>实验结果</h2><p><img src="/2022/10/22/2022-10-20-Online_Extrinsic_Camera_Calibration/image-20221021163302120.png" alt="image-20221021163302120" loading="lazy"></p>]]>
    </content>
    <id>https://grobenis.github.io/2022/10/22/2022-10-20-Online_Extrinsic_Camera_Calibration/</id>
    <link href="https://grobenis.github.io/2022/10/22/2022-10-20-Online_Extrinsic_Camera_Calibration/"/>
    <published>2022-10-22T11:41:51.955Z</published>
    <summary>
      <![CDATA[<p>本文是论文《基于车道宽度先验与车道边界观测的时间一致 IPM 在线外部相机校准》的翻译与学习笔记。论文提出利用消失点估计俯仰角与偏航角，再通过最小化车道宽度观测值与先验的差异求解横滚角与相机高度，并由扩展卡尔曼滤波顺序更新参数，从而生成时间一致的 IPM 鸟瞰图。</p>
<blockquote>
<p><strong>稳定的外参，才有稳定一致的俯视图</strong></p>
</blockquote>]]>
    </summary>
    <title>在线外部相机标定论文学习</title>
    <updated>2026-08-25T13:18:00.941Z</updated>
  </entry>
  <entry>
    <author>
      <name>guoben</name>
    </author>
    <content>
      <![CDATA[<p>参考链接 : <a href="https://mp.weixin.qq.com/s/q6oJrPlzx2l4OihTGW50hw">从零开始一起学习SLAM | 用四元数插值来对齐IMU和图像帧</a></p><p>本文介绍 IMU 与相机帧之间的时间戳同步方法，针对 IMU 帧率远高于图像帧率的问题，讲解四元数线性插值（Nlerp）与球面线性插值（Slerp）的原理、差异及实现注意事项。文章还给出将 IMU 四元数对齐到图像帧时间戳的编程示例与完整 C++ 代码。</p><blockquote><p><strong>插值对齐时间戳，多传感器步调一致</strong></p></blockquote><span id="more"></span><p>IMU与相机帧的互补</p><p><img src="/2022/07/25/2022-7-25-%E6%97%B6%E9%97%B4%E6%88%B3%E5%90%8C%E6%AD%A5/640.jpeg" loading="lazy"></p><h2 id="IMU与相机帧之间的同步"><a href="#IMU与相机帧之间的同步" class="headerlink" title="IMU与相机帧之间的同步"></a>IMU与相机帧之间的同步</h2><p>各种插值方法</p><p><img src="/2022/07/25/2022-7-25-%E6%97%B6%E9%97%B4%E6%88%B3%E5%90%8C%E6%AD%A5/640-16587184562393.png" loading="lazy"></p><h3 id="双线性插值"><a href="#双线性插值" class="headerlink" title="双线性插值"></a>双线性插值</h3><p>在图像处理和计算机视觉领域，应用比较多的双线性插值。双线性插值的效果不是最好的，但相较最邻近插值和线性插值的简单粗暴，其获得图像的效果还是更令人满意的，而且双线性插值的计算量和易于理解程度会优于双三次插值和三次样条插值等高阶插值方法。因此双线性插值还是最受广大图像研究者喜爱的。</p><h3 id="四元数的插值方法"><a href="#四元数的插值方法" class="headerlink" title="四元数的插值方法"></a>四元数的插值方法</h3><p>线性插值-&gt;归一化线性插值<br>$$q_{t}=\operatorname{Nerp}\left(q_{0}, q_{1}, t\right)=\frac{(1-t) q_{0}+t q_{1}}{\left\|(1-t) q_{0}+t q_{1}\right\|}$$<br><img src="/2022/07/25/2022-7-25-%E6%97%B6%E9%97%B4%E6%88%B3%E5%90%8C%E6%AD%A5/640-16587194570506.jpeg" loading="lazy"></p><p>线性插值即在$q_0$,$q_1$上进行插值得到$q_t$，对于匀速的变换，其扫过的面积却不是匀速的<br>$$\begin{aligned}&amp;q_{t}=\operatorname{Serp}\left(q_{0}, q_{1}, t\right)=\frac{\sin ((1-t) \theta)}{\sin (\theta)} q_{0}+\frac{\sin (t \theta)}{\sin (\theta)} q_{1} \\&amp;\theta=\operatorname{acos}\left(q_{0} \cdot q_{1}\right)\end{aligned}$$</p><h3 id="注意事项"><a href="#注意事项" class="headerlink" title="注意事项"></a>注意事项</h3><p>理论上是这样的，不过，在编程实现Slerp插值的时候还是有几个问题需要注意一下。</p><p>1、如果单位四元数之间的夹角θ非常小，那么sin(θ)可能会由于浮点数的误差被近似为0.0，从而导致除以0的错误．所以，我们在实施 Slerp 之前，需要检查两个四元数的夹角是否过小（或者完全相同）。一旦发现这种问题，我们就必须改用 Nlerp 对两个四元数进行插值，这时候 Nlerp 的误差非常小，所以基本不会与真正的 Slerp 有什么区别。</p><p>2、在对两个单位四元数进行插值之前，我们需要先检测q0与q1之间是否是钝角，即检测它们点积的结果q0⋅q1 是否为负数。如果 q0⋅q1&lt;0，那么我们就反转其中的一个四元数，比如说将q1改为−q1 ，并使用q0与−q1之间新的夹角来进行插值，这样才能保证插值的路径是最短的．</p><h3 id="编程实现四元数球面线性插值。"><a href="#编程实现四元数球面线性插值。" class="headerlink" title="编程实现四元数球面线性插值。"></a><strong>编程实现四元数球面线性插值。</strong></h3><p>我们用智能手机采集了图像序列和IMU数据，由于IMU帧率远大于图像帧率，需要你用Slerp方法进行四元数插值，使得插值后的IMU和图像帧对齐。</p><p>已知某帧图像的时间戳为：t &#x3D;700901880170406</p><p>离该图像帧最近的前后两个时刻IMU时间戳为：t1 &#x3D; 700901879318945，t2 &#x3D; 700901884127851</p><p>IMU在t1, t2时刻测量得的旋转四元数为：</p><p>$q_1x=0.509339, q_1y=0.019188, q_1z=0.049596, q1_w=0.858921$</p><p>$q_2x=0.509443, q_2y=0.018806, q_2z=0.048944,q_2w=0.858905$</p><p>根据上述信息求IMU对齐到图像帧的插值后的四元数。</p><p>参考结果已经给出。</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br></pre></td><td class="code"><pre><span class="line"><span class="function">Quaterniond <span class="title">slerp</span><span class="params">(<span class="type">double</span> t, Quaterniond &amp;q1, Quaterniond &amp;q2)</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line"><span class="comment">// ---- 开始你的代码 ----- //</span></span><br><span class="line"><span class="comment">// 参考公众号推送文章</span></span><br><span class="line">Quaterniond deltaQ = q1.<span class="built_in">inverse</span>() * q2;</span><br><span class="line"><span class="comment">// Eigen::AngleAxisd rotation_vector(deltaQ);</span></span><br><span class="line"><span class="comment">// double theta = rotation_vector.angle();</span></span><br><span class="line"></span><br><span class="line"><span class="type">double</span> theta = <span class="built_in">acos</span>(q1.dot(q2));</span><br><span class="line"><span class="comment">// cout &lt;&lt; rotation_vector.angle() &lt;&lt; endl;</span></span><br><span class="line"><span class="comment">// cout &lt;&lt; rotation_vector.matrix() &lt;&lt; endl;</span></span><br><span class="line"><span class="type">double</span> q_slerp_w, q_slerp_x, q_slerp_y, q_slerp_z;</span><br><span class="line"><span class="keyword">if</span> (theta &lt; <span class="number">0.001</span>)</span><br><span class="line">&#123;</span><br><span class="line">q_slerp_w = (<span class="number">1</span> - t) * q1.<span class="built_in">w</span>() + t * q2.<span class="built_in">w</span>();</span><br><span class="line">q_slerp_x = (<span class="number">1</span> - t) * q1.<span class="built_in">x</span>() + t * q2.<span class="built_in">x</span>();</span><br><span class="line">q_slerp_y = (<span class="number">1</span> - t) * q1.<span class="built_in">y</span>() + t * q2.<span class="built_in">y</span>();</span><br><span class="line">q_slerp_z = (<span class="number">1</span> - t) * q1.<span class="built_in">z</span>() + t * q2.<span class="built_in">z</span>();</span><br><span class="line">&#125;</span><br><span class="line"><span class="keyword">else</span></span><br><span class="line">&#123;</span><br><span class="line">q_slerp_w = <span class="built_in">sin</span>((<span class="number">1</span> - t) * theta) / <span class="built_in">sin</span>(theta) * q1.<span class="built_in">w</span>() + <span class="built_in">sin</span>(t * theta) / <span class="built_in">sin</span>(theta) * q2.<span class="built_in">w</span>();</span><br><span class="line">q_slerp_x = <span class="built_in">sin</span>((<span class="number">1</span> - t) * theta) / <span class="built_in">sin</span>(theta) * q1.<span class="built_in">x</span>() + <span class="built_in">sin</span>(t * theta) / <span class="built_in">sin</span>(theta) * q2.<span class="built_in">x</span>();</span><br><span class="line">q_slerp_y = <span class="built_in">sin</span>((<span class="number">1</span> - t) * theta) / <span class="built_in">sin</span>(theta) * q1.<span class="built_in">y</span>() + <span class="built_in">sin</span>(t * theta) / <span class="built_in">sin</span>(theta) * q2.<span class="built_in">y</span>();</span><br><span class="line">q_slerp_z = <span class="built_in">sin</span>((<span class="number">1</span> - t) * theta) / <span class="built_in">sin</span>(theta) * q1.<span class="built_in">z</span>() + <span class="built_in">sin</span>(t * theta) / <span class="built_in">sin</span>(theta) * q2.<span class="built_in">z</span>();</span><br><span class="line">&#125;</span><br><span class="line"><span class="function">Quaterniond <span class="title">q_slerp</span><span class="params">(q_slerp_w, q_slerp_x, q_slerp_y, q_slerp_z)</span></span>;</span><br><span class="line"><span class="keyword">if</span> (theta &lt; <span class="number">0.001</span>)</span><br><span class="line">&#123;</span><br><span class="line">q_slerp = q_slerp.<span class="built_in">Identity</span>();</span><br><span class="line">&#125;</span><br><span class="line"><span class="comment">// ---- 结束你的代码 ----- //</span></span><br><span class="line"><span class="keyword">return</span> q_slerp;</span><br><span class="line">&#125;</span><br><span class="line"><span class="function"><span class="type">int</span> <span class="title">main</span><span class="params">(<span class="type">int</span> argc, <span class="type">char</span> **argv)</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line"><span class="function"><span class="type">double</span> <span class="title">t_img</span><span class="params">(<span class="number">700901880170406</span>)</span>, <span class="title">t1_imu</span><span class="params">(<span class="number">700901879318945</span>)</span>, <span class="title">t2_imu</span><span class="params">(<span class="number">700901884127851</span>)</span></span>;</span><br><span class="line">Quaterniond q1 = <span class="built_in">Quaterniond</span>(<span class="number">0.858921</span>, <span class="number">0.509339</span>, <span class="number">0.019188</span>, <span class="number">0.049596</span>);</span><br><span class="line">Quaterniond q2 = <span class="built_in">Quaterniond</span>(<span class="number">0.858905</span>, <span class="number">0.509443</span>, <span class="number">0.018806</span>, <span class="number">0.048944</span>);</span><br><span class="line"><span class="type">double</span> t = (t_img - t1_imu) / (t2_imu - t1_imu);</span><br><span class="line">Quaterniond q_slerp = <span class="built_in">slerp</span>(t, q1, q2);</span><br><span class="line">cout &lt;&lt; <span class="string">&quot;插值后的四元数：q_slerp =\n&quot;</span></span><br><span class="line"> &lt;&lt; q_slerp.<span class="built_in">coeffs</span>() &lt;&lt; endl; <span class="comment">// coeffs的顺序是(x,y,z,w)</span></span><br><span class="line"></span><br><span class="line"><span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure>]]>
    </content>
    <id>https://grobenis.github.io/2022/07/25/2022-7-25-%E6%97%B6%E9%97%B4%E6%88%B3%E5%90%8C%E6%AD%A5/</id>
    <link href="https://grobenis.github.io/2022/07/25/2022-7-25-%E6%97%B6%E9%97%B4%E6%88%B3%E5%90%8C%E6%AD%A5/"/>
    <published>2022-07-25T02:59:52.000Z</published>
    <summary>
      <![CDATA[<p>参考链接 : <a href="https://mp.weixin.qq.com/s/q6oJrPlzx2l4OihTGW50hw">从零开始一起学习SLAM | 用四元数插值来对齐IMU和图像帧</a></p>
<p>本文介绍 IMU 与相机帧之间的时间戳同步方法，针对 IMU 帧率远高于图像帧率的问题，讲解四元数线性插值（Nlerp）与球面线性插值（Slerp）的原理、差异及实现注意事项。文章还给出将 IMU 四元数对齐到图像帧时间戳的编程示例与完整 C++ 代码。</p>
<blockquote>
<p><strong>插值对齐时间戳，多传感器步调一致</strong></p>
</blockquote>]]>
    </summary>
    <title>时间戳同步方法</title>
    <updated>2026-08-25T13:18:00.987Z</updated>
  </entry>
  <entry>
    <author>
      <name>guoben</name>
    </author>
    <category term="学习" scheme="https://grobenis.github.io/categories/%E5%AD%A6%E4%B9%A0/"/>
    <category term="SLAM" scheme="https://grobenis.github.io/tags/SLAM/"/>
    <category term="相机标定" scheme="https://grobenis.github.io/tags/%E7%9B%B8%E6%9C%BA%E6%A0%87%E5%AE%9A/"/>
    <content>
      <![CDATA[<p>本文介绍SLAM系统中不同传感器以及之间的标定原理与步骤。</p><p><strong>极简相机标定步骤：</strong></p><blockquote><p>1、打印一张棋盘格，把它贴在一个平面上，作为标定物。<br>2、通过调整标定物或摄像机的方向，为标定物拍摄一些不同方向的照片。<br>3、从照片中提取棋盘格角点。<br>4、估算理想无畸变的情况下，五个内参和六个外参。<br>5、应用最小二乘法估算实际存在径向畸变下的畸变系数。<br>6、极大似然法，优化估计，提升估计精度。</p></blockquote><p>参考来源：<a href="https://mp.weixin.qq.com/s/q-RIpy4YZUrrt9qCdR9Xvw">一文详解相机标定算法原理</a></p><p>本文讲解 SLAM 中传感器的标定原理，重点介绍张正友棋盘格标定法，从相机内参、外参与畸变模型入手，推导平面单应性矩阵与求解内参的两个基本约束。文章进一步说明通过极大似然估计和 L-M 算法迭代优化参数，并简要提及 IMU 标定及相机与 IMU 之间的外参标定。</p><blockquote><p><strong>一块棋盘格，求解相机内外参</strong></p></blockquote><span id="more"></span><h2 id="相机标定"><a href="#相机标定" class="headerlink" title="相机标定"></a>相机标定</h2><h3 id="问题引出"><a href="#问题引出" class="headerlink" title="问题引出"></a>问题引出</h3><h4 id="一-为什么要相机标定？"><a href="#一-为什么要相机标定？" class="headerlink" title="一. 为什么要相机标定？"></a>一. 为什么要相机标定？</h4><blockquote><p>​    在图像测量过程以及机器视觉应用中，为确定空间物体表面某点的三维几何位置与其在图像中对应点之间的相互关系，必须建立相机成像的几何模型，这些几何模型参数就是相机参数。      </p><p>​    【1】进行摄像机标定的目的：求出相机的内、外参数，以及畸变参数。<br>​    【2】标定相机后通常是想做两件事：一个是由于每个镜头的畸变程度各不相同，通过相机标定可以校正这种镜头畸变矫正畸变，生成矫正后的图像；另一个是根据获得的图像重构三维场景。</p><p>摄像机标定过程，简单的可以简单的描述为通过标定板，如下图，可以得到n个对应的世界坐标三维点Xi和对应的图像坐标二维点xi，这些三维点到二维点的转换都可以通过上面提到的相机内参K，相机外参R和t，以及畸变参数D，经过一系列的矩阵变换得到。</p></blockquote><h4 id="二-什么叫相机标定？"><a href="#二-什么叫相机标定？" class="headerlink" title="二. 什么叫相机标定？"></a>二. 什么叫相机标定？</h4><blockquote><p>​    在大多数条件下这些参数必须通过实验与计算才能得到，这个求解参数的过程就称之为相机标定（或摄像机标定）</p></blockquote><h4 id="三-为什么相机标定很重要？"><a href="#三-为什么相机标定很重要？" class="headerlink" title="三. 为什么相机标定很重要？"></a>三. 为什么相机标定很重要？</h4><blockquote><p>   无论是在图像测量或者机器视觉应用中，相机参数的标定都是非常关键的环节，其标定结果的精度及算法的稳定性直接影响相机工作产生结果的准确性。因此，做好相机标定是做好后续工作的前提，提高标定精度是科研工作的重点所在。</p></blockquote><h4 id="四-相机标定方法有哪些？"><a href="#四-相机标定方法有哪些？" class="headerlink" title="四. 相机标定方法有哪些？"></a>四. 相机标定方法有哪些？</h4><blockquote><p>相机标定方法有：传统相机标定法、主动视觉相机标定方法、相机自标定法。</p><ol><li><p>传统相机标定法需要使用尺寸已知的标定物，通过建立标定物上坐标已知的点与其图像点之间的对应，利用一定的算法获得相机模型的内外参数。根据标定物的不同可分为三维标定物和平面型标定物。三维标定物可由单幅图像进行标定，标定精度较高，但高精密三维标定物的加工和维护较困难。平面型标定物比三维标定物制作简单，精度易保证，但标定时必须采用两幅或两幅以上的图像。传统相机标定法在标定过程中始终需要标定物，且标定物的制作精度会影响标定结果。同时有些场合不适合放置标定物也限制了传统相机标定法的应用。</p></li><li><p>目前出现的自标定算法中主要是利用相机运动的约束。相机的运动约束条件太强，因此使得其在实际中并不实用。利用场景约束主要是利用场景中的一些平行或者正交的信息。其中空间平行线在相机图像平面上的交点被称为消失点，它是射影几何中一个非常重要的特征，所以很多学者研究了基于消失点的相机自标定方法。自标定方法灵活性强，可对相机进行在线定标。但由于它是基于绝对二次曲线或曲面的方法，其算法鲁棒性差。</p></li><li><p>基于主动视觉的相机标定法是指已知相机的某些运动信息对相机进行标定。该方法不需要标定物，但需要控制相机做某些特殊运动，利用这种运动的特殊性可以计算出相机内部参数。基于主动视觉的相机标定法的优点是算法简单，往往能够获得线性解，故鲁棒性较高，缺点是系统的成本高、实验设备昂贵、实验条件要求高，而且不适合于运动参数未知或无法控制的场合。</p></li></ol></blockquote><h3 id="相机标定的概念"><a href="#相机标定的概念" class="headerlink" title="相机标定的概念"></a>相机标定的概念</h3><p>相机标定就是通过输入带有标定pattern的标定板来计算相机参数，来用简单地数学模型来表达复杂的成像过程。求解这个数学模型，也就是求解相机的参数，包括相机的内参，外参以及畸变参数。有了这个数学模型，我们可以对相机拍摄的图片进行畸变校正，也可以用多个相机拍摄图片来进行三维重建，测量物体距离以及其他的计算机视觉的应用。</p><p>相机成像需要通过一系列变换组成</p><p><img src="/2022/07/25/2022-7-25-%E7%9B%B8%E6%9C%BA%E6%A0%87%E5%AE%9A%E5%8E%9F%E7%90%86/image-20220725194347996.png" loading="lazy"></p><p>从世界坐标系到像素坐标系，写成矩阵形式，可以得到：<br>$$\begin{array}{r}z_{c} \cdot\left[\begin{array}{c}u \\v \\1\end{array}\right]=\left[\begin{array}{ccc}\frac{1}{d x} &amp; 0 &amp; u_{0} \\0 &amp; \frac{1}{d y} &amp; v_{0} \\0 &amp; 0 &amp; 1\end{array}\right]\left[\begin{array}{llll}f &amp; 0 &amp; 0 &amp; 0 \\0 &amp; f &amp; 0 &amp; 0 \\0 &amp; 0 &amp; 1 &amp; 0\end{array}\right] \cdot\left[\begin{array}{cc}R_{3 \times 3} &amp; T_{3 \times 1} \\O &amp; 1\end{array}\right] \cdot\left[\begin{array}{c}x_{w} \\y_{w} \\z_{w} \\1\end{array}\right] \\=\mathbf{M}_{1} \mathbf{M}_{2}\left[\begin{array}{c}x_{w} \\y_{w} \\z_{w} \\1\end{array}\right]\end{array}$$</p><p>上式子中，$M_1$为相机内参，包括相机的教具，光轴与图像平面的角点位置等内部参数，和外部因素无关，表示为:<br>$$M_1 = \left[\begin{array}{ccc}f_x &amp; 0 &amp; u_{0} \\0 &amp; f_y &amp; v_{0} \\0 &amp; 0 &amp; 1\end{array}\right]$$<br>如果相机像素坐标系的坐标轴不是完全垂直的吗，会有一个偏斜系数，坐标轴不垂直的的话，<strong>偏斜系数</strong>不为0。<br>$$M_1 = \left[\begin{array}{ccc}f_x &amp; s &amp; u_{0} \\0 &amp; f_y &amp; v_{0} \\0 &amp; 0 &amp; 1\end{array}\right]$$<br>$M_2$为相机外参，表征世界坐标系到相机坐标系的旋转关系，是相机在世界坐标系的下的位置姿态矩阵，认为是摄像机的外参，一般情况下，如果世界坐标系设置为相机坐标系，即二者重合，那么这个外参就是一个单位矩阵。<br>$$M_{2}=\left[\begin{array}{ll}R_{3 \times 3} &amp; T_{3 \times 1}\end{array}\right]=\left[\begin{array}{llll}r_{11} &amp; r_{12} &amp; r_{13} &amp; t_{1} \\r_{21} &amp; r_{22} &amp; r_{23} &amp; t_{2} \\r_{31} &amp; r_{32} &amp; r_{33} &amp; t_{3}\end{array}\right]$$</p><h4 id="镜头畸变"><a href="#镜头畸变" class="headerlink" title="镜头畸变"></a>镜头畸变</h4><p>实际使用中，相机镜头的畸变主要包括切向畸变与径向畸变两种。</p><ol><li>径向畸变是由于相机的透镜形状造成的；在光学上，桶形畸变是由于图像放大率随着离光轴的距离增加而减小，而枕形畸变却恰好相反。在这两种畸变中，穿过图像中心和光轴有交点的直线还能保持形状不变。</li><li>切向畸变则是整个相机的组装过程中造成的；</li></ol><p>图像的径向畸变往往被描述为一个低阶多项式模型：<br>$$\begin{aligned}&amp;x_{\text {corrected }}=x\left(1+k_{1} r^{2}+k_{2} r^{4}+k_{3} r^{6}\right) \\&amp;y_{\text {corrected }}=y\left(1+k_{1} r^{2}+k_{2} r^{4}+k_{3} r^{6}\right)\end{aligned}$$<br>注意它们都是归一化平面上的点，而不是像素平面上的点。</p><p>在式（5.11）描述的纠正模型中，对于畸变较小的图像中心区域，畸变纠正主要是 $k_1$起作用。而对于畸变较大的边缘区域主要是 $k_2$起作用。普通摄像头用这两个系数就能很好的纠正径向畸变。对畸变很大的摄像头，比如鱼眼镜头，可以加入 $k_3$ 畸变项对畸变进行纠正。</p><p>另一方面，对于切向畸变，可以使用另外的两个参数 <em>p</em>1*, p*2 来进行纠正:<br>$$\left\{\begin{array}{l}x_{\text {corrected }}=x\left(1+k_{1} r^{2}+k_{2} r^{4}+k_{3} r^{6}\right)+2 p_{1} x y+p_{2}\left(r^{2}+2 x^{2}\right) \\y_{\text {corrected }}=y\left(1+k_{1} r^{2}+k_{2} r^{4}+k_{3} r^{6}\right)+p_{1}\left(r^{2}+2 y^{2}\right)+2 p_{2} x y\end{array} .\right.$$</p><h4 id="相机待标定的参数"><a href="#相机待标定的参数" class="headerlink" title="相机待标定的参数"></a>相机待标定的参数</h4><ol><li><p>与镜头畸变有关的参数;$k_1$, $k_2$，$k_3$，$p_1$，$p_2$。</p><p>镜头畸变包括切向畸变和径向畸变两种；</p></li><li><p>外参，相机坐标系与世界坐标系之间的旋转与平移R, t；</p></li></ol><h3 id="相机标定原理"><a href="#相机标定原理" class="headerlink" title="相机标定原理"></a>相机标定原理</h3><h4 id="单应性矩阵"><a href="#单应性矩阵" class="headerlink" title="单应性矩阵"></a>单应性矩阵</h4><p>平面的单应性关系定义为从一个平面到另一个平面的投影映射。从世界坐标系中物体牌面到相机坐标系的成像平面指尖的映射就是平面单应性。我们采用的是标定板，可以认为是用到的模型平面。模型平面上的一点$Q=[X,Y,Z,1]^T$映射到成像平面上的一点$q = [x,y,1]^T$，他们之间的单应性关系可以描述为:<br>$$q = s H Q$$<br>单应性矩阵H本来有9个元素，但由于是单应性矩阵，考虑到其次坐标，可以提出到尺度因子中，所以有8个参数需要求解，自由度是8，求解8个参数至少需要四个点，两个平面中的对应的四组，可以得到8个方程。求解方程可以得到单应性关系。采用更多的点可以减小噪音和其他不确定因素的影响。我们是完全可以在不知道内外参的 情况下求解单应性关系的。一般情况下我们会有更多的点，opencv使用<code>findHomography </code>接口来求解单应性，有多种求解的算法。method为0，考虑所有点，计算结果使重投影 的误差最小，即最小化均方误差。method为RANSAC，从所有点集中随机选择子集，计算需要这样的随机样本，保留具有最大内部点的样本。</p><h4 id="相机求内参的两个基本约束"><a href="#相机求内参的两个基本约束" class="headerlink" title="相机求内参的两个基本约束"></a>相机求内参的两个基本约束</h4><p>给定标定平面的一张图片，它的单应性可以被估计，假设$H = [h_1,h_2,h_3]$,由公式可以得出<br>$$\left[\begin{array}{lll}\mathrm{h}_{1} &amp; \mathrm{~h}_{2} &amp; \mathrm{~h}_{3}\end{array}\right]=\lambda \mathbf{A}\left[\begin{array}{lll}\mathrm{r}_{1} &amp; \mathrm{r}_{2} &amp; \mathrm{t}\end{array}\right]$$<br>分解后可以得到：<br>$$\mathbf{h}_{1}=\lambda \cdot \mathbf{A} \cdot \mathbf{r}_{1}, \mathbf{h}_{2}=\lambda \cdot \mathbf{A} \cdot \mathbf{r}_{2}, \mathbf{h}_{3}=\lambda \cdot \mathbf{A} \cdot \mathrm{t}$$<br>$$\mathrm{r}_{1}=\frac{1}{\lambda} \cdot \mathbf{A}^{-1} \cdot \mathrm{h}_{1}, \mathrm{r}_{2}=\frac{1}{\lambda} \cdot \mathbf{A}^{-1} \cdot \mathrm{h}_{2}, \mathrm{t}=\frac{1}{\lambda} \cdot \mathbf{A}^{-1} \cdot \mathrm{h}_{3}$$</p><p>公式中，$\lambda$是一个尺度因子。由于$r_1$和$r_2$分别是绕着x, y轴旋转的，x轴,y轴均垂直于z轴，所以是正交的。所以我们可以得到．</p><ol><li><p>x轴与y轴正交<br>$$   \mathbf{h}_{1}^{T} \mathbf{A}^{-T} \mathbf{A}^{-1} \mathbf{h}_{2}=0   $$</p></li><li><p>旋转向量的模相等<br>$$   \mathbf{h}_{1}^{T} \mathbf{A}^{-T} \mathbf{A}^{-1} \mathbf{h}_{1}=\mathbf{h}_{2}^{T} \mathbf{A}^{-T} \mathbf{A}^{-1} \mathbf{h}_{2}   $$<br>对于一个给定的单应性矩阵，这是对内参的两个基础的约束条件。一个单应性矩阵 有8个自由度，六个参数（旋转3个角度，平移用三个偏移量），然而我们只得到了内参 的两个限制。接下里的章节，我们会给出一个几何解释。</p></li></ol><h4 id="相机标定内参的求解"><a href="#相机标定内参的求解" class="headerlink" title="相机标定内参的求解"></a>相机标定内参的求解</h4><p>令<br>$$\begin{gathered}\mathbf{B}=\mathbf{A}^{-T} \mathbf{A}^{-1} \equiv\left[\begin{array}{ccc}B_{11} &amp; B_{12} &amp; B_{13} \\B_{21} &amp; B_{22} &amp; B_{23} \\B_{31} &amp; B_{32} &amp; B_{33} p\end{array}\right]= \\{\left[\begin{array}{ccc}\frac{1}{\alpha^{2}} &amp; -\frac{\gamma}{\alpha^{2} \beta} &amp; \frac{v_{0} \gamma-u_{0} \beta}{\alpha^{2} \beta} \\-\frac{\gamma}{\alpha^{2} \beta} &amp; \frac{\gamma^{2}}{\alpha^{2} \beta^{2}}+\frac{1}{\beta^{2}} &amp; -\frac{\gamma\left(v_{0} \gamma-u_{0} \beta\right)}{\alpha^{2} \beta^{2}}-\frac{v_{0}}{\beta^{2}} \\\frac{v_{0} \gamma-u_{0} \beta}{\alpha^{2} \beta} &amp; -\frac{\gamma\left(v_{0} \gamma-u_{0} \beta\right)}{\alpha^{2} \beta^{2}}-\frac{v_{0}}{\beta^{2}} &amp; \frac{\left(v_{0} \gamma-u_{0} \beta\right)^{2}}{\alpha^{2} \beta^{2}}+\frac{v_{0}^{0}}{\beta^{2}}+1\end{array}\right]}\end{gathered}$$</p><p>注意到 $B$ 是对称的, 定义一个 6 维向量:<br>$$b=\left[B_{11}, B_{12}, B_{22}, B_{13}, B_{23}, B_{33}\right]^{T}$$<br>令矩阵 $H$ 的第 $i^{\text {th }}$ 个列向量为 $h_{i}=\left[h_{i 1}, h_{i 2}, h_{i 3}\right]^{T}$, 代人公式 $1.26$ 可以得到:<br>$$h_{i}^{T} \mathbf{B} h_{j}=v_{i j}^{T} b$$<br>式中:<br>$$v_{i j}=\left[h_{i 1} h_{j 1}, h_{i 1} h_{j 2}+h_{i 2} h_{j 1}, h_{i 2} h_{j 2}, h_{i 3} h_{j 1}+h_{i 1} h_{j 3}, h_{i 3} h_{j 2}+h_{i 2} h_{j 3}, h_{i 3} h_{j 3}\right]^{T}$$<br>$$\left[\begin{array}{c}\mathbf{v}_{12}^{T} \\\left(\mathbf{v}_{11}-\mathbf{v}_{22}\right)^{T}\end{array}\right] \mathbf{b}=\mathbf{0}$$</p><p>如果观察了 $n$ 张模型平面的图片, 可以将 $n$ 个 $1.33$ 这样的方程放在一起, 可以得到:<br>$$\mathbf{V} \cdot \mathbf{b}=0$$<br>公式中 $\mathbf{V}$ 是一个 $2 n \times 6$ 的矩阵 (对应 $2 n$ 个方程), $b$ 有六个参数。所以要求 $n&gt;=3$, 至少需要三张图片, 才可以得到唯一解 $b$ 。 $H$ 矩阵是可以根据单应性关系求解出来的, 每 一组标定板的 3D 真实坐标和二维坐标点就可以一个单应性关系。 $n=3$, 即三个单应性 矩阵, 也就是拍摄三张标定板的图片。<br>在张正友的论文中, 对不同的平面数进行了性能测试, 即使用不同的图片数, 来做 相机标定, 比较相机参数的正确性。如图1.10从实验结果可以看出, pattern 数从 2 到 3 , 误差降低非常明显。</p><p><img src="/2022/07/25/2022-7-25-%E7%9B%B8%E6%9C%BA%E6%A0%87%E5%AE%9A%E5%8E%9F%E7%90%86/image-20220726084842841.png" loading="lazy"></p><p>如果 $n=2$, 可以让畸变参数 $\gamma=0$, 那么 $[0,1,0,0,0,0] \mathbf{b}=0$,<br>一旦 $\mathbf{b}$ 被估计出来, 我们可以计算所有的相机内参矩阵 $\mathbf{A}$ :<br>$$\begin{aligned}v_{0} &amp;=\left(B_{12} B_{13}-B_{11} B_{23}\right) /\left(B_{11} B_{22}-B_{12}^{2}\right) \\\lambda &amp;=B_{33}-\left[B_{13}^{2}+v_{0}\left(B_{12} B_{13}-B_{11} B_{23}\right)\right] / B_{11} \\\alpha &amp;=\sqrt{\lambda / B_{11}} \\\beta &amp;=\sqrt{\lambda B_{11} /\left(B_{11} B_{22}-B_{12}^{2}\right)} \\\gamma &amp;=-B_{12} \alpha^{2} \beta / \lambda \\u_{0} &amp;=\gamma v_{0} / \beta-B_{13} \alpha^{2} / \lambda\end{aligned}$$<br>通过内参 $A$, 便可以求解每一个视图的外参矩阵, 通过公式 $1.23$, 可得到 :<br>$$$\mathbf{r}_{1}=\lambda^{-1} \cdot \mathbf{A}^{-1} \cdot \mathbf{h}_{1}, \mathbf{r}_{2}=\lambda^{-1} \cdot \mathbf{A}^{-1} \cdot \mathbf{h}_{2}, \mathbf{r}_{3}=\mathbf{r}_{1} \times \mathbf{r}_{2}, \mathbf{t}=\lambda^{-1} \cdot \mathbf{A}^{-1} \cdot \mathbf{h}_{3}$$<br>当然，因为数据中噪声的存在，这么计算得到的矩阵R&#x3D;“2，门]一般来说不能满足 旋转矩阵的特性。 公式L34我们可以认为是2“个方程，一般情况下，我们得到的方程数要多余未知数， 所以可以采用最小二乘法求解。</p><h4 id="极大似然求解"><a href="#极大似然求解" class="headerlink" title="极大似然求解"></a>极大似然求解</h4><p><strong>张正友标定法所提供的是一套获取初始相机参数的过程</strong>。而且，上述的方法是通过最小化代数距离（最小二乘法）来获得，这种方法不具有实际的物理意义。我们可以通过极大似然估计的方法来改善它，使用张正友标定得到的相机参数进行初始化，然后进行迭代求解。 我们可以得到一个标定板平面的<code>n</code>张照片，在这个标定板上有<code>m</code>个点。假设图像上 点的噪声，服从独立同分布。可以通过最小化下列的函数来得到极大似然估计：<br>$$\sum_{i=1}^{n} \sum_{j=1}^{m}\left\|\mathbf{m}_{i j}-\hat{\mathbf{m}}\left(\mathbf{A}, \mathbf{R}_{i}, \mathbf{t}_{i}, \mathbf{M}_{j}\right)\right\|^{2}$$<br>上式中 $\hat{m}\left(\mathbf{A}, \mathbf{R}_{i}, \mathbf{t}_{i}, M_{j}\right)$ 是空间中的点 $\mathbf{M}_{j}$ 在图像 $i$ 中的投影点, 可以根据公式1.22计算得到。求解公式上1.42的最小值是一个非线性优化问题，可以通过<code>levenberg-Marquardt</code>算法解决。 我们为什么不直接适用上述优化方法，求解这个最小二乘问题呢，是因为待求解的 参数比较多，很难找到一个好的初始化参数，所以<strong>张正友标定法的重点是提供了一个好的初始化</strong>。</p><p><strong>以上推导都是假设不存在畸变参数的情况下成立的</strong>。但是事实上，相机是存在畸变参数的，因此，张正友标定法还需要通过L-M算法对于参数进行迭代优化。</p><p>事实上，张正友标定法提供的是内参与外参矩阵的优化初始值。</p><p>张正友标定法的总逻辑：</p><p>1 在进行内参矩阵和外参矩阵的求解的时候，我们假设不存在畸变；<br>2 在进行畸变系数的求解的时候，我们假设求得的内参矩阵和外参矩阵是无误差的。<br>3 我们再通过L-M算法对于参数进行迭代优化。</p><h4 id="求解径向畸变参数"><a href="#求解径向畸变参数" class="headerlink" title="求解径向畸变参数"></a>求解径向畸变参数</h4><h3 id="张正友标定法"><a href="#张正友标定法" class="headerlink" title="张正友标定法"></a>张正友标定法</h3><h4 id="基本介绍"><a href="#基本介绍" class="headerlink" title="基本介绍"></a>基本介绍</h4><p>  ”张正友标定”是指张正友教授1998年提出的单平面棋盘格的摄像机标定方法[1]。文中提出的方法介于传统标定法和自标定法之间，但克服了传统标定法需要的高精度标定物的缺点，而仅需使用一个打印出来的棋盘格就可以。同时也相对于自标定而言，提高了精度，便于操作。因此张氏标定法被广泛应用于计算机视觉方面。</p><h2 id="IMU标定"><a href="#IMU标定" class="headerlink" title="IMU标定"></a>IMU标定</h2><h2 id="相机与IMU之间的外参标定"><a href="#相机与IMU之间的外参标定" class="headerlink" title="相机与IMU之间的外参标定"></a>相机与IMU之间的外参标定</h2><h2 id="RGBD相机的标定"><a href="#RGBD相机的标定" class="headerlink" title="RGBD相机的标定"></a>RGBD相机的标定</h2><h3 id="参考文献"><a href="#参考文献" class="headerlink" title="参考文献"></a>参考文献</h3><blockquote><p>1、Zhang, Zhengyou - 《IEEE Transactions on Pattern Analysis &amp; Machine Intelligence》 - 2000<br>2、J.More.Thelevenberg-marquardtalgorithm,implementationandtheory.InG.A.Watson, editor,NumericalAnalysis,LectureNotesinMathematics630.Springer-Verlag,1977.</p></blockquote>]]>
    </content>
    <id>https://grobenis.github.io/2022/07/25/2022-7-25-%E7%9B%B8%E6%9C%BA%E6%A0%87%E5%AE%9A%E5%8E%9F%E7%90%86/</id>
    <link href="https://grobenis.github.io/2022/07/25/2022-7-25-%E7%9B%B8%E6%9C%BA%E6%A0%87%E5%AE%9A%E5%8E%9F%E7%90%86/"/>
    <published>2022-07-25T02:44:00.000Z</published>
    <summary>
      <![CDATA[<p>本文介绍SLAM系统中不同传感器以及之间的标定原理与步骤。</p>
<p><strong>极简相机标定步骤：</strong></p>
<blockquote>
<p>1、打印一张棋盘格，把它贴在一个平面上，作为标定物。<br>2、通过调整标定物或摄像机的方向，为标定物拍摄一些不同方向的照片。<br>3、从照片中提取棋盘格角点。<br>4、估算理想无畸变的情况下，五个内参和六个外参。<br>5、应用最小二乘法估算实际存在径向畸变下的畸变系数。<br>6、极大似然法，优化估计，提升估计精度。</p>
</blockquote>
<p>参考来源：<a href="https://mp.weixin.qq.com/s/q-RIpy4YZUrrt9qCdR9Xvw">一文详解相机标定算法原理</a></p>
<p>本文讲解 SLAM 中传感器的标定原理，重点介绍张正友棋盘格标定法，从相机内参、外参与畸变模型入手，推导平面单应性矩阵与求解内参的两个基本约束。文章进一步说明通过极大似然估计和 L-M 算法迭代优化参数，并简要提及 IMU 标定及相机与 IMU 之间的外参标定。</p>
<blockquote>
<p><strong>一块棋盘格，求解相机内外参</strong></p>
</blockquote>]]>
    </summary>
    <title>传感器的标定</title>
    <updated>2026-08-25T13:18:01.010Z</updated>
  </entry>
  <entry>
    <author>
      <name>guoben</name>
    </author>
    <category term="学习" scheme="https://grobenis.github.io/categories/%E5%AD%A6%E4%B9%A0/"/>
    <category term="SLAM" scheme="https://grobenis.github.io/tags/SLAM/"/>
    <category term="Nerf" scheme="https://grobenis.github.io/tags/Nerf/"/>
    <content>
      <![CDATA[<p>Nerf(neural  radiance  field)神经辐射场</p><p>本文用来学习Nerf；</p><p>本文解读 NeRF（神经辐射场）论文，介绍如何用 MLP 神经网络将静态 3D 场景隐式表示为连续的 5D 函数，输出各点的体积密度与视角相关颜色，并沿相机光线做可微体素渲染。文章重点讲解了场景表示、基于分段随机采样的离散近似渲染算法，以及位置编码、多层级采样等训练技巧，最终实现从任意视角合成逼真的新视图。</p><blockquote><p><strong>以连续辐射场隐式建模场景，任意视角皆可渲染</strong></p></blockquote><span id="more"></span><h2 id="NeRF-Representing-Scenes-asNeural-Radiance-Fields-for-View-Synthesis"><a href="#NeRF-Representing-Scenes-asNeural-Radiance-Fields-for-View-Synthesis" class="headerlink" title="NeRF: Representing Scenes asNeural Radiance Fields for View Synthesis"></a>NeRF: Representing Scenes asNeural Radiance Fields for View Synthesis</h2><h3 id="概览"><a href="#概览" class="headerlink" title="概览"></a>概览</h3><p>NeRF可以简要概括为用一个MLP神经网络去隐式地学习一个静态3D场景。为了训练网络，针对一个静态场景，需要提供大量相机参数已知的图片。基于这些图片训练好的神经网络，即可以从任意角度渲染出图片结果了。理解NeRF主要需要理解三个方面：</p><ul><li>如何用NeRF来表示3D场景？</li><li>如何基于NeRF渲染出2D图像？</li><li>如何训练NeRF？</li></ul><p>即我们要理解NeRF是如何从一系列2D图像中学习到3D场景，又是如何渲染出2D图像。</p><h3 id="摘要"><a href="#摘要" class="headerlink" title="摘要"></a><strong>摘要</strong></h3><p>我们提出了一种方法，该方法通过使用一组稀疏输入视图优化底层连续体积场景函数来合成复杂场景的新视图，从而获得最先进的结果。我们的算法使用全连接（非卷积）深度网络表示场景，其输入是单个连续 5D 坐标（空间位置 $(x,y,z)$ 和观察方向 $(θ,φ)$），其输出是该空间位置的体积密度和与视图相关的发射辐射。我们通过沿相机光线查询 5D 坐标来合成视图，并使用经典的体积渲染技术将输出颜色和密度投影到图像中。因为体绘制自然可微，优化我们的表示所需的唯一输入是一组具有已知相机姿势的图像。我们描述了如何有效地优化神经辐射场以渲染具有复杂几何形状和外观的场景的逼真的新视图，并展示了优于先前在神经渲染和视图合成方面的工作的结果。查看合成结果最好以视频形式观看，因此我们敦促读者观看我们的补充视频以进行令人信服的比较。</p><p><strong>关键词</strong>：场景表示，视图合成，基于图像的渲染，体绘制，3D深度学习</p><h3 id="引言"><a href="#引言" class="headerlink" title="引言"></a>引言</h3><p>​本文将静态场景表示为一个连续的5D 函数，它输出空间中每个点 $(x,y,z)$ 在每个方向$ (θ,φ)$ 发射的辐射度，以及每个点的密度，其作用类似于控制辐射度的微分不透明度 由穿过 $(x,y,z) $的射线累积。</p><p>​本文方法优化了一个没有任何卷积层的深度全连接神经网络（通常称为多层感知器或 MLP），通过从单个 5D 坐标$ (x,y,z,θ,φ)$ 回归到单个体积密度和视图来表示此函数- 依赖 RGB 颜色。为了从特定视点渲染这个神经辐射场 (NeRF)，我们：</p><ol><li>将相机光线穿过场景以生成一组采样的 3D 点，</li><li>使用这些点及其对应的 2D 观察方向作为神经网络的输入来生成输出集颜色和密度，以及</li><li>使用经典的体积渲染技术将这些颜色和密度累积到 2D 图像中。</li></ol><p>​因为这个过程是自然可微的，我们可以使用梯度下降来优化这个模型，通过最小化每个观察到的图像和从我们的表示中渲染的相应视图之间的误差。跨多个视图最小化此错误可鼓励网络通过将高体积密度和准确颜色分配给包含真正底层场景内容的位置来预测场景的连贯模型。</p><p><img src="/2022/07/24/2022-7-24-Nerf/image-20220802001655110.png" loading="lazy"></p><p>图 1：我们提出了一种方法，可以优化来自一组输入图像的场景的连续 5D 神经辐射场表示（任意连续位置的体积密度和视图相关颜色）。 我们使用体积渲染技术沿光线累积此场景表示的样本，以从任何视点渲染场景。 在这里，我们可视化在周围半球上随机捕获的合成鼓场景的 100 个输入视图集，并展示了从优化的 NeRF 表示中呈现的两个新视图。</p><p>​图 2 可视化了整个流程。我们发现，针对复杂场景优化神经辐射场表示的基本实现不能收敛到足够高分辨率的表示，并且在每个摄像机光线所需的样本数量方面效率低下。我们通过使用位置编码转换输入 5D 坐标来解决这些问题，使 MLP 能够表示更高频率的函数，并且我们提出了一种分层采样程序来减少充分采样这种高频场景表示所需的查询数量。我们的方法继承体积表示的好处：两者都可以表示复杂的现实世界几何和外观，并且非常适合使用投影图像进行基于梯度的优化。至关重要的是，我们的方法在对复杂场景进行高分辨率建模时克服了离散体素网格的高昂存储成本。总之，我们的技术贡献是：– 一种将具有复杂几何形状和材料的连续场景表示为 5D 神经辐射场的方法，参数化为基本 MLP 网络。– 基于经典体积渲染技术的可微渲染过程，我们使用它来优化这些来自标准 RGB 图像的表示。这包括一种分层采样策略，用于将 MLP 的容量分配给具有可见场景内容的空间。</p><p>​我们发现，针对复杂场景优化神经辐射场表示的基本实现不能收敛到足够高分辨率的表示，并且在每摄像机光线所需的样本数量方面效率低下。我们通过使用位置编码转换输入 5D 坐标来解决这些问题，使 MLP 能够表示更高频率的函数，并且我们提出了一种分层采样程序来减少充分采样这种高频场景表示所需的查询数量。</p><p>​我们的方法继承了体积表示法的好处：两者都可以表示复杂的现实世界几何和外观，并且非常适合使用投影图像进行基于梯度的优化。至关重要的是，我们的方法在对复杂场景进行高分辨率建模时克服了离散体素网格的高昂存储成本。总之，我们的技术贡献是：</p><ol><li>一种将具有复杂几何形状和材料的连续场景表示为 5D 神经辐射场的方法，参数化为基本 MLP 网络。</li><li>基于经典体积渲染技术的可微渲染过程，我们使用它来优化这些来自标准 RGB 图像的表示。这包括一种分层采样策略，用于将 MLP 的容量分配给具有可见场景内容的空间。</li><li>将每个输入 5D 坐标映射到更高维空间的位置编码，这使我们能够成功地优化神经辐射场以表示高频场景内容。</li></ol><p>​我们证明了我们得到的神经辐射场方法在数量和质量上都优于 state-of-最先进的视图合成方法，包括将神经 3D 表示拟合到场景的工作以及训练深度卷积网络以预测采样体积表示的工作。据我们所知，本文提出了第一个连续的神经场景表示，它能够从在自然环境中捕获的 RGB 图像中渲染真实物体和场景的高分辨率照片般逼真的新颖视图。</p><h3 id="三-用神经辐射场表示场景"><a href="#三-用神经辐射场表示场景" class="headerlink" title="三 用神经辐射场表示场景"></a>三 用神经辐射场表示场景</h3><p><img src="/2022/07/24/2022-7-24-Nerf/image-20220803074258414.png" loading="lazy"></p><p>NeRF函数是将一个连续的场景表示为一个输入为5D向量的函数，包括一个空间点的3D坐标位置 x&#x3D;(x,y,z) ，以及视角方向 d&#x3D;(θ,ϕ) 。这个神经网络可以写作：<br>$$F_Θ:(x,d)→(c,σ)$$<br>输出结果中， σ是对应3D位置（或者说是体素）的密度，而 c&#x3D;(r,g,b) 是视角相关的该3D点颜色。在具体的实现中， x 首先输入到MLP网络中，并输出 σ 和中间特征，中间特征和 d 再输入到额外的全连接层中并预测颜色。因此，体素密度只和空间位置有关，而颜色则与空间位置以及观察的视角都有关系。基于view dependent 的颜色预测，能够得到不同视角下不同的光照效果，非常惊艳。可以看出，NeRF函数的表示非常简单。</p><h4 id="原文翻译"><a href="#原文翻译" class="headerlink" title="原文翻译"></a>原文翻译</h4><p>我们将连续场景表示为 5D 向量值函数，其输入是 3D 位置 x&#x3D; (x,y,z) 和 2D 观察方向 (θ,φ)，其输出是发射颜色 c&#x3D; (r,g,b) 和 体积密度σ。 在实践中，我们表示</p><h3 id="四-基于辐射场的体素渲染算法"><a href="#四-基于辐射场的体素渲染算法" class="headerlink" title="四 基于辐射场的体素渲染算法"></a>四 基于辐射场的体素渲染算法</h3><p>NeRF 函数得到的是一个3D空间点的颜色和密度信息，但当用一个相机去对这个场景成像时，所得到的2D 图像上的一个像素实际上对应了一条从相机出发的射线上的所有连续空间点。我们需要通过渲染算法从这条射线上的所有点得到这条射线的最终渲染颜色。同时，为了保证网络可以训练，NeRF中需要采用可微的渲染方法。</p><h4 id="经典的提速渲染方法"><a href="#经典的提速渲染方法" class="headerlink" title="经典的提速渲染方法"></a>经典的提速渲染方法</h4><p>论文中首先介绍了经典的体素渲染volume rendering [2] 方法。体素密度 σ(x) 可以被理解为，一条射线 r 在经过 x 处的一个无穷小的粒子时被终止的概率，这个概率是可微的。换句话说，有点类似于这个点的不透明度。由于一条射线上的点是连续的，自然的想法是这条射线的颜色可以由积分的方式得到。将一个相机射线标记为 r(t)&#x3D;o+td ，这里 o 是射线原点， d 是前述的相机射线角度，t 的近段和远端边界分别为 tn 以及 tf 。那么这条射线的颜色，则可以用积分的方式表示为：<br>$$C(r)=\int_{t_{n}}^{t_{f}} T(t) \cdot \sigma(\boldsymbol{r}(t)) \cdot \boldsymbol{c}(\boldsymbol{r}(t), \boldsymbol{d}) d t$$<br>此处, $T(t)$ 是射线从 $t_{n}$ 到 $t$ 这一段路径上的累计透明度，可以被理解为这条射线从 tn 到 t 一路上没有击中任何粒子的概率。其具体的形式为：<br>$$T(t)=\exp \left(-\int_{t_{n}}^{t} \sigma(\boldsymbol{r}(s)) d s\right)$$<br>但是，实际应用中，我们并不可能够用NeRF去估计连续的3D点的信息，因此就需要数值近似的方法。这里也是NeRF中非常核心的一个部分。</p><h4 id="基于分段随机采样的离散近似volume-rendering方式"><a href="#基于分段随机采样的离散近似volume-rendering方式" class="headerlink" title="基于分段随机采样的离散近似volume rendering方式"></a>基于分段随机采样的离散近似volume rendering方式</h4><p>一个直观且很常用的思路是，在需要求积的区域均匀地采样N个点进行近似计算。但作者提出，这样的方式会导致MLP只需要学习一系列离散点的信息，最终会限制NeRF的分辨率，使得最终生成的结果不够清晰。作为替代，作者提出了一种简单有效的方法，如上图所示，首先将射线需要积分的区域分为N份，然后在每一个小区域中进行均匀随机采样。这样的方式能够在只采样离散点的前提下，保证采样位置的连续性。第i个采样点可以表示为：<br>$$t_{i}=\boldsymbol{U}\left[t_{n}+\frac{i-1}{N}\left(t_{f}-t_{n}\right), t_{n}+\frac{i}{N}\left(t_{f}-t_{n}\right)\right]$$<br>基于这些采样点, 我们可以将上面的积分简化为求和的形式：<br>$$\hat{C}(\boldsymbol{r})=\sum_{i=1}^{N} T_{i} \cdot\left(1-\exp \left(-\sigma_{i} \cdot \delta_{i}\right)\right) \cdot \boldsymbol{c}_{i}$$<br>其中, $\delta_{i}=t_{i+1}-t_{i}$ 是邻近两个采样点之间的距离。此处 $T(t)$ 为:<br>$$T_{i}=\exp \left(-\sum_{j=1}^{i-1} \sigma_{j} \delta_{j}\right)$$<br>同样, 基于分段采样的渲染方式也是可微的。因此, 基于这样的渲染方式, 我们就可以用NeRF函 数从任意角度中洹染出图片。</p><h3 id="五-NERF的训练过程"><a href="#五-NERF的训练过程" class="headerlink" title="五 NERF的训练过程"></a>五 NERF的训练过程</h3><p>介绍完NeRF的基本原理，论文接下来介绍了NeRF中的两个重要Trick，以及训练方式。</p><h4 id="1-训练高质量NeRF的重要技巧-——-位置信息编码"><a href="#1-训练高质量NeRF的重要技巧-——-位置信息编码" class="headerlink" title="1. 训练高质量NeRF的重要技巧 —— 位置信息编码"></a>1. 训练高质量NeRF的重要技巧 —— 位置信息编码</h4><p>NeRF函数的输入为位置和角度信息，作者发现直接将位置和角度作为网络的输入得到的结果是相对模糊的（见实验部分）。而用positon encoding 的方式将位置信息映射到高频则能有效提升清晰度效果。具体而言，这里采用的是与Transformer 中类似的正余弦周期函数的形式。我的理解是，采用position encoding 能够使得网络更容易的理解并建模位置信息。</p><h4 id="2-训练高质量NeRF的重要技巧-——-多层级体素采样"><a href="#2-训练高质量NeRF的重要技巧-——-多层级体素采样" class="headerlink" title="2. 训练高质量NeRF的重要技巧 —— 多层级体素采样"></a>2. 训练高质量NeRF的重要技巧 —— 多层级体素采样</h4><p>NeRF的渲染过程计算量很大，每条射线都要采样很多点。但实际上，一条射线上的大部分区域都是空区域，或者是被遮挡的区域，对最终的颜色没有啥贡献。因此，作者采用了一种“coarse to fine” 的形式，同时优化coarse 网络和fine 网络。这一部分也非常有趣。首先对于coarse 网络，我们可以采样较为稀疏的 Nc 个点，并将前述的离散求和函数重新表示为：<br>$$\hat{C}_c(r)=∑_{i=1}^{N_c}w_ic_i ，其中 w_i=T_i⋅(1−exp(−σ_i⋅δ_i))$$<br>接下来，可以对 wi 做归一化<br>$$\hat{w}_i=\frac{w_i}{\sum^{N_c}_{j=1}w_j}$$<br>此处的 $\hat{w}_i$ 可以看作是沿着射线的概率密度函数（PDF），如下图所示。通过这个概率密度函数，我们可以粗略地得到射线上物体的分布情况。</p><img src="https://pic4.zhimg.com/80/v2-acd57623f49c25dd61730007288cc63f_720w.jpg" alt="img" style="zoom: 33%;" /><p>接下来，基于得到的概率密度函数来采样 Nf 个点，并用这Nf 个点和前面的Nc 个点一同计算fine 网络的渲染结果 C^f(r) 。虽然coarse to fine 是计算机视觉领域中常见的一个思路，但这篇方法中用coarse 网络来生成概率密度函数，再基于概率密度函数采样更精细的点算的上是很有趣新颖的做法了。二阶段的采样示意图如下所示：</p><img src="https://pic2.zhimg.com/80/v2-f87f541db573e2673e5c514198e5dba5_720w.jpg" alt="img" style="zoom:33%;" /><h4 id="损失函数与一些训练细节"><a href="#损失函数与一些训练细节" class="headerlink" title="损失函数与一些训练细节"></a>损失函数与一些训练细节</h4><p>最后，训练损失的定义倒是非常简单，直接定义在渲染结果上的L2损失(同时优化coarse 和 fine）：<br>$$L=∑_{r∈R}[‖\hat{C}_c(r)−C(r)‖^2_2−‖\hat{C}_f(r)−C(r)‖^2_2]$$<br>训练时长方面，论文中提及的速度是一个场景要用单卡V100 训练1-2天左右。</p><h4 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h4><p>NeRF的效果非常酷炫，同时也提供了一种3D场景建模的新形式。NeRF本身当然存在很多的问题，最重要的是其渲染速度很慢，渲染一帧约要1分钟左右。但后续的新方法提出了很多优化的方式，大大优化了速度问题，也使得NeRF有了在实际场景应用的可能。除了速度，另外一个值得关注的点在于，NeRF本身就可以看作是对场景的一种conditional 的表示（NeRF中的视角就是一种condition），那么自然的想法就是拓展condition来建模动态场景。比如最近的AD-NeRF 就是将语音特征作为condition，使得NeRF可以实现Talking-head Animation 的效果。总的来说，NeRF简洁的表示方式和优秀的效果带来了丰富的可玩性和拓展性，是非常棒的工作。此外，在论文附录中，还对论文中所使用的归一化设备坐标系（Normalized Device Coordinate, NDC)进行了详细的推导，感兴趣的话可以看看。</p><h3 id="相关链接"><a href="#相关链接" class="headerlink" title="相关链接"></a>相关链接</h3><ul><li>Code: <a href="https://link.zhihu.com/?target=https://github.com/yenchenlin/nerf-pytorch">yenchenlin&#x2F;nerf-pytorch</a></li><li>Awesome-Nerf: <a href="https://link.zhihu.com/?target=https://github.com/yenchenlin/awesome-NeRF">yenchenlin&#x2F;awesome-NeRF</a></li></ul>]]>
    </content>
    <id>https://grobenis.github.io/2022/07/24/2022-7-24-Nerf/</id>
    <link href="https://grobenis.github.io/2022/07/24/2022-7-24-Nerf/"/>
    <published>2022-07-24T15:48:54.000Z</published>
    <summary>
      <![CDATA[<p>Nerf(neural  radiance  field)神经辐射场</p>
<p>本文用来学习Nerf；</p>
<p>本文解读 NeRF（神经辐射场）论文，介绍如何用 MLP 神经网络将静态 3D 场景隐式表示为连续的 5D 函数，输出各点的体积密度与视角相关颜色，并沿相机光线做可微体素渲染。文章重点讲解了场景表示、基于分段随机采样的离散近似渲染算法，以及位置编码、多层级采样等训练技巧，最终实现从任意视角合成逼真的新视图。</p>
<blockquote>
<p><strong>以连续辐射场隐式建模场景，任意视角皆可渲染</strong></p>
</blockquote>]]>
    </summary>
    <title>Nerf论文学习</title>
    <updated>2026-08-25T13:18:00.971Z</updated>
  </entry>
  <entry>
    <author>
      <name>guoben</name>
    </author>
    <category term="SLAM" scheme="https://grobenis.github.io/categories/SLAM/"/>
    <category term="角点检测" scheme="https://grobenis.github.io/tags/%E8%A7%92%E7%82%B9%E6%A3%80%E6%B5%8B/"/>
    <content>
      <![CDATA[<p>本文总结 SLAM 中常用的角点检测算法：先介绍人工设计特征点应具备的可重复性、可区别性、高效率与本地性，以及关键点和描述子的组成；再逐一讲解 FAST、Harris、Shi-Tomas、ORB、SIFT、SURF 的原理、优缺点及其在 SLAM 中的适用性。</p><blockquote><p><strong>小而稳定的特征，撑起整个位姿估计</strong></p></blockquote><span id="more"></span><h2 id="特征点"><a href="#特征点" class="headerlink" title="特征点"></a>特征点</h2><p>人工设计的特征点该具有的特性</p><blockquote><ol><li>可重复性（Repeatability）：相同的“区域”可以在不同的图像中被找到。</li><li>可区别性（Distinctiveness）：不同的“区域”有不同的表达。</li><li>高效率（Efficiency）：同一图像中，特征点的数量应远小于像素的数量。</li><li>本地性（Locality）：特征仅与一小片图像区域相关。</li></ol></blockquote><h3 id="组成"><a href="#组成" class="headerlink" title="组成"></a>组成</h3><ol><li><p>关键点</p></li><li><p>描述子</p><p>描述子设计原则：外观相似的特征应该又相似的描述子；</p></li></ol><h2 id="常用角点检测算法"><a href="#常用角点检测算法" class="headerlink" title="常用角点检测算法"></a>常用角点检测算法</h2><p>SLAM中常用的角点检测算法整理如下：</p><p>本文根据ORBSLAM3中的综述表格进行总结：</p><p><img src="/2022/05/31/2022-05-31-SLAM_Feature_detecter/image-20220531214022010.png" loading="lazy"></p><h3 id="FAST关键点"><a href="#FAST关键点" class="headerlink" title="FAST关键点"></a>FAST关键点</h3><ol><li><p>单纯的FAST关键点，没有描述子</p></li><li><p>FAST 是一种角点，主要检测局部像素灰度变化明显的地方，以速度快著称。它的思<br>想是：如果一个像素与它邻域的像素差别较大（过亮或过暗）, 那它更可能是角点。相比于其他角点检测算法，FAST 只需比较像素亮度的大小，十分快捷。它的检测过程如下（见图7-3 ）：</p><p><img src="/2022/05/31/2022-05-31-SLAM_Feature_detecter/image-20220615150207855.png" alt="image-20220615150207855" loading="lazy"></p><ol><li>在图像中选取像素p，假设它的亮度为$I_p$。</li><li>设置一个阈值T(比如$I_p$ 的20%)。</li><li>以像素p 为中心, 选取半径为3 的圆上的16 个像素点。</li><li>假如选取的圆上，有连续的N 个点的亮度大于Ip + T 或小于Ip - T，那么像素p可以被认为是特征点(N 通常取12，即为FAST-12。其它常用的N 取值为9 和11，他们分别被称为FAST-9，FAST-11)。</li><li>循环以上四步，对每一个像素执行相同的操作。</li></ol></li></ol><p>在FAST-12 算法中，为了更高效，可以添加一项预测试操作，以快速地排除绝大多数不是角点的像素。具体操作为，对于每个像素，直接检测邻域圆上的第1，5，9，13 个像素的亮度。只有当这四个像素中有三个同时大于Ip + T 或小于Ip 􀀀 T 时，当前像素才有可能是一个角点，否则应该直接排除。这样的预测试操作大大加速了角点检测。此外，原始的FAST 角点经常出现“扎堆”的现象。所以在第一遍检测之后，还需要用非极大值抑制（Non-maximal suppression），在一定区域内仅保留响应极大值的角点，避免角点集中的问题。</p><h4 id="缺点"><a href="#缺点" class="headerlink" title="缺点"></a>缺点</h4><p>FAST 特征点的计算仅仅是比较像素间亮度的差异，速度非常快，但它也有一些问题。</p><ul><li>首先，FAST 特征点数量很大且不确定，而我们往往希望对图像提取固定数量的特征。因此，在ORB 中，对原始的FAST 算法进行了改进。我们可以指定最终要提取的角点数量N，对原始FAST 角点分别计算Harris 响应值，然后选取前N 个具有最大响应值的角点，<br>作为最终的角点集合。</li><li>其次，FAST 角点不具有方向信息。而且，由于它固定取半径为3 的圆，存在尺度问题：远处看着像是角点的地方，接近后看可能就不是角点了。</li></ul><h3 id="Harris特征点"><a href="#Harris特征点" class="headerlink" title="Harris特征点"></a>Harris特征点</h3><p>OPENCV描述：In the last chapter, we saw that corners are regions in the image with large variation in intensity in all the directions. One early attempt to find these corners was done by <strong>Chris Harris &amp; Mike Stephens</strong> in their paper <strong>A Combined Corner and Edge Detector</strong> in 1988, so now it is called the Harris Corner Detector. He took this simple idea to a mathematical form. It basically finds the difference in intensity for a displacement of (u,v) in all directions. </p><p><img src="/2022/05/31/2022-05-31-SLAM_Feature_detecter/image-20220615153225850.png" loading="lazy"></p><p><img src="/2022/05/31/2022-05-31-SLAM_Feature_detecter/image-20220615154043028.png" loading="lazy"></p><blockquote><p><a href="https://docs.opencv.org/4.x/dc/d0d/tutorial_py_features_harris.html">openCV链接</a></p></blockquote><p><img src="/2022/05/31/2022-05-31-SLAM_Feature_detecter/image-20220615154149887.png" loading="lazy"></p><h3 id="Shi-Tomas特征点"><a href="#Shi-Tomas特征点" class="headerlink" title="Shi-Tomas特征点"></a>Shi-Tomas特征点</h3><blockquote><p>Harris 和 Shi-Tomasi 都是基于梯度计算的角点检测方法，Shi-Tomasi 的效果要好一些。</p><ul><li>基于梯度的检测方法有一些缺点: 计算复杂度高，图像中的噪声可以阻碍梯度计算。</li></ul><p>想要提高检测速度的话，可以考虑基于模板的方法：FAST角点检测算法。该算法原理比较简单，但实时性很强。</p><p>Harris 角点检测中每个窗口的分数公式是将矩阵 M 的行列式与 M 的迹相减：</p><p><img src="https://www.zhihu.com/equation?tex=R+=+%CE%BB_1%CE%BB_2+%E2%88%92+k+(%CE%BB_1+++%CE%BB_2)%5E2+%5C%5C" alt="[公式]" loading="lazy"></p><p>由于 Harris 角点检测算法的稳定性和 k 值有关，而 k 是个经验值，不好设定最佳值。</p><p>Shi-Tomasi 发现，角点的稳定性其实和矩阵 M 的较小特征值有关，于是直接用较小的那个特征值作为分数。这样就不用调整k值了。</p><p>所以 Shi-Tomasi 将分数公式改为如下形式：</p><p><img src="https://www.zhihu.com/equation?tex=R+=+min+(%CE%BB_1,+%CE%BB_2)+%5C%5C" alt="[公式]" loading="lazy"></p><p>和 Harris 一样，如果该分数大于设定的阈值，我们就认为它是一个角点。</p><p>我们可以把它绘制到 λ1 ～ λ2 空间中，就会得到下图：</p><p><img src="/2022/05/31/2022-05-31-SLAM_Feature_detecter/v2-56a5bd60f1e79bc8b6f0b652dee589bc_1440w.jpg" loading="lazy"></p></blockquote><h3 id="ORB特征点"><a href="#ORB特征点" class="headerlink" title="ORB特征点"></a>ORB特征点</h3><ol><li>改进了FAST关键点;</li><li>ORB特征点有方向，采用BRIEF描述子进行描述；</li><li>具有旋转和尺度不变性，速度快；</li></ol><h4 id="改进FAST关键点"><a href="#改进FAST关键点" class="headerlink" title="改进FAST关键点"></a>改进FAST关键点</h4><p>针对FAST 角点不具有方向性和尺度的弱点，ORB 添加了尺度和旋转的描述。尺度不变性由构建图像金字塔¬，并在金字塔的每一层上检测角点来实现。而特征的旋转是由灰度质心法（Intensity Centroid）实现的。我们稍微介绍一下。质心是指以图像块灰度值作为权重的中心。其具体操作步骤如下[35]：</p><ol><li><p>在一个小的图像块B 中，定义图像块的矩为：<br>  $$  m_{pq} = \sum_{x;y\in \Beta} x^py^qI(x; y); p,q = \{0,1\}  $$</p></li><li><p>通过矩可以找到图像块的质心：<br>  $$  C = (\frac{m_{10}}{m_{00}}，\frac{m_{10}}{m_{00}})  $$<br>  连接图像块的几何中心O 与质心C，得到一个方向向量$OC$,于是特征点的方向可以定义为$\theta = \arctan{\frac{m01}{m10}}$。</p></li></ol><p>通过以上方法，FAST 角点便具有了尺度与旋转的描述，大大提升了它们在不同图像之间表述的鲁棒性。所以在ORB 中，把这种改进后的FAST 称为Oriented FAST。</p><h4 id="BRIEF-描述子"><a href="#BRIEF-描述子" class="headerlink" title="BRIEF 描述子"></a>BRIEF 描述子</h4><p>​BRIEF 是一种二进制描述子，它的描述向量由许多个0 和1 组成，这里的0 和1 编码了关键点附近两个像素（比如说p 和q）的大小关系：如果p 比q 大，则取1，反之就取0。如果我们取了128 个这样的p; q，最后就得到128 维由0，1 组成的向量。那么，p和q 如何选取呢？在作者原始的论文中给出了若干种挑选方法，大体上都是按照某种概率分布，随机地挑选p 和q 的位置，读者可以阅读BRIEF 论文或OpenCV 源码以查看它的具体实现[34]。BRIEF 使用了随机选点的比较，速度非常快，而且由于使用了二进制表达，存储起来也十分方便，适用于实时的图像匹配。原始的BRIEF 描述子不具有旋转不变性的，因此在图像发生旋转时容易丢失。而ORB 在FAST 特征点提取阶段计算了关键点的方向，所以可以利用方向信息，计算了旋转之后的“Steer BRIEF”特征，使ORB 的描述子具有较好的旋转不变性。<br>由于考虑到了旋转和缩放，使得ORB 在平移、旋转、缩放的变换下仍有良好的表现。同时，FAST 和BRIEF 的组合也非常的高效，使得ORB 特征在实时SLAM 中非常受欢迎。</p><h3 id="SIFT"><a href="#SIFT" class="headerlink" title="SIFT"></a>SIFT</h3><blockquote><p>SIFT(Scale-Invariant Feature Transform)特征，即尺度不变特征变换，是一种计算机视觉的特征提取算法，用来侦测与描述图像中的局部性特征。</p><p>实质上，它是在不同的尺度空间上查找关键点(特征点)，并计算出关键点的方向。SIFT所查找到的关键点是一些十分突出、不会因光照、仿射变换和噪音等因素而变化的点，如角点、边缘点、暗区的亮点及亮区的暗点等。</p><ol><li>具有尺度不变性，充分考虑了图像变换过程中出现的光照，尺度，旋转等变化；计算量较大；</li><li>无法实时计算SIFT特征，无法用在定位与建图中；</li></ol></blockquote><h3 id="SURF"><a href="#SURF" class="headerlink" title="SURF"></a>SURF</h3><blockquote><p>SIFT和SURF由于计算复杂度的原因在SLAM中应用较少</p><p>链接：<a href="https://www.cnblogs.com/multhree/p/11296945.html">https://www.cnblogs.com/multhree/p/11296945.html</a></p></blockquote>]]>
    </content>
    <id>https://grobenis.github.io/2022/05/31/2022-05-31-SLAM_Feature_detecter/</id>
    <link href="https://grobenis.github.io/2022/05/31/2022-05-31-SLAM_Feature_detecter/"/>
    <published>2022-05-31T13:38:32.000Z</published>
    <summary>
      <![CDATA[<p>本文总结 SLAM 中常用的角点检测算法：先介绍人工设计特征点应具备的可重复性、可区别性、高效率与本地性，以及关键点和描述子的组成；再逐一讲解 FAST、Harris、Shi-Tomas、ORB、SIFT、SURF 的原理、优缺点及其在 SLAM 中的适用性。</p>
<blockquote>
<p><strong>小而稳定的特征，撑起整个位姿估计</strong></p>
</blockquote>]]>
    </summary>
    <title>SLAM中常用的角点检测算法</title>
    <updated>2026-08-25T13:18:00.909Z</updated>
  </entry>
  <entry>
    <author>
      <name>guoben</name>
    </author>
    <content>
      <![CDATA[<p>原文链接：<a href="https://arxiv.org/abs/2007.11898">https://arxiv.org/abs/2007.11898</a></p><p>参考翻译文章：<a href="https://zhuanlan.zhihu.com/p/403299796">https://zhuanlan.zhihu.com/p/403299796</a></p><p>本文学习 ORB-SLAM3 论文，该系统是首个在单目、双目与 RGB-D 相机上同时支持纯视觉、视觉惯性与多地图的 SLAM 系统。文章重点介绍两大创新：基于最大后验估计的紧耦合视觉惯性前端，以及依赖高召回率位置识别算法的多地图机制——跟踪丢失时开启新地图、回访建图区域时无缝合并，从而在全流程中复用历史信息并显著提升精度。</p><blockquote><p><strong>多地图无缝衔接，SLAM永不迷航</strong></p></blockquote><span id="more"></span><h2 id="摘要"><a href="#摘要" class="headerlink" title="摘要"></a>摘要</h2><p>本文提出的ORBSLAM3，采用<strong>针孔相机和鱼眼镜头模型</strong>，<strong>第一个在单目、双目以及RGBD相机上都能执行纯视觉、视觉-惯性以及多地图的SLAM系统</strong>。</p><ol><li>第一个主要的创新点是<strong>基于特征的紧耦合视觉惯性SLAM系统</strong>，<strong>完全依赖于最大后验估计MAP算法</strong>，甚至在IMU初始化阶段期间。这个SLAM系统，<strong>在实时的、小型或大型、室内或室外环境中，都能鲁棒地运行系统</strong>，并且<strong>要比之前的方法精确2到5倍</strong>。</li><li>第二个主要的创新点是<strong>多地图系统</strong>，依赖于一个提升召回率的新的位置识别算法。基于此，ORB-SLAM3在长期缺乏视觉信息的情况下依旧能够运行：当特征跟踪丢失时，它将会开启一个新地图，遇到建图过的区域将会无缝合并之前的地图。与那些只是用前几秒信息的视觉里程计系统相比，ORBSLAM3系统是第一个，能在每个算法阶段，复用之前的所有信息。这个允许包括在光束平差法共视关键帧阶段，其提供提升精度的高视差观测，即使在时间序列上是分隔的，或者他们来自之前的建图会话Session。</li></ol><p>我们的实验显示，在所有的传感器配置中，ORBSLAM3和已有文献中最好的系统一样鲁棒，并且要更加精确。注意的是，我们的双目-惯性SLAM在EuRoC飞行器数据集上达到了平均3.6cm的精度，在TUM-VI（AR&#x2F;VR场景代表）快速手持运动环境的室内数据集中达到了平均9mm的精度。为了社区的明天，本文开源了代码。</p><h2 id="引言"><a href="#引言" class="headerlink" title="引言"></a>引言</h2><p>表1给出了最具代表的视觉和视觉-惯性系统的总结，显示了用于估计和数据关联的主要技术。定性的精度对比和鲁棒性评级包括在表中，对于现代SLAM系统主要基于第7小节中介绍的对比方法，而针对经典的系统，主要基于文献【2】【52】中先前的比较。</p><p>不同SLAM系统之间的相同点与不通电</p><p><img src="/2022/05/31/2022-05-31-ORBSLAM3/image-20220531180642864.png" loading="lazy"></p>]]>
    </content>
    <id>https://grobenis.github.io/2022/05/31/2022-05-31-ORBSLAM3/</id>
    <link href="https://grobenis.github.io/2022/05/31/2022-05-31-ORBSLAM3/"/>
    <published>2022-05-31T09:57:26.000Z</published>
    <summary>
      <![CDATA[<p>原文链接：<a href="https://arxiv.org/abs/2007.11898">https://arxiv.org/abs/2007.11898</a></p>
<p>参考翻译文章：<a href="https://zhuanlan.zhihu.com/p/403299796">https://zhuanlan.zhihu.com/p/403299796</a></p>
<p>本文学习 ORB-SLAM3 论文，该系统是首个在单目、双目与 RGB-D 相机上同时支持纯视觉、视觉惯性与多地图的 SLAM 系统。文章重点介绍两大创新：基于最大后验估计的紧耦合视觉惯性前端，以及依赖高召回率位置识别算法的多地图机制——跟踪丢失时开启新地图、回访建图区域时无缝合并，从而在全流程中复用历史信息并显著提升精度。</p>
<blockquote>
<p><strong>多地图无缝衔接，SLAM永不迷航</strong></p>
</blockquote>]]>
    </summary>
    <title>ORB-SLAM3 学习笔记</title>
    <updated>2026-08-25T13:18:00.895Z</updated>
  </entry>
  <entry>
    <author>
      <name>guoben</name>
    </author>
    <category term="学习" scheme="https://grobenis.github.io/categories/%E5%AD%A6%E4%B9%A0/"/>
    <category term="Androd" scheme="https://grobenis.github.io/tags/Androd/"/>
    <content>
      <![CDATA[<p>本文介绍 安卓学习过程中的知识点，</p><p>参考书籍：《第一行代码》</p><p>本文整理安卓学习中的核心知识点，重点讲解 Activity、Service、Content Provider、Broadcast Receiver 四大组件的定义、注册、激活与关闭机制，并介绍任务栈与生命周期等概念。</p><blockquote><p><strong>四大组件了然，安卓开发方能入门</strong></p></blockquote><span id="more"></span><h2 id="安卓四大组件"><a href="#安卓四大组件" class="headerlink" title="安卓四大组件"></a>安卓四大组件</h2><p>Android四大组件分别为activity、service、content provider、broadcast receiver。</p><h3 id="一、android四大组件详解"><a href="#一、android四大组件详解" class="headerlink" title="一、android四大组件详解"></a>一、android四大组件详解</h3><h4 id="1、activity"><a href="#1、activity" class="headerlink" title="1、activity"></a>1、activity</h4><p>（1）一个Activity通常就是一个单独的屏幕（窗口）。</p><p>（2）Activity之间通过Intent进行通信。</p><p>（3）android应用中每一个Activity都必须要在AndroidManifest.xml配置文件中声明，否则系统将不识别也不执行该Activity。</p><h4 id="2、service"><a href="#2、service" class="headerlink" title="2、service"></a>2、service</h4><p>（1）service用于在后台完成用户指定的操作。service分为两种：</p><p>（a）started（启动）：当应用程序组件（如activity）调用startService()方法启动服务时，服务处于started状态。</p><p>（b）bound（绑定）：当应用程序组件调用bindService()方法绑定到服务时，服务处于bound状态。</p><p>（2）startService()与bindService()区别：</p><p>（a）started service（启动服务）是由其他组件调用startService()方法启动的，这导致服务的onStartCommand()方法被调用。当服务是started状态时，其生命周期与启动它的组件无关，并且可以在后台无限期运行，即使启动服务的组件已经被销毁。因此，服务需要在完成任务后调用stopSelf()方法停止，或者由其他组件调用stopService()方法停止。</p><p>（b）使用bindService()方法启用服务，调用者与服务绑定在了一起，调用者一旦退出，服务也就终止，大有“不求同时生，必须同时死”的特点。</p><p>（3）开发人员需要在应用程序配置文件中声明全部的service，使用<service></service>标签。</p><p>（4）Service通常位于后台运行，它一般不需要与用户交互，因此Service组件没有图形用户界面。Service组件需要继承Service基类。Service组件通常用于为其他组件提供后台服务或监控其他组件的运行状态。</p><h4 id="3、content-provider"><a href="#3、content-provider" class="headerlink" title="3、content provider"></a>3、content provider</h4><p>（1）android平台提供了Content Provider使一个应用程序的指定数据集提供给其他应用程序。其他应用可以通过ContentResolver类从该内容提供者中获取或存入数据。</p><p>（2）只有需要在多个应用程序间共享数据是才需要内容提供者。例如，通讯录数据被多个应用程序使用，且必须存储在一个内容提供者中。它的好处是统一数据访问方式。</p><p>（3）ContentProvider实现数据共享。ContentProvider用于保存和获取数据，并使其对所有应用程序可见。这是不同应用程序间共享数据的唯一方式，因为android没有提供所有应用共同访问的公共存储区。</p><p>（4）开发人员不会直接使用ContentProvider类的对象，大多数是通过ContentResolver对象实现对ContentProvider的操作。</p><p>（5）ContentProvider使用URI来唯一标识其数据集，这里的URI以content:&#x2F;&#x2F;作为前缀，表示该数据由ContentProvider来管理。</p><h3 id="4、broadcast-receiver"><a href="#4、broadcast-receiver" class="headerlink" title="4、broadcast receiver"></a>4、broadcast receiver</h3><p>（1）你的应用可以使用它对外部事件进行过滤，只对感兴趣的外部事件(如当电话呼入时，或者数据网络可用时)进行接收并做出响应。广播接收器没有用户界面。然而，它们可以启动一个activity或serice来响应它们收到的信息，或者用NotificationManager来通知用户。通知可以用很多种方式来吸引用户的注意力，例如闪动背灯、震动、播放声音等。一般来说是在状态栏上放一个持久的图标，用户可以打开它并获取消息。</p><p>（2）广播接收者的注册有两种方法，分别是程序动态注册和AndroidManifest文件中进行静态注册。</p><p>（3）动态注册广播接收器特点是当用来注册的Activity关掉后，广播也就失效了。静态注册无需担忧广播接收器是否被关闭，只要设备是开启状态，广播接收器也是打开着的。也就是说哪怕app本身未启动，该app订阅的广播在触发时也会对它起作用。</p><h3 id="二、android四大组件总结"><a href="#二、android四大组件总结" class="headerlink" title="二、android四大组件总结"></a>二、android四大组件总结</h3><blockquote><h4 id="（1）4大组件的注册"><a href="#（1）4大组件的注册" class="headerlink" title="（1）4大组件的注册"></a>（1）4大组件的注册</h4><p>4大基本组件都需要注册才能使用，每个Activity、service、Content Provider都需要在AndroidManifest文件中进行配置。AndroidManifest文件中未进行声明的activity、服务以及内容提供者将不为系统所见，从而也就不可用。而broadcast receiver广播接收者的注册分静态注册（在AndroidManifest文件中进行配置）和通过代码动态创建并以调用Context.registerReceiver()的方式注册至系统。需要注意的是在AndroidManifest文件中进行配置的广播接收者会随系统的启动而一直处于活跃状态，只要接收到感兴趣的广播就会触发（即使程序未运行）。</p><h4 id="（2）4大组件的激活"><a href="#（2）4大组件的激活" class="headerlink" title="（2）4大组件的激活"></a>（2）4大组件的激活</h4><p>内容提供者的激活：当接收到ContentResolver发出的请求后，内容提供者被激活。而其它三种组件activity、服务和广播接收器被一种叫做intent的异步消息所激活。</p><h4 id="（3）4大组件的关闭"><a href="#（3）4大组件的关闭" class="headerlink" title="（3）4大组件的关闭"></a>（3）4大组件的关闭</h4><p>内容提供者仅在响应ContentResolver提出请求的时候激活。而一个广播接收器仅在响应广播信息的时候激活。所以，没有必要去显式的关闭这些组件。Activity关闭：可以通过调用它的finish()方法来关闭一个activity。服务关闭：对于通过startService()方法启动的服务要调用Context.stopService()方法关闭服务，使用bindService()方法启动的服务要调用Contex.unbindService()方法关闭服务。</p><h4 id="（4）android中的任务（activity栈）"><a href="#（4）android中的任务（activity栈）" class="headerlink" title="（4）android中的任务（activity栈）"></a>（4）android中的任务（activity栈）</h4><p>（a）任务其实就是activity的栈，它由一个或多个Activity组成，共同完成一个完整的用户体验。栈底的是启动整个任务的Activity，栈顶的是当前运行的用户可以交互的Activity，当一个activity启动另外一个的时候，新的activity就被压入栈，并成为当前运行的activity。而前一个activity仍保持在栈之中。当用户按下BACK键的时候，当前activity出栈，而前一个恢复为当前运行的activity。栈中保存的其实是对象，栈中的Activity永远不会重排，只会压入或弹出。</p><p>（b）任务中的所有activity是作为一个整体进行移动的。整个的任务（即activity栈）可以移到前台，或退至后台。</p><p>（c）Android系统是一个多任务(Multi-Task)的操作系统，可以在用手机听音乐的同时，也执行其他多个程序。每多执行一个应用程序，就会多耗费一些系统内存，当同时执行的程序过多，或是关闭的程序没有正确释放掉内存，系统就会觉得越来越慢，甚至不稳定。为了解决这个问题，Android引入了一个新的机制，即生命周期(Life Cycle)。</p></blockquote>]]>
    </content>
    <id>https://grobenis.github.io/2022/04/05/2022-04-05-%E5%AE%89%E5%8D%93%E5%9F%BA%E7%A1%80/</id>
    <link href="https://grobenis.github.io/2022/04/05/2022-04-05-%E5%AE%89%E5%8D%93%E5%9F%BA%E7%A1%80/"/>
    <published>2022-04-05T15:55:39.000Z</published>
    <summary>
      <![CDATA[<p>本文介绍 安卓学习过程中的知识点，</p>
<p>参考书籍：《第一行代码》</p>
<p>本文整理安卓学习中的核心知识点，重点讲解 Activity、Service、Content Provider、Broadcast Receiver 四大组件的定义、注册、激活与关闭机制，并介绍任务栈与生命周期等概念。</p>
<blockquote>
<p><strong>四大组件了然，安卓开发方能入门</strong></p>
</blockquote>]]>
    </summary>
    <title>安卓基础</title>
    <updated>2026-08-25T13:18:00.880Z</updated>
  </entry>
  <entry>
    <author>
      <name>guoben</name>
    </author>
    <category term="学习" scheme="https://grobenis.github.io/categories/%E5%AD%A6%E4%B9%A0/"/>
    <category term="大数据" scheme="https://grobenis.github.io/tags/%E5%A4%A7%E6%95%B0%E6%8D%AE/"/>
    <content>
      <![CDATA[<p>跟着同事学习一下大数据。</p><p>本文简述大数据的基本面貌，梳理了静态与动态数据的分类及其来源，并介绍分布式存储、MapReduce 分析流程等大数据框架，涵盖 YARN 资源调度与 ZooKeeper 分布式协调服务等关键组件。</p><blockquote><p><strong>分而治之，方驭海量数据</strong></p></blockquote><span id="more"></span><h2 id="无处不在的大数据"><a href="#无处不在的大数据" class="headerlink" title="无处不在的大数据"></a>无处不在的大数据</h2><h3 id="分类"><a href="#分类" class="headerlink" title="分类"></a>分类</h3><ol><li>静态数据</li><li>动态数据（年龄，喜好，推荐）</li></ol><h3 id="来源"><a href="#来源" class="headerlink" title="来源"></a>来源</h3><ol><li>谷歌文件系统（GFS）</li><li>MapReduce</li><li>BigTable</li><li>hadoop</li><li>spark</li></ol><h3 id="大数据框架"><a href="#大数据框架" class="headerlink" title="大数据框架"></a>大数据框架</h3><p>分布式处理的软件框架的总称，</p><ol><li>存储<ol><li>分布式存储<ol><li>NameNode:记录每个块的信息</li><li>DataNode：存储源信息</li></ol></li><li>降低硬件成本</li><li>软件提供可靠性<ol><li>分开存储；可用性不能保证</li></ol></li></ol></li><li>分析——MapReduce<ol><li>分而治之</li><li>Map</li><li>Shuffle</li><li>Reduce</li></ol></li></ol><h4 id="里程碑-YARN"><a href="#里程碑-YARN" class="headerlink" title="里程碑-YARN"></a>里程碑-YARN</h4><p>接受任务&#x2F;资源调度</p><ol><li>Resource Manager：接受任务</li><li>Node Manager：启动容器</li></ol><p>ZooKeeper:分布式协调服务</p><ol><li>命令服务</li><li>配置管理</li><li>集群管理</li></ol><p>脑裂现象：</p><ol><li>投票机制~ 过半当leader</li></ol>]]>
    </content>
    <id>https://grobenis.github.io/2022/03/31/2022-03-31-Big-data/</id>
    <link href="https://grobenis.github.io/2022/03/31/2022-03-31-Big-data/"/>
    <published>2022-03-31T11:06:43.000Z</published>
    <summary>
      <![CDATA[<p>跟着同事学习一下大数据。</p>
<p>本文简述大数据的基本面貌，梳理了静态与动态数据的分类及其来源，并介绍分布式存储、MapReduce 分析流程等大数据框架，涵盖 YARN 资源调度与 ZooKeeper 分布式协调服务等关键组件。</p>
<blockquote>
<p><strong>分而治之，方驭海量数据</strong></p>
</blockquote>]]>
    </summary>
    <title>大数据简述</title>
    <updated>2026-08-25T12:04:04.716Z</updated>
  </entry>
  <entry>
    <author>
      <name>guoben</name>
    </author>
    <category term="学习" scheme="https://grobenis.github.io/categories/%E5%AD%A6%E4%B9%A0/"/>
    <category term="机器学习" scheme="https://grobenis.github.io/tags/%E6%9C%BA%E5%99%A8%E5%AD%A6%E4%B9%A0/"/>
    <content>
      <![CDATA[<p>联邦学习</p><p>本文介绍联邦机器学习的基本概念与应用生态，说明其在保护数据隐私与合法合规前提下实现共同建模的思路，梳理横向、纵向与联邦迁移学习的分类，并列举 Google、FATE、微众银行等平台及相关法律依据。</p><blockquote><p><strong>数据不动模型动，隐私与智能兼得</strong></p></blockquote><span id="more"></span><h2 id="联邦学习"><a href="#联邦学习" class="headerlink" title="联邦学习"></a>联邦学习</h2><p>机器学习框架，保护数据隐私安全级合法合规的基础上，实现共同建模，提升AI模型的效果；</p><ol><li>GOOGLE</li><li>FATE</li></ol><p>商业化价值</p><p>端训练-&gt;模型参数-&gt;上传-&gt;训练</p><p>分类</p><ol><li>横向<ol><li>业态相同或相似，</li><li>特征重叠多，用户重叠少</li><li>性能指标；无限接近联合学习的结果</li></ol></li><li>纵向<ol><li>带来一些危害</li><li>特性<ol><li>用户相同或相似</li><li>特征重叠少，用户重叠多</li><li>特征联合</li></ol></li><li>计数架构<ol><li>样本对齐，基于加密技术<ol><li>把id对齐，通过加密（同态加密）</li></ol></li><li>加密训练</li></ol></li><li>联邦线下回归<ol><li>xgboost</li><li>c</li></ol></li></ol></li><li>联邦迁移学习<ol><li>也太用户均交际较少</li><li>特征、用户重叠少</li></ol></li></ol><h2 id="应用生态"><a href="#应用生态" class="headerlink" title="应用生态"></a>应用生态</h2><ol><li>隐私保护</li><li>模型<ol><li>决策树</li><li>随机森林</li><li>线性回归</li></ol></li><li>增效</li><li>其它技术<ol><li>分布式技术</li><li>推荐系统</li><li>自然语言处理</li><li>计算机视觉</li></ol></li><li>平台<ol><li>微众银行</li><li>谷歌</li><li>百度</li><li>字节</li></ol></li><li>场景</li></ol><p>独立同分布，</p><h2 id="相关法律"><a href="#相关法律" class="headerlink" title="相关法律"></a>相关法律</h2><ol><li>中华人民共和国网络安全法</li><li>中华人民共和国数据安全法</li><li>中华人民共和国个人信息保护法</li></ol>]]>
    </content>
    <id>https://grobenis.github.io/2022/03/24/2022-03-24-19-20-00-fedral-machine-learning/</id>
    <link href="https://grobenis.github.io/2022/03/24/2022-03-24-19-20-00-fedral-machine-learning/"/>
    <published>2022-03-24T11:22:01.000Z</published>
    <summary>
      <![CDATA[<p>联邦学习</p>
<p>本文介绍联邦机器学习的基本概念与应用生态，说明其在保护数据隐私与合法合规前提下实现共同建模的思路，梳理横向、纵向与联邦迁移学习的分类，并列举 Google、FATE、微众银行等平台及相关法律依据。</p>
<blockquote>
<p><strong>数据不动模型动，隐私与智能兼得</strong></p>
</blockquote>]]>
    </summary>
    <title>联邦机器学习</title>
    <updated>2026-08-25T13:18:00.857Z</updated>
  </entry>
  <entry>
    <author>
      <name>guoben</name>
    </author>
    <category term="SLAM" scheme="https://grobenis.github.io/categories/SLAM/"/>
    <category term="SLAM" scheme="https://grobenis.github.io/tags/SLAM/"/>
    <category term="C" scheme="https://grobenis.github.io/tags/C/"/>
    <category term="Work" scheme="https://grobenis.github.io/tags/Work/"/>
    <content>
      <![CDATA[<p>简尔言之：</p><ol><li>RAM: valgrind –tool&#x3D;massif –stacks&#x3D;yes –time-unit&#x3D;B –alloc-fn&#x3D;generic_malloc .&#x2F;demo</li><li>ROM: arm-none-eabi-size -t *.a</li><li>检查内存泄漏：valgrind –tool&#x3D;memcheck –leak-check&#x3D;full –show-reachable&#x3D;yes –trace-children&#x3D;yes build_linux-x86_64&#x2F;main</li></ol><p>本文整理工程中统计资源占用的实用方法：用 Valgrind Massif 统计程序 RAM 峰值与栈占用，配合 massif-visualizer 可视化分析；用 arm-none-eabi-size 查看静态库的 ROM 占用；用 Valgrind Memcheck 检查内存泄漏。</p><blockquote><p><strong>资源占用可量化，内存问题无处藏</strong></p></blockquote><span id="more"></span><h2 id="RAM统计"><a href="#RAM统计" class="headerlink" title="RAM统计"></a>RAM统计</h2><p>统计工具Valgrind<br>统计内存占用可以用下valgrind的工具 ubuntu可以使用apt直接安装的 使用的方法是写一个demo 调用算法的接口 编译成可执行文件 然后通过valgrind执行它。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">valgrind --tool=massif --stacks=yes --time-unit=B --alloc-fn=generic_malloc  ./swimming_analyzer_demo</span><br></pre></td></tr></table></figure><p>然后在当前目录会生成一个文件 比如：massif.out.1250590 后面的数字是个id<br>再安装一个软件 <code>massif-visualizer</code>, 使用这个软件打开生成的这个文件massif.out.<id>.</p><p><img src="/2022/03/16/2022-03-16-Souce_static_in_project/image-20220316163119363.png" alt="image-20220316163119363" loading="lazy"></p><p>也可以辅助分析它的内存RAM消耗。</p><h2 id="ROM统计"><a href="#ROM统计" class="headerlink" title="ROM统计"></a>ROM统计</h2><p>关于程序占用ROM的计算借助arm交叉编译器实现</p><p>可以通过以下命令查看文件占用flash和ram的大小。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">arm-none-eabi-size -t *.a</span><br></pre></td></tr></table></figure><p>显示结果如下：</p><p><img src="/2022/03/16/2022-03-16-Souce_static_in_project/image-20220318103734602.png" alt="image-20220318103734602" loading="lazy"></p><p>上图中，TOTALS所在的dec指示的数字即是占用ROM空间的情况，单位为字节（Byte）。上图中占用为4863&#x2F;1024 &#x3D;4.74KB。</p><h3 id="利用工具统计资源泄露情况"><a href="#利用工具统计资源泄露情况" class="headerlink" title="利用工具统计资源泄露情况"></a>利用工具统计资源泄露情况</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">valgrind --tool=memcheck --leak-check=full --show-reachable=yes --trace-children=yes build_linux-x86_64/L67main</span><br></pre></td></tr></table></figure>]]>
    </content>
    <id>https://grobenis.github.io/2022/03/16/2022-03-16-Souce_static_in_project/</id>
    <link href="https://grobenis.github.io/2022/03/16/2022-03-16-Souce_static_in_project/"/>
    <published>2022-03-16T08:28:29.000Z</published>
    <summary>
      <![CDATA[<p>简尔言之：</p>
<ol>
<li>RAM: valgrind –tool&#x3D;massif –stacks&#x3D;yes –time-unit&#x3D;B –alloc-fn&#x3D;generic_malloc .&#x2F;demo</li>
<li>ROM: arm-none-eabi-size -t *.a</li>
<li>检查内存泄漏：valgrind –tool&#x3D;memcheck –leak-check&#x3D;full –show-reachable&#x3D;yes –trace-children&#x3D;yes build_linux-x86_64&#x2F;main</li>
</ol>
<p>本文整理工程中统计资源占用的实用方法：用 Valgrind Massif 统计程序 RAM 峰值与栈占用，配合 massif-visualizer 可视化分析；用 arm-none-eabi-size 查看静态库的 ROM 占用；用 Valgrind Memcheck 检查内存泄漏。</p>
<blockquote>
<p><strong>资源占用可量化，内存问题无处藏</strong></p>
</blockquote>]]>
    </summary>
    <title>在工程中统计资源占用情况</title>
    <updated>2026-08-25T13:18:00.837Z</updated>
  </entry>
  <entry>
    <author>
      <name>guoben</name>
    </author>
    <category term="SLAM" scheme="https://grobenis.github.io/categories/SLAM/"/>
    <category term="SLAM" scheme="https://grobenis.github.io/tags/SLAM/"/>
    <category term="ROS" scheme="https://grobenis.github.io/tags/ROS/"/>
    <category term="VINS" scheme="https://grobenis.github.io/tags/VINS/"/>
    <content>
      <![CDATA[<p>本章想要抽空把之前的VINS_mono系统地学习一下。</p><p>本文是对 VINS-mono 的系统学习笔记，梳理这一单目视觉惯性系统的完整框架：前端通过 KLT 光流跟踪特征并对 IMU 测量做预积分，初始化恢复位姿、速度与重力矢量初值，滑动窗口内联合优化视觉与惯性测量，最后经闭环检测与四自由度位姿图优化消除累积漂移。</p><blockquote><p><strong>视觉惯性紧耦合，小代价换高精度</strong></p></blockquote><span id="more"></span><h2 id="简介"><a href="#简介" class="headerlink" title="简介"></a>简介</h2><h2 id="论文"><a href="#论文" class="headerlink" title="论文"></a>论文</h2><h3 id="摘要"><a href="#摘要" class="headerlink" title="摘要"></a>摘要</h3><p>本文提出了一种用于在各种环境中进行状态估计的单目视觉惯性系统 (VINS)。单目相机和低成本的惯性测量单元 (IMU) 构成了用于六自由度状态估计的最小传感器套件。本文算法迭代地优化有界滑动窗口中的视觉和惯性测量，以执行准确的状态估计。视觉结构由滑动窗口中的关键帧保持，而惯性度量测量则通过关键帧之间的预积分来保持。我们的系统对未知状态的初始化、在线相机-IMU 外部参数校准、在球体上定义的统一重投影误差、循环检测和四自由度位姿图优化具有鲁棒性。这些特性使我们的系统实用且易于使用。我们通过与其他最先进的算法进行比较来验证我们系统在公共数据集和真实世界实验中的性能。我们还在 MAV 平台上执行机载闭环自主飞行，并将算法移植到 iOS 应用程序。我们强调所提出的工作是一个可靠且完整的系统，可以在其他智能平台上轻松操作。我们开源了我们的实现代码和 iOS 移动设备上的增强现实 (AR) 应用程序。</p><h3 id="I-总体简介"><a href="#I-总体简介" class="headerlink" title="I. 总体简介"></a>I. 总体简介</h3><p>本文所提出的视觉惯性系统的结构如下图所示。</p><p><img src="/2022/03/01/2022-03-01-VINS-mono/image-20220506142521109.png" alt="image-20220506142521109" loading="lazy"></p><ol><li>第一部分是数据测量处理前端，它从每个新图像帧提取、跟踪特征并在两帧之间预积分所有 IMU 数据。</li><li>第二部分是初始化过程，它提供必要的初始值（姿态、速度、重力矢量、陀螺仪偏差和 3D 特征位置）来引导非线性系统。</li><li>第三部分实现非线性图优化，通过优化所有视觉、惯性信息来解决我们滑动窗口中的状态。</li><li>最后，位姿图优化模块（参见第 VIII 节）接收几何验证的重定位结果，并执行全局优化以消除漂移。 它还实现了位姿图重用。 （VIO 和位姿图优化模块分别在单独的线程中同时运行）</li></ol><p>与适用于立体相机的最先进的 VIO 算法 OKVIS [15] 相比，我们的算法是专门为单目相机设计的。 因此，我们特别提出了初始化程序、关键帧选择标准，并使用和处理大视场 (FOV) 相机以获得更好的跟踪性能。 此外，我们的算法提出了一个完整的系统，具有闭环和位姿图重用模块。</p><p>符号定义：我们将 $(·)^w$ 视为世界坐标系，其中重力矢量的方向与其 z 轴对齐。 $(·)^b$ 是与 IMU 框架对齐的主体框架。$ (·)^c$ 是相机框架。我们同时使用 R 和四元数 q 来表示旋转矩阵。四元数对应于汉密尔顿符号。我们主要在状态向量中使用四元数，但旋转矩阵也用于 3-D 向量的方便旋转。 $q^w_b$ , $p^w_b$ 是从body坐标系到世界坐标系的旋转和平移。 $b_k$ 是拍摄第 k 张图像时的身体帧。 $c_k$ 是拍摄第 k 张图像时的相机帧。 ⊗ 是两个四元数的乘法运算。 $g^w = [0, 0, g]^T$ 是世界坐标系中的重力矢量。</p><h3 id="II-测量预处理"><a href="#II-测量预处理" class="headerlink" title="II 测量预处理"></a>II 测量预处理</h3><p>我们在本节中预处理视觉和惯性测量。</p><ul><li>对于视觉测量，我们跟踪连续帧之间的特征并检测最新帧中的新特征。</li><li>对于 IMU 测量，我们将它们在两个连续帧之间进行预积分。</li></ul><p>请注意，IMU 测量受偏差和噪声的影响。 因此，我们在 IMU 预集成和优化部分特别考虑了偏差，这对于低成本 IMU 芯片至关重要。</p><h4 id="A-视觉测量前段"><a href="#A-视觉测量前段" class="headerlink" title="A. 视觉测量前段"></a>A. 视觉测量前段</h4><p>对于每张新图像，现有特征由 KLT 稀疏光流算法 [26] 跟踪。同时，检测新的角点特征 [27] 以保持每个图像中的最小数量（100-300）特征。检测器通过设置两个相邻特征之间的最小像素间隔来强制实现均匀的特征分布。在通过异常值拒绝后，特征被投影到一个单位球体。异常值剔除由基本矩阵测试中的 RANSAC 步骤执行。</p><p>在此步骤中还选择了关键帧。我们有两个关键帧选择标准。</p><ol><li>其中之一是平均视差。如果跟踪到的特征的平均视差超过某个阈值，我们将这张图像视为关键帧。请注意，不仅平移而且旋转都会引起视差；但是，不能在仅旋转运动中对特征进行三角剖分。为了避免这种情况，我们在计算视差时使用 IMU 传播结果来补偿旋转。</li><li>另一个标准是跟踪质量。如果跟踪特征的数量低于某个阈值，我们也将此帧视为关键帧。</li></ol><h4 id="B-IMU预积分"><a href="#B-IMU预积分" class="headerlink" title="B. IMU预积分"></a>B. IMU预积分</h4><p>我们通过结合 IMU 偏差校正 [28] 扩展了我们之前的工作 [7] 中提出的 IMU 预积分。 与[28]积分，我们推导出连续时间动态中的噪声传播。 此外，IMU 预积分结果也用于初始化过程以校准初始状态。</p><p>1）IMU噪声与偏差</p><p>$$\hat{a}_t = a_t + b_{a_t} +R^t_w g^w +n_a$$</p><p>$$\hat{w}_t = w_t + b_{w_t} +n_w$$</p><p>我们假设加速度和角速度测量中的额外的噪声都是高斯白噪声，$n_a\sim \mathcal{N}(0, \sigma^2_a)$, $n_w\sim\mathcal{N}(0,\sigma_w^2)$</p><p>加速度偏差和陀螺仪偏差被建模为随机游走，其导数是高斯白噪声，$n_{b_a}\sim \mathcal{N}(0, \sigma^2_{b_a})$, $n_{b_w}\sim \mathcal{N}(0, \sigma^2_{b_w})$</p><p>$$\dot{b}_{a_t} = n_{b_a}\\\dot{b}_{w_t} = n_{b_w}$$</p><p>2)预积分，给定对应于图像帧$b_k$ 和 $b_{k+1}$ 的两个时刻，状态变量受时间间隔 $[k, k + 1]$ 期间的惯性测量约束：</p><p>$$p^w_{b_{l+1}} = p^w_{b_k} +v^w_{b_k}\Delta t_k + \int\int_{t\in[k, k+1]}(R^w_t(\hat{a}_t - b_{a_t})-g^w)dt^2$$</p><p>$$v^w_{b_{k+1}} = v^w_{b_k} + \int_{t\in[k,t+1]}(R^w_t(\hat{a}_t-b_{a_t})-g^w)dt$$</p><p>$$q^w_{b_{k+1}} = q^w_{b_k} \otimes\int_{t \in [k, k+1]}\frac{1}{2}\Omega(\hat{w}_t - b_{w_t})q^{b_k}_t dt$$</p><p>其中：</p><p>$$\Omega (w) = \begin{bmatrix} -\lfloor w \rfloor_{\times}  &amp; w \\ -w^T &amp; 0 \end{bmatrix}$$</p><p>$$\lfloor w \rfloor _ \times =\begin{bmatrix} -\lfloor 0 &amp; - w_z  &amp; w_y \\ w_z &amp; 0 &amp; -w_x \\ - w_y &amp; w_x &amp; 0 \end{bmatrix}$$</p><p>$\Delta t_x$ 是时间间隔$[k ,k+1]$的持续时间， $\hat{w}_t$, $\hat{a}_t$是原始的IMU测量。它在相机坐标系下，并且被加速度偏差$b_a$, 加速度偏差$b_\omega$ 以及噪声影响。</p><p>可以看出，IMU 状态传播需要帧 $b_k$ 的旋转、位置和速度。 当这些起始状态发生变化时，我们需要重新传播 IMU 测量值。 特别是在基于优化的算法中，每次调整姿势时，我们都需要在它们之间重新传播 IMU 测量值。 这种传播策略是计算消耗的。 为了避免重复传播，我们采用预积分算法。</p><p>将 IMU 传播的参考坐标系改为局部坐标系 $b_k$ 后，我们只能对与线加速度 $\hat{a}$ 和角速度$\hat{\omega}$ 相关的部分进行预积分，如下所示：</p><p>$$R^{b_k}_\omega p^w_{b_{k+1}} = R.b$$</p><p>3） 误差校准：</p><p>如果偏差的估计变化很小，我们通过它们关于偏差的一阶近似值进行调整$\alpha^{b_k}_{b_{k+1}}$,$\beta^{b_k}_{b_{k+1}}$, $\gamma^{b_k}_{b_{k+1}}$，</p><p>$$\alpha^{b_k}_{b_{k+1}} \simeq \hat{\alpha}^{b_k}_{b_{k+1}} + J^\alpha_{b_a} \delta b_{a_k} + J^\alpha_{b_w} \delta b_{w_k}$$</p><p>$$\beta^{b_k}_{b_{k+1}} \simeq \hat{\beta}^{b_k}_{b_{k+1}} + J^\beta_{b_a} \delta b_{a_k} + J^\beta_{b_w} \delta b_{w_k}$$</p><p>$$\gamma^{b_k}_{b_{k+1}} \simeq \hat{\gamma}^{b_k}_{b_{k+1}} \otimes\begin{bmatrix}1 \\ \frac{1}{2}J^\gamma_{b_w} \delta b_{w_k}\end{bmatrix}$$</p><p>否则，当偏差的估计发生显著变化时，我们会在新的偏差估计下进行重新传播。 这种策略为基于优化的算法节省了大量的计算资源，因为我们不需要重复传播 IMU 测量。</p><h3 id="V-估计初始化"><a href="#V-估计初始化" class="headerlink" title="V 估计初始化"></a>V 估计初始化</h3><p>单目紧耦合VIO是一个高度非线性的系统，因此在一开始就需要一个准确的初始估计。本文则通过松耦合的IMU与积分与基于视觉重建进行对齐。</p><h4 id="A-滑动窗口中纯视觉的SFM"><a href="#A-滑动窗口中纯视觉的SFM" class="headerlink" title="A 滑动窗口中纯视觉的SFM"></a>A 滑动窗口中纯视觉的SFM</h4><p>初始化过程从纯视觉 SfM 开始，以估计最大比例的相机姿势和特征位置图。<br>我们在一个滑动窗口中维护几个帧，以实现有限的计算复杂度。首先，我们检查最新帧和所有先前帧之间的特征对应关系。如果我们能在滑动窗口中找到最新帧与任何其他帧之间的稳定特征跟踪（超过 30 个跟踪特征）和足够的视差（超过 20 个像素）。我们使用五点算法 [34] 恢复这两个帧之间的相对旋转和按比例平移。然后，我们任意设置比例并对这两帧中观察到的所有特征进行三角测量。基于这些三角特征，执行透视点（PnP）方法[35]来估计窗口中所有其他帧的姿势。最后，应用全局全束调整 [36] 以最小化所有特征观察的总重投影误差。</p><p>由于我们还没有关于世界坐标系的任何知识，我们将第一个相机坐标系 $(·)^{c_0}$ 设置为 SfM 的参考坐标系。</p><p>所有帧的位置$(\bar{p}^{c_0}_{c_k},q^{c_0}_{c_k})$和特征位置都通过与$(·)^{c_0}$。给定相机与IMU之间的外参，我们即可把坐标从相机坐标系转换到体坐标系:</p><p>$$a^{c_0}_{b_k} = q^{c_0}_{c_k} \otimes(q^b_c)^{-1}\\s \bar{p}^{c_0}_{b_k} =s \bar{p}^{c_0}_{c_k} - R^{C_0}_{b_K}p^b_c$$</p><p>其中s是位置的尺度参数，将会在接下来的章节中解决。</p><h4 id="B-视觉-惯性对齐"><a href="#B-视觉-惯性对齐" class="headerlink" title="B 视觉-惯性对齐"></a>B 视觉-惯性对齐</h4><p>视觉惯性对其的示意图如下图所示。主要的目的就是对其带尺度的IMU预积分信息与视觉架构信息。</p><p>1） 陀螺仪误差校准：考虑窗口中的两个连续帧$b_k$和$b_{k+1}$，我们从视觉SfM得到旋转$q^{c_0}_{b_k}$和$q^{c_0}_{b_{k+1}}$，以及从IMU预积分得到相对约束$y^{b_k}_{b_{k+1}}$。 我们将 IMU 预积分项相对于陀螺仪偏差线性化，并最小化以下损失函数：</p><p>$$\min_{\delta b_w} \sum_{k\in\mathcal{B}}||{q^{c_0}_{b_{k+1}}}^{-1}\otimes q^{c_0}_{b_k} \otimes \gamma ^{b_k}_{b_{k+1}}||^2$$</p><p>$$\gamma^{b_k}_{b_{k+1}} \approx \hat{\gamma}^{b_k}_{b_{k+1}} \otimes\begin{bmatrix}1 \\ \frac{1}{2}J^\gamma_{b_w} \delta b_{w_k}\end{bmatrix}$$</p><p>其中，$\mathcal{B}$ 表示窗口中所有的帧数，通过这种方法，我们获得了陀螺仪偏差的初始标定值$b_w$, 之后，我们通过新的陀螺仪误差重新传播了所有的预积分项$\hat{\alpha}^{b_k}_{b_{k+1}}$， $\hat{\beta}^{b_k}_{b_{k+1}}$, 以及$\gamma^{b_k}_{b_{k+1}}$。</p><p>2）速度，重力矢量，矩阵尺度因子初始化</p><p>陀螺仪偏差初始化后，我们继续初始化导航的其他基本状态，即速度、重力矢量和度量尺度</p><p>$$\chi_I  = [v^{b_0}_{b_0},v^{b_1}_{b_1},v^{b_n}_{b_n}, g^{c_0}, s]$$</p><p>其中$v^{b_k}_{b_{k}}$是拍摄第k张图像时身体帧中的速度，$g^{c_0}$是$c_0$帧中的重力向量，并将单目SfM缩放为公制单位。</p><p>考虑窗口中连续的两个帧$b_k$和$b_{k+1}$，我们有以下等式：</p><p>$$α^{b_k}_{b_{k+1}}=R^{b_k}_{c_0}(s(\bar{p}^{c_0}_{b_{k+1}}−\bar{p}^{c_0}_{b_k})+\frac{1}{2}g^{c_0}Δt^2_k−R^{c_0}_{b_k}v^{b_k}_{b_k}Δt_k) \\β^{b_k}_{b_{k+1}}=R^{b_k}_{c_0}(R^{c_0}_{b_{k+1}}v^{b_{k+1}}_{b_{k+1}}+g^{c_0}Δt_k−R^{c_0}_{b_k}v^{b_k}_{b_k})$$</p><p>我们可以将(6)和(9)组合成以下线性测量模型：</p><p><img src="/2022/03/01/2022-03-01-VINS-mono/image-20220510000932239.png" alt="image-20220510000932239" loading="lazy"></p><p>可以看出，$R^{c_0}_{b_k}$、$R^{c_0}_{b_{k+1}}$、$\bar{p}^{c_0}_{c_k}$、$\bar{p}^{c_0}_{c_{k+1}}$是从up-to-scale单目视觉SfM得到的。Δtki是两个连续帧之间的时间间隔。</p><p>通过求解这个线性最小二乘问题:</p><p>$$\min_{\chi_I}∑_{k∈B}||\hat{z}^{b_k}_{b_{k+1}}−H^{b_k}_{b_{k+1}}\chi_I||^2$$</p><p>我们可以得到窗口中每一帧的体帧速度，视觉参考帧中的重力向量( ·)c0，以及比例参数。</p><p>3)尺度优化，从前面的线性初始化步骤获得的重力矢量可以通过限制大小来细化。在大多数情况下，重力矢量的大小是已知的。这导致重力矢量只剩下 2个自由度。因此，我们在其切线空间上使用两个变量来扰动重力，从而保留 2-DOF。我们的重力向量受到$\mathcal{g}(\bar{\hat{\mathcal{g}}}+δg)$的扰动，$δg=w_1b_1+w_2b_2$，其中g是已知的重力大小，$\hat{\bar{g}}$是表示重力方向的单位向量。$b_1$和$b_2$是跨越切平面的两个正交基，如图图 4.$w_1$ 和 $w_2$ 分别是对 $b_1$ 和 $b_2$ 的二维扰动。我们可以任意找到$b_1$和$b_2$的任意集合旋转切线空间。然后，我们将(9)代入$\mathcal{g}(\bar{\hat{\mathcal{g}}}+δg)$，并与其他状态变量一起求解2-Dδg。这个过程反复多次，直到 g 收敛。</p><p><img src="/2022/03/01/2022-03-01-VINS-mono/image-20220510105935314.png" alt="image-20220510105935314" loading="lazy"></p><p>4）完成初始化：对重力向量进行细化后，我们可以通过将重力旋转到z轴得到世界坐标系和相机坐标系$c_0$之间的旋转$q^w_{c_0}$。然后我们将所有变量从参考坐标系(·)c0 旋转到世界坐标系(·)w。身体坐标系速度也将旋转到世界坐标系。来自视觉 SfM 的平移分量将被缩放为公制单位。至此，初始化过程完成，所有这些度量值将被馈送到紧密耦合的单目 VIO。</p><h3 id="VI-稠密紧耦合的单目VIO"><a href="#VI-稠密紧耦合的单目VIO" class="headerlink" title="VI 稠密紧耦合的单目VIO"></a>VI 稠密紧耦合的单目VIO</h3><p>在估计器初始化之后，我们继续使用基于滑动窗口的紧密耦合单目 VIO 来进行高精度和鲁棒的状态估计。 滑动窗口公式的说明如图 5 所示。</p><p><img src="/2022/03/01/2022-03-01-VINS-mono/image-20220510111548794.png" alt="image-20220510111548794" loading="lazy"></p><h4 id="A-形式"><a href="#A-形式" class="headerlink" title="A. 形式"></a>A. 形式</h4><p>滑动窗口中的全状态向量定义为</p><p><img src="/2022/03/01/2022-03-01-VINS-mono/image-20220510141038929.png" alt="image-20220510141038929" loading="lazy"></p><p>其中 $x_{k}$ 是捕获第 k 个图像时的 IMU 状态。它包含了IMU在世界坐标系中的位置、速度和方向，以及IMU体坐标系中的加速度偏差和陀螺仪偏差。n是关键帧的总数，m是滑动窗口中的特征总数。λ是第一个特征来自它的第一次观察的逆深度。</p><p>我们使用视觉惯性束调整公式。将所有测量残差的先验和马氏范数之和最小化，以获得最大后验估计为</p><p><img src="/2022/03/01/2022-03-01-VINS-mono/image-20220510141249686.png" alt="image-20220510141249686" loading="lazy"></p><p>其中 Huber 范数 [37] 定义为：</p><p><img src="/2022/03/01/2022-03-01-VINS-mono/image-20220510141257958.png" alt="image-20220510141257958" loading="lazy"></p><p>其中，$r_B(\hat{z}^{b_k}_{b_{k+1}},\chi)$和$r_C(\hat{z}^{c_j}_l,\chi)$分别是IMU和视觉测量的残差。残差项的详细定义将在第 VI-B 和 VI-C 节中介绍。B 是所有 IMU 测量的集合，C 是在当前滑动窗口中至少观察到两次的特征集合。${r_p,H_p}$是边缘化的先验信息。 Ceres 求解器 [38] 用于解决这个非线性问题。</p><ul><li>huber损失函数可以降低误匹配的影响；</li></ul><h4 id="B-IMU测量残差"><a href="#B-IMU测量残差" class="headerlink" title="B. IMU测量残差"></a>B. IMU测量残差</h4><p>考虑滑动窗口中两个连续帧 $b_k$ 和 $b_{k+1}$ 内的 IMU 测量值，预积分 IMU 测量值的残差可以定义为:</p><p><img src="/2022/03/01/2022-03-01-VINS-mono/image-20220510141608837.png" alt="image-20220510141608837" loading="lazy"></p><p>其中$[·]_{xyz}$提取四元数q的向量部分以表示错误状态。$δθ^{b_k}_{b_{k+1}}$是四元数的3-D错误状态表示。$[\hat{α}^{b_k}_{b_{k+1}},\hat{β}^{b_k}_{b_{k+1}},\hat{γ}^{b_k}_{b_{k+1}}$是预积分IMU 两个连续图像帧之间的测量项。 加速度计和陀螺仪偏差也包含在在线校正的残差项中。</p><ul><li>零偏在质量不好的IMU下，变化比较快</li></ul><h4 id="C-视觉测量残差"><a href="#C-视觉测量残差" class="headerlink" title="C. 视觉测量残差"></a>C. 视觉测量残差</h4><p>与在广义图像平面上定义重投影误差的传统针孔相机模型相比，我们在单位球面上定义相机测量残差。 几乎所有类型相机的光学器件，包括广角相机、鱼眼相机或全向相机，都可以建模为连接单位球体表面的单位射线。</p><p>考虑在第 i 个图像中首先观察到的第 l 个特征，在第 j 个图像中的特征观察的残差定义为：</p><p><img src="/2022/03/01/2022-03-01-VINS-mono/image-20220510115512351.png" alt="image-20220510115512351" loading="lazy"></p><p>其中$[\hat{u}^{c_i}_l,\hat{v}^{c_i}_l]$是第l个特征发生在第i个图像中的第$l$个观测值。$[\hat{u}^{c_j}_l,\hat{v}^{c_j}_l]$是第j个图像中相同特征的观测值。$π^{−1}_c$是反投影函数，它将像素位置转换为 使用相机内在参数的单位向量。 由于视觉残差的自由度为 2，我们将残差向量投影到切平面上。$b_1$ 和 $b_2$ 是两个任意选择的正交基，它们跨越$\hat{\bar{\mathcal{P}}}^{c_j}_l$ 的切平面，如图 6 所示。方差 $P^{c_j}_l$，如 (14) 中使用的 , 也从像素坐标传播到单位球体上。</p><p><img src="/2022/03/01/2022-03-01-VINS-mono/image-20220510121402361.png" alt="image-20220510121402361" loading="lazy"></p><h4 id="D-边缘化"><a href="#D-边缘化" class="headerlink" title="D. 边缘化"></a>D. 边缘化</h4><p>为了限制我们基于优化的 VIO 的计算复杂度，引入了边缘化。我们从滑动窗口中选择性地边缘化IMU状态$x_k$和特征$λ_l$，同时将边缘化状态对应的测量值转换为先验。</p><p><img src="/2022/03/01/2022-03-01-VINS-mono/image-20220510113823249.png" alt="image-20220510113823249" loading="lazy"></p><p>如图7所示，当第二个最新帧是关键帧时，它将留在窗口中，最旧的帧被边缘化掉及其相应的测量值。否则，如果第二个最新帧是非关键帧，我们会抛出视觉测量并保留连接到该非关键帧的 IMU 测量。为了保持系统的稀疏性，我们不会边缘化所有非关键帧的测量。我们的边缘化方案旨在将空间分离的关键帧保留在窗口中。这确保了特征三角测量有足够的视差，并最大限度地提高了在大激励下保持加速度计测量的概率。边缘化是使用 Schur 补码 [39] 进行的。我们基于与移除状态相关的所有边缘化测量构建一个新的先验。新的先验被添加到现有的先验上。我们注意到边缘化导致线性化点的早期修复，这可能导致次优估计结果。但是，由于 VIO 可以接受小的漂移，因此认为边缘化造成的负面影响并不重要。</p><h4 id="E-Camera-Rate-State-估计的仅运动视觉惯性优化"><a href="#E-Camera-Rate-State-估计的仅运动视觉惯性优化" class="headerlink" title="E. Camera-Rate State 估计的仅运动视觉惯性优化"></a>E. Camera-Rate State 估计的仅运动视觉惯性优化</h4><p>对于手机等计算能力较低的设备，由于非线性优化的大量计算需求，紧耦合的单目 VIO 无法实现相机级输出。为此，除了完全优化之外，我们还采用了轻量级的仅运动视觉惯性优化来将状态估计提升到相机速率（30 Hz）。仅运动视觉惯性优化的成本函数是与 (14) 中的单眼 VIO 相同。</p><p>核心思想：<strong>然而，我们并没有优化滑动窗口中的所有状态，而是只优化固定数量的最新 IMU 状态的位姿和速度。我们将特征深度、外部参数、偏差和我们不想优化的旧 IMU 状态视为恒定值。</strong></p><p>我们确实使用所有视觉和惯性测量来进行仅运动优化。这导致比单帧 PnP 方法更平滑的状态估计。所提出策略的说明如图 8 所示。与在最先进的嵌入式计算机上可能导致超过 50 ms 的完全紧密耦合的单目 VIO 相比，仅运动视觉惯性优化只需要大约 5 毫秒的计算时间。这实现了低延迟相机速率姿态估计，这对无人机和 AR 应用特别有益。</p><ol><li>通过限制优化规模，固定某些优化项以提高速率；</li></ol><h4 id="F-用于-IMU-速率状态估计的-IMU-前向传播"><a href="#F-用于-IMU-速率状态估计的-IMU-前向传播" class="headerlink" title="F. 用于 IMU 速率状态估计的 IMU 前向传播"></a>F. 用于 IMU 速率状态估计的 IMU 前向传播</h4><p>IMU 测量的速率远高于视觉测量。 尽管我们的 VIO 的频率受到图像捕获频率的限制，但我们仍然可以使用最近的 IMU 测量直接传播最新的 VIO 估计，使性能达到 IMU速率。高频状态估计可以用作闭环闭合的状态反馈。 使用该 IMU 速率状态估计的自主飞行实验在第 IX-C 节中介绍。</p><h3 id="VII-重定位"><a href="#VII-重定位" class="headerlink" title="VII 重定位"></a>VII 重定位</h3><p>我们的滑动窗口和边缘化方案限制了计算复杂度，但它也为系统引入了累积漂移。 为了消除漂移，提出了一种与单目 VIO 无缝集成的紧密耦合重定位模块。 重新定位过程从一个回环检测模块开始，该模块识别已经访问过的地方。 然后在闭环候选和当前帧之间建立特征级连接。 这些特征对应紧密集成到单目 VIO 模块中，从而以最少的计算实现无漂移状态估计。 多个特征的多次观测直接用于重定位，从而获得更高的精度和更好的状态估计平滑度。 图 9(a) 显示了重定位过程的图解说明。</p><p><img src="/2022/03/01/2022-03-01-VINS-mono/image-20220510162114496.png" alt="image-20220510162114496" loading="lazy"></p><p><img src="/2022/03/01/2022-03-01-VINS-mono/image-20220510162949029.png" alt="image-20220510162949029" loading="lazy"></p><h4 id="A-回环检测"><a href="#A-回环检测" class="headerlink" title="A. 回环检测"></a>A. 回环检测</h4><p>我们利用 DBoW2 [29]，一种最先进的词袋位置识别方法，用于回环检测。 除了用于单目 VIO 的角点特征外，BRIEF 描述子 [40] 还检测和描述了 500 个角点。 额外的角点特征用于在回环检测中实现更好的召回率。 描述符被视为视觉词来查询视觉数据库。 DBoW2 在时间和几何一致性检查后返回闭环候选。 我们保留所有的简要描述符用于特征检索，但丢弃原始图像以减少内存消耗。</p><h4 id="B-特征检索"><a href="#B-特征检索" class="headerlink" title="B. 特征检索"></a>B. 特征检索</h4><p>当检测到循环时，通过检索特征对应关系来建立局部滑动窗口和闭环候选之间的连接。 通过BRIEF描述符匹配找到对应关系。 描述符匹配可能会导致一些错误的匹配对。 为此，我们使用两步几何异常值剔除，如图 10 所示。</p><p><img src="/2022/03/01/2022-03-01-VINS-mono/image-20220510163210403.png" alt="image-20220510163210403" loading="lazy"></p><ol><li>2-D–2-D：使用 RANSAC [33] 进行的基本矩阵测试。我们使用当前图像和闭环候选图像中检索到的特征的 2-D 观察来执行基本矩阵测试。</li><li>3 -D–2-D：使用 RANSAC [35] 进行的 PnP 测试。 基于已知的特征在局部滑动窗口中的 3-D 位置和闭环候选图像中的 2-D 观察结果，我们执行 PnP 测试。</li></ol><p>在异常值拒绝后，我们将此候选作为正确的回环检测并执行重新定位。</p><h4 id="C-紧耦合的重定位"><a href="#C-紧耦合的重定位" class="headerlink" title="C. 紧耦合的重定位"></a>C. 紧耦合的重定位</h4><p>重定位过程有效地将当前滑动窗口与过去的姿势对齐。在重定位期间，我们将所有闭环帧的姿态视为常数。我们使用<strong>所有 IMU 测量、局部视觉测量测量和检索到的特征对应</strong>来共同优化滑动窗口。我们可以很容易地为闭环框架观察到的检索特征写出与 VIO 中的视觉测量相同的模型，如（17）。唯一的区别是闭环帧的位姿 $(\hat{q}^w_v,\hat{p}^w_v)$，取自位姿图（参见第 VIII 节），或直接取自过去的里程计输出（如果这是第一次重新定位），被视为一个常数。为此，我们可以用额外的循环项稍微修改（14）中的非线性成本函数</p><p><img src="/2022/03/01/2022-03-01-VINS-mono/image-20220510163341867.png" alt="image-20220510163341867" loading="lazy"></p><p>其中$\mathcal{L}$是在闭环帧中检索到的特征的观察集合。$(l，v)$表示在闭环帧v中观察到的第一个特征。</p><p>请注意，尽管成本函数与（14）略有不同，但要解决的状态的维数保持不变，因为闭环帧的位姿被认为是常数。</p><p>当使用当前滑动窗口建立多个闭环时，我们同时使用来自所有帧的所有闭环特征对应关系进行优化。这为重定位提供了多视图约束，从而获得更高的精度和更好的平滑度。重新定位后保持一致性的全局优化将在第 VIII 节中讨论。</p><h3 id="VIII全局图优化与地图重用"><a href="#VIII全局图优化与地图重用" class="headerlink" title="VIII全局图优化与地图重用"></a>VIII全局图优化与地图重用</h3><p>重新定位后，开发了额外的位姿图优化步骤，以确保将过去的位姿集注册到全局一致的配置中。</p><h4 id="A-四个累积漂移方向"><a href="#A-四个累积漂移方向" class="headerlink" title="A. 四个累积漂移方向"></a>A. 四个累积漂移方向</h4><p>得益于重力的惯性测量，在 VINS 中可以完全观察到滚动角和俯仰角。 如图 11 所示，随着物体的移动，3-D位置和旋转相对于参考系发生了相对变化。 然而，我们可以通过重力矢量来确定水平面，这意味着我们一直在观察绝对横滚和俯仰角。 因此，滚动和俯仰区域是世界坐标系中的绝对状态，而 x、y、z 和偏航是相对于参考坐标系的相对估计。 累积漂移仅发生在 x、y、z 和偏航角。 为了充分利用有效信息并有效地纠正漂移，我们修复了无漂移横滚和俯仰，并且仅在 4-DOF 中进行位姿图优化。</p><p><img src="/2022/03/01/2022-03-01-VINS-mono/image-20220510164146080.png" alt="image-20220510164146080" loading="lazy"></p><ul><li>涉及到vins的能观性：Kelly[6] ， Li[5]分析了Vins有四个自由度是不可观的：1）绝对的位置position; 2) 重力方向绝对的yaw角. 我们目前估计的位置和yaw都是相对于参考系来说的.</li></ul><h4 id="B-将关键帧添加到姿态图中"><a href="#B-将关键帧添加到姿态图中" class="headerlink" title="B. 将关键帧添加到姿态图中"></a>B. 将关键帧添加到姿态图中</h4><p>关键帧在 VIO 过程之后添加到姿态图中。每个关键帧作为位姿图中的一个顶点，并通过两种类型的边与其他顶点连接，如图 12.1 所示</p><p><img src="/2022/03/01/2022-03-01-VINS-mono/image-20220510165613363.png" alt="image-20220510165613363" loading="lazy"></p><p>1） 序列边：一个关键帧与其先前的关键帧建立了几个序列边。序列边表示两个关键帧之间的相对变换，直接取自 VIO。考虑到关键帧$i$和它之前的关键帧$j$，顺序边只包含相对位置$\hat{p}^i_{i,j}$和偏航角$\hat{\phi}_{ij}$</p><p><img src="/2022/03/01/2022-03-01-VINS-mono/image-20220510165533490.png" alt="image-20220510165533490" loading="lazy"></p><ol start="2"><li>闭环边：如果关键帧有回环连接，它通过位姿图中的闭环边连接闭环框架。类似地，闭环边缘仅包含一个 4-DOF 相对位姿变换，其定义与 (19) 相同。使用重定位的结果获得闭环边缘的值。</li></ol><h4 id="C-4-DOF-位姿图优化"><a href="#C-4-DOF-位姿图优化" class="headerlink" title="C. 4-DOF 位姿图优化"></a>C. 4-DOF 位姿图优化</h4><p>定义第i帧和第j帧之间边缘的残差最小为</p><p><img src="/2022/03/01/2022-03-01-VINS-mono/image-20220510170255263.png" alt="image-20220510170255263" loading="lazy"></p><p>其中$\hat{\phi}_i$和$\hat{θ}_i$是从单目 VIO 获得的滚动角和俯仰角的固定估计值。</p><p>通过最小化以下成本函数来优化序列边和闭环边的整个图：</p><p><img src="/2022/03/01/2022-03-01-VINS-mono/image-20220510171356328.png" alt="image-20220510171356328" loading="lazy"></p><p>其中 $\mathcal{S}$ 是所有连续边的集合，$\mathcal{L}$ 是所有闭环边的集合。尽管紧耦合的重定位已经有助于消除错误的闭环，但我们添加了另一个 Huber normρ(·) 以进一步减少任何可能的错误循环的影响。相反，我们没有对序列边使用任何鲁棒的正则项，因为这些边是从 VIO 中提取的，它已经包含足够的异常值拒绝机制。</p><p>位姿图优化和重定位（参见第 VII-C 节）在两个单独的线程中异步运行。这使得无论何时可用，都可以立即使用最优化的位姿图进行重新定位。类似地，即使当前的位姿图优化尚未完成，仍然可以使用现有的位姿图配置进行重新定位。该过程如图 9(b) 所示。</p><h4 id="D-位姿图合并"><a href="#D-位姿图合并" class="headerlink" title="D. 位姿图合并"></a>D. 位姿图合并</h4><p>位姿图不仅可以优化当前地图，还可以将当前地图与之前构建的地图合并。如果我们加载了以前构建的地图并检测到两个地图之间的循环连接，我们可以将它们合并在一起。由于所有边都是相对约束，因此位姿图优化通过回环连接自动将两个地图合并在一起。如图 13 所示，当前地图被循环边缘拉入前一个地图。每个顶点和每条边都是相对变量，因此，我们只需要固定位姿图中的第一个顶点。</p><p><img src="/2022/03/01/2022-03-01-VINS-mono/image-20220510172205335.png" alt="image-20220510172205335" loading="lazy"></p><h4 id="E-姿态图保存"><a href="#E-姿态图保存" class="headerlink" title="E. 姿态图保存"></a>E. 姿态图保存</h4><p>我们的位姿图的结构非常简单。我们只需要保存顶点和边，以及每个关键帧（顶点）的描述符。丢弃原始图像以减少内存消耗。具体来说，我们为关键帧保存的状态是</p><p><img src="/2022/03/01/2022-03-01-VINS-mono/image-20220510173848820.png" alt="image-20220510173848820" loading="lazy"></p><p>其中i是帧索引，$\hat{p}^w_i$和$\hat{q}^w_i$分别是来自VIO的位置和方向。如果这个框架有一个闭环框架，相对于闭环框架的索引。$\hat{p}^i_{iv}$和$\hat{ψ}_{iv}$是这两个框架之间的相对位置和偏航角，它是通过重定位获得的。$D(u, v,des)$是特征集。每个特征都包含二维位置及其简要描述符。</p><h4 id="F-姿态图加载"><a href="#F-姿态图加载" class="headerlink" title="F. 姿态图加载"></a>F. 姿态图加载</h4><p>我们使用相同的保存格式来加载关键帧。每个关键帧都是位姿图中的一个顶点。顶点的初始位姿是$\hat{p}^w_i$和$\hat{q}^w_i$。循环边缘由回环信息$\hat{p}^i_{iv},\hat{\psi}^i_{iv}$直接建立。每个关键帧与它的相邻关键帧建立几个连续的边缘，如（19）。加载位姿图后，我们立即执行全局 4-DOF 位姿图。位姿图保存和加载的速度与位姿图的大小呈线性相关。</p><h3 id="IX-实验结果"><a href="#IX-实验结果" class="headerlink" title="IX 实验结果"></a>IX 实验结果</h3><p>我们执行数据集和真实世界的实验以及两个应用程序来评估所提出的 VINS-Mono 系统。 在第一个实验中，我们在公共数据集上将所提出的算法与另一种最先进的算法进行了比较。 我们进行数值分析以详细展示我们系统的准确性。然后我们在室内环境中测试我们的系统以评估重复场景中的性能。 进行了大规模的实验，说明了长期的实用性。 此外，我们将建议的系统应用于两个应用程序。 Foraerial 机器人应用程序，我们使用 VINS-Mono 作为位置反馈来控制无人机遵循预定义的轨迹。然后我们将我们的方法移植到 iOS 移动设备上。</p><h4 id="A-公开数据集"><a href="#A-公开数据集" class="headerlink" title="A 公开数据集"></a>A 公开数据集</h4><h4 id="B-现实场景实验"><a href="#B-现实场景实验" class="headerlink" title="B 现实场景实验"></a>B 现实场景实验</h4><h4 id="C-应用"><a href="#C-应用" class="headerlink" title="C 应用"></a>C 应用</h4><h3 id="X-结论与未来工作"><a href="#X-结论与未来工作" class="headerlink" title="X 结论与未来工作"></a>X 结论与未来工作</h3><p>在本文中，我们提出了一种强大且通用的单目视觉惯性估计器。我们的方法在 IMU 预集成、估计初始化、在线外部校准、紧耦合 VIO、重定位和高效全局优化方面具有最先进和新颖的解决方案。我们通过与其他最先进的开源实现进行比较来展示卓越的性能。为了社区的利益，我们开源了 PC 和 iOS 实现。虽然基于特征的 VINS 估计器已经达到了实际部署的成熟度，但我们仍然有未来研究的方向。根据运动和环境的不同，单眼 VINS 可能会达到微弱可观察甚至退化的条件。我们对评估单目VINS 的可观测性特性的在线方法以及在线生成运动计划以恢复可观测性感兴趣。</p><p>另一个研究方向涉及单目 VINS 在各种消费设备（如 Android 手机）上的大规模部署。此应用程序需要对几乎所有传感器的内在和外在参数进行在线校准，以及校准质量的在线识别。最后，我们有兴趣在给定单目 VINS 结果的情况下生成密集地图。我们在无人机导航中应用的单目视觉惯性密集映射的第一个结果在 [47] 中提出。但是，仍然需要进行广泛的研究以进一步提高系统的准确性和鲁棒性。</p><h3 id="附录-基于四元数的IMU预积分"><a href="#附录-基于四元数的IMU预积分" class="headerlink" title="附录 基于四元数的IMU预积分"></a>附录 基于四元数的IMU预积分</h3><h2 id="代码框架"><a href="#代码框架" class="headerlink" title="代码框架"></a>代码框架</h2><h3 id="1-前端"><a href="#1-前端" class="headerlink" title="1. 前端"></a>1. 前端</h3><blockquote><ol><li>cv::goodfeaturesToTrack检测Harris角点</li><li>cv::CalcOpticalFlowPyrLK跟踪相邻帧的角点</li><li>cv::findFundamentalMat 去除异常点</li><li>统一的球面模型</li></ol></blockquote><h3 id="2-VINS主估计器"><a href="#2-VINS主估计器" class="headerlink" title="2. VINS主估计器"></a>2. VINS主估计器</h3><h3 id="2-后端"><a href="#2-后端" class="headerlink" title="2. 后端"></a>2. 后端</h3><blockquote><p>将所有路标点边缘化掉可以得到一个共视矩阵；</p><p>路标点的参数化形式；</p><p>数据关联 代表的 特征点之间的匹配啥的；</p><p>工程化的角度的看，VIO中好一点的还是VINS，</p><p>IMU预积分的P V Q来自于IMU的预积分，先得到初始值，然后通过联合优化进行进一步的优化；</p><p>VINS没有处理过纯旋转问题，可以检测一下纯相信IMU； 求单应性矩阵的问题；</p><p>VINS相对于OKVIS 效率要高一些；</p><p>像素矩阵的2*2矩阵通过标定值得到协方差</p><p>边缘化后，fill in 与信息损失怎么平衡？</p><p>精度提高：</p><ul><li>特征关联，光流</li><li>传感器，时间戳同步，少一点模糊</li></ul><p>安卓的rolling shutter不准；运动模糊比较重，安卓运算能力有限；</p><p>bias与噪声的话 bias影响更大一些，噪声给个先验值得话 其实差不多；</p><p>双目VIO对初始化得要求高吗？</p><p>VINS的共视非常稠密，因此边缘化对滑窗内所有帧都有影响；</p><p>IMU误差的雅可比矩阵与IMU传播的雅可比</p><ul><li>IMU传播的雅可比是关于bias的一个雅克比矩阵；</li></ul></blockquote><h3 id="3-初始化"><a href="#3-初始化" class="headerlink" title="3. 初始化"></a>3. 初始化</h3><blockquote><p><strong>VINS的初始化过程</strong></p><ol><li>先做一个纯视觉的SfM，计算初始位置，然后通过三角化恢复出初始位姿</li><li>IMU与单目要进行对齐，在滑动窗口计算出初始值（尺度，速度，位移，零偏，外参）</li></ol></blockquote><h3 id="4-回环检测"><a href="#4-回环检测" class="headerlink" title="4. 回环检测"></a>4. 回环检测</h3><blockquote><p><strong>关键帧的选择</strong></p><ol><li>像素差（归一化像素平面的像素差）</li><li>需要保证两帧间的视差，避免纯旋转；</li></ol><p>关键帧需要回环检测，四自由度的位姿图优化</p></blockquote><h4 id="5-后端优化"><a href="#5-后端优化" class="headerlink" title="5. 后端优化"></a>5. 后端优化</h4><blockquote><p>四自由度的因子图优化；</p></blockquote><h2 id="相关问题"><a href="#相关问题" class="headerlink" title="相关问题"></a>相关问题</h2><ol><li>vins的输出位姿的单位标准的单位，米和弧度</li><li>单目视觉里程计用来做定位并不准确，定位更像是做一个回环检测好一点</li><li>VINS跑飘了怎么办？IMU参数没有调好，特征点没有找着；尽量稳定一些，不要手持；</li><li>电脑版本与安卓版本区别大，硬件规格不同；</li><li>OKVIS双目更好，VINS单目更好容易<strong>扩展</strong>；</li><li>ORB初始化容易失败；</li><li>相机IMU外参最好尺子量；</li><li>IMU是否优化零偏，对结果影响不大</li><li>IMU随机游走类似于一种无规律的布朗运动</li><li>IMU积分两次飘得可能会很厉害，所以需要相机对其进行bias修正</li><li>2000~200的IMU可能都不在怎么样，需要进行调参；最好在范围内选择最贵的；</li><li>不能按照固定时间间隔取关键帧；</li><li>虚拟现实中会有一些纯旋转的动作；</li><li>IMU硬件同步；</li><li>相机畸变对相机影响；</li></ol><p><strong>1. 无人机飞的很高时，vinsmono的尺度无法恢复， 原因?</strong></p><p>答：近点特征点对三角化得到的三维点坐标比较准确，可以提供旋转、平移。 远点特征点，由于视差过小而不能可靠得到深度的点，无法提供比较准确的尺度和平移信息。仅能够提供相对准确的旋转信息。</p><p><strong>2. imu,轮速计，相机三个传感器，在二维场景下，传感器丢掉了什么约束，需要如何互补？</strong> 不是很会</p><p><strong>3.利用视觉和IMU估计外参的时候没有估计平移量是为什么？</strong></p><p>答：因为视觉尺度是不确定的，一般经过仪器测量得到， 如果需要估计，必须是估计尺度之后，在对外参的平移量进行估计。</p><p><strong>4. 买了一个imu，跑了一下Vins，发现acc_n和gyr_n设置这么大的时候，程序才不会跑飞，这是什么原因？谢谢</strong></p><img src="75&e=1664553599&token=kIxbL07-8jAj8w1n4s9zv64FuZZNEATmlU_Vm6zDttakjIRqYIMlyqvz0CA-wd5H-kA=.jpeg" alt="img" style="zoom:33%;" /><p>可能是因为IMU比较便宜</p>]]>
    </content>
    <id>https://grobenis.github.io/2022/03/01/2022-03-01-VINS-mono/</id>
    <link href="https://grobenis.github.io/2022/03/01/2022-03-01-VINS-mono/"/>
    <published>2022-03-01T15:55:29.000Z</published>
    <summary>
      <![CDATA[<p>本章想要抽空把之前的VINS_mono系统地学习一下。</p>
<p>本文是对 VINS-mono 的系统学习笔记，梳理这一单目视觉惯性系统的完整框架：前端通过 KLT 光流跟踪特征并对 IMU 测量做预积分，初始化恢复位姿、速度与重力矢量初值，滑动窗口内联合优化视觉与惯性测量，最后经闭环检测与四自由度位姿图优化消除累积漂移。</p>
<blockquote>
<p><strong>视觉惯性紧耦合，小代价换高精度</strong></p>
</blockquote>]]>
    </summary>
    <title>VINS-mono 学习笔记</title>
    <updated>2026-08-25T13:18:00.822Z</updated>
  </entry>
  <entry>
    <author>
      <name>guoben</name>
    </author>
    <category term="C++" scheme="https://grobenis.github.io/categories/C/"/>
    <category term="正则表达式" scheme="https://grobenis.github.io/tags/%E6%AD%A3%E5%88%99%E8%A1%A8%E8%BE%BE%E5%BC%8F/"/>
    <content>
      <![CDATA[<p>在实际应用中，有效得利用正则表达式可以极大地提高工作效率。借着LeetCode每日一题，今天学习一下C++语言中的正则表达式。</p><p>学习自：</p><ol><li><a href="https://www.cnblogs.com/jerrywossion/p/10086051.html">C++正则表达式</a></li><li><a href="https://leetcode-cn.com/problems/complex-number-multiplication/">LeetCode复数乘法</a></li></ol><p>本文借 LeetCode 复数乘法一题学习 C++ 的正则表达式，介绍 regex 库的基本类型以及 regex_match、regex_search、regex_replace 三大操作，并通过手工解析与正则解法的对比展示正则的高效简洁。</p><blockquote><p><strong>正则一行，胜过代码百行</strong></p></blockquote><span id="more"></span><h2 id="C-正则表达式"><a href="#C-正则表达式" class="headerlink" title="C++正则表达式"></a>C++正则表达式</h2><h3 id="一-简介"><a href="#一-简介" class="headerlink" title="一 简介"></a>一 简介</h3><p>​ C++ 中的regex 库提供了用于表示正则表达式和匹配结果的基本类型，以及搜寻、匹配、替换等函数。使用正则表达式处理字符串的流程包括：</p><ol><li>用正则表达式定义要匹配的字符串的<strong>规则</strong>；</li><li>然后对目标字符串进行<strong>匹配</strong>；</li><li>最后对匹配到的<strong>结果</strong>进行操作。</li></ol><h3 id="二-基本类型"><a href="#二-基本类型" class="headerlink" title="二 基本类型"></a>二 基本类型</h3><h4 id="2-1-basic-regex-及其实例化类型-regex、wregex"><a href="#2-1-basic-regex-及其实例化类型-regex、wregex" class="headerlink" title="2.1 basic_regex 及其实例化类型 regex、wregex"></a>2.1 <code>basic_regex</code> 及其实例化类型 <code>regex</code>、<code>wregex</code></h4><p>模板类型 <code>basic_regex</code> 用于表示正则表达式对象，<code>&lt;regex&gt;</code> 库提供了它的两种实例化类型：</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">Copytypedef basic_regex&lt;<span class="type">char</span>&gt;     regex;</span><br><span class="line"><span class="keyword">typedef</span> basic_regex&lt;<span class="type">wchar_t</span>&gt;  wregex;</span><br></pre></td></tr></table></figure><p>构造函数一般接受一个字符串作为参数，如</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="function">Copystd::regex <span class="title">reg</span> <span class="params">(<span class="string">&quot;[0-9A-Z]+&quot;</span>)</span></span>;</span><br></pre></td></tr></table></figure><p>构造函数中还有一个默认参数flags，默认值为 <code>std::regex::ECMAScript</code>，该参数可以用来设置正则表达式所采用的语法，如 <code>std::regex::grep, std::regex::awk</code> 等，也可以设置 case insensitive。flags 的多个值用比特位 OR 操作<code>|</code>连接，如</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line"><span class="function">Copystd::regex <span class="title">ninth</span> <span class="params">(<span class="string">&quot;\\bd\\w+&quot;</span>, ECMAScript | icase )</span></span>;</span><br></pre></td></tr></table></figure><p>但是正则语法的flag只能设置一种。</p><p>具体的构造函数声明及fags值的定义参考<a href="http://www.cplusplus.com/reference/regex/basic_regex/basic_regex/">basic_regex::basic_regex - C++ Reference</a>。</p><h4 id="2-2-match-results、sub-match-及其实例化类型"><a href="#2-2-match-results、sub-match-及其实例化类型" class="headerlink" title="2.2 match_results、sub_match 及其实例化类型"></a>2.2 <code>match_results</code>、<code>sub_match</code> 及其实例化类型</h4><p><code>match_results</code> 是一种 container-like 的模板类，用于存放对目标串执行完 regex matching 操作后匹配到的结果，其中的每个元素（即每个匹配结果）是一个 <code>sub_match</code> 的实例化对象。</p><p><code>match_results</code> 的实例类型有下列四种：</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">Copytypedef match_results&lt;<span class="type">const</span> <span class="type">char</span>*&gt; cmatch;</span><br><span class="line"><span class="keyword">typedef</span> match_results&lt;<span class="type">const</span> <span class="type">wchar_t</span>*&gt; wcmatch;</span><br><span class="line"><span class="keyword">typedef</span> match_results&lt;string::const_iterator&gt; smatch;</span><br><span class="line"><span class="keyword">typedef</span> match_results&lt;wstring::const_iterator&gt; wsmatch;</span><br></pre></td></tr></table></figure><p>与 <code>match_results</code> 相似，<code>sub_match</code> 的实例化类型也有四种：</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">Copytypedef sub_match&lt;<span class="type">const</span> <span class="type">char</span>*&gt; csub_match;</span><br><span class="line"><span class="keyword">typedef</span> sub_match&lt;<span class="type">const</span> <span class="type">wchar_t</span>*&gt; wcsub_match;</span><br><span class="line"><span class="keyword">typedef</span> sub_match&lt;string::const_iterator&gt; ssub_match;</span><br><span class="line"><span class="keyword">typedef</span> sub_match&lt;wstring::const_iterator&gt; wssub_match;</span><br></pre></td></tr></table></figure><p><code>sub_match</code> 对象存储的不是匹配到的字符串本身，而是指向这个串开始（initial）和结束后一个字符（past-the-end）的 <code>bidirectional iterators</code>，但是它们表现起来类似于一个字符串：</p><ul><li>成员函数 <code>length</code> 返回字符串长度；</li><li><code>compare</code> 用于和一个字符串或另一个 <code>sub_match</code> 对象比较，相等则返回0，不等则：若它比参数小（字典序）返回一个负值，否则返回一个正值；</li><li><code>str</code> 返回对应的字符串。</li></ul><p>根据一次匹配的结果，<code>match_results</code> 可能为空也可能不空，使用 <code>match_results::empty()</code> 来判空，使用 <code>match_results::size()</code> 来获取元素个数。对于一个不空的 <code>match_results</code> 对象，其第一个 <code>sub_match</code> 元素（<code>[0]</code>）对应着整个<strong>完整匹配</strong>，后续的元素对应着正则表达式中的 sub-match（即用<code>()</code>括起来的各个分组匹配），未匹配的部分可以通过 <code>prefix</code> 和 <code>suffix</code> 来获取。下面是一个简单的例子，参考自 <a href="https://www.boost.org/doc/libs/1_46_1/libs/regex/doc/html/boost_regex/captures.html">boost的文档</a>：</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br></pre></td><td class="code"><pre><span class="line">Copy<span class="meta">#<span class="keyword">include</span> <span class="string">&lt;regex&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;iostream&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;string&gt;</span></span></span><br><span class="line"></span><br><span class="line"><span class="function"><span class="type">int</span> <span class="title">main</span><span class="params">()</span> </span>&#123;</span><br><span class="line">    std::string target = <span class="string">&quot;@abc def--&quot;</span>;</span><br><span class="line">    <span class="function">std::regex <span class="title">e</span><span class="params">(<span class="string">&quot;(\\w+)\\W+(\\w+)&quot;</span>)</span></span>;</span><br><span class="line">    std::smatch sm;</span><br><span class="line">    std::<span class="built_in">regex_search</span>(target, sm, e);</span><br><span class="line"></span><br><span class="line">    std::cout &lt;&lt; <span class="string">&quot;sm.prefix: &quot;</span> &lt;&lt; sm.<span class="built_in">prefix</span>() &lt;&lt; std::endl;</span><br><span class="line">    <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">0</span>; i &lt; sm.<span class="built_in">size</span>(); ++i) &#123;</span><br><span class="line">        std::cout &lt;&lt; <span class="string">&quot;sm[&quot;</span> &lt;&lt; i &lt;&lt; <span class="string">&quot;]: &quot;</span> &lt;&lt; sm[i] &lt;&lt; std::endl;</span><br><span class="line">    &#125;</span><br><span class="line">    std::cout &lt;&lt; <span class="string">&quot;sm.suffix: &quot;</span> &lt;&lt; sm.<span class="built_in">suffix</span>() &lt;&lt; std::endl;</span><br><span class="line"></span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">&#125;</span><br><span class="line">CopyOutput:</span><br><span class="line">======================================</span><br><span class="line">sm.prefix: @</span><br><span class="line">sm[<span class="number">0</span>]: abc def</span><br><span class="line">sm[<span class="number">1</span>]: abc</span><br><span class="line">sm[<span class="number">2</span>]: def</span><br><span class="line">sm.suffix: --</span><br></pre></td></tr></table></figure><h3 id="三-正则操作"><a href="#三-正则操作" class="headerlink" title="三 正则操作"></a>三 正则操作</h3><h4 id="3-1-regex-match"><a href="#3-1-regex-match" class="headerlink" title="3.1 regex_match"></a>3.1 <code>regex_match</code></h4><p>用于将目标串和正则表达式匹配，返回一个 bool 值，<code>true</code> 为匹配，<code>false</code> 为不匹配。匹配的含义是目标字符串必须<strong>完全</strong>和正则表达式相匹配，不能有多余的字符，如果需要部分匹配则应使用<code>regex_search</code>。函数签名有很多，详见<a href="http://www.cplusplus.com/reference/regex/regex_match/">cplusplus.com</a>。</p><p>构造函数分成两种：</p><ul><li>一种接受一个 <code>match_results</code> 作为对象，参数顺序为：<br>- 待匹配的字符串，<br>- <code>match_results</code> 对象，<br>- 正则表达式对象；</li><li>另一种没有 <code>match_results</code>，参数顺序为：<br>- 待匹配的字符串，<br>- 正则表达式对象。<br>此外，构造函数还接受一个可选的 <code>flags</code> 参数，用于控制匹配选项，详见上述链接。</li></ul><h4 id="3-2-regex-search"><a href="#3-2-regex-search" class="headerlink" title="3.2 regex_search"></a>3.2 <code>regex_search</code></h4><p>只要目标字串中有一个字串（无论该子串在字符串中什么位置）能和正则表达式相匹配就返回<code>true</code>。函数签名同<code>regex_match</code>相似。</p><h4 id="3-3-regex-replace"><a href="#3-3-regex-replace" class="headerlink" title="3.3 regex_replace"></a>3.3 <code>regex_replace</code></h4><p>该函数有多个重载版本，常用的版本中参数顺序基本上为 <code>s</code>，<code>rgx</code>，<code>fmt</code>，<code>flags</code>，其中：</p><ul><li><code>s</code> 为要处理的字符串，</li><li><code>rgx</code> 为要匹配的正则表达式，</li><li><code>fmt</code> 为要替换的字符串，其中可以包含格式化字符，</li><li><code>flags</code>为可选的参数，用于设置一些选项。</li></ul><p>参数 <code>fmt</code> 中可以包含下列格式化字符：</p><table><thead><tr><th align="center">characters</th><th>replacement</th></tr></thead><tbody><tr><td align="center">$n</td><td>表示第n组匹配，n大于0</td></tr><tr><td align="center">$&amp;</td><td>表示整个匹配</td></tr><tr><td align="center">$&#96;</td><td>prefix</td></tr><tr><td align="center">$´</td><td>suffix</td></tr><tr><td align="center">$$     | 表示`$&#96;这个字符本身</td><td></td></tr></tbody></table><p>在替换时，会先将参数 <code>fmt</code> 中的格式化字符替换成相应的内容，然后再将这个字符串替换掉目标字符串中的完整匹配（相当于 <code>$&amp;</code> 或 <code>match_results[0]</code>）。</p><hr><h3 id="LeetCode例题-复数乘法"><a href="#LeetCode例题-复数乘法" class="headerlink" title="LeetCode例题 : 复数乘法"></a>LeetCode例题 : <strong>复数乘法</strong></h3><p>题目：复数 可以用字符串表示，遵循 “实部+虚部i” 的形式，并满足下述条件：</p><ul><li>实部是一个整数，取值范围是 [-100, 100]</li><li>虚部也是一个整数，取值范围是 [-100, 100]</li><li>$i^2 == -1$</li></ul><p>给你两个字符串表示的复数 num1 和 num2 ，请你遵循复数表示形式，返回表示它们乘积的字符串。</p><p><strong>示例 1:</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">输入：num1 = &quot;1+1i&quot;, num2 = &quot;1+1i&quot;</span><br><span class="line">输出：&quot;0+2i&quot;</span><br><span class="line">解释：(1 + i) * (1 + i) = 1 + i2 + 2 * i = 2i ，你需要将它转换为 0+2i 的形式。</span><br></pre></td></tr></table></figure><p><strong>示例 2:</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">输入：num1 = &quot;1+-1i&quot;, num2 = &quot;1+-1i&quot;</span><br><span class="line">输出：&quot;0+-2i&quot;</span><br><span class="line">解释：(1 - i) * (1 - i) = 1 + i2 - 2 * i = -2i ，你需要将它转换为 0+-2i 的形式。</span><br></pre></td></tr></table></figure><p>自己写的不用正则表达式的笨办法</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br><span class="line">54</span><br><span class="line">55</span><br><span class="line">56</span><br><span class="line">57</span><br><span class="line">58</span><br><span class="line">59</span><br><span class="line">60</span><br><span class="line">61</span><br><span class="line">62</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;string&gt;</span></span></span><br><span class="line"><span class="keyword">class</span> <span class="title class_">Solution</span> &#123;</span><br><span class="line"><span class="keyword">public</span>:</span><br><span class="line">    <span class="function">string <span class="title">complexNumberMultiply</span><span class="params">(string num1, string num2)</span> </span>&#123;</span><br><span class="line">        <span class="type">int</span> a1 = <span class="number">0</span>, b1 = <span class="number">0</span>, a2 = <span class="number">0</span>,b2 = <span class="number">0</span>;</span><br><span class="line">        string temp_num_str = <span class="string">&quot;&quot;</span>;</span><br><span class="line">        <span class="type">bool</span> aflag = <span class="literal">false</span>;</span><br><span class="line">        <span class="type">int</span> temp_num = <span class="number">0</span>;</span><br><span class="line">        <span class="type">int</span> temp_flag = <span class="number">1</span>;</span><br><span class="line">        <span class="keyword">for</span>(<span class="type">int</span> i=<span class="number">0</span>;i&lt;num1.<span class="built_in">size</span>();i++)&#123;</span><br><span class="line">            <span class="keyword">if</span>(num1[i]==<span class="string">&#x27;-&#x27;</span>)&#123;</span><br><span class="line">                temp_num_str = <span class="string">&quot;&quot;</span>;</span><br><span class="line">                temp_flag = <span class="number">-1</span>;</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">else</span> <span class="keyword">if</span>(num1[i]==<span class="string">&#x27;i&#x27;</span>)&#123;</span><br><span class="line">                temp_num = <span class="built_in">stoi</span>(temp_num_str);</span><br><span class="line">                temp_num_str = <span class="string">&quot;&quot;</span>;</span><br><span class="line">                b1 = temp_flag * temp_num;</span><br><span class="line">                temp_flag = <span class="number">1</span>;</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">else</span> <span class="keyword">if</span>(num1[i]==<span class="string">&#x27;+&#x27;</span>)&#123;</span><br><span class="line">                temp_num = <span class="built_in">stoi</span>(temp_num_str);</span><br><span class="line">                temp_num_str = <span class="string">&quot;&quot;</span>;</span><br><span class="line">                a1 = temp_flag * temp_num;</span><br><span class="line">                temp_flag = <span class="number">1</span>;</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">else</span>&#123;</span><br><span class="line">                temp_num_str+=num1[i];</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">        temp_num_str = <span class="string">&quot;&quot;</span>;</span><br><span class="line">        aflag = <span class="literal">false</span>;</span><br><span class="line">        temp_num = <span class="number">0</span>;</span><br><span class="line">        temp_flag = <span class="number">1</span>;</span><br><span class="line">        <span class="keyword">for</span>(<span class="type">int</span> i=<span class="number">0</span>;i&lt;num2.<span class="built_in">size</span>();i++)&#123;</span><br><span class="line">            <span class="keyword">if</span>(num2[i]==<span class="string">&#x27;-&#x27;</span>)&#123;</span><br><span class="line">                temp_num_str = <span class="string">&quot;&quot;</span>;</span><br><span class="line">                temp_flag = <span class="number">-1</span>;</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">else</span> <span class="keyword">if</span>(num2[i]==<span class="string">&#x27;i&#x27;</span>)&#123;</span><br><span class="line">                cout&lt;&lt;temp_num_str&lt;&lt;endl;</span><br><span class="line">                temp_num = <span class="built_in">stoi</span>(temp_num_str);</span><br><span class="line">                temp_num_str = <span class="string">&quot;&quot;</span>;</span><br><span class="line">                b2 = temp_flag*temp_num;</span><br><span class="line">                temp_flag = <span class="number">1</span>;</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">else</span> <span class="keyword">if</span>(num2[i]==<span class="string">&#x27;+&#x27;</span>)&#123;</span><br><span class="line">                cout&lt;&lt;temp_num_str&lt;&lt;endl;</span><br><span class="line">                temp_num = <span class="built_in">stoi</span>(temp_num_str);</span><br><span class="line">                temp_num_str = <span class="string">&quot;&quot;</span>;</span><br><span class="line">                a2 = temp_flag*temp_num;</span><br><span class="line">                temp_flag = <span class="number">1</span>;</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">else</span>&#123;</span><br><span class="line">                temp_num_str+=num2[i];</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line"></span><br><span class="line">        string ret = <span class="built_in">to_string</span>(a1*a2-b1*b2) + <span class="string">&quot;+&quot;</span> + <span class="built_in">to_string</span>(b1*a2+a1*b2) + <span class="string">&#x27;i&#x27;</span>;</span><br><span class="line">        <span class="keyword">return</span> ret;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>这样的代码，真可谓是又臭又长；接下来看一下LeetCode官方给出的解法</p><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Solution</span> &#123;</span><br><span class="line"><span class="keyword">public</span>:</span><br><span class="line">    <span class="function">string <span class="title">complexNumberMultiply</span><span class="params">(string num1, string num2)</span> </span>&#123;</span><br><span class="line">        <span class="function">regex <span class="title">re</span><span class="params">(<span class="string">&quot;\\+|i&quot;</span>)</span></span>;</span><br><span class="line">        <span class="function">vector&lt;string&gt; <span class="title">complex1</span><span class="params">(sregex_token_iterator(num1.begin(), num1.end(), re, <span class="number">-1</span>), std::sregex_token_iterator())</span></span>;</span><br><span class="line">        <span class="function">vector&lt;string&gt; <span class="title">complex2</span><span class="params">(sregex_token_iterator(num2.begin(), num2.end(), re, <span class="number">-1</span>), std::sregex_token_iterator())</span></span>;</span><br><span class="line">        <span class="type">int</span> real1 = <span class="built_in">stoi</span>(complex1[<span class="number">0</span>]);</span><br><span class="line">        <span class="type">int</span> imag1 = <span class="built_in">stoi</span>(complex1[<span class="number">1</span>]);</span><br><span class="line">        <span class="type">int</span> real2 = <span class="built_in">stoi</span>(complex2[<span class="number">0</span>]);</span><br><span class="line">        <span class="type">int</span> imag2 = <span class="built_in">stoi</span>(complex2[<span class="number">1</span>]);</span><br><span class="line">        <span class="keyword">return</span> <span class="built_in">to_string</span>(real1 * real2 - imag1 * imag2) + <span class="string">&quot;+&quot;</span> + <span class="built_in">to_string</span>(real1 * imag2 + imag1 * real2) + <span class="string">&quot;i&quot;</span>;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>官方题解使用了正则表达式，代码简洁高效。</p><h3 id="取巧解法"><a href="#取巧解法" class="headerlink" title="取巧解法"></a>取巧解法</h3><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Solution</span> &#123;</span><br><span class="line"><span class="keyword">public</span>:</span><br><span class="line">    <span class="function">string <span class="title">complexNumberMultiply</span><span class="params">(string a, string b)</span> </span>&#123;</span><br><span class="line"></span><br><span class="line">        <span class="type">int</span> a1, b1, a2, b2;</span><br><span class="line">        <span class="built_in">sscanf</span>(a.<span class="built_in">c_str</span>(), <span class="string">&quot;%d+%di&quot;</span>, &amp;a1, &amp;b1);</span><br><span class="line">        <span class="built_in">sscanf</span>(b.<span class="built_in">c_str</span>(), <span class="string">&quot;%d+%di&quot;</span>, &amp;a2, &amp;b2);</span><br><span class="line"></span><br><span class="line">        <span class="type">int</span> aa = a1 * a2 - b1 * b2;</span><br><span class="line">        <span class="type">int</span> bb = a1 * b2 + a2 * b1;</span><br><span class="line"></span><br><span class="line">        <span class="keyword">return</span> <span class="built_in">string</span>(<span class="built_in">to_string</span>(aa) + <span class="string">&quot;+&quot;</span> + <span class="built_in">to_string</span>(bb) + <span class="string">&quot;i&quot;</span>);</span><br><span class="line">    &#125;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>]]>
    </content>
    <id>https://grobenis.github.io/2022/02/26/2022-02-26-Cpp%E6%AD%A3%E5%88%99%E8%A1%A8%E8%BE%BE%E5%BC%8F/</id>
    <link href="https://grobenis.github.io/2022/02/26/2022-02-26-Cpp%E6%AD%A3%E5%88%99%E8%A1%A8%E8%BE%BE%E5%BC%8F/"/>
    <published>2022-02-26T04:59:41.000Z</published>
    <summary>
      <![CDATA[<p>在实际应用中，有效得利用正则表达式可以极大地提高工作效率。借着LeetCode每日一题，今天学习一下C++语言中的正则表达式。</p>
<p>学习自：</p>
<ol>
<li><a href="https://www.cnblogs.com/jerrywossion/p/10086051.html">C++正则表达式</a></li>
<li><a href="https://leetcode-cn.com/problems/complex-number-multiplication/">LeetCode复数乘法</a></li>
</ol>
<p>本文借 LeetCode 复数乘法一题学习 C++ 的正则表达式，介绍 regex 库的基本类型以及 regex_match、regex_search、regex_replace 三大操作，并通过手工解析与正则解法的对比展示正则的高效简洁。</p>
<blockquote>
<p><strong>正则一行，胜过代码百行</strong></p>
</blockquote>]]>
    </summary>
    <title>C++正则表达式</title>
    <updated>2026-08-25T13:18:00.778Z</updated>
  </entry>
  <entry>
    <author>
      <name>guoben</name>
    </author>
    <category term="学习" scheme="https://grobenis.github.io/categories/%E5%AD%A6%E4%B9%A0/"/>
    <category term="安卓" scheme="https://grobenis.github.io/tags/%E5%AE%89%E5%8D%93/"/>
    <category term="开发" scheme="https://grobenis.github.io/tags/%E5%BC%80%E5%8F%91/"/>
    <content>
      <![CDATA[<p>根据项目的需求，我们用纯c实现的模块，需要移植到android设备中，制作java版本的sdk，我们需要用到jni和ndk。</p><p>本文将学习制作SDK的流程。</p><p>本文完整演示如何用 NDK&#x2F;JNI 将纯 C 模块打包成 aar 格式的 Android SDK：从认识 AAR 与 NDK 入手，依次经历创建 Library 模块、javah 生成 JNI 头文件、实现 C 接口函数、配置 Android.mk 与 build.gradle、ndk-build 编译 so 并打包 aar，最后将 aar 导入工程中通过 gradle 依赖使用。</p><blockquote><p><strong>写好 JNI 接口，C 库即可服务安卓世界</strong></p></blockquote><span id="more"></span><h2 id="目标"><a href="#目标" class="headerlink" title="目标"></a>目标</h2><p>根据项目的需求，我们用纯c实现的模块，需要移植到android设备中，制作java版本的sdk，我们需要用到jni和ndk。</p><p><code>mylib.h</code></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line">//</span><br><span class="line">// Created by 郭犇 on 2021/4/1.</span><br><span class="line">//</span><br><span class="line"></span><br><span class="line">#ifndef MYDEMO_MY_LIB_H</span><br><span class="line">#define MYDEMO_MY_LIB_H</span><br><span class="line"></span><br><span class="line">#include &lt;stdlib.h&gt;</span><br><span class="line"></span><br><span class="line">char* count_mean(float *a ,int num);</span><br><span class="line">#endif //MYDEMO_MY_LIB_H</span><br><span class="line"></span><br></pre></td></tr></table></figure><p><code>mylib.c</code></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br></pre></td><td class="code"><pre><span class="line">//</span><br><span class="line">// Created by 郭犇 on 2021/4/1.</span><br><span class="line">//</span><br><span class="line"></span><br><span class="line">char* count_mean(float *a ,int num)&#123;</span><br><span class="line">    float sum = 0.0f;</span><br><span class="line">    for(int i = 0;i&lt;=num;i++)&#123;</span><br><span class="line">        sum += a[i];</span><br><span class="line">    &#125;</span><br><span class="line">    float mean = sum/num;</span><br><span class="line"></span><br><span class="line">    int n = 5; // 显示小数点后5位</span><br><span class="line">    int p,c;</span><br><span class="line">    //char *str=fcvt(mean,n,&amp;p,&amp;c);</span><br><span class="line">    char *str;</span><br><span class="line">    sprintf(str,&quot;%5.2f&quot;,mean);</span><br><span class="line">    return str;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>下面用实例一步一步介绍这个过程。</p><h2 id="一-认识AAR"><a href="#一-认识AAR" class="headerlink" title="一 认识AAR"></a>一 认识AAR</h2><p>Android 库的结构与 Android 应用模块的结构相同。它可以提供构建应用所需的一切内容，包括源代码、资源文件和 Android 清单。不过，Android 库将编译为您可以用作 Android 应用模块依赖项的 Android ARchive (AAR) 文件，而不是编译为在设备上运行的 APK。</p><p>与 JAR 文件不同，AAR 文件会为 Android 应用提供以下功能：</p><ul><li>AAR 文件可以包含多项 Android 资源和一个清单文件，让您除了能够在 Java 类和方法中进行捆绑以外，还能够在布局和可绘制对象等共享资源中进行捆绑。</li><li>AAR 文件可以<a href="https://developer.android.com/studio/build/native-dependencies?hl=zh-cn">包含 C&#x2F;C++ 库</a>，供应用模块的 C&#x2F;C++ 代码使用。</li></ul><p>*.aar，AAR（Android Archive）包是一个Android库项目的二进制归档文件。我们随便找一个aar文件，然后修改后缀名为‘zip’或者‘rar’格式，然后解压该文件，打开解压后的文件夹，截图如下所示：（每个aar解压后的内容可能不完全一样，但是都会包含AndroidManifest.xml，classes.jar，res，R.txt）。</p><p><img src="/2021/07/04/2021-07-04-%E9%80%9A%E8%BF%87ndkd%E6%89%93%E5%8C%85aar%E6%A0%BC%E5%BC%8F%E7%9A%84SDK/image-20210330150317969.png" loading="lazy"></p><p>*.aar文件中包含所有资源，class以及res资源文件。</p><h2 id="二-NDK"><a href="#二-NDK" class="headerlink" title="二 NDK"></a>二 NDK</h2><h3 id="简介"><a href="#简介" class="headerlink" title="简介"></a>简介</h3><p>本部分简要介绍了 NDK 的工作原理。Android NDK 是一组使您能将 C 或 C++（“原生代码”）嵌入到 Android 应用中的工具。能够在 Android 应用中使用原生代码对于想执行以下一项或多项操作的开发者特别有用：</p><ul><li>在平台之间移植其应用。</li><li>重复使用现有库，或者提供其自己的库供重复使用。</li><li>在某些情况下提高性能，特别是像游戏这种计算密集型应用。</li></ul><p><strong>Application.mk</strong>文件</p><p>不一定要有，可以用来配置编译平台相关内容，常用的估计只是APP_ABI字段，它用来指定我们需要基于哪些CPU架构的.so文件，当然你可以配置多个平台（如果ndk-build 没有指定 Application.mk，则默认编译出所有平台的.so文件）：</p><blockquote><p>APP_ABI :&#x3D; armeabi armeabi-v7a x86 mips</p></blockquote><p>这个文件还可以配置Android.mk文件</p><blockquote><p>APP_BUILD_SCRIPT :&#x3D; Android.mk</p></blockquote><h2 id="三-利用Android-Studio生成aar文件"><a href="#三-利用Android-Studio生成aar文件" class="headerlink" title="三 利用Android Studio生成aar文件"></a>三 利用Android Studio生成aar文件</h2><p>生成<code>.aar</code>形式SDK的整个过程我们可以利用AS实现。</p><h3 id="3-1-建立一个项目ProjectDemo"><a href="#3-1-建立一个项目ProjectDemo" class="headerlink" title="3.1 建立一个项目ProjectDemo"></a>3.1 建立一个项目ProjectDemo</h3><h4 id="1-新建项目"><a href="#1-新建项目" class="headerlink" title="1 新建项目"></a>1 新建项目</h4><p><img src="/2021/07/04/2021-07-04-%E9%80%9A%E8%BF%87ndkd%E6%89%93%E5%8C%85aar%E6%A0%BC%E5%BC%8F%E7%9A%84SDK/image-20210401170348808.png" loading="lazy"></p><h4 id="2-配置项目"><a href="#2-配置项目" class="headerlink" title="2 配置项目"></a>2 配置项目</h4><!-- <img src="image-20210401170459101.png" alt="image-20210401170459101" style="zoom: 67%;" /> 图片源文件已丢失 --><p>点finish结束即可。</p><h3 id="3-2-建立一个library的模块库MySDK"><a href="#3-2-建立一个library的模块库MySDK" class="headerlink" title="3.2 建立一个library的模块库MySDK"></a>3.2 建立一个library的模块库MySDK</h3><h4 id="1-新建模块"><a href="#1-新建模块" class="headerlink" title="1 新建模块"></a>1 新建模块</h4><p><img src="/2021/07/04/2021-07-04-%E9%80%9A%E8%BF%87ndkd%E6%89%93%E5%8C%85aar%E6%A0%BC%E5%BC%8F%E7%9A%84SDK/image-20210401170658032.png" loading="lazy"></p><h4 id="2-选择Library的模块"><a href="#2-选择Library的模块" class="headerlink" title="2 选择Library的模块"></a>2 选择Library的模块</h4><p><img src="/2021/07/04/2021-07-04-%E9%80%9A%E8%BF%87ndkd%E6%89%93%E5%8C%85aar%E6%A0%BC%E5%BC%8F%E7%9A%84SDK/image-20210401170716786.png" loading="lazy"></p><h4 id="3-配置该模块"><a href="#3-配置该模块" class="headerlink" title="3 配置该模块"></a>3 配置该模块</h4><p><img src="/2021/07/04/2021-07-04-%E9%80%9A%E8%BF%87ndkd%E6%89%93%E5%8C%85aar%E6%A0%BC%E5%BC%8F%E7%9A%84SDK/image-20210401171021523.png" loading="lazy"></p><p>现在建议从Android视图切换到Project视图，这样有利于我看看清楚整个项目的目录结构</p><p><img src="/2021/07/04/2021-07-04-%E9%80%9A%E8%BF%87ndkd%E6%89%93%E5%8C%85aar%E6%A0%BC%E5%BC%8F%E7%9A%84SDK/image-20210401171346953.png" loading="lazy"></p><h3 id="3-3-建立MySDK类-编写Native方法，生成jni头文件"><a href="#3-3-建立MySDK类-编写Native方法，生成jni头文件" class="headerlink" title="3.3 建立MySDK类,编写Native方法，生成jni头文件"></a>3.3 建立MySDK类,编写Native方法，生成jni头文件</h3><h4 id="1-建立类"><a href="#1-建立类" class="headerlink" title="1 建立类"></a>1 建立类</h4><p>接下来，我们建立一个名为MySDK的java类，作为SDK的接口。</p><p><img src="/2021/07/04/2021-07-04-%E9%80%9A%E8%BF%87ndkd%E6%89%93%E5%8C%85aar%E6%A0%BC%E5%BC%8F%E7%9A%84SDK/image-20210401171535574.png" loading="lazy"></p><p>生成的函数类如下：</p><p><img src="/2021/07/04/2021-07-04-%E9%80%9A%E8%BF%87ndkd%E6%89%93%E5%8C%85aar%E6%A0%BC%E5%BC%8F%E7%9A%84SDK/image-20210401171822235.png" loading="lazy"></p><h4 id="2-书写方法名"><a href="#2-书写方法名" class="headerlink" title="2 书写方法名"></a>2 书写方法名</h4><p>接下来，我们可以需要写类中的方法，目前只需要写函数名即可。我定义了一个用于计算链表平均值的函数，输入为一个</p><p><img src="/2021/07/04/2021-07-04-%E9%80%9A%E8%BF%87ndkd%E6%89%93%E5%8C%85aar%E6%A0%BC%E5%BC%8F%E7%9A%84SDK/image-20210401172213525.png" loading="lazy"></p><p>（为了展示后边Jni的书写方法，我用了一个链表的数据结构。）目前定义的这个类不需要实现他的方法，仅仅用于生成JNI的头文件。（由于JNI的头文件书写极其复杂，因此我们使用自动的方法生成）。</p><h4 id="3-利用javah生成头文件"><a href="#3-利用javah生成头文件" class="headerlink" title="3 利用javah生成头文件"></a>3 利用javah生成头文件</h4><p>利用javah生成头文件的操作， 我们可以使用AS自带的命令行完成。</p><p><img src="/2021/07/04/2021-07-04-%E9%80%9A%E8%BF%87ndkd%E6%89%93%E5%8C%85aar%E6%A0%BC%E5%BC%8F%E7%9A%84SDK/image-20210401172646427.png" loading="lazy"></p><p>在terminal中，输入以下的命令：</p><p><img src="/2021/07/04/2021-07-04-%E9%80%9A%E8%BF%87ndkd%E6%89%93%E5%8C%85aar%E6%A0%BC%E5%BC%8F%E7%9A%84SDK/image-20210401172733132.png" loading="lazy"></p><p>我们可以在目录中看到文件<code>com_xiaomi_phonesleepsdk_PhoneSleepSDK.h</code>：</p><p><img src="/2021/07/04/2021-07-04-%E9%80%9A%E8%BF%87ndkd%E6%89%93%E5%8C%85aar%E6%A0%BC%E5%BC%8F%E7%9A%84SDK/image-20210401172919207.png" loading="lazy"></p><p>内容长这样：</p><p><img src="/2021/07/04/2021-07-04-%E9%80%9A%E8%BF%87ndkd%E6%89%93%E5%8C%85aar%E6%A0%BC%E5%BC%8F%E7%9A%84SDK/image-20210401173031982.png" loading="lazy"></p><h3 id="3-4-建立JNI文件夹，建立main-c，实现函数，编写编译文件Android-mk"><a href="#3-4-建立JNI文件夹，建立main-c，实现函数，编写编译文件Android-mk" class="headerlink" title="3.4 建立JNI文件夹，建立main.c，实现函数，编写编译文件Android.mk"></a>3.4 建立JNI文件夹，建立main.c，实现函数，编写编译文件Android.mk</h3><h4 id="1-建立JNI文件夹"><a href="#1-建立JNI文件夹" class="headerlink" title="1 建立JNI文件夹"></a>1 建立JNI文件夹</h4><p><img src="/2021/07/04/2021-07-04-%E9%80%9A%E8%BF%87ndkd%E6%89%93%E5%8C%85aar%E6%A0%BC%E5%BC%8F%E7%9A%84SDK/image-20210401173247121.png" loading="lazy"></p><h4 id="2-建立main-c文件"><a href="#2-建立main-c文件" class="headerlink" title="2 建立main.c文件"></a>2 建立main.c文件</h4><p>main.c文件将实现前文中文件中的方法。作为JAVA转C的接口。</p><p><img src="/2021/07/04/2021-07-04-%E9%80%9A%E8%BF%87ndkd%E6%89%93%E5%8C%85aar%E6%A0%BC%E5%BC%8F%E7%9A%84SDK/image-20210401173328777.png" loading="lazy"></p><p><img src="/2021/07/04/2021-07-04-%E9%80%9A%E8%BF%87ndkd%E6%89%93%E5%8C%85aar%E6%A0%BC%E5%BC%8F%E7%9A%84SDK/image-20210401173501401.png" loading="lazy"></p><h4 id="3-实现接口函数"><a href="#3-实现接口函数" class="headerlink" title="3 实现接口函数"></a>3 实现接口函数</h4><p>在实现函数之前，我们需要把生成的头文件，移动或者复制到jni文件夹</p><p><img src="/2021/07/04/2021-07-04-%E9%80%9A%E8%BF%87ndkd%E6%89%93%E5%8C%85aar%E6%A0%BC%E5%BC%8F%E7%9A%84SDK/image-20210401173600997.png" loading="lazy"></p><p>然后可以把自己的C库复制到jni目录下边。</p><p><img src="/2021/07/04/2021-07-04-%E9%80%9A%E8%BF%87ndkd%E6%89%93%E5%8C%85aar%E6%A0%BC%E5%BC%8F%E7%9A%84SDK/image-20210401193013539.png" loading="lazy"></p><p>接下来，我们需要利用JNI实现main.c中的接口函数。</p><p>main.c内容如下：</p><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br></pre></td><td class="code"><pre><span class="line"><span class="comment">//</span></span><br><span class="line"><span class="comment">// Created by 郭犇 on 2021/4/1.</span></span><br><span class="line"><span class="comment">//</span></span><br><span class="line"></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;stdlib.h&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&quot;com_xiaomi_mysdk_MySDK.h&quot;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&quot;my_lib.h&quot;</span></span></span><br><span class="line"></span><br><span class="line"><span class="type">static</span> jstring <span class="title function_">stoJstring</span><span class="params">(JNIEnv* env, <span class="type">char</span>* pat)</span>;</span><br><span class="line"></span><br><span class="line">JNIEXPORT jstring JNICALL <span class="title function_">Java_com_xiaomi_mysdk_MySDK_my_1count_1mean_1funtion</span></span><br><span class="line">   <span class="params">(JNIEnv *env, jobject thisz , jobject a, jint b)</span>&#123;</span><br><span class="line">        <span class="type">int</span> num = b; <span class="comment">//JNI语法中，int与jint支持强制转换</span></span><br><span class="line">        <span class="comment">//链表的类型转换比较复杂</span></span><br><span class="line"></span><br><span class="line">        jclass list_class = (*env)-&gt;FindClass(env,<span class="string">&quot;java/util/ArrayList&quot;</span>);</span><br><span class="line">        jmethodID list_get =  (*env)-&gt;GetMethodID(env, list_class, <span class="string">&quot;get&quot;</span>, <span class="string">&quot;(I)Ljava/lang/Object;&quot;</span>);</span><br><span class="line">        jmethodID list_size = (*env)-&gt;GetMethodID(env, list_class, <span class="string">&quot;size&quot;</span>, <span class="string">&quot;()I&quot;</span>);</span><br><span class="line"></span><br><span class="line">        jclass float_class = (*env)-&gt;FindClass(env,<span class="string">&quot;java/lang/Float&quot;</span>);</span><br><span class="line">        jmethodID get_float_value = (*env)-&gt;GetMethodID(env, float_class, <span class="string">&quot;floatValue&quot;</span>,<span class="string">&quot;()F&quot;</span>);</span><br><span class="line"></span><br><span class="line">        <span class="type">float</span> *l = (<span class="type">float</span> *)<span class="built_in">malloc</span>(num*(<span class="keyword">sizeof</span>(<span class="type">float</span>)));</span><br><span class="line">        <span class="keyword">for</span> (<span class="type">int</span> i=<span class="number">0</span>; i &lt; num; i++) &#123;</span><br><span class="line">            jobject obj_element = (*env)-&gt;CallObjectMethod(env, a, list_get, i);</span><br><span class="line">            <span class="type">float</span> float_ele = (*env)-&gt;CallFloatMethod(env, obj_element, get_float_value);</span><br><span class="line">            l[i] = float_ele;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="type">char</span>* str = count_mean(l ,num);</span><br><span class="line">        jstring ret = stoJstring(env, str);</span><br><span class="line">        <span class="keyword">return</span> ret;</span><br><span class="line">   &#125;</span><br><span class="line"></span><br><span class="line"><span class="type">static</span> jstring <span class="title function_">stoJstring</span><span class="params">(JNIEnv* env, <span class="type">char</span>* pat)</span>&#123;</span><br><span class="line">    jclass strClass = (*env)-&gt;FindClass(env, <span class="string">&quot;java/lang/String&quot;</span>);</span><br><span class="line">    jmethodID ctorID = (*env)-&gt;GetMethodID(env, strClass, <span class="string">&quot;&lt;init&gt;&quot;</span>, <span class="string">&quot;([BLjava/lang/String;)V&quot;</span>);</span><br><span class="line">    jbyteArray bytes = (*env)-&gt;NewByteArray(env, <span class="built_in">strlen</span>(pat));</span><br><span class="line">    (*env)-&gt;SetByteArrayRegion(env, bytes, <span class="number">0</span>, <span class="built_in">strlen</span>(pat), (jbyte*)pat);</span><br><span class="line">    jstring encoding = (*env)-&gt;NewStringUTF(env, <span class="string">&quot;utf-8&quot;</span>);</span><br><span class="line">    <span class="keyword">return</span> (jstring)(*env)-&gt;NewObject(env, strClass, ctorID, bytes, encoding);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="4-配置-Android-mk"><a href="#4-配置-Android-mk" class="headerlink" title="4 配置 Android.mk"></a>4 配置 Android.mk</h4><p>该文件内容如下：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">LOCAL_PATH := $(call my-dir)</span><br><span class="line"></span><br><span class="line">include $(CLEAR_VARS)</span><br><span class="line"></span><br><span class="line">LOCAL_MODULE    := MySDK</span><br><span class="line">LOCAL_SRC_FILES := main.c</span><br><span class="line">LOCAL_SRC_FILES += my_lib.c</span><br><span class="line"></span><br><span class="line">include $(BUILD_SHARED_LIBRARY)</span><br></pre></td></tr></table></figure><p>其中，LOCAL_MODULE :&#x3D; mysdklibrary，这里的定义，最终生成的so文件是 libMySDK.so。</p><p>此外可以加上以下语句，用于生成最终的log。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">LOCAL_LDLIBS :=-llog</span><br></pre></td></tr></table></figure><h4 id="5-配置-MySDK下的build-gradle"><a href="#5-配置-MySDK下的build-gradle" class="headerlink" title="5 配置 MySDK下的build.gradle"></a>5 配置 MySDK下的build.gradle</h4><p>修改图下的位置</p><p><img src="/2021/07/04/2021-07-04-%E9%80%9A%E8%BF%87ndkd%E6%89%93%E5%8C%85aar%E6%A0%BC%E5%BC%8F%E7%9A%84SDK/image-20210402191344975.png" loading="lazy"></p><p>其中，moduleName对应的是 SDK的名字。cFlags表示C语言对应的标准。</p><p>sourceSets设置中。jni.srcDirs指的是我们放置C语言源代码的目录。</p><p>jniLibs.srcDirs指的是生成的so库所在的位置。（注：这条语句不是用来指示生成的位置，只是为后续打包aar时找到so库用的）。</p><p>到这里，jni部分的工作基本完成了。</p><h4 id="6-调用ndk-build生成so库。"><a href="#6-调用ndk-build生成so库。" class="headerlink" title="6 调用ndk-build生成so库。"></a>6 调用ndk-build生成so库。</h4><p><img src="/2021/07/04/2021-07-04-%E9%80%9A%E8%BF%87ndkd%E6%89%93%E5%8C%85aar%E6%A0%BC%E5%BC%8F%E7%9A%84SDK/image-20210401193741347.png" loading="lazy"></p><p>生成的so库在如下的位置</p><p><img src="/2021/07/04/2021-07-04-%E9%80%9A%E8%BF%87ndkd%E6%89%93%E5%8C%85aar%E6%A0%BC%E5%BC%8F%E7%9A%84SDK/image-20210402191907379.png" loading="lazy"></p><h3 id="3-5-把源代码和so库一并打包到aar文件中"><a href="#3-5-把源代码和so库一并打包到aar文件中" class="headerlink" title="3.5 把源代码和so库一并打包到aar文件中"></a>3.5 把源代码和so库一并打包到aar文件中</h3><p>我们可以直接利用AS自带的右方的gradle按钮。按照如图的目录找到assembleRelease</p><p><img src="/2021/07/04/2021-07-04-%E9%80%9A%E8%BF%87ndkd%E6%89%93%E5%8C%85aar%E6%A0%BC%E5%BC%8F%E7%9A%84SDK/image-20210402192057568.png" loading="lazy"></p><p>运行结束之后，我们就可以在MyDemo&#x2F;MySDK&#x2F;build&#x2F;outputs&#x2F;aar&#x2F;目录中找到MySDK-release.aar文件。</p><p><img src="/2021/07/04/2021-07-04-%E9%80%9A%E8%BF%87ndkd%E6%89%93%E5%8C%85aar%E6%A0%BC%E5%BC%8F%E7%9A%84SDK/image-20210402192217160.png" loading="lazy"></p><p>双击aar文件可以看到如下所示的目录；</p><p><img src="/2021/07/04/2021-07-04-%E9%80%9A%E8%BF%87ndkd%E6%89%93%E5%8C%85aar%E6%A0%BC%E5%BC%8F%E7%9A%84SDK/image-20210402192323837.png" loading="lazy"></p><p>可以发现包含了各种文件。jni文件夹下是四个so库。可供Java程序调用的so库就在jni目录中。</p><p><img src="/2021/07/04/2021-07-04-%E9%80%9A%E8%BF%87ndkd%E6%89%93%E5%8C%85aar%E6%A0%BC%E5%BC%8F%E7%9A%84SDK/image-20210402192431250.png" loading="lazy"></p><p>至此，<strong>打包成功！</strong>。</p><h2 id="四-将-aar导入项目中进行使用"><a href="#四-将-aar导入项目中进行使用" class="headerlink" title="四 将.aar导入项目中进行使用"></a>四 将.aar导入项目中进行使用</h2><p>下面主要看看在Android Studio中如何加载本地的*.aar文件。</p><ol><li><p>把aar文件放在一个文件目录内，比如就放在libs目录内；</p><p><img src="/2021/07/04/2021-07-04-%E9%80%9A%E8%BF%87ndkd%E6%89%93%E5%8C%85aar%E6%A0%BC%E5%BC%8F%E7%9A%84SDK/image-20210402193110508.png" loading="lazy"></p></li><li><p>在app的build.gradle文件添加如下内容；</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">repositories &#123;</span><br><span class="line">    flatDir &#123;</span><br><span class="line">        dirs <span class="string">&#x27;libs&#x27;</span></span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></li><li><p>之后只需要添加一句gradle依赖便方便的引用了该aar文件；</p><figure class="highlight gradle"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">dependencies</span> &#123;</span><br><span class="line">    implementation <span class="string">&#x27;androidx.appcompat:appcompat:1.2.0&#x27;</span></span><br><span class="line">    implementation <span class="string">&#x27;com.google.android.material:material:1.3.0&#x27;</span></span><br><span class="line">    implementation <span class="string">&#x27;androidx.constraintlayout:constraintlayout:2.0.4&#x27;</span></span><br><span class="line"></span><br><span class="line"><span class="comment">//添加以下这句</span></span><br><span class="line">    implementation <span class="keyword">fileTree</span>(dir: <span class="string">&#x27;libs&#x27;</span>, <span class="keyword">include</span>: [<span class="string">&#x27;*.aar&#x27;</span>])</span><br><span class="line"></span><br><span class="line">    testImplementation <span class="string">&#x27;junit:junit:4.+&#x27;</span></span><br><span class="line">    androidTestImplementation <span class="string">&#x27;androidx.test.ext:junit:1.1.2&#x27;</span></span><br><span class="line">    androidTestImplementation <span class="string">&#x27;androidx.test.espresso:espresso-core:3.3.0&#x27;</span></span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>至此，在Android Studio中加载本地的*.aar文件就结束。</p></li><li><p>修改主活动</p><figure class="highlight java"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">public</span> <span class="keyword">class</span> <span class="title class_">MainActivity</span> <span class="keyword">extends</span> <span class="title class_">AppCompatActivity</span> &#123;</span><br><span class="line">    <span class="keyword">private</span> <span class="keyword">static</span> <span class="keyword">final</span> <span class="type">String</span> <span class="variable">TAG</span> <span class="operator">=</span> <span class="string">&quot;AppCompatActivity&quot;</span>;</span><br><span class="line"></span><br><span class="line">    <span class="meta">@Override</span></span><br><span class="line">    <span class="keyword">protected</span> <span class="type">void</span> <span class="title function_">onCreate</span><span class="params">(Bundle savedInstanceState)</span> &#123;</span><br><span class="line">        <span class="built_in">super</span>.onCreate(savedInstanceState);</span><br><span class="line">        setContentView(R.layout.activity_main);</span><br><span class="line">        Log.d(TAG, <span class="string">&quot;1&quot;</span>);</span><br><span class="line">        <span class="type">List</span>&lt;Float&gt; <span class="variable">a</span> <span class="operator">=</span> <span class="keyword">new</span> <span class="title class_">ArrayList</span>&lt;&gt;();</span><br><span class="line">        a.add(<span class="number">1.0f</span>);</span><br><span class="line">        a.add(<span class="number">2.0f</span>);</span><br><span class="line">        a.add(<span class="number">3.0f</span>);</span><br><span class="line">        <span class="type">int</span> <span class="variable">b</span> <span class="operator">=</span> <span class="number">3</span>;</span><br><span class="line">        <span class="type">float</span> <span class="variable">ret</span> <span class="operator">=</span> MySDK.my_count_mean_funtion(a,b);</span><br><span class="line">        Log.d(<span class="string">&quot;ret&quot;</span>, Float.toString(ret));</span><br><span class="line">    &#125;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></li><li><p>运行程序得到正确结果</p><p><img src="/2021/07/04/2021-07-04-%E9%80%9A%E8%BF%87ndkd%E6%89%93%E5%8C%85aar%E6%A0%BC%E5%BC%8F%E7%9A%84SDK/image-20210419201234352.png" loading="lazy"></p></li></ol><h2 id="注意事项"><a href="#注意事项" class="headerlink" title="注意事项"></a>注意事项</h2><ol><li>jni接口文件的编写是能否成功的关键，考验编程能力</li><li>为了便于调试，建议先在将主程序与module连接起来调试通，最后再生成aar形式的SDK；</li></ol>]]>
    </content>
    <id>https://grobenis.github.io/2021/07/04/2021-07-04-%E9%80%9A%E8%BF%87ndkd%E6%89%93%E5%8C%85aar%E6%A0%BC%E5%BC%8F%E7%9A%84SDK/</id>
    <link href="https://grobenis.github.io/2021/07/04/2021-07-04-%E9%80%9A%E8%BF%87ndkd%E6%89%93%E5%8C%85aar%E6%A0%BC%E5%BC%8F%E7%9A%84SDK/"/>
    <published>2021-07-04T05:06:09.000Z</published>
    <summary>
      <![CDATA[<p>根据项目的需求，我们用纯c实现的模块，需要移植到android设备中，制作java版本的sdk，我们需要用到jni和ndk。</p>
<p>本文将学习制作SDK的流程。</p>
<p>本文完整演示如何用 NDK&#x2F;JNI 将纯 C 模块打包成 aar 格式的 Android SDK：从认识 AAR 与 NDK 入手，依次经历创建 Library 模块、javah 生成 JNI 头文件、实现 C 接口函数、配置 Android.mk 与 build.gradle、ndk-build 编译 so 并打包 aar，最后将 aar 导入工程中通过 gradle 依赖使用。</p>
<blockquote>
<p><strong>写好 JNI 接口，C 库即可服务安卓世界</strong></p>
</blockquote>]]>
    </summary>
    <title>通过NDK打包aar格式的SDK</title>
    <updated>2026-08-25T13:18:00.716Z</updated>
  </entry>
  <entry>
    <author>
      <name>guoben</name>
    </author>
    <category term="学习" scheme="https://grobenis.github.io/categories/%E5%AD%A6%E4%B9%A0/"/>
    <category term="算法" scheme="https://grobenis.github.io/tags/%E7%AE%97%E6%B3%95/"/>
    <category term="可穿戴" scheme="https://grobenis.github.io/tags/%E5%8F%AF%E7%A9%BF%E6%88%B4/"/>
    <content>
      <![CDATA[<p>​ 以后要坚持每周更新一篇博客，所以今天先记载一下自己调研的可穿戴算法中的压力监测算法。</p><p>本文调研可穿戴设备中的压力监测算法，先介绍压力的心理学定义、组成与 PSS、PSTR、DASS-21 等评估量表。文章重点讲解基于心率变异性（HRV）的时域、频域特征及其与压力的关系，并梳理 FirstBeat 压力监测系统的检测流程，最后对比了华为、苹果、小米等市面产品的实现方案。</p><blockquote><p><strong>心率变异性，映射身心压力状态</strong></p></blockquote><span id="more"></span><h2 id="一-压力"><a href="#一-压力" class="headerlink" title="一 压力"></a>一 压力</h2><h3 id="1-1-压力"><a href="#1-1-压力" class="headerlink" title="1.1 压力"></a>1.1 压力</h3><h4 id="概念"><a href="#概念" class="headerlink" title="概念"></a>概念</h4><p>压力是压力源和压力反应共同构成的一种认知和行为体验过程。心理压力（Mental Stress）被定义为<strong>人们认为（评价）他们的需求超出其应付能力的程度</strong>。</p><h4 id="组成"><a href="#组成" class="headerlink" title="组成"></a>组成</h4><p>从生理-社会-心理学取向的角度可以把压力理解为一种复杂的身心历程，它包含三大部分：</p><ol><li><strong>压力源</strong>（stressor）任何情境或刺激具有伤害或威胁个人的潜在因素，统称为压力源，即压力来源。</li><li><strong>认知评估</strong>（cognitive appraisal） 当事人认为经历的刺激或情境，对于个人确实有所威胁时，此时即构成压力，但如果认为是种解脱或乐趣而不是威胁时，则不构成压力，此历程即为认知评估。</li><li><strong>焦虑反应</strong>（anxiety reaction）当事人意识到他生理的健康、身体的安全、心理的安静、事业的成败或自尊的维护，甚至自己所关心的人等正处于危险的状况或受到威胁时所做的反应，即为焦虑反应。</li></ol><p>因此，压力产生的身心历程是：压力的来源-威胁的知觉-焦虑的反应。</p><h4 id="意义"><a href="#意义" class="headerlink" title="意义"></a>意义</h4><p>压力是正常日常生活的一部分，在生理上表现为ANS系统的交感神经活动和副交感神经活动的减少。</p><p><strong>正性压力</strong>是有益的压力，产生于个体被激发和鼓舞的情景中，当压力持续增加，正性压力会逐渐转化为负性压力，绩效或健康状况随之下降，生病的危险加大。</p><p><strong>中性压力</strong>是一些不会引发后续效应的感官刺激，它们无所谓好坏。比如，看到一则关于遥远的城市发生火灾的新闻，或是听说某明星的婚姻出现危机……</p><p><strong>负性压力</strong>是有害的压力，经常被简称为压力，比如险些发生交通事故、工作中频繁的加班、夜晚隔壁邻居家吵闹的音乐声等。负性压力又可以分为两类：急性压力和慢性压力，前者来势汹汹但迅速消退；后者出现的时候不甚强烈，但旷日持久。</p><h2 id="二-压力评估方法"><a href="#二-压力评估方法" class="headerlink" title="二 压力评估方法"></a>二 压力评估方法</h2><p>在国内，大部分研究以心理学评估量表为准，而部分高校也开始了基于生理信号的心理压力评估的研究，并且取得了一定的进展。</p><p><img src="/2021/06/19/2021-06-19-%E5%8F%AF%E7%A9%BF%E6%88%B4%E7%AE%97%E6%B3%95%E4%B8%AD%E7%9A%84%E5%8E%8B%E5%8A%9B%E7%9B%91%E6%B5%8B/image-20210203150149633.png" loading="lazy"></p><h3 id="2-1-心理学评估量表"><a href="#2-1-心理学评估量表" class="headerlink" title="2.1 心理学评估量表"></a>2.1 心理学评估量表</h3><h4 id="量表的可信度分析。"><a href="#量表的可信度分析。" class="headerlink" title="量表的可信度分析。"></a>量表的可信度分析。</h4><p><strong>α信度</strong>：</p><p>α信度是检视<a href="images/https://zh.wikipedia.org/wiki/%E4%BF%A1%E5%BA%A6">信度</a>的一种方法，由<a href="images/https://zh.wikipedia.org/wiki/%E6%9D%8E%C2%B7%E5%85%8B%E9%9A%86%E5%B7%B4%E8%B5%AB">李·克隆巴赫</a>在1951年提出。它克服了部分折半法的缺点，是目前<a href="images/https://zh.wikipedia.org/wiki/%E7%A4%BE%E4%BC%9A%E7%A7%91%E5%AD%A6">社会科学</a>研究最常使用的信度分析方法。也可表示内在一致性。</p><table><thead><tr><th align="center">Cronbach’s alpha</th><th align="center">Internal consistency</th></tr></thead><tbody><tr><td align="center">α ≥ 0.9</td><td align="center">Excellent</td></tr><tr><td align="center">0.9 &gt; α ≥ 0.8</td><td align="center">Good</td></tr><tr><td align="center">0.8 &gt; α ≥ 0.7</td><td align="center">Acceptable</td></tr><tr><td align="center">0.7 &gt; α ≥ 0.6</td><td align="center">Questionable</td></tr><tr><td align="center">0.6 &gt; α ≥ 0.5</td><td align="center">Poor</td></tr><tr><td align="center">0.5 &gt; α</td><td align="center">Unacceptable</td></tr></tbody></table><h4 id="A-PSS知觉压力量表"><a href="#A-PSS知觉压力量表" class="headerlink" title="A PSS知觉压力量表"></a>A PSS知觉压力量表</h4><p><a href="images/https://www.psychspace.com/psych/action-tag-tagname-%D1%B9%EF%BF%BD%EF%BF%BD.html">知觉压力量表</a> (Chinese 14-item PSS) (Perceived Stress Scale, PSS) (Cohen, Kamarck, &amp; Mermelstein, 1983)是1983并能提出的评估心理压力的量表。知觉压力量表（Perceived Stress Scale）可较好地衡量一个人在生活中所受到的压力情况。PSS已被广泛运用于测量知觉压力，评估情况压力，减压干预的有效性，以及心理压力与心理压力精神和身体失调等方面的研究。此量表已经证实信度和效度良好，其$\alpha$系数为0.78。量表的总分与各个项目之间的相关系数为0.37-0.53，表面的同质性与内部一致性比较高。本研究中失控感因子的信度为0.688，紧张感因子的信度为0.576。</p><p>*PSS预测早期健康问题更为有效；另外实施PSS测量，还可以评估个人不良习惯造成的慢性压力。</p><h5 id="内容"><a href="#内容" class="headerlink" title="内容"></a>内容</h5><p>该量表主要是询问您关于您上个月的感受和想法。在下面的每一种情况下，都需要您指出你感觉到或想到的频率。虽然有些问题是相似的，但它们之间也有一些区别，你应该把每一个问题作为单独的问题来对待，最好的方法是快速回答每个问题。也就是说，不要试图数出你觉得的某一种特定方式的次数，而是指出一个较为合理的近似估计。</p><p>对于每个问题，请从以下选项中选择：0 从不 ；1几乎不 ；2有时；4 经常；5 总是</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line">In the last month, how often have you been upset because of something that happened unexpectedly?</span><br><span class="line">In the last month, how often have you felt that you were unable to control the important things in your life?</span><br><span class="line">In the last month, how often have you felt nervous and &quot;stressed&quot;?</span><br><span class="line">In the last month, how often have you dealt successfully with irritating life hassles?</span><br><span class="line">In the last month, how often have you felt that you were effectively coping with important changes that were occurring in your life?</span><br><span class="line">In the last month, how often have you felt confident about your ability to handle your personal problems?</span><br><span class="line">In the last month, how often have you felt that things were going your way?</span><br><span class="line">In the last month, how often have you found that you could not cope with all the things that you had to do?</span><br><span class="line">In the last month, how often have you been able to control irritations in your life?</span><br><span class="line">In the last month, how often have you felt that you were on top of things?</span><br><span class="line">In the last month, how often have you been angered because of things that happened that were outside of your control?</span><br><span class="line">In the last month, how often have you found yourself thinking about things that you have to accomplish?</span><br><span class="line">In the last month, how often have you been able to control the way you spend your time?</span><br><span class="line">In the last month, how often have you felt difficulties were piling up so high that you could not overcome them?</span><br></pre></td></tr></table></figure><h5 id="评定标准"><a href="#评定标准" class="headerlink" title="评定标准"></a>评定标准</h5><table><thead><tr><th>分数</th><th>知觉压力程度</th><th>分析</th></tr></thead><tbody><tr><td>14-28</td><td>较低</td><td>你当前的压力处于低水平，<br>你对自己当前的生活有足够的掌控和控制，不会因为一些无法预期的事情发生而感到心烦意乱和惊慌失措。</td></tr><tr><td>29-42</td><td>适中</td><td>这个分数指出你的生活中的兴奋与压力量也许是相当适中的。<br>偶尔会有一段时间压力太多，但你也许有能力去享受压力，并且很快地回到平静状态,<br>因此你面临的压力对你健康并不会造成威胁。<br>不过做一些松弛的练习仍是有益的。</td></tr><tr><td>43-56</td><td>较高</td><td>你当前经历较高的压力，它可能已经对你的身心健康造成负面影响，需要你采取措施加以调节。</td></tr><tr><td>57-70</td><td>非常高</td><td>你的压力过大，身体可能会有一些症状，急需减压，可以寻求专业人员的帮助。<br>你在面对那些模糊的、难以改变的、长期的压力问题就会显得更束手无策和焦灼不安。<br>这个分数表示你确实正以极度的压力反应在伤害你自己的心理健康。<br>你需要专业<a href="images/https://www.psychspace.com/psych/action-tag-tagname-%EF%BF%BD%EF%BF%BD%EF%BF%BD%EF%BF%BD%EF%BF%BD%EF%BF%BD%D1%AF.html">心理咨询</a>师给予一些忠告，他可以帮助你消减你对于压力器的知觉，并帮助你改良生活的品质。</td></tr></tbody></table><h4 id="B-PSTR心理压力测量表"><a href="#B-PSTR心理压力测量表" class="headerlink" title="B PSTR心理压力测量表"></a>B PSTR心理压力测量表</h4><p>PSTR心理压力量表由瑞士心理学家爱德沃兹于1983年编制，以德国心理学家穆瑞在1968年提出的心理压力因素理论为基础。</p><p>对于每个问题，请从以下选项中选择：A(总是) 4分; B(经常) 3分; C(有时) 2分; D(很少) 1分; E(从未) 0分。</p><h5 id="内容-1"><a href="#内容-1" class="headerlink" title="内容"></a>内容</h5><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br></pre></td><td class="code"><pre><span class="line">我受背疼之苦</span><br><span class="line">我的睡眠不足、且睡不安稳</span><br><span class="line">我有头疼</span><br><span class="line">我额部疼痛</span><br><span class="line">若须等候，我会不安</span><br><span class="line">我的后颈感到疼痛</span><br><span class="line">我比多数人更神经紧张</span><br><span class="line">我很难入睡</span><br><span class="line">我的头感到紧或疼</span><br><span class="line">我的胃有病</span><br><span class="line">我对自己没有信心</span><br><span class="line">我对自己说话</span><br><span class="line">我忧虑财务回题</span><br><span class="line">人见面时，我会窘怯</span><br><span class="line">我怕发生可怕的事</span><br><span class="line">白天我觉得累</span><br><span class="line">下午我感到喉咙痛，但并非由于染上感冒</span><br><span class="line">我心情不安，无法静坐</span><br><span class="line">我感到非常口</span><br><span class="line">我心脏有病</span><br><span class="line">我觉得自己不是很有用</span><br><span class="line">我吸烟</span><br><span class="line">我肚子不舒服</span><br><span class="line">我觉得不快乐</span><br><span class="line">我流汗</span><br><span class="line">我喝酒</span><br><span class="line">我很自觉</span><br><span class="line">我觉得自己像四分五裂</span><br><span class="line">我的眼睛又酸又累</span><br><span class="line">我的腿或脚抽筋</span><br><span class="line">我的心跳过速</span><br><span class="line">我怕结识人</span><br><span class="line">我手脚冰冷</span><br><span class="line">我患便秘</span><br><span class="line">我未经医师指示使用各种药</span><br><span class="line">我发现自己很容易哭</span><br><span class="line">我消化不良</span><br><span class="line">我咬指甲</span><br><span class="line">我耳中有嗡嗡声</span><br><span class="line">我小便频密</span><br><span class="line">我有胄溃疡</span><br><span class="line">我有皮肤方面的病</span><br><span class="line">我的咽喉很紧</span><br><span class="line">我有十二指肠溃疡病</span><br><span class="line">我担心我的工作</span><br><span class="line">我口腔溃烂</span><br><span class="line">我为琐事忧虑</span><br><span class="line">我呼吸浅促</span><br><span class="line">我觉得胸部紧迫</span><br><span class="line">我发现很难做诀定</span><br></pre></td></tr></table></figure><h5 id="评定标准-1"><a href="#评定标准-1" class="headerlink" title="评定标准"></a><strong>评定标准</strong></h5><p>一般,43-65分,表示压力适中;低于43分,表示压力过小,需要适度增加压力;高于65分,表示压力过大,需要适当降低。</p><h5 id="具体标准"><a href="#具体标准" class="headerlink" title="具体标准"></a>具体标准</h5><table><thead><tr><th>分数</th><th>分析</th></tr></thead><tbody><tr><td><strong>93以上</strong></td><td>表示处于高度应激反应中,身心遭受压力伤害。<br>你需要专业心理治疗师给与一些忠告，它可以帮助你削减你对压力的知觉，并帮助你改良生活的品质。</td></tr><tr><td><strong>82-92</strong></td><td>这个分数表示是你正在经历太大的压力，身心健康正在受到损害，并令你的人际关系发生问题。你的行为会伤害自己，也会影响他人。<br/>因此，对你来说，学会如何减除自己的压力反应是非常必要的。你可能必须花时间做练习，学习控制压力，也可以寻求专业的帮助。</td></tr><tr><td><strong>71-81</strong></td><td>这个分数显示你的压力程度中等，可能正开始对健康不利。<br/>你可以仔细反省自己对压力器如何做出反应，并学习在压力器出现时，控制自己的肌肉紧张，已消除生理激活反应。</td></tr><tr><td><strong>60-70</strong></td><td>这个分数指出你生活中的兴奋与压力也许是相当适中的。<br/>偶尔会有一段时间压力太大，但你也许有能力去享受压力，并且很快回到平衡状态，因此对你的健康不会造成威胁。</td></tr><tr><td><strong>49-59</strong></td><td>这个分数表示你能够控制你自己的压力反应，你是一个相当放松的人。<br/>也许你对于所遇到的各种压力，并没有将他们解释为威胁，所以你很容易与人相处，可以毫无惧怕担任工作，也没有失去信心。</td></tr><tr><td><strong>38-48：</strong></td><td>这个分数表示你对所遭遇的压力很不易为所动，甚至不当一回事，好像并没有发生过一样。<br/>这对你的健康不会有什么负面影响，但你的生活缺乏适度的兴奋，因此趣味也就有限。</td></tr><tr><td><strong>27-37</strong></td><td>这个分数表明你的生活可能是相当沉闷的，即使刺激或有趣的事情发生了，你也很少作反应。<br/>可能你必须参加更多的社会活动或娱乐活动，以增加你的压力激活反应。</td></tr><tr><td><strong>16-26：</strong></td><td>如果你的分数值落在这个范围内，也许意味着你在生活中所经历的的压力经验不够，或是你没有正确地分析自己。<br/>你最好更主动些，在工作、社交、娱乐等活动上多增加些刺激。</td></tr></tbody></table><h4 id="C-DASS-21"><a href="#C-DASS-21" class="headerlink" title="C DASS-21"></a>C DASS-21</h4><h5 id="内容-2"><a href="#内容-2" class="headerlink" title="内容"></a>内容</h5><p><img src="/2021/06/19/2021-06-19-%E5%8F%AF%E7%A9%BF%E6%88%B4%E7%AE%97%E6%B3%95%E4%B8%AD%E7%9A%84%E5%8E%8B%E5%8A%9B%E7%9B%91%E6%B5%8B/c8382a0f-0954-4ea9-9372-d84afcc6ea4e1.gif" loading="lazy"></p><h5 id="评价标准"><a href="#评价标准" class="headerlink" title="评价标准"></a>评价标准</h5><img src="http://www.lunwenstudy.com/uploads/allimg/200804/36-200P4141513348.jpg" alt="表1 DASS-C21各分量表分级标准（分）" style="zoom:67%;" /><h3 id="2-2-其他压力评估方法"><a href="#2-2-其他压力评估方法" class="headerlink" title="2.2 其他压力评估方法"></a>2.2 其他压力评估方法</h3><p>基于GSR的压力监测</p><p>基于GSR监测压力</p><p>基于EEG的心理压力测量</p><h2 id="三-基本原理"><a href="#三-基本原理" class="headerlink" title="三 基本原理"></a>三 基本原理</h2><p><strong>基本原理：</strong>人体心率变异性(Heart rate variability)曲线表示心率节奏的快慢随时间的变化，可以反应自主神经系统的活动。</p><p>随着现代心理学和生理学的发展，人们已逐渐认识到，利用相应设备检测出的生理 数据，可以较为准确、客观地反映当事人的心理状态。心率变异性（HRV）是反映自主神经系统活性和定量评估心脏交感神经与迷走神经张力及其平衡性，从而判断其对心血管疾病的病情及预防，可能是预测心脏性猝死和心律失常性事件的一个有价值的指标。</p><p>也就是说<strong>较低的HRV意味着更高的焦虑、抑郁发病率风险和心血管疾病死亡率。</strong> <strong>而拥有较高HRV的人，普遍具有更好的心血管功能和抗压能力。</strong></p><p>简单地说，HRV越高，人的压力越小。</p><h3 id="2-1-HRV"><a href="#2-1-HRV" class="headerlink" title="2.1 HRV"></a>2.1 HRV</h3><p>心率变异性（HRV）是连续心跳之间的时间间隔变化的生理现象。</p><!-- OneDrive 同步路径已不可达，源文件丢失 --><!-- <img src="../../../OneDrive - buaa.edu.cn/健康算法/压力监测/压力检测调研.assets/hrv_20160426.png" alt="Heart rate variability graph" style="zoom:50%;" /> --><p>心率变异性表示心跳的不规律性</p><img src="https://pic1.zhimg.com/80/v2-e7aafc51ad03630fa6d8ec1f0b9fb01c_720w.jpg" alt="img" style="zoom:80%;" /><p><img src="/2021/06/19/2021-06-19-%E5%8F%AF%E7%A9%BF%E6%88%B4%E7%AE%97%E6%B3%95%E4%B8%AD%E7%9A%84%E5%8E%8B%E5%8A%9B%E7%9B%91%E6%B5%8B/image-20210203115052112.png" loading="lazy"></p><p>人的精神状态与HRV曲线之间的关系：<strong>HRV曲线的变化呈现规律的正弦形状时，人体多个器官的活动处于协调状态，此时自主神经系统达到平衡，即高协调状态。此时，人体的各项机能，包括生理、心理以及认知方面的表现，都有明显的提升。 相反，若处于相对紧张的压力下，HRV曲线则呈现不规则曲线。</strong></p><img src="https://pic2.zhimg.com/80/v2-1a9fdd6627ba85745a0b0f4c7d8718e0_720w.jpg?source=1940ef5c" alt="img" style="zoom:80%;" /><p><img src="/2021/06/19/2021-06-19-%E5%8F%AF%E7%A9%BF%E6%88%B4%E7%AE%97%E6%B3%95%E4%B8%AD%E7%9A%84%E5%8E%8B%E5%8A%9B%E7%9B%91%E6%B5%8B/v2-88efc63ccc62a611e6e6cbec320e0df1_720w.jpg" loading="lazy"></p><h3 id="2-2-HRV与自主神经系统"><a href="#2-2-HRV与自主神经系统" class="headerlink" title="2.2 HRV与自主神经系统"></a>2.2 HRV与自主神经系统</h3><p>自主神经系统（ANS）由交感神经（Sympathetic）和副交感神经（parasympathetic）分支组成，调节各种各样的生理功能。这些生理功能很少通过我们的意识的进行控制，但对于维持各种不同的生命和表现、活动和需求至关重要。其中，交感神经被认为是“战斗或逃跑”系统，使我们准备采取行动，作出反应和执行，而副交感神经系统通常被称为“休息和消化”系统。两种系统通常同时起作用，但彼此相反，从而激活或抑制特定的生理反应。（<em>来自于 FirstBeat 论文</em> [5] ）</p><p>交感神经和副交感神经神经线始于中枢神经系统，并通向整个人体的不同目标器官。交感和副交感通常在彼此相对的同时起作用。<strong>副交感神经分裂主要涉及放松，帮助身体休息和恢复。交感神经通过加速身体机能使身体做好战斗准备，与压力有关。</strong></p><p><img src="/2021/06/19/2021-06-19-%E5%8F%AF%E7%A9%BF%E6%88%B4%E7%AE%97%E6%B3%95%E4%B8%AD%E7%9A%84%E5%8E%8B%E5%8A%9B%E7%9B%91%E6%B5%8B/image-20210201201730132.png" loading="lazy"></p><p>ANS系统对心脏的作用尤为明显。<strong>当身体面临压力时，ANS会通过刺激压力激素的产生以增加心脏的收缩率和收缩力（心输出量）</strong>。<strong>相反，在压力或运动时，副交感神经刺激会降低心率以恢复体内平衡</strong>。精确地测量心脏跳动能为我们提供了有关身体的大量信息。它通常被视为自主神经系统活动的非侵入性标记，并且是观察交感神经系统和副交感神经系统之间的活动水平和相互作用的有力工具。多种生理现象会影响HRV的水平。例如;吸入和呼气（一种症状，称为呼吸窦性心律失常：吸气时心率增加而HRV降低，而呼气时心率降低而HRV升高），激素反应，代谢过程，运动，运动，认知过程，压力和恢复（任务欧洲心脏病学会的力量，北美起搏电生理学会（1996年）。</p><p><strong>在正常情况下，HRV应该在放松活动中自然增加，尤其是在副交感神经（迷走神经）激活增加的睡眠期间。</strong>另一方面，当有同情心的活动有助于身体满足需求时，HRV在压力下会降低。因此，当心脏缓慢跳动时，HRV通常较高，而当心脏开始跳动时，HRV通常较低。 HRV每天都会自然变化。根据活动水平和与工作有关的压力，例如，与工作有关的压力，但是如果一个人长期处于压力或超负荷状态（身体或精神上），则这两个系统之间的自然相互作用会被破坏，并且身体可以不断保持在充满同情心的战斗状态中，HRV较低。</p><p>运动员由于训练太多或太过密集而与其他生活压力源之间的不平衡导致的长期超负荷与足够的康复被称为过度训练。研究表明，<strong>过度训练通常以增加心肺和交感神经系统的激活以及降低HRV为特征</strong>。除了超负荷和外部压力外，诸如<strong>营养不良，饮酒和各种疾病之类的内部压力因素也会降低HRV</strong>。遗传因素可解释总体HRV水平的30％，但通常认为较高的HRV可以指示心脏健康，而较高的HRV也可降低发病率和死亡率，降低心理健康，以及生活质量（。一个人可以通过改善整体健康状况和体质来增加个人的HRV水平。</p><p><img src="/2021/06/19/2021-06-19-%E5%8F%AF%E7%A9%BF%E6%88%B4%E7%AE%97%E6%B3%95%E4%B8%AD%E7%9A%84%E5%8E%8B%E5%8A%9B%E7%9B%91%E6%B5%8B/image-20210203115608438.png" loading="lazy"></p><p>图4 心率变异性（HRV）与活动性体位性任务在阻塞之前（无药物）以及副交感神经（阿托品）和交感神经（美托洛尔）自主神经阻滞后的时间成函数关系[16]。 该示例来自一个坐了5分钟，站起来并保持站立3分钟的对象。</p><h3 id="2-3-HRV曲线波动的影响因素"><a href="#2-3-HRV曲线波动的影响因素" class="headerlink" title="2.3 HRV曲线波动的影响因素"></a>2.3 HRV曲线波动的影响因素</h3><p>影响因素包括：年龄、姿势、每天的时间和健康状况，情绪、身体压力和心理经历。</p><p>一般来说，心率变异性高通常表示身体健康。HRV读数低则通常与压力有关。患有糖尿病、心脏病和胆固醇等疾病的人通常心率变异性较低。具体地，在心理健康方面，<strong>低心率变异</strong>与抑郁、社交孤立、丧亲、创伤后应激障碍和<strong>压抑</strong>的愤怒有关，抗抑郁药并不总是能改善心率变异，这可能部分地引起了心肌梗死后抑郁个体心脏死亡风险的显著增加。即使在其他的健康个体中，压力也被证明会改变短期的心率变异分布，例如，当外科医生进行高度压力的手术时，医生在打电话时和打电话后都会改变其心率变异的分布。<a href="images/https://www.zhihu.com/question/59539324/answer/1249671910">知乎链接</a>。</p><p>环境和工作相关因素也会影响心率变异，但大多数研究结果表明污染和烟草烟雾使心率变异恶化。暴露于工厂的有毒物质、工作压力和轮班工作都降低了心率变异，而改善工作环境和环境光改善了心率变异和皮质醇节律。令人不安的是，日本的一项研究表明，连续14周食用大眼金枪鱼和箭鱼的-甲基汞临时耐受周摄取量(PTWI) (3.4 mcg&#x2F;kg&#x2F;周)的人，其交感迷走神经失衡显著增加，15周后恢复正常；对照组没有变化。</p><h3 id="2-4-HRV与压力的关系"><a href="#2-4-HRV与压力的关系" class="headerlink" title="2.4 HRV与压力的关系"></a>2.4 HRV与压力的关系</h3><p>HRV对与压力有关的ANS活性变化（即SNS和PNS中的变化）敏感。在大多数研究中，HRV变量随各种方法引起的压力而变化。与HRV变量变化相关的最常报告的因素是副交感活性低，其特征是HF降低和LF升高。 HRV可能与神经结构的灵活网络的活动相关，该神经网络是根据环境挑战而动态组织的。实际上，神经影像学研究表明，HRV可能与减少的威胁感知有关，这是由参与评估压力情况的皮质区域（例如，mPFC的腹侧）介导的。在临床情况下，HRV可被认为是反映心脏活动和整体自主神经健康的一种工具，比特定的精神疾病或疾病状态更为重要。因此，在评估压力与HRV之间的关系时，必须考虑整体自主神经环境以及患者的病历和心理史。</p><p>对健康受试者的HRV对心理压力的反应性研究Sloan等人[35]分析了33名健康受试者的24小时心电图记录，以检查RR间隔，HRV反应与测量身体位置，负面影响的定期日记条目之间的关联，以及一天中的时间。正如预期的那样，他们的结果表明，压力增加与RR间隔的减少有关。此外，心理压力与LF &#x2F; HF比值的升高显着相关，这表明在一天的压力时期SNS活动增加。35通过问卷调查和223名健康男性白领的短期HRV记录， Kageyama等人[36]研究了工作压力源的数量，自我报告的睡眠质量与白天自主活动之间的关系。他们发现HRV参数与五个工作压力或得分之间没有相关性。36但是，随后的研究报告说，一些HRV指标反映了心理压力。表3总结了健康人的研究，这些人研究了与心理压力状况相关的HRV变异。关于所使用的引起压力的任务类型和HRV反应性（即实验室环境任务）的研究之间存在异质性。或主观压力报告）。此外，HRV反应性研究是短期（5分钟）或连续的24小时研究。在大多数研究中，HRV变量会因各种方法引起的压力而变化。但是，HRV变量存在显着变化是不一致的。与HRV变量变化相关的最常报告的因素是副交感活性低，其特征是HF降低和LF升高。 Dimitriev等人[37]最近报道说，精神压力导致可预测性，RR间隔规律性和复杂性降低。这反映了在压力下朝向更稳定和周期性的HR行为的变化。降低的HRV和抑制的副交感神经激活增加了对未来压力的抵抗力。</p><p>Hjortskov 等[11]的实验设计为电脑工作压力和休息两种对比状态。他们在实验室测试 12 名女性被试者在电脑工作压力和休息状态下的血压和脑电两种生理参数，通过数据分析，得出：(1)HRV 的高频部分降低，低频部分没有发生变化；(2) HRV 能很好地反映出人的心理压力变化。</p><h3 id="2-5-HRV度量特征-4"><a href="#2-5-HRV度量特征-4" class="headerlink" title="2.5 HRV度量特征[4]"></a>2.5 HRV度量特征[4]</h3><p>先介绍根据HRV可提取到的特征</p><h4 id="时域特征"><a href="#时域特征" class="headerlink" title="时域特征"></a>时域特征</h4><p>时域上可定义许多HRV度量。 一些有希望的度量包括：平均RR间隔（mRR），平均心率（mHR），RR间隔标准差（SDRR），心率标准差（SDHR），RR间隔的方差系数（CVRR），均方根连续差（ RMSSD），与所有RR间隔（pRR20）相差超过20 ms的相邻RR间隔对的数量，以及与所有RR间隔（pRR50）相差50 ms以上的相邻RR间隔的对数量。</p><p>时域中的HRV特征的计算公式如下表所示：</p><p>| No  | 特征  | 单位 |                        公式                        |                    |                            |<br>| :-: | :—: | :–: | :————————————————: | —————— | ————————– | — | — |<br>|  1  |  mRR  |  ms  |           $\frac{\sum^N_{i=1}(RR_i)}{N}$           |                    |                            |<br>|  2  |  mHR  | bpm  |         $\frac{\sum^N_i=1(6000/RR_i)}{N}$          |                    |                            |<br>|  3  | SDRR  |  ms  |   $\sqrt{\frac{\sum^N_{i=1}(RR_i-mRR)^2}{N-1}}$    |                    |                            |<br>|  4  | SDHR  | bpm  | $\sqrt{\frac{\sum^N_{i=1}()(60000/RR_i))^2}{N-1}}$ |                    |                            |<br>|  5  | CVRR  |      |            $\frac{SDRR\times100}{mRR}$             |                    |                            |<br>|  6  | RMSSD |  ms  |                    $\sqrt{mean                     | (RR\_{i+1}-RR_i)^2 | }$                         |     |     |<br>|  7  | pRR20 |  %   |                   $\frac{Count(                    | RR\_{i+1}-RR_i     | )\_{&gt;20ms}\times100}{N-1}$ |     |     |<br>|  8  | pRR50 |  %   |                   $\frac{Count(                    | RR\_{i+1}-RR_i     | )\_{&gt;50ms}\times100}{N-1}$ |     |     |</p><h4 id="频域特征"><a href="#频域特征" class="headerlink" title="频域特征"></a>频域特征</h4><p>频域方法通常涉及对RR间隔信号的功率谱密度（PSD）的估计。 基本措施是极低频功率谱（VLF），低频功率谱（LF）和高频功率谱（HF）。 此外，我们还考虑了从基本量度得出的量度，例如归一化极低频频谱（nVLF），归一化低频频谱（nLF），归一化高频频谱（nHF），归一化低频频谱与归一化高频的差异 频谱（dLFHF），交感调制指数（SMI），迷走神经调制指数（VMI），交感卵平衡指数（SVI）[1]。</p><p>计算公式如下表所示：</p><p>| No  | 特征 |  单位  |               公式               |         |<br>| :-: | :–: | :—-: | :——————————: | ——- | — | — |<br>|  1  | VLF  | $ms^2$ |     从0.003到0.04hz的功率谱      |         |<br>|  2  |  LF  | $ms^2$ |      从0.04到0.15hz的功率谱      |         |<br>|  3  |  HF  | $ms^2$ |      从0.15到0.4hz的功率谱       |         |<br>|  4  | nVLF |   %    | $VLF\times\frac{100}{VLF+HF+LF}$ |         |<br>|  5  | nLF  |   %    | $LF\times\frac{100}{VLF+HF+LF}$  |         |<br>|  6  | nHF  |   %    | $HF\times\frac{100}{VLF+HF+LF}$  |         |<br>|  7  | dLHF |   %    |                $                 | nLF-nHF | $   |     |<br>|  8  | SMI  |        |        $\frac{LF}{LF+HF}$        |         |<br>|  9  | VMI  |        |        $\frac{HF}{LF+HF}$        |         |<br>| 10  | SVI  |        |          \frac{LF}{HF}           |         |</p><h4 id="可表征压力的HRV特征"><a href="#可表征压力的HRV特征" class="headerlink" title="可表征压力的HRV特征"></a>可表征压力的HRV特征</h4><p>实验证明可用于表征压力的HRV特征包括：</p><p>mRR、mHR、nLF、dLFHF、SVI</p><p><img src="/2021/06/19/2021-06-19-%E5%8F%AF%E7%A9%BF%E6%88%B4%E7%AE%97%E6%B3%95%E4%B8%AD%E7%9A%84%E5%8E%8B%E5%8A%9B%E7%9B%91%E6%B5%8B/image-20210202154337974.png" loading="lazy"></p><p><img src="/2021/06/19/2021-06-19-%E5%8F%AF%E7%A9%BF%E6%88%B4%E7%AE%97%E6%B3%95%E4%B8%AD%E7%9A%84%E5%8E%8B%E5%8A%9B%E7%9B%91%E6%B5%8B/image-20210202154402852.png" loading="lazy"></p><h3 id="2-6-压力监测系统"><a href="#2-6-压力监测系统" class="headerlink" title="2.6 压力监测系统"></a>2.6 压力监测系统</h3><p>【FirstBeat算法】本节将更详细地描述用于分析不同生理状态的过程。下图阐明了用于检测压力，恢复和身体活动的分析程序。</p><ol><li>首先，收集并准备心跳频率数据。</li><li>然后，计算用于状态检测所需的生理信息，包括HRV变量，呼吸速率，氧气消耗（VO2）和运动后多余的氧气消耗（EPOC）。</li><li>此后，识别生理上的固定区段，并识别不同的状态。</li></ol><!-- OneDrive 同步路径已不可达，源文件丢失 --><!-- <img src="../../../OneDrive - buaa.edu.cn/健康算法/压力监测/压力检测调研.assets/image-20210201203949490.png" alt="image-20210201203949490" style="zoom: 67%;" /> --><h4 id="1-数据采集与预处理"><a href="#1-数据采集与预处理" class="headerlink" title="1 数据采集与预处理"></a>1 数据采集与预处理</h4><p>首先，利用可记录个人心跳的心率监视器在期望的时间段例如24-72小时内的心跳数据（RR间隔数据）。收集数据后，在开始分析过程之前。首先设置背景信息（年龄，性别，身高，体重和体育锻炼类别）。再根据这些信息，该方法可以估算出患者的最大（Heart Rate）HR（HRmax），最大呼吸频率和最大耗氧量（VO2max）。生理变量的各个范围是从RR间隔数据中获得的。这意味着例如如果分别观察到较低或较高的值，则静息HR和HRmax从记录的数据自动更新。</p><p>通过人工监测滤波器扫描记录的动态RR间隔数据，以对错误检测，错过和过早的心跳进行初始校正[有关更多信息，请参阅37-38]。然后，通过使用线性插值以5 Hz的速率对连续的伪影校正的RR间隔进行重新采样，以获得等距采样的时间序列[有关更多详细信息，请参见[39]。从重新采样的数据中，该软件使用多项式滤波器和数字FIR带通（0.03–1.2 Hz）滤波器，去除了所关注频段上下的低频趋势和方差。</p><h4 id="2-信号特征计算"><a href="#2-信号特征计算" class="headerlink" title="2 信号特征计算"></a>2 信号特征计算</h4><ol><li>计算用于检测生理状态的不同变量的值。状态检测所需的HRV变量是时域和频域的HRV变量，例如连续RR间隔的均方根（RMSSD），高频功率（HFP，0.15–0.40 Hz），低频功率（LFP 0.04–0.15 Hz） ），以及呼吸窦性心律不齐的幅度。需要能够处理非平稳生理信号的灵活方法[39-41]。</li><li>使用HRV变量和数据的神经网络建模，该软件将计算代表不同生理现象的值以及检测不同身体状态所需的值。这些现象是呼吸速率，氧气消耗（VO2）和运动后过量的氧气消耗（EPOC）。</li><li>仅在RR间隔信息的基础上计算的上述生理参数的有效性和准确性已在几项研究中报告[42-48]。有关呼吸频率计算的详细说明，请参见“从心律周期测量中得出有关呼吸活动的可靠信息的程序[49]”文件。有关VO2和VO2得出的能源消耗计算的说明，请参见相关的Firstbeat白皮书[50-51]。有关EPOC的计算及其在运动训练中的使用的更多信息，请参见其他两份白皮书[52-53]。</li><li>在检测不同状态之前，还必须将运动，身体活动的开始和姿势变化与影响心脏活动的其他非代谢因素区分开。这些计算中利用了站立时已知的HRV变化。</li></ol><h4 id="3-监测物理活动、恢复、压力"><a href="#3-监测物理活动、恢复、压力" class="headerlink" title="3 监测物理活动、恢复、压力"></a>3 监测物理活动、恢复、压力</h4><p>进一步的分析是基于通过使用神经网络建模和其他数学技术将每秒第二数据分割成各种固定段，即包括生理上一致的数据的段。然后使用逐步决策树对这些段进行分类，如图6所示。在这些阶段的每个阶段，都会评估RR间隔得出的数据的可靠性，以确保正确检测到不同的生理状态。</p><h5 id="A-活动检测"><a href="#A-活动检测" class="headerlink" title="A 活动检测"></a>A 活动检测</h5><p>首先，检测与强度不同的体育活动有关的数据段，以及与从体育活动中恢复有关的数据段。 身体活动的检测基于耗氧量和加速度计数据。 耗氧是代谢的基本指标，也是衡量运动强度的黄金标准[54-55]。 如果计算出的VO2大于个人最大VO2max的30％，则该细分被确定为身体活动，如果VO2为最大VO2max的20-30％，则该状态被确定为每日身体活动。 加速度计数据进一步提高了该方法识别与体内激活水平提高有关的运动的能力。 如果EPOC已达到高于某个阈值（表明身体活动）的值，并且之后EPOC下降，则检测到从身体活动中恢复。</p><h5 id="B-恢复检测"><a href="#B-恢复检测" class="headerlink" title="B 恢复检测"></a>B 恢复检测</h5><p>对于与运动无关的数据段，确定当身体处于恢复状态，压力状态或无法识别状态时的段。 恢复状态定义为ANS的副交感（迷走神经）优势状态。 因此，与ANS的副交感神经支配有关的变量用于检测恢复。 这些包括例如HFP和HR。 在恢复状态期间，HR个别较低，HRV较大且均匀。 康复通常发生在睡眠，放松，休息和&#x2F;或安宁的工作中，这意味着身体的总体活化程度较低。</p><h5 id="C-压力检测"><a href="#C-压力检测" class="headerlink" title="C 压力检测"></a>C 压力检测</h5><p>最后，如果该片段不对应于身体活动，从身体活动中恢复或恢复（放松），则该片段被确定为压力状态或无法识别的状态。应激状态被定义为当交感神经系统活动占主导并且副交感神经（迷走神经）活化较低时，体内激活的增加。与ANS的交感优势有关的变量用于压力检测。这些包括例如HFP，LFP，呼吸频率和HR。在压力状态下，相对于HR，个人HR升高，HRV从基本静止水平降低，呼吸频率低。最后，将未被识别为上述任何状态或伪影超过75％的数据段确定为无法识别（其他）状态。</p><p>对于应力和恢复，该方法基于上述变量提供表示反应强度的绝对指数。 此外，该方法可以在估计所测时期内人体资源是否积累或消耗时，通过考虑反应强度和状态持续时间来评估压力与恢复之间的平衡（参见图7）。</p><p><img src="/2021/06/19/2021-06-19-%E5%8F%AF%E7%A9%BF%E6%88%B4%E7%AE%97%E6%B3%95%E4%B8%AD%E7%9A%84%E5%8E%8B%E5%8A%9B%E7%9B%91%E6%B5%8B/image-20210202100037354.png" loading="lazy"></p><p>有关本文介绍的方法的更多信息，请参见专利申请（已授权）“通过分段检测压力并分析心跳信号的过程”。 [56]。</p><p><strong>示例</strong></p><p>某发明中的压力监测系统构成：</p><!-- OneDrive 同步路径已不可达，源文件丢失 --><!-- <img src="../../../OneDrive - buaa.edu.cn/健康算法/压力监测/压力检测调研.assets/image-20210201103913597.png" alt="image-20210201103913597" style="zoom: 80%;" /> --><p>输入：信号传感器采集到的心率</p><p>信号处理流程：</p><p><img src="/2021/06/19/2021-06-19-%E5%8F%AF%E7%A9%BF%E6%88%B4%E7%AE%97%E6%B3%95%E4%B8%AD%E7%9A%84%E5%8E%8B%E5%8A%9B%E7%9B%91%E6%B5%8B/image-20210201103948561.png" loading="lazy"></p><h2 id="四-市面相关产品"><a href="#四-市面相关产品" class="headerlink" title="四 市面相关产品"></a>四 市面相关产品</h2><h3 id="A-华为"><a href="#A-华为" class="headerlink" title="A 华为"></a>A 华为</h3><p>华为的Watch GT2 Pro中采用的传感器一共有5个：加速度传感器、陀螺仪传感器、地磁传感器、光学心率传感器、气压传感器；</p><p>压力监测功能一定是通过心率传感器做的。</p><p><strong>操作方法</strong></p><p>压力检测可分为单次压力检测和周期压力检测。</p><ul><li>单次压力检测：在华为运动健康App <strong>压力</strong> 卡片中点击 <strong>压力检测</strong> 按钮进行单次压力检测。初次使用压力检测功能，请根据华为运动健康App的提示进行压力校准。</li><li>周期压力检测：在华为运动健康App的设备详情页，选择健康监测 &gt; 压力自动检测，打开压力自动检测开关并根据提示进行 <strong>压力校准</strong>，日常正确佩戴手表，周期性压力跟踪检测会自动进行。</li></ul><p>查看压力数据：</p><ul><li>手表上查看：点击手表上键进入应用列表，选择 <strong>压力</strong>，即可查看单日压力趋势情况，包含全天的压力柱状图、等级和压力占比。</li><li>App上查看：打开华为运动健康App，在首页的 <strong>压力</strong> 卡片区域可以看到最近一次的压力分数和等级，点击可进入压力详情页面，查看日、周、月、年的压力曲线及对应建议。</li></ul><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">压力检测过程中请您正确佩戴手表并保持身体静止。处于运动状态或手腕活动频繁时，无法准确评估压力，该情况下手表不会进行压力跟踪检测。</span><br><span class="line">咖啡因、尼古丁、酒精及部分精神类药品的摄入会影响压力检测的准确性。此外，心脏病、哮喘、体育运动或佩戴位置的肢体压迫也会对压力检测结果产生影响。</span><br></pre></td></tr></table></figure><p>压力监测同样分为被动和主动两种方式，其中被动方式主要是设备（华为手环B6）间隔性采集数据并分析，主动方式则是通过APP主动发起监测后、设备采集心率信号、然再进行分析。</p><img src="https://pic1.zhimg.com/v2-4dda6e3d712168fa6d937b9576b1574c_r.jpg" alt="preview" style="zoom: 25%;" /><img src="https://exp-picture.cdn.bcebos.com/b955ead0b503c8d2f65f82fb498333bf3aef213b.jpg?x-bce-process=image%2Fresize%2Cm_lfit%2Cw_500%2Climit_1%2Fformat%2Cf_jpg%2Fquality%2Cq_80" alt="华为手表实时压力检测，全天候助您身心健康！" style="zoom: 50%;" /><h3 id="B-苹果"><a href="#B-苹果" class="headerlink" title="B 苹果"></a>B 苹果</h3><p>苹果商店中有一个StressMonitor应用。</p><p>在压力监测中，苹果的算法读取和处理了Apple Watch在Apple Health中记录的数据。Stress Monitor根据这些数据计算出多个HRV特征，如SDNN、RMSSD、SD1、pNN50等。进一步，这些数据被人工智能模型处理以检测压力。<a href="images/https://www.stress-radar.app/how-to-measure">链接</a></p><h4 id="操作方法"><a href="#操作方法" class="headerlink" title="操作方法"></a>操作方法</h4><ol><li><p><strong>准备测量</strong>。进入舒适的位置，以记录您的测量结果。您可以站立或坐着-没关系。为了获得最佳结果，我们建议您将Apple Watch朝上进行测量时，将手臂放在桌子上或膝盖上。保持手腕平坦，并尽可能保持不动。如果在测量之前刚参加运动，请在测量前休息10分钟。</p></li><li><p>更好的配合意味着更好的读数。确保Apple Watch紧贴但手腕舒适。如果您的Apple Watch没有固定住，或者传感器无法读取您的心律，请稍微拉紧表带。 Apple Watch的背面需要触摸手腕。</p></li><li><p>在“呼吸”应用程序中呼吸2分钟。</p><p>*按Digital Crown进入主屏幕，然后打开Breathe应用程序。<br>*旋转数字王冠以设置会话的长度。我们建议呼吸2分钟以达到最佳效果。<br>*准备好后，点击开始。正常呼吸，忽略说明。<br>*记住在呼吸时保持静止。如果呼吸时移动可能无法保存数据。*等待会话结束，您的手表会轻按两次并发出提示音。完成后，您可以看到您的心律。</p></li><li><p>我们将尽快在Apple Health中提供您Apple Watch记录的数据，以显示压力结果。通常大约需要30秒才能获取数据。为了加快此过程，我们强烈建议您在呼吸结束后立即锁定和解锁iPhone屏幕。</p></li></ol><blockquote><ol><li>Get ready for measurement.<br>Get into a comfortable position where you can record your measurement. You can stand or sit - it doesn’t matter. For the best result we recommend to rest your arms on a table or in your lap while you take a measurement with the Apple Watch facing up. Keep your wrist flat, and hold as still as you can.<br>If you were physically active just before the measurement, take a break for 10 minutes before the measurement.</li><li>A better fit means better readings.<br>Make sure that your Apple Watch is snug but comfortable on your wrist. If your Apple Watch doesn’t stay in place, or the sensors aren’t reading your heart rate, tighten the band a bit. The back of your Apple Watch needs to be touching your wrist.</li><li>Breathe in Breathe app for 2 min.<ul><li>Press the Digital Crown to go to the Home screen, then open the Breathe app.</li><li>Turn the Digital Crown to set the session’s length. We recommend to breath for 2 min for the best result.</li><li>Tap Start when you’re ready. Breath normally, ignoring the instructions.</li><li>Remember to stay still while you breathe. The data may not be saved if you move while breathing.</li><li>Wait until the session ends and your watch taps you twice and chimes. When you’re done, you can see your heart rate.</li></ul></li><li>We’ll show your stress result as soon as the data, that your Apple Watch recordedbecomes available in Apple Health.<br>Usually it takes about 30 sec to get the data. To speed up the process we highly recommend to lock and unlock the screen of your iPhone right after you finish breathing.</li></ol></blockquote><h3 id="C-小米"><a href="#C-小米" class="headerlink" title="C 小米"></a>C 小米</h3><p>小米手环5&#x2F;小米手表</p><!-- OneDrive 同步路径已不可达，源文件丢失 --><!-- <img src="../../../OneDrive - buaa.edu.cn/健康算法/压力监测/压力检测调研.assets/image-20210203140823019.png" alt="image-20210203140823019" style="zoom:50%;" data-broken="true" /> -->![](image-20210203141349290.png)<h4 id="操作方法-1"><a href="#操作方法-1" class="headerlink" title="操作方法"></a>操作方法</h4><ol><li>打开小米运动进入后，选择右下方我的选项。</li><li>选择小米手环5的设备进入。</li><li>向上滑动，点击心率监测的选项。</li><li>选择压力监测选项后方的开关。</li><li>点击将其打开后，手环在静止状态下，就会监测自己的压力。</li></ol><p>监测压力都需要处于静止状态下。</p><h5 id="算法现状"><a href="#算法现状" class="headerlink" title="算法现状"></a>算法现状</h5><p>现有的手表&#x2F;手环压力测试基本都是HRV心率变异性的。华为，华米，佳明等等绝大多数是用firstbeat的算法</p><h2 id="参考来源"><a href="#参考来源" class="headerlink" title="参考来源"></a>参考来源</h2><p>[1] <a href="images/https://www.wareable.com/health-and-wellbeing/stress-monitoring-wearables-explained-7969">可穿戴设备与压力监测</a></p><p>[2] <a href="images/https://webs.wofford.edu/steinmetzkr/teaching/Psy150/Lecture%20PDFs/PSS.pdf">A global measure of perceived stress</a></p><p>[4] Comparison of Heart Rate Variability Measures for Mental Stress Detection</p><p>[5] Stress and Recovery Analysis Method Based on 24-hour Heart Rate Variability（2014）</p><p>[11] The effect of mental stress on heart rate variability and blood pressure during computer work</p>]]>
    </content>
    <id>https://grobenis.github.io/2021/06/19/2021-06-19-%E5%8F%AF%E7%A9%BF%E6%88%B4%E7%AE%97%E6%B3%95%E4%B8%AD%E7%9A%84%E5%8E%8B%E5%8A%9B%E7%9B%91%E6%B5%8B/</id>
    <link href="https://grobenis.github.io/2021/06/19/2021-06-19-%E5%8F%AF%E7%A9%BF%E6%88%B4%E7%AE%97%E6%B3%95%E4%B8%AD%E7%9A%84%E5%8E%8B%E5%8A%9B%E7%9B%91%E6%B5%8B/"/>
    <published>2021-06-19T14:58:33.000Z</published>
    <summary>
      <![CDATA[<p>​ 以后要坚持每周更新一篇博客，所以今天先记载一下自己调研的可穿戴算法中的压力监测算法。</p>
<p>本文调研可穿戴设备中的压力监测算法，先介绍压力的心理学定义、组成与 PSS、PSTR、DASS-21 等评估量表。文章重点讲解基于心率变异性（HRV）的时域、频域特征及其与压力的关系，并梳理 FirstBeat 压力监测系统的检测流程，最后对比了华为、苹果、小米等市面产品的实现方案。</p>
<blockquote>
<p><strong>心率变异性，映射身心压力状态</strong></p>
</blockquote>]]>
    </summary>
    <title>可穿戴领域中的压力监测算法</title>
    <updated>2026-08-25T13:18:00.677Z</updated>
  </entry>
  <entry>
    <author>
      <name>guoben</name>
    </author>
    <category term="工作" scheme="https://grobenis.github.io/categories/%E5%B7%A5%E4%BD%9C/"/>
    <category term="可穿戴" scheme="https://grobenis.github.io/tags/%E5%8F%AF%E7%A9%BF%E6%88%B4/"/>
    <category term="综述" scheme="https://grobenis.github.io/tags/%E7%BB%BC%E8%BF%B0/"/>
    <category term="特性" scheme="https://grobenis.github.io/tags/%E7%89%B9%E6%80%A7/"/>
    <content>
      <![CDATA[<p>很早就想抽空写一篇关于智能穿戴中各种特性的综述文章。今天逼迫自己开个头。总体来说，在当前的智能穿戴领域，主要分为运动和健康两大块。</p><p>本文综述智能穿戴设备领域的功能特性，将其划分为运动与健康两大板块，运动侧涵盖监测、动作识别与定位，健康侧重点讲解睡眠分期依赖的多种生理信号，并列举压力、血氧、心率等监测方向。</p><blockquote><p><strong>腕间方寸，洞察健康全局</strong></p></blockquote><span id="more"></span><h2 id="一、可穿戴设备中的传感器"><a href="#一、可穿戴设备中的传感器" class="headerlink" title="一、可穿戴设备中的传感器"></a>一、可穿戴设备中的传感器</h2><p>可穿戴设备的功能特性建立在传感器硬件之上，常见传感器包括：</p><ol><li><strong>惯性传感器（IMU）</strong>：加速度计、陀螺仪、磁力计，用于运动监测、姿态解算与动作识别；</li><li><strong>光学心率传感器（PPG）</strong>：通过绿光&#x2F;红光照射皮肤，利用光电容积脉搏波描记法获取心率、血氧等生理参数；</li><li><strong>心电电极（ECG）</strong>：采集单导联或多导联心电信号，用于心脏健康监测；</li><li><strong>温度传感器</strong>：测量皮肤温度，辅助发热预警与睡眠监测；</li><li><strong>定位模块</strong>：GPS 用于室外运动轨迹记录与定位。</li></ol><h2 id="二、运动特性"><a href="#二、运动特性" class="headerlink" title="二、运动特性"></a>二、运动特性</h2><h3 id="1、运动监测"><a href="#1、运动监测" class="headerlink" title="1、运动监测"></a>1、运动监测</h3><p>运动监测是可穿戴设备最基础的功能，主要包括：</p><ul><li><strong>计步与距离</strong>：基于加速度计的周期性步态检测，结合步幅估算距离；</li><li><strong>卡路里消耗</strong>：综合步数、心率与运动类型估算能耗；</li><li><strong>运动类型识别</strong>：自动区分走路、跑步、骑行、游泳、爬楼等运动，游泳场景还会结合气压计判断泳姿与趟数。</li></ul><h3 id="2、动作识别"><a href="#2、动作识别" class="headerlink" title="2、动作识别"></a>2、动作识别</h3><p>动作识别利用 IMU 的加速度、角速度时序数据，通过特征提取（时域、频域特征）与机器学习分类器（SVM、决策树、神经网络）识别特定动作，如跌倒检测、挥拍动作、健身动作计数等。相比单纯的计步，动作识别需要更精细的信号处理与模型训练。</p><h3 id="3、室外定位"><a href="#3、室外定位" class="headerlink" title="3、室外定位"></a>3、室外定位</h3><p>室外运动（跑步、骑行、登山）依赖 GPS 进行定位与轨迹记录，核心指标包括定位精度、冷启动时间与功耗。部分设备支持北斗、GLONASS、Galileo 多系统联合定位以提升精度；室内场景则借助 Wi-Fi、蓝牙定位作为补充。</p><h2 id="三、健康特性"><a href="#三、健康特性" class="headerlink" title="三、健康特性"></a>三、健康特性</h2><h3 id="1、睡眠分期"><a href="#1、睡眠分期" class="headerlink" title="1、睡眠分期"></a>1、睡眠分期</h3><p>睡眠分期主要是用来对睡眠过程进行监测，计算睡眠过程中各个期间的时长与周期。</p><p>用于睡眠分期的信号主要包括如下信号：</p><ol><li>单通道脑电（electroencephalogram，EEG）；</li><li>多通道脑电 EEG；</li><li>心电（electrocardiogram，ECG）；</li><li>眼电（electro-oculogram，EOG）；</li><li>肌电（electromyogram，EMG）；</li><li>呼吸等生理信号。</li><li>体动传感器，如acc、陀螺仪</li></ol><p>消费级可穿戴设备通常无法佩戴脑电电极，因此多采用<strong>体动信号 + 心率（PPG）</strong>的组合来估计睡眠分期：通过整夜的体动剧烈程度与心率变化趋势，将睡眠划分为清醒、浅睡、深睡、快速眼动（REM）等阶段。</p><h3 id="2、压力检测"><a href="#2、压力检测" class="headerlink" title="2、压力检测"></a>2、压力检测</h3><p>压力检测主要依赖<strong>心率变异性（HRV）</strong>分析：当人处于压力状态时，交感神经兴奋，HRV 的时域指标（如 RMSSD）与频域指标（如 LF&#x2F;HF 比值）会发生可观测的变化。设备通过长时间佩戴连续采集 PPG 或 ECG 信号，结合量表（如 PSS）校准，给出压力等级评估。</p><h3 id="3、血氧检测"><a href="#3、血氧检测" class="headerlink" title="3、血氧检测"></a>3、血氧检测</h3><p>血氧饱和度（SpO2）通过 PPG 双波长原理测量：氧合血红蛋白与还原血红蛋白对不同波长光（红光 660nm、红外光 940nm）的吸收率不同，通过两者光强的比值即可估算 SpO2。该功能常用于高原旅行、睡眠呼吸暂停辅助筛查与高强度运动监测。</p><h3 id="4、血糖检测"><a href="#4、血糖检测" class="headerlink" title="4、血糖检测"></a>4、血糖检测</h3><p>血糖检测是智能穿戴领域的难点。目前主流技术方向为<strong>非侵入式</strong>血糖监测，包括近红外光谱、拉曼光谱、射频阻抗等方案，但受汗液、肤色、温度等干扰因素影响，精度尚未达到医疗级标准，市面产品多为辅助参考性质。侵入式或微创方案（连续血糖监测 CGM）则是医疗领域的主流。</p><h3 id="5、血脂检测"><a href="#5、血脂检测" class="headerlink" title="5、血脂检测"></a>5、血脂检测</h3><p>血脂（胆固醇、甘油三酯等）目前没有成熟的可穿戴检测方案，主要依赖抽血化验。部分研究尝试通过光学或生化传感器实现长期趋势监测，但均处于实验室阶段，短期内难以落地。</p><h3 id="6、血压监测"><a href="#6、血压监测" class="headerlink" title="6、血压监测"></a>6、血压监测</h3><p>血压监测有两种路线：一是传统的<strong>示波法</strong>，通过充气袖带测量，精度高但体积大、难以连续佩戴；二是基于 <strong>PPG 脉搏波传导时间（PTT）</strong>的估测方法，无需袖带、可连续测量，但需要定期校准且精度受运动干扰影响较大。当前消费级手表多采用后者，作为健康参考而非医疗诊断依据。</p><h3 id="7、心脏健康"><a href="#7、心脏健康" class="headerlink" title="7、心脏健康"></a>7、心脏健康</h3><p>心脏健康监测通过单导联 ECG 电极采集心电信号，可进行<strong>房颤（AFib）筛查</strong>、早搏检测与心率变异性分析。配合算法，设备能识别不规则心律并在异常时提醒用户及时就医。部分高端设备已获得医疗器械认证，具备临床参考价值。</p><h3 id="8、心率监测"><a href="#8、心率监测" class="headerlink" title="8、心率监测"></a>8、心率监测</h3><p>心率监测是可穿戴设备最成熟、最核心的健康功能，基于 PPG 光电容积脉搏波原理，通过分析皮肤下毛细血管血流体积变化推算瞬时心率。除实时心率外，还衍生出静息心率、运动心率区间（燃脂&#x2F;有氧&#x2F;无氧）、最大摄氧量（VO2max）等指标，为运动训练提供指导。</p><h2 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h2><p>智能穿戴设备以传感器为硬件基础，运动侧围绕 IMU 与 GPS 构建监测、识别、定位能力，健康侧围绕 PPG、ECG 构建睡眠、压力、血氧、心率等监测能力。血糖、血脂等生化指标的连续无创监测仍是行业待突破的方向。</p>]]>
    </content>
    <id>https://grobenis.github.io/2021/04/10/2021_04_10_Introduction-of-various-features-in-the-field-of-smart-wear/</id>
    <link href="https://grobenis.github.io/2021/04/10/2021_04_10_Introduction-of-various-features-in-the-field-of-smart-wear/"/>
    <published>2021-04-10T13:49:13.000Z</published>
    <summary>
      <![CDATA[<p>很早就想抽空写一篇关于智能穿戴中各种特性的综述文章。今天逼迫自己开个头。总体来说，在当前的智能穿戴领域，主要分为运动和健康两大块。</p>
<p>本文综述智能穿戴设备领域的功能特性，将其划分为运动与健康两大板块，运动侧涵盖监测、动作识别与定位，健康侧重点讲解睡眠分期依赖的多种生理信号，并列举压力、血氧、心率等监测方向。</p>
<blockquote>
<p><strong>腕间方寸，洞察健康全局</strong></p>
</blockquote>]]>
    </summary>
    <title>可穿戴设备特性综述</title>
    <updated>2026-08-25T13:18:00.748Z</updated>
  </entry>
  <entry>
    <author>
      <name>guoben</name>
    </author>
    <category term="SLAM" scheme="https://grobenis.github.io/categories/SLAM/"/>
    <category term="IMU" scheme="https://grobenis.github.io/tags/IMU/"/>
    <content>
      <![CDATA[<p>该文用来介绍仿真IMU数据的方法</p><p>本文介绍基于 GroundTruth 轨迹仿真生成 IMU 数据的方法，先利用矩阵对数与指数映射描述相机位姿变换，再通过 B 样条插值将离散轨迹拟合成连续曲线。在此基础上对曲线求导，即可得到带误差的加速度计与陀螺仪测量值，文章最后给出了 TUM-GT2IMU 工具的目录结构与各文件作用。</p><blockquote><p><strong>由轨迹拟合曲线，从曲线求导得 IMU</strong></p></blockquote><span id="more"></span><h2 id="理论基础"><a href="#理论基础" class="headerlink" title="理论基础"></a>理论基础</h2><h3 id="1、定义相机变换矩阵"><a href="#1、定义相机变换矩阵" class="headerlink" title="1、定义相机变换矩阵"></a>1、定义相机变换矩阵</h3><p>首先使用4*4的变换矩阵来来表示相机之间的变换T：</p><img src="image-20200723220416181.png" alt="image-20200723220416181" style="zoom: 50%;" /><p>​ 使用$T_{b,a}$表示时间Δt内，帧$T_{w,a}$与帧$T_{w,b}$(其中w为世界坐标系)之间运动的速度（该速度恒定且包括角速度和线速度）可表示为矩阵形式：</p><img src="image-20200723220712114.png" alt="image-20200723220712114" style="zoom: 50%;" /><p>​ 其中log是矩阵对数。对于矩阵组SE3，对数映射和它的逆(指数映射)就可以在闭域内计算。</p><h3 id="2、根据离散的点拟合连续的曲线"><a href="#2、根据离散的点拟合连续的曲线" class="headerlink" title="2、根据离散的点拟合连续的曲线"></a>2、根据离散的点拟合连续的曲线</h3><p>为了得到IMU数据，我们需要知晓轨迹的数学形式，为此，我们需要先根据离散的点轨迹拟合曲线，这个过程通过B样条插值来完成。</p><p>曲线p(t)可以使用点$P_i$和基本方程$B_{i,k}(t)$来表示:</p><p>$$p(t) = \sum^n_{i=0}p_iB_{i,k}(t)$$</p><p>其中$p_i$表示控制点，$B_{i,k}(t)$是基础函数，可以使用De Boor-Cox递归公式进行计算得到。上式可以组织为如下形式：</p><p>$$p(t)=p_0\tilde{B}_{0,k}(t)+\sum^{n}_{i=1}(p_i-p_{i-1})\tilde{B}_{i,k}(t)$$</p><p>其中$\tilde{B}_{i,k}(t) = \sum^{n}_{j=i}B_{j,k}(t)$是累加的基础函数。利用对数和指数映射，把$p_0$映射到李代数上：$p_0 = \log(T_{w,0})$.</p><p>将$p_i,p_{i-1}$两两个点之间的转移方程映射到李代数上，可表示为$\Omega_i = \log(T^{-1}_{w,i-1}T_{w,i})$.</p><p>可以把由等式2描述的李群中的轨迹重写为如下形式。</p><p>$$T_{w,s}(t) = \exp(\tilde{B_{0,k}(t)})\log(T_{w,0}))\prod^n_{i=1}\exp(\tilde{B}_{i,k}(t)\Omega_i)$$</p><p>其中，$R_{w,s}(t)\in SE^3$是t可是沿着样条线的位姿。 $T_{w,s}(t)$ 可以看作是轨迹</p><h3 id="3-根据曲线生成IMU的加速度和陀螺仪角速度数据"><a href="#3-根据曲线生成IMU的加速度和陀螺仪角速度数据" class="headerlink" title="3. 根据曲线生成IMU的加速度和陀螺仪角速度数据"></a>3. 根据曲线生成IMU的加速度和陀螺仪角速度数据</h3><p>​ 累积的B样条参数化满足可求导性，因此可轻松地得到加速度计和陀螺仪的带误差的测量值。计算公式如下：</p><p><img src="/2020/09/19/2020-09-19-Generate-Imu-Data-Based-on-GroundTruth/image-20200723171456502.png" loading="lazy"></p><p>其中,$\dot{R}_{w,s}$和$\ddot{s}_w$分别是$\dot{T}_{w,s}$和$\ddot{T_{w,s}}$相应的子矩阵. $g_w$是世界坐标系下的重力加速度.</p><p>其中，$T_{w,s}$表示s系中的点在w系中的变换，$R^T_{w,s}$是的子矩阵，表示纯旋转。$R_{w,s}$是的子矩阵，$s_w(u)$是$T_{w,s}$的子矩阵，代表平移，$g_w$是重力在w系中的加速度。</p><h2 id="实践环节"><a href="#实践环节" class="headerlink" title="实践环节"></a>实践环节</h2><h3 id="TUM-GT2IMU"><a href="#TUM-GT2IMU" class="headerlink" title="TUM-GT2IMU"></a>TUM-GT2IMU</h3><p>程序语言 （Matlab）</p><h4 id="文档目录结构"><a href="#文档目录结构" class="headerlink" title="文档目录结构"></a>文档目录结构</h4><figure class="highlight cmd"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line">(base) guoben@guoben-Ubuntu-<span class="number">1804</span>:~/Project/TUM-RGBD-GT2IMU$ <span class="built_in">tree</span>.</span><br><span class="line">├── GT2IMU.m</span><br><span class="line">├── IMUgenerate2.m</span><br><span class="line">├── IMUgenerate.m</span><br><span class="line">├── test1.m</span><br><span class="line">├── trajEst.m</span><br><span class="line">├── trajGenerate.m</span><br><span class="line">├── trajRead.m</span><br><span class="line">├── trajShow.m</span><br><span class="line">└── trajWrite.m</span><br><span class="line">├── tools</span><br><span class="line">│   ├── asm2v.m</span><br><span class="line">│   ├── c2q.m</span><br><span class="line">│   ├── pose2T.m</span><br><span class="line">│   ├── q2c.m</span><br><span class="line">│   ├── q4inv.m</span><br><span class="line">│   ├── q4mult.m</span><br><span class="line">│   ├── RK4.m</span><br><span class="line">│   ├── state_pro.m</span><br><span class="line">│   ├── T2J.m</span><br><span class="line">│   ├── v2asm.m</span><br><span class="line">│   └── zeta2J.m</span><br></pre></td></tr></table></figure><h3 id="各文件函数与作用"><a href="#各文件函数与作用" class="headerlink" title="各文件函数与作用"></a>各文件函数与作用</h3><h4 id="1-GT2IMU-m：源文件，在这里设置轨迹文件作为输入，然后生成IMU轨迹"><a href="#1-GT2IMU-m：源文件，在这里设置轨迹文件作为输入，然后生成IMU轨迹" class="headerlink" title="1. GT2IMU.m：源文件，在这里设置轨迹文件作为输入，然后生成IMU轨迹"></a>1. GT2IMU.m：源文件，在这里设置轨迹文件作为输入，然后生成IMU轨迹</h4><h4 id="2-trajRead：读取轨迹文件"><a href="#2-trajRead：读取轨迹文件" class="headerlink" title="2. trajRead：读取轨迹文件"></a>2. trajRead：读取轨迹文件</h4><p>输入：<code>groundtruth.txt</code>文件</p><p>输出：<code>GT</code>文件，<code>timestamp</code>文件，<code>pose</code>文件</p><p>其中，时间戳文件是一维矩阵，pose文件是7维矩阵，格式：四元数+平移</p><h4 id="3-trajEst：仿真轨迹估计"><a href="#3-trajEst：仿真轨迹估计" class="headerlink" title="3. trajEst：仿真轨迹估计"></a>3. trajEst：仿真轨迹估计</h4><p>读取<code>GT</code>文件，生成B样条插值函数</p><figure class="highlight matlab"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">save(<span class="string">&#x27;Bs&#x27;</span>,<span class="string">&#x27;kT&#x27;</span>,<span class="string">&#x27;Tk&#x27;</span>,<span class="string">&#x27;dT&#x27;</span>,<span class="string">&#x27;tIMU&#x27;</span>,<span class="string">&#x27;Te&#x27;</span>,<span class="string">&#x27;dt&#x27;</span>,<span class="string">&#x27;B0&#x27;</span>,<span class="string">&#x27;B1&#x27;</span>,<span class="string">&#x27;B2&#x27;</span>,<span class="string">&#x27;B3&#x27;</span>,<span class="string">&#x27;tl&#x27;</span>)</span><br></pre></td></tr></table></figure><p>生成仿真函数的过程</p><ul><li>统计节点Node个数 计算对应的时间</li><li>最小化由实际观测值与预测观测值之差形成的目标函数，来批量或在窗口上求解样条曲线和摄像机参数。</li></ul>]]>
    </content>
    <id>https://grobenis.github.io/2020/09/19/2020-09-19-Generate-Imu-Data-Based-on-GroundTruth/</id>
    <link href="https://grobenis.github.io/2020/09/19/2020-09-19-Generate-Imu-Data-Based-on-GroundTruth/"/>
    <published>2020-09-19T15:36:55.000Z</published>
    <summary>
      <![CDATA[<p>该文用来介绍仿真IMU数据的方法</p>
<p>本文介绍基于 GroundTruth 轨迹仿真生成 IMU 数据的方法，先利用矩阵对数与指数映射描述相机位姿变换，再通过 B 样条插值将离散轨迹拟合成连续曲线。在此基础上对曲线求导，即可得到带误差的加速度计与陀螺仪测量值，文章最后给出了 TUM-GT2IMU 工具的目录结构与各文件作用。</p>
<blockquote>
<p><strong>由轨迹拟合曲线，从曲线求导得 IMU</strong></p>
</blockquote>]]>
    </summary>
    <title>基于真实轨迹生成 IMU 数据</title>
    <updated>2026-08-25T13:18:00.647Z</updated>
  </entry>
  <entry>
    <author>
      <name>guoben</name>
    </author>
    <category term="学习" scheme="https://grobenis.github.io/categories/%E5%AD%A6%E4%B9%A0/"/>
    <category term="计算机网络" scheme="https://grobenis.github.io/tags/%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C/"/>
    <category term="HTTP" scheme="https://grobenis.github.io/tags/HTTP/"/>
    <category term="HTTPS" scheme="https://grobenis.github.io/tags/HTTPS/"/>
    <content>
      <![CDATA[<p>本文学习自《图解HTTP》，目的是为了系统的学习这个协议</p><p>本文系统学习 HTTP 协议，从 TCP&#x2F;IP 分层模型与 URI、URL 概念入手，讲解请求与响应报文结构、无状态特性，以及 GET、POST、PUT 等八种请求方法。文章还覆盖持久连接与管线化、Cookie 状态管理，并延伸至 HTTP 报文、状态码、首部字段与 HTTPS 的加密认证机制。</p><blockquote><p><strong>无状态协议，用 Cookie 记住你</strong></p></blockquote><span id="more"></span><p>HTTP的</p><h2 id="背景"><a href="#背景" class="headerlink" title="背景"></a>背景</h2><p>最初设想的基本理念是:借助多文档之间相互关联形成的超文本(HyperText),连成可相互参阅的 WWW(World Wide Web,万维网)。</p><h3 id="发展"><a href="#发展" class="headerlink" title="发展"></a>发展</h3><h4 id="HTTP-x2F-1-0"><a href="#HTTP-x2F-1-0" class="headerlink" title="HTTP&#x2F;1.0"></a>HTTP&#x2F;1.0</h4><p>HTTP 正式作为标准被公布是在 1996 年的 5 月,版本被命名为HTTP&#x2F;1.0,并记载于 RFC1945。虽说是初期标准,但该协议标准至今仍被广泛使用在服务器端。</p><h4 id="HTTP-x2F-1-1"><a href="#HTTP-x2F-1-1" class="headerlink" title="HTTP&#x2F;1.1"></a>HTTP&#x2F;1.1</h4><p>1997 年 1 月公布的 HTTP&#x2F;1.1 是目前主流的 HTTP 协议版本。当初的标准是 RFC2068,之后发布的修订版 RFC2616 就是当前的最新版本。</p><h2 id="基础"><a href="#基础" class="headerlink" title="基础"></a>基础</h2><h3 id="TCP-x2F-IP"><a href="#TCP-x2F-IP" class="headerlink" title="TCP&#x2F;IP"></a>TCP&#x2F;IP</h3><p>TCP&#x2F;IP 协议族里重要的一点就是分层。TCP&#x2F;IP 协议族按层次分别分为以下 4 层:应用层、传输层、网络层和数据链路层。</p><p><img src="/2020/09/14/2020-09-14-%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%E5%BA%94%E7%94%A8%E5%B1%82HTTP%E5%8D%8F%E8%AE%AE/image-20200914102826121.png" loading="lazy"></p><table><thead><tr><th></th><th>各层的作用</th></tr></thead><tbody><tr><td>应用层</td><td>决定了向用户提供应用服务时通信的活动</td></tr><tr><td>传输层</td><td>传输层对上层应用层,提供处于网络连接中的两台计算机之间的数据传输。</td></tr><tr><td>网络层</td><td>网络层用来处理在网络上流动的数据包。数据包是网络传输的最小数据单位。<br />该层规定了通过怎样的路径(所谓的传输路线)到达对方计算机,并把数据包传送给对方。</td></tr><tr><td>网络接口层</td><td>用来处理连接网络的硬件部分。</td></tr></tbody></table><p>与 HTTP 关系密切的协议 : IP、TCP 和DNS，其中IP协议负责传输，TCP协议确保可靠，DNS服务负责域名解析。</p><p>各个协议与HTTP协议之间的关系</p><p><img src="/2020/09/14/2020-09-14-%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%E5%BA%94%E7%94%A8%E5%B1%82HTTP%E5%8D%8F%E8%AE%AE/image-20200914103657745.png" loading="lazy"></p><h3 id="URI和URL"><a href="#URI和URL" class="headerlink" title="URI和URL"></a>URI和URL</h3><p>URI 用字符串标识某一互联网资源,而 URL 表示资源的地点(互联网上所处的位置)。可见 URL 是 URI 的子集。</p><h2 id="HTTP协议"><a href="#HTTP协议" class="headerlink" title="HTTP协议"></a>HTTP协议</h2><p> HTTP 协议用于客户端和服务器端之间的通信</p><h3 id="1-通过请求和响应的交换达成通信"><a href="#1-通过请求和响应的交换达成通信" class="headerlink" title="1 通过请求和响应的交换达成通信"></a>1 通过请求和响应的交换达成通信</h3><h4 id="请求报文"><a href="#请求报文" class="headerlink" title="请求报文"></a>请求报文</h4><p>请求报文是由请求方法、请求URI、协议版本、可选的请求首部字段和内容实体构成的。</p><h4 id="响应报文"><a href="#响应报文" class="headerlink" title="响应报文"></a>响应报文</h4><p>响应报文基本上由协议版本、状态码(表示请求成功或失败的数字代码)、用以解释状态码的原因短语、可选的响应首部字段以及实体主体构成。稍后我们会对这些内容进行详细说明。</p><h3 id="2-HTTP-是不保存状态的协议"><a href="#2-HTTP-是不保存状态的协议" class="headerlink" title="2 HTTP 是不保存状态的协议"></a>2 HTTP 是不保存状态的协议</h3><p>HTTP 是一种不保存状态,即无状态(stateless)协议。HTTP 协议自身不对请求和响应之间的通信状态进行保存。也就是说在 HTTP 这个级别,协议对于发送过的请求或响应都不做持久化处理。</p><p>HTTP&#x2F;1.1 虽然是无状态协议,但为了实现期望的保持状态功能,于是引入了 Cookie 技术。有了 Cookie 再用 HTTP 协议通信,就可以管理状态了。有关 Cookie 的详细内容稍后讲解。</p><h3 id="3-方法"><a href="#3-方法" class="headerlink" title="3 方法"></a>3 方法</h3><h4 id="1-GET-获取资源"><a href="#1-GET-获取资源" class="headerlink" title="1 GET: 获取资源"></a>1 GET: 获取资源</h4><p>GET 方法用来请求访问已被 URI 识别的资源。</p><p>指定的资源经服务器端解析后返回响应内容。也就是说,如果请求的资源是文本,那就保持原样返回;如果是像 CGI(Common Gateway Interface,通用网关接口)那样的程序,则返回经过执行后的输出结果。</p><p><img src="/2020/09/14/2020-09-14-%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%E5%BA%94%E7%94%A8%E5%B1%82HTTP%E5%8D%8F%E8%AE%AE/image-20200914104731577.png" loading="lazy"></p><h4 id="2-POST-传输实体主体"><a href="#2-POST-传输实体主体" class="headerlink" title="2 POST: 传输实体主体"></a>2 POST: 传输实体主体</h4><p>POST 方法用来传输实体的主体。</p><p>虽然用 GET 方法也可以传输实体的主体,但一般不用 GET 方法进行传输,而是用 POST 方法。虽说 POST 的功能与 GET 很相似,但POST 的主要目的并不是获取响应的主体内容。</p><p><img src="/2020/09/14/2020-09-14-%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%E5%BA%94%E7%94%A8%E5%B1%82HTTP%E5%8D%8F%E8%AE%AE/image-20200914104844902.png" loading="lazy"></p><h4 id="3-PUT-传输文件"><a href="#3-PUT-传输文件" class="headerlink" title="3 PUT:传输文件"></a>3 PUT:传输文件</h4><p>PUT 方法用来传输文件。就像 FTP 协议的文件上传一样,要求在请求报文的主体中包含文件内容,然后保存到请求 URI 指定的位置。</p><p>但是,鉴于 HTTP&#x2F;1.1 的 PUT 方法自身不带验证机制,任何人都可以上传文件 , 存在安全性问题,因此一般的 Web 网站不使用该方法。</p><p>若配合 Web 应用程序的验证机制,或架构设计采用REST(REpresentational State Transfer,表征状态转移)标准的同类Web 网站,就可能会开放使用 PUT 方法。</p><p><img src="/2020/09/14/2020-09-14-%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%E5%BA%94%E7%94%A8%E5%B1%82HTTP%E5%8D%8F%E8%AE%AE/image-20200914105045445.png" loading="lazy"></p><h4 id="4-HEAD-获得报文首部"><a href="#4-HEAD-获得报文首部" class="headerlink" title="4 HEAD:获得报文首部"></a>4 HEAD:获得报文首部</h4><p>HEAD 方法和 GET 方法一样,只是不返回报文主体部分。用于确认<br>URI 的有效性及资源更新的日期时间等。</p><p><img src="/2020/09/14/2020-09-14-%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%E5%BA%94%E7%94%A8%E5%B1%82HTTP%E5%8D%8F%E8%AE%AE/image-20200914105138317.png" loading="lazy"></p><h4 id="5-DELETE-删除文件"><a href="#5-DELETE-删除文件" class="headerlink" title="5 DELETE:删除文件"></a>5 DELETE:删除文件</h4><p>DELETE 方法用来删除文件,是与 PUT 相反的方法。</p><p>DELETE 方法按请求 URI 删除指定的资源。但是,HTTP&#x2F;1.1 的 DELETE 方法本身和 PUT 方法一样不带验证机制,所以一般的 Web 网站也不使用 DELETE 方法。当配合 Web 应用程序的验证机制,或遵守 REST 标准时还是有可能会开放使用的。</p><p><img src="/2020/09/14/2020-09-14-%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%E5%BA%94%E7%94%A8%E5%B1%82HTTP%E5%8D%8F%E8%AE%AE/image-20200914105210975.png" loading="lazy"></p><h4 id="6-OPTIONS-询问支持的方法"><a href="#6-OPTIONS-询问支持的方法" class="headerlink" title="6 OPTIONS:询问支持的方法"></a>6 OPTIONS:询问支持的方法</h4><p>OPTIONS 方法用来查询针对请求 URI 指定的资源支持的方法。</p><p><img src="/2020/09/14/2020-09-14-%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%E5%BA%94%E7%94%A8%E5%B1%82HTTP%E5%8D%8F%E8%AE%AE/image-20200914105231225.png" loading="lazy"></p><h4 id="7-TRACE-追踪路径"><a href="#7-TRACE-追踪路径" class="headerlink" title="7 TRACE:追踪路径"></a>7 TRACE:追踪路径</h4><p>TRACE 方法是让 Web 服务器端将之前的请求通信环回给客户端的方法。</p><p>发送请求时,在 Max-Forwards 首部字段中填入数值,每经过一个服务器端就将该数字减 1,当数值刚好减到 0 时,就停止继续传输,最后接收到请求的服务端则返回状态码 200 OK 的响应。客户端通过 TRACE 方法可以查询发送出去的请求是怎样被加工修改&#x2F; 篡改的。这是因为,请求想要连接到源目标服务器可能会通过代理中转,TRACE 方法就是用来确认连接过程中发生的一系列操作。但是,TRACE 方法本来就不怎么常用,再加上它容易引发XST(Cross-Site Tracing,跨站追踪)攻击,通常就更不会用到了。</p><p><img src="/2020/09/14/2020-09-14-%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%E5%BA%94%E7%94%A8%E5%B1%82HTTP%E5%8D%8F%E8%AE%AE/image-20200914105432225.png" loading="lazy"></p><p><img src="/2020/09/14/2020-09-14-%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%E5%BA%94%E7%94%A8%E5%B1%82HTTP%E5%8D%8F%E8%AE%AE/image-20200914105506925.png" loading="lazy"></p><p><img src="/2020/09/14/2020-09-14-%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%E5%BA%94%E7%94%A8%E5%B1%82HTTP%E5%8D%8F%E8%AE%AE/image-20200914105512891.png" loading="lazy"></p><h4 id="8-CONNECT-要求用隧道协议连接代理"><a href="#8-CONNECT-要求用隧道协议连接代理" class="headerlink" title="8 CONNECT: 要求用隧道协议连接代理"></a>8 CONNECT: 要求用隧道协议连接代理</h4><p>CONNECT 方法要求在与代理服务器通信时建立隧道,实现用隧道协议进行 TCP 通信。</p><p>主要使用 SSL(Secure Sockets Layer,安全套接层) 和 TLS(Transport Layer Security,传输层安全)协议把通信内容加密后经网络隧道传输。</p><p>CONNECT 方法的格式如下所示。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">CONNECT 代理服务器名:端口号 HTTP版本</span><br></pre></td></tr></table></figure><p><img src="/2020/09/14/2020-09-14-%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%E5%BA%94%E7%94%A8%E5%B1%82HTTP%E5%8D%8F%E8%AE%AE/image-20200914105636433.png" loading="lazy"></p><h3 id="4-使用方法下达命令"><a href="#4-使用方法下达命令" class="headerlink" title="4 使用方法下达命令"></a>4 使用方法下达命令</h3><p>向请求 URI 指定的资源发送请求报文时,采用称为方法的命令。<br>方法的作用在于,可以指定请求的资源按期望产生某种行为。方法中有 GET、POST 和 HEAD 等。</p><p><img src="/2020/09/14/2020-09-14-%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%E5%BA%94%E7%94%A8%E5%B1%82HTTP%E5%8D%8F%E8%AE%AE/image-20200914105745371.png" loading="lazy"></p><h3 id="5-持久连接节省通信量"><a href="#5-持久连接节省通信量" class="headerlink" title="5 持久连接节省通信量"></a>5 持久连接节省通信量</h3><p>使用浏览器浏览一个包含多张图片的 HTML 页面时,在发送请求访问 HTML 页面资源的同时,也会请求该 HTML 页面里包含的其他资源。因此,每次的请求都会造成无谓的 TCP 连接建立和断开,增加通信量的开销</p><h4 id="持久连接"><a href="#持久连接" class="headerlink" title="持久连接"></a>持久连接</h4><p>为解决上述 TCP 连接的问题,HTTP&#x2F;1.1 和一部分的 HTTP&#x2F;1.0 想出了持久连接(HTTP Persistent Connections,也称为 HTTP keep-alive 或HTTP connection reuse)的方法。</p><p>持久连接的特点是,<strong>只要任意一端没有明确提出断开连接,则保持 TCP 连接状态。</strong></p><p><img src="/2020/09/14/2020-09-14-%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%E5%BA%94%E7%94%A8%E5%B1%82HTTP%E5%8D%8F%E8%AE%AE/image-20200914110115581.png" loading="lazy"></p><p>持久连接的好处在于减少了 TCP 连接的重复建立和断开所造成的额外开销,减轻了服务器端的负载。另外,减少开销的那部分时间,使HTTP 请求和响应能够更早地结束,这样 Web 页面的显示速度也就相应提高了。</p><p>在 HTTP&#x2F;1.1 中,所有的连接默认都是持久连接,但在 HTTP&#x2F;1.0 内并未标准化。虽然有一部分服务器通过非标准的手段实现了持久连接,但服务器端不一定能够支持持久连接。毫无疑问,除了服务器端,客户端也需要支持持久连接。</p><h4 id="管线化"><a href="#管线化" class="headerlink" title="管线化"></a>管线化</h4><p>持久连接使得多数请求以管线化(pipelining)方式发送成为可能。从前发送请求后需等待并收到响应,才能发送下一个请求。</p><p>管线化技术出现后,不用等待响应亦可直接发送下一个请求。这样就能够做到同时并行发送多个请求,而不需要一个接一个地等待响应了。</p><p><img src="/2020/09/14/2020-09-14-%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%E5%BA%94%E7%94%A8%E5%B1%82HTTP%E5%8D%8F%E8%AE%AE/image-20200914110212175.png" loading="lazy"></p><h3 id="6-使用-Cookie-的状态管理"><a href="#6-使用-Cookie-的状态管理" class="headerlink" title="6 使用 Cookie 的状态管理"></a>6 使用 Cookie 的状态管理</h3><p>HTTP 是无状态协议,它不对之前发生过的请求和响应的状态进行管理。也就是说,无法根据之前的状态进行本次的请求处理。</p><p>假设要求登录认证的 Web 页面本身无法进行状态的管理(不记录已登录的状态),那么每次跳转新页面不是要再次登录,就是要在每次请求报文中附加参数来管理登录状态。</p><p>不可否认,无状态协议当然也有它的优点。由于不必保存状态,自然可减少服务器的 CPU 及内存资源的消耗。从另一侧面来说,也正是因为 HTTP 协议本身是非常简单的,所以才会被应用在各种场景里。</p><p>保留无状态协议这个特征的同时又要解决类似的矛盾问题,于是引入了 Cookie 技术。</p><p>Cookie 技术通过在请求和响应报文中写入 Cookie 信息来控制客户端的状态。</p><p>Cookie 会根据从服务器端发送的响应报文内的一个叫做 Set-Cookie 的首部字段信息,通知客户端保存 Cookie。当下次客户端再往该服务器发送请求时,客户端会自动在请求报文中加入 Cookie 值后发送出去。服务器端发现客户端发送过来的 Cookie 后,会去检查究竟是从哪一个客户端发来的连接请求,然后对比服务器上的记录,最后得到之前的状态信息。</p><p>上图展示了发生 Cookie 交互的情景,HTTP 请求报文和响应报文的内容如下。</p><p><img src="/2020/09/14/2020-09-14-%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%E5%BA%94%E7%94%A8%E5%B1%82HTTP%E5%8D%8F%E8%AE%AE/image-20200914110504696.png" loading="lazy"></p><h2 id="HTTP-报文"><a href="#HTTP-报文" class="headerlink" title="HTTP 报文"></a>HTTP 报文</h2><p>用于 HTTP 协议交互的信息被称为 HTTP 报文。</p><ul><li>请求端(客户端)的HTTP 报文叫做请求报文。</li><li>响应端(服务器端)的叫做响应报文。</li></ul><p>HTTP 报文本身是由多行(用 CR+LF 作换行符)数据构成的字符串文本。<br>HTTP 报文大致可分为报文首部和报文主体两块。两者由最初出现的空行(CR+LF)来划分。通常,并不一定要有报文主体。</p><p><img src="/2020/09/14/2020-09-14-%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%E5%BA%94%E7%94%A8%E5%B1%82HTTP%E5%8D%8F%E8%AE%AE/image-20200914111139781.png" loading="lazy"></p><h3 id="请求报文及响应报文的结构"><a href="#请求报文及响应报文的结构" class="headerlink" title="请求报文及响应报文的结构"></a>请求报文及响应报文的结构</h3><p><img src="/2020/09/14/2020-09-14-%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%E5%BA%94%E7%94%A8%E5%B1%82HTTP%E5%8D%8F%E8%AE%AE/image-20200914111057252.png" loading="lazy"></p><p><img src="/2020/09/14/2020-09-14-%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%E5%BA%94%E7%94%A8%E5%B1%82HTTP%E5%8D%8F%E8%AE%AE/image-20200914111104870.png" loading="lazy"></p><table><thead><tr><th>请求行</th><th>包含用于请求的方法,请求 URI 和 HTTP 版本。</th></tr></thead><tbody><tr><td>状态行</td><td>包含表明响应结果的状态码,原因短语和 HTTP 版本。</td></tr><tr><td>首部字段</td><td>包含表示请求和响应的各种条件和属性的各类首部。</td></tr></tbody></table><p>一般有 4 种首部,分别是:通用首部、请求首部、响应首部和实体首部。其他可能包含 HTTP 的 RFC 里未定义的首部(Cookie 等)。</p><h3 id="编码提升传输速率"><a href="#编码提升传输速率" class="headerlink" title="编码提升传输速率"></a>编码提升传输速率</h3><p>HTTP 在传输数据时可以按照数据原貌直接传输,但也可以在传输过程中通过编码提升传输速率。通过在传输时编码,能有效地处理大量的访问请求。但是,编码的操作需要计算机来完成,因此会消耗更多的 CPU 等资源。</p><h2 id="HTTP-状态码"><a href="#HTTP-状态码" class="headerlink" title="HTTP 状态码"></a>HTTP 状态码</h2><p><img src="/2020/09/14/2020-09-14-%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%E5%BA%94%E7%94%A8%E5%B1%82HTTP%E5%8D%8F%E8%AE%AE/image-20200914111710636.png" loading="lazy"></p><p><img src="/2020/09/14/2020-09-14-%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%E5%BA%94%E7%94%A8%E5%B1%82HTTP%E5%8D%8F%E8%AE%AE/image-20200914111718030.png" loading="lazy"></p><h2 id="HTTP-首部"><a href="#HTTP-首部" class="headerlink" title="HTTP 首部"></a>HTTP 首部</h2><p><img src="/2020/09/14/2020-09-14-%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%E5%BA%94%E7%94%A8%E5%B1%82HTTP%E5%8D%8F%E8%AE%AE/image-20200914112312857.png" loading="lazy"></p><h3 id="HTTP-首部字段结构"><a href="#HTTP-首部字段结构" class="headerlink" title="HTTP 首部字段结构"></a>HTTP 首部字段结构</h3><p>HTTP 首部字段是由首部字段名和字段值构成的,中间用冒号“:” 分隔。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">首部字段名: 字段值</span><br></pre></td></tr></table></figure><p>例如,在 HTTP 首部中以 Content-Type 这个字段来表示报文主体的 对象类型。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Content-Type: text/html</span><br></pre></td></tr></table></figure><p>就以上述示例来看,首部字段名为 Content-Type,字符串 text&#x2F;html 是字段值。</p><p>另外,字段值对应单个 HTTP 首部字段可以有多个值,如下所示。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">Keep-Alive: timeout=15, max=100</span><br></pre></td></tr></table></figure><p>若 HTTP 首部字段重复了会如何当 HTTP 报文首部中出现了两个或两个以上具有相同首部字段名时会怎么样?这种情况在规范内尚未明确,根据浏览器内部处理逻辑的不同,结果可能并不一致。有些浏览器会优先处理第一次出现的首部字段,而有些则会优先处理最后出现的首部字段。</p><h3 id="首部字段类型"><a href="#首部字段类型" class="headerlink" title="首部字段类型"></a>首部字段类型</h3><ol><li>通用首部字段(General Header Fields)，请求报文和响应报文两方都会使用的首部。</li><li>请求首部字段(Request Header Fields)，从客户端向服务器端发送请求报文时使用的首部。补充了请求的附加内容、客户端信息、响应内容相关优先级等信息。</li><li>响应首部字段(Response Header Fields)，从服务器端向客户端返回响应报文时使用的首部。补充了响应的附加内容,也会要求客户端附加额外的内容信息。</li><li>实体首部字段(Entity Header Fields)，针对请求报文和响应报文的实体部分使用的首部。补充了资源内容更新时间等与实体有关的信息。</li></ol><h2 id="HTTPS协议"><a href="#HTTPS协议" class="headerlink" title="HTTPS协议"></a>HTTPS协议</h2><h3 id="HTTP的缺点"><a href="#HTTP的缺点" class="headerlink" title="HTTP的缺点"></a>HTTP的缺点</h3><ul><li>通信使用明文(不加密),内容可能会被窃听</li><li>不验证通信方的身份,因此有可能遭遇伪装</li><li>无法证明报文的完整性,所以有可能已遭篡改</li></ul><h4 id="通信使用明文可能会被窃听"><a href="#通信使用明文可能会被窃听" class="headerlink" title="通信使用明文可能会被窃听"></a>通信使用明文可能会被窃听</h4><h4 id="HTTP-加密-认证-完整性保护-x3D-HTTPS"><a href="#HTTP-加密-认证-完整性保护-x3D-HTTPS" class="headerlink" title="HTTP+ 加密 + 认证 + 完整性保护&#x3D;HTTPS"></a>HTTP+ 加密 + 认证 + 完整性保护&#x3D;HTTPS</h4>]]>
    </content>
    <id>https://grobenis.github.io/2020/09/14/2020-09-14-%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%E5%BA%94%E7%94%A8%E5%B1%82HTTP%E5%8D%8F%E8%AE%AE/</id>
    <link href="https://grobenis.github.io/2020/09/14/2020-09-14-%E8%AE%A1%E7%AE%97%E6%9C%BA%E7%BD%91%E7%BB%9C%E5%BA%94%E7%94%A8%E5%B1%82HTTP%E5%8D%8F%E8%AE%AE/"/>
    <published>2020-09-14T02:22:42.000Z</published>
    <summary>
      <![CDATA[<p>本文学习自《图解HTTP》，目的是为了系统的学习这个协议</p>
<p>本文系统学习 HTTP 协议，从 TCP&#x2F;IP 分层模型与 URI、URL 概念入手，讲解请求与响应报文结构、无状态特性，以及 GET、POST、PUT 等八种请求方法。文章还覆盖持久连接与管线化、Cookie 状态管理，并延伸至 HTTP 报文、状态码、首部字段与 HTTPS 的加密认证机制。</p>
<blockquote>
<p><strong>无状态协议，用 Cookie 记住你</strong></p>
</blockquote>]]>
    </summary>
    <title>HTTP协议</title>
    <updated>2026-08-25T13:18:00.618Z</updated>
  </entry>
  <entry>
    <author>
      <name>guoben</name>
    </author>
    <category term="工作" scheme="https://grobenis.github.io/categories/%E5%B7%A5%E4%BD%9C/"/>
    <category term="排序" scheme="https://grobenis.github.io/tags/%E6%8E%92%E5%BA%8F/"/>
    <category term="回溯" scheme="https://grobenis.github.io/tags/%E5%9B%9E%E6%BA%AF/"/>
    <category term="字符串" scheme="https://grobenis.github.io/tags/%E5%AD%97%E7%AC%A6%E4%B8%B2/"/>
    <category term="链表" scheme="https://grobenis.github.io/tags/%E9%93%BE%E8%A1%A8/"/>
    <category term="数字计算，计算几何" scheme="https://grobenis.github.io/tags/%E6%95%B0%E5%AD%97%E8%AE%A1%E7%AE%97%EF%BC%8C%E8%AE%A1%E7%AE%97%E5%87%A0%E4%BD%95/"/>
    <content>
      <![CDATA[<p>本文旨在整理面试过程中经典的考题</p><p>本文整理面试中高频的经典代码题，覆盖字符串操作、排序与 Top K、链表、回溯、数字游戏、计算几何与动态规划等类别，每题附核心思路与参考实现，是面试前查漏补缺的实用清单。</p><blockquote><p><strong>经典题在手，面试心不慌</strong></p></blockquote><span id="more"></span><h2 id="一、字符串操作"><a href="#一、字符串操作" class="headerlink" title="一、字符串操作"></a>一、字符串操作</h2><h3 id="1-字符串转数字-atoi-x2F-stoi"><a href="#1-字符串转数字-atoi-x2F-stoi" class="headerlink" title="1 字符串转数字(atoi&#x2F;stoi)"></a>1 字符串转数字(atoi&#x2F;stoi)</h3><blockquote><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">int</span> <span class="title">StrToInt</span><span class="params">(string str)</span> </span>&#123;</span><br><span class="line">     <span class="type">int</span> len = str.<span class="built_in">length</span>();</span><br><span class="line">     <span class="keyword">if</span> (len == <span class="number">0</span>) <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line">     <span class="type">int</span> i = <span class="number">0</span>;</span><br><span class="line">     <span class="keyword">while</span> (i &lt; len &amp;&amp; str[i] == <span class="string">&#x27; &#x27;</span>)  ++i;  <span class="comment">// 排除开头的空格</span></span><br><span class="line">     <span class="keyword">if</span> (i == len) <span class="keyword">return</span> <span class="number">0</span>;</span><br><span class="line"></span><br><span class="line">     <span class="keyword">if</span> (!<span class="built_in">isdigit</span>(str[i]) &amp;&amp; str[i] != <span class="string">&#x27;+&#x27;</span> &amp;&amp; str[i] != <span class="string">&#x27;-&#x27;</span>) <span class="keyword">return</span> <span class="number">0</span>; <span class="comment">//判断第一个符号</span></span><br><span class="line">     <span class="type">bool</span> neg = str[i]==<span class="string">&#x27;-&#x27;</span> ? <span class="literal">true</span> : <span class="literal">false</span>;</span><br><span class="line">     i = <span class="built_in">isdigit</span>(str[i]) ? i : i<span class="number">+1</span>;</span><br><span class="line">     <span class="type">long</span> <span class="type">long</span> ans = <span class="number">0L</span>;</span><br><span class="line"></span><br><span class="line">     <span class="keyword">while</span> (i &lt; len &amp;&amp; <span class="built_in">isdigit</span>(str[i])) &#123;</span><br><span class="line">         ans = ans * <span class="number">10</span> + (str[i++]-<span class="string">&#x27;0&#x27;</span>);</span><br><span class="line">         <span class="keyword">if</span> (!neg &amp;&amp; ans &gt; INT_MAX) &#123;</span><br><span class="line">             ans = INT_MAX;</span><br><span class="line">             <span class="keyword">break</span>;</span><br><span class="line">         &#125;</span><br><span class="line">         <span class="keyword">if</span> (neg &amp;&amp; ans &gt; <span class="number">1L</span> + INT_MAX) &#123;</span><br><span class="line">             ans = <span class="number">1L</span> + INT_MAX;</span><br><span class="line">             <span class="keyword">break</span>;</span><br><span class="line">         &#125;</span><br><span class="line">     &#125;</span><br><span class="line">     <span class="keyword">if</span> (i != len) <span class="keyword">return</span> <span class="number">0</span>; <span class="comment">// 不要此处，就是atoi()库函数的实现</span></span><br><span class="line">     <span class="keyword">return</span> !neg ? <span class="keyword">static_cast</span>&lt;<span class="type">int</span>&gt;(ans) : <span class="keyword">static_cast</span>&lt;<span class="type">int</span>&gt;(-ans);</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></blockquote><h4 id="2-编程实现strcpy函数"><a href="#2-编程实现strcpy函数" class="headerlink" title="2 编程实现strcpy函数"></a>2 编程实现strcpy函数</h4><figure class="highlight c"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;stdio.h&gt;</span></span></span><br><span class="line"></span><br><span class="line"><span class="type">char</span>* <span class="title function_">strcpy</span><span class="params">(<span class="type">char</span>* dest_str,<span class="type">const</span> <span class="type">char</span> src_str)</span>&#123;</span><br><span class="line">    <span class="keyword">if</span>(dest_str==<span class="literal">NULL</span>||src_str==<span class="literal">NULL</span>)</span><br><span class="line">        <span class="keyword">return</span> <span class="literal">NULL</span>;</span><br><span class="line">    <span class="type">char</span>* strdestCopy = dest_str;</span><br><span class="line">    <span class="keyword">while</span>((*dest_str++=*src_str++)!=<span class="string">&#x27;\0&#x27;</span>);</span><br><span class="line">    <span class="keyword">return</span> strdestCopy;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h2 id="二、排序"><a href="#二、排序" class="headerlink" title="二、排序"></a>二、排序</h2><h3 id="2-求第k大的数的方法及各自的复杂度"><a href="#2-求第k大的数的方法及各自的复杂度" class="headerlink" title="2. 求第k大的数的方法及各自的复杂度"></a>2. 求第k大的数的方法及各自的复杂度</h3><blockquote><p>1 首先使用<strong>快速排序算法</strong>将数组按照从大到小排序，然后取第k个，其时间复杂度最快为<code>O(nlogn)</code></p><p>2 使用<strong>堆排序</strong>，建立最大堆，然后调整堆，知道获得第k个元素，其时间复杂度为<code>O(n+klogn)</code></p><p>3 首先利用哈希表统计数组中个元素出现的次数，然后利用<strong>计数排序</strong>的思想，线性从大到小扫描过程中，前面有k-1个数则为第k大的数</p><p>利用<strong>快排思想</strong>，从数组中随机选择一个数i，然后将数组分成两部分$D_l$,$D_r$，$D_l$的元素都小于i,Dr的元素都大于i。然后统计Dr元素个数，如果Dr元素个数等于k-1,那么第k大的数即为k，如果Dr元素个数小于k,那么继续求Dl中第k-Dr大的元素；如果Dr元素个数大于k,那么继续求Dr中第k大的元素。</p></blockquote><p>另外追问一下，当有相同元素时，还可以使用什么不同的方法求第k大的元素</p><blockquote><p>当有相同元素的时候，首先利用哈希表统计数组中个元素出现的次数，然后利用计数排序的思想，线性从大到小扫描过程中，前面有k-1个数则为第k大的数，平均情况下时间复杂度为O(n)</p></blockquote><h3 id="3-Top-K问题：海量数据"><a href="#3-Top-K问题：海量数据" class="headerlink" title="3.  Top K问题：海量数据"></a>3.  Top K问题：海量数据</h3><blockquote><h4 id="1-直接全部排序（只适用于内存够的情况）"><a href="#1-直接全部排序（只适用于内存够的情况）" class="headerlink" title="1. 直接全部排序（只适用于内存够的情况）"></a>1. 直接全部排序（只适用于内存够的情况）</h4><p>当数据量较小的情况下，内存中可以容纳所有数据。则最简单也是最容易想到的方法是将数据全部排序，然后取排序后的数据中的前K个。</p><p>这种方法对数据量比较敏感，当数据量较大的情况下，内存不能完全容纳全部数据，这种方法便不适应了。即使内存能够满足要求，该方法将全部数据都排序了，而题目只要求找出top K个数据，所以该方法并不十分高效，不建议使用。</p><h4 id="2-快速排序的变形-（只使用于内存够的情况）"><a href="#2-快速排序的变形-（只使用于内存够的情况）" class="headerlink" title="2. 快速排序的变形 （只使用于内存够的情况）"></a>2. 快速排序的变形 （只使用于内存够的情况）</h4><p>这是一个基于快速排序的变形，因为第一种方法中说到将所有元素都排序并不十分高效，只需要找出前K个最大的就行。</p><p>这种方法类似于快速排序，首先选择一个划分元，将比这个划分元大的元素放到它的前面，比划分元小的元素放到它的后面，此时完成了一趟排序。如果此时这个划分元的序号index刚好等于K，那么这个划分元以及它左边的数，刚好就是前K个最大的元素；如果index  &gt; K，那么前K大的数据在index的左边，那么就继续递归的从index-1个数中进行一趟排序；如果index &lt; K，那么再从划分元的右边继续进行排序，直到找到序号index刚好等于K为止。再将前K个数进行排序后，返回Top K个元素。这种方法就避免了对除了Top K个元素以外的数据进行排序所带来的不必要的开销。</p><h4 id="3-最小堆法"><a href="#3-最小堆法" class="headerlink" title="3. 最小堆法"></a>3. 最小堆法</h4><p>这是一种局部淘汰法。先读取前K个数，建立一个最小堆。然后将剩余的所有数字依次与最小堆的堆顶进行比较，如果小于或等于堆顶数据，则继续比较下一个；否则，删除堆顶元素，并将新数据插入堆中，重新调整最小堆。当遍历完全部数据后，最小堆中的数据即为最大的K个数。</p><h4 id="4-分治法"><a href="#4-分治法" class="headerlink" title="4. 分治法"></a>4. 分治法</h4><p>将全部数据分成N份，前提是每份的数据都可以读到内存中进行处理，找到每份数据中最大的K个数。此时剩下N<em>K个数据，如果内存不能容纳N</em>K个数据，则再继续分治处理，分成M份，找出每份数据中最大的K个数，如果M*K个数仍然不能读到内存中，则继续分治处理。直到剩余的数可以读入内存中，那么可以对这些数使用快速排序的变形或者归并排序进行处理。</p><h4 id="5-Hash法"><a href="#5-Hash法" class="headerlink" title="5. Hash法"></a>5. Hash法</h4><p>如果这些数据中有很多重复的数据，可以先通过hash法，把重复的数去掉。这样如果重复率很高的话，会减少很大的内存用量，从而缩小运算空间。处理后的数据如果能够读入内存，则可以直接排序；否则可以使用分治法或者最小堆法来处理数据。</p></blockquote><p>详细代码：</p><ol><li>利用快速排序的思想进行查找</li></ol><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Finder</span> &#123;</span><br><span class="line"><span class="keyword">public</span>:</span><br><span class="line">    <span class="function"><span class="type">int</span> <span class="title">findKth</span><span class="params">(vector&lt;<span class="type">int</span>&gt; a, <span class="type">int</span> n, <span class="type">int</span> K)</span> </span>&#123;</span><br><span class="line">        <span class="comment">// write code here</span></span><br><span class="line">        <span class="keyword">return</span> <span class="built_in">quickfind</span>(a,<span class="number">0</span>,n<span class="number">-1</span>,K);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="function"><span class="type">int</span> <span class="title">quickfind</span><span class="params">(vector&lt;<span class="type">int</span>&gt; &amp;a,<span class="type">int</span> low,<span class="type">int</span> high,<span class="type">int</span> K)</span></span>&#123;</span><br><span class="line">        <span class="type">int</span> pivot = <span class="built_in">partition</span>(a,low,high);</span><br><span class="line">        <span class="keyword">if</span>(pivot<span class="number">+1</span>&lt;K) <span class="comment">//中轴位置少于K个，在右子数组中继续查找</span></span><br><span class="line">            <span class="keyword">return</span> <span class="built_in">quickfind</span>(a,pivot<span class="number">+1</span>,high,K);</span><br><span class="line">        <span class="keyword">else</span> <span class="keyword">if</span>(pivot<span class="number">+1</span>&gt;K) <span class="comment">//中轴位置大于K个，在左子数组中继续查找</span></span><br><span class="line">            <span class="keyword">return</span> <span class="built_in">quickfind</span>( a, low, pivot<span class="number">-1</span>,K);</span><br><span class="line">        <span class="keyword">return</span> a[pivot];</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="function"><span class="type">int</span> <span class="title">partition</span><span class="params">(vector&lt;<span class="type">int</span>&gt; &amp;a,<span class="type">int</span> low, <span class="type">int</span> high)</span></span>&#123;</span><br><span class="line">        <span class="type">int</span> val = a[low];</span><br><span class="line">        <span class="keyword">while</span>(low&lt;high)&#123;</span><br><span class="line">            <span class="keyword">while</span>(low&lt;high&amp;&amp;a[high]&lt;=val) high--;    <span class="comment">// 从右往左扫描，直到遇到比基准元素大的元素</span></span><br><span class="line">            a[low] = a[high];</span><br><span class="line">            <span class="keyword">while</span>(low&lt;high&amp;&amp;a[low]&gt;=val) low++;    <span class="comment">//从左往右扫描，直到遇到比基准元素小的元素</span></span><br><span class="line">            a[high] = a[low];</span><br><span class="line">        &#125;</span><br><span class="line">        a[low] = val;</span><br><span class="line">        <span class="keyword">return</span> low;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><h2 id="三、链表"><a href="#三、链表" class="headerlink" title="三、链表"></a>三、链表</h2><h3 id="1-LRU缓存机制"><a href="#1-LRU缓存机制" class="headerlink" title="1. LRU缓存机制"></a>1. <a href="https://leetcode-cn.com/problems/lru-cache/">LRU缓存机制</a></h3><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">LRUCache</span> &#123;</span><br><span class="line">    <span class="type">int</span> capacity;</span><br><span class="line">    list&lt;pair&lt;<span class="type">int</span>,<span class="type">int</span>&gt;&gt; ls;</span><br><span class="line">    unordered_map&lt;<span class="type">int</span>,list&lt;pair&lt;<span class="type">int</span>,<span class="type">int</span>&gt;&gt;::iterator&gt; key_add; <span class="comment">//获取数据需要用哈希表 // 见与链表中的地址</span></span><br><span class="line">    </span><br><span class="line"><span class="keyword">public</span>:</span><br><span class="line">    <span class="built_in">LRUCache</span>(<span class="type">int</span> capacity) &#123;</span><br><span class="line">        <span class="keyword">this</span>-&gt;capacity = capacity;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="type">int</span> <span class="title">get</span><span class="params">(<span class="type">int</span> key)</span> </span>&#123;</span><br><span class="line">        <span class="comment">//如果要访问的节点不存在</span></span><br><span class="line">        <span class="keyword">if</span>(key_add.<span class="built_in">find</span>(key)==key_add.<span class="built_in">end</span>())</span><br><span class="line">            <span class="keyword">return</span> <span class="number">-1</span>;</span><br><span class="line">        <span class="comment">// 把访问的节点移动到最前边的一个；</span></span><br><span class="line">        pair&lt;<span class="type">int</span>,<span class="type">int</span>&gt; kv = *key_add[key];</span><br><span class="line">        ls.<span class="built_in">erase</span>(key_add[key]);</span><br><span class="line">        ls.<span class="built_in">push_front</span>(kv);</span><br><span class="line">        key_add[key] = ls.<span class="built_in">begin</span>();</span><br><span class="line">        <span class="keyword">return</span> kv.second;</span><br><span class="line">    &#125;</span><br><span class="line">    </span><br><span class="line">    <span class="function"><span class="type">void</span> <span class="title">put</span><span class="params">(<span class="type">int</span> key, <span class="type">int</span> value)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">if</span>(key_add.<span class="built_in">find</span>(key)==key_add.<span class="built_in">end</span>())&#123;</span><br><span class="line">            ls.<span class="built_in">push_front</span>(&#123;key,value&#125;);</span><br><span class="line">            key_add[key] = ls.<span class="built_in">begin</span>();</span><br><span class="line"></span><br><span class="line">            <span class="keyword">if</span>(ls.<span class="built_in">size</span>()&gt;capacity)&#123;</span><br><span class="line">                <span class="type">int</span> lastkey = ls.<span class="built_in">back</span>().first;</span><br><span class="line">                key_add.<span class="built_in">erase</span>(lastkey);</span><br><span class="line">                ls.<span class="built_in">pop_back</span>();</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">else</span>&#123;</span><br><span class="line">            ls.<span class="built_in">erase</span>(key_add[key]);</span><br><span class="line">            ls.<span class="built_in">push_front</span>(&#123;key,value&#125;);</span><br><span class="line">            key_add[key] = ls.<span class="built_in">begin</span>();</span><br><span class="line">        &#125;   </span><br><span class="line">    &#125;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><h3 id="2-合并n个有序数组"><a href="#2-合并n个有序数组" class="headerlink" title="2. 合并n个有序数组"></a>2. 合并n个有序数组</h3><p>假设你得到了k个排好序的数组，每个都有n个元素，希望将它们整体组合成一个有着kn个元素的有序数组。考虑以下方法：将k个数组分为k&#x2F;2对，并使用归并排序来合并每对，则你有了k&#x2F;2个排序好的数组；重复此方法，直到合并结束。那么程序运行的时间复杂度是<code>O(nklog⁡n)</code></p><h3 id="3-链表翻转"><a href="#3-链表翻转" class="headerlink" title="3. 链表翻转"></a>3. 链表翻转</h3><p>利用哨兵</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br></pre></td><td class="code"><pre><span class="line"><span class="function">Void <span class="title">reversal_list</span><span class="params">(node * head)</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">     node* pre_node = <span class="literal">nullptr</span>;</span><br><span class="line">     node* cur_node = head-&gt;next;</span><br><span class="line">     node* next_node = cur_node-&gt;next;</span><br><span class="line">     <span class="keyword">if</span>(cur_node == <span class="literal">nullptr</span>) <span class="keyword">return</span> ;</span><br><span class="line">     <span class="keyword">while</span>(<span class="number">1</span>)</span><br><span class="line">     &#123;</span><br><span class="line">         cur_node-&gt;next = forward_node;</span><br><span class="line">         pre_node = cur_node;</span><br><span class="line">         cur_node = next_node;</span><br><span class="line">         <span class="keyword">if</span>(cur_node == <span class="literal">nullptr</span>)</span><br><span class="line">             <span class="keyword">break</span>;</span><br><span class="line">        next_node = cur_node-&gt;next;</span><br><span class="line">     &#125;</span><br><span class="line">     head-&gt;next = pre_node;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><p>双指针</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Solution</span> &#123;</span><br><span class="line"><span class="keyword">public</span>:</span><br><span class="line">    <span class="function">ListNode* <span class="title">reverseList</span><span class="params">(ListNode* head)</span> </span>&#123;</span><br><span class="line">        ListNode* cur = <span class="literal">NULL</span>, *pre = head;</span><br><span class="line">        <span class="keyword">while</span> (pre != <span class="literal">NULL</span>) &#123;</span><br><span class="line">            ListNode* t = pre-&gt;next;</span><br><span class="line">            pre-&gt;next = cur;</span><br><span class="line">            cur = pre;</span><br><span class="line">            pre = t;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> cur;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>简单的递归</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Solution</span> &#123;</span><br><span class="line"><span class="keyword">public</span>:</span><br><span class="line">    <span class="function">ListNode* <span class="title">reverseList</span><span class="params">(ListNode* head)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">if</span> (head == <span class="literal">NULL</span> || head-&gt;next == <span class="literal">NULL</span>) &#123;</span><br><span class="line">            <span class="keyword">return</span> head;</span><br><span class="line">        &#125;</span><br><span class="line">        ListNode* ret = <span class="built_in">reverseList</span>(head-&gt;next);</span><br><span class="line">        head-&gt;next-&gt;next = head;</span><br><span class="line">        head-&gt;next = <span class="literal">NULL</span>;</span><br><span class="line">        <span class="keyword">return</span> ret;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><p>妖魔化的双指针</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Solution</span> &#123;</span><br><span class="line"><span class="keyword">public</span>:</span><br><span class="line">    <span class="function">ListNode* <span class="title">reverseList</span><span class="params">(ListNode* head)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">if</span> (head == <span class="literal">NULL</span>) &#123; <span class="keyword">return</span> <span class="literal">NULL</span>; &#125;</span><br><span class="line">        ListNode* cur = head;</span><br><span class="line">        <span class="keyword">while</span> (head-&gt;next != <span class="literal">NULL</span>) &#123;</span><br><span class="line">            ListNode* t = head-&gt;next-&gt;next;</span><br><span class="line">            head-&gt;next-&gt;next = cur;</span><br><span class="line">            cur = head-&gt;next;</span><br><span class="line">            head-&gt;next = t;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> cur;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><h3 id="4-表示数值的字符串"><a href="#4-表示数值的字符串" class="headerlink" title="4. 表示数值的字符串"></a>4. 表示数值的字符串</h3><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Solution</span> &#123;</span><br><span class="line"><span class="keyword">public</span>:</span><br><span class="line">    <span class="function"><span class="type">bool</span> <span class="title">isNumber</span><span class="params">(string s)</span> </span>&#123;</span><br><span class="line">        <span class="type">int</span> len = s.<span class="built_in">length</span>();       </span><br><span class="line">        <span class="type">int</span> i = <span class="number">0</span>;</span><br><span class="line">        <span class="keyword">while</span>(i&lt;len&amp;&amp;s[i]==<span class="string">&#x27; &#x27;</span>) i++; <span class="comment">//忽略空格</span></span><br><span class="line">        <span class="type">bool</span> flag = <span class="built_in">GetSignedIntBaseNumber</span>(s,len,i);</span><br><span class="line">        <span class="keyword">if</span>(i&lt;len&amp;&amp;s[i]==<span class="string">&#x27;.&#x27;</span>)&#123;</span><br><span class="line">            i++;</span><br><span class="line">            flag = <span class="built_in">GetUnSignedIntBaseNumber</span>(s,len,i)||flag;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">if</span>(i&lt;len&amp;&amp;i!=<span class="number">0</span> &amp;&amp; (s[i]==<span class="string">&#x27;e&#x27;</span>||s[i]==<span class="string">&#x27;E&#x27;</span>))&#123;</span><br><span class="line">            i++;</span><br><span class="line">            flag=flag&amp;&amp;<span class="built_in">GetSignedIntBaseNumber</span>(s,len,i);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">while</span>(i&lt;len&amp;&amp;s[i]==<span class="string">&#x27; &#x27;</span>) i++; <span class="comment">//忽略空格</span></span><br><span class="line">        <span class="keyword">return</span> i&gt;=len&amp;&amp;flag;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="type">bool</span> <span class="title">GetSignedIntBaseNumber</span><span class="params">(string &amp;str,<span class="type">int</span> len,<span class="type">int</span> &amp;i)</span></span>&#123;</span><br><span class="line">        <span class="keyword">if</span>(str[i]==<span class="string">&#x27;+&#x27;</span>||str[i]==<span class="string">&#x27;-&#x27;</span>)</span><br><span class="line">            i++;</span><br><span class="line">        <span class="keyword">return</span> <span class="built_in">GetUnSignedIntBaseNumber</span>(str,len,i);</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="function"><span class="type">bool</span> <span class="title">GetUnSignedIntBaseNumber</span><span class="params">(string &amp;str,<span class="type">int</span> len,<span class="type">int</span> &amp;i)</span></span>&#123;</span><br><span class="line">        <span class="type">int</span> tmp = i;</span><br><span class="line">        <span class="keyword">while</span>(i&lt;len&amp;&amp;<span class="built_in">isdigit</span>(str[i]))</span><br><span class="line">            i++;</span><br><span class="line">        <span class="keyword">return</span> i&gt;tmp; </span><br><span class="line">    &#125;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><h3 id="5-删除链表的重复元素"><a href="#5-删除链表的重复元素" class="headerlink" title="5. 删除链表的重复元素"></a>5. 删除链表的重复元素</h3><h4 id="82-删除排序链表中的重复元素-II"><a href="#82-删除排序链表中的重复元素-II" class="headerlink" title="82. 删除排序链表中的重复元素 II"></a><a href="https://leetcode-cn.com/problems/remove-duplicates-from-sorted-list-ii/">82. 删除排序链表中的重复元素 II</a></h4><p>给定一个排序链表，删除所有含有重复数字的节点，只保留原始链表中 <em>没有重复出现</em> 的数字。</p><p><strong>示例 1:</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">输入: 1-&gt;2-&gt;3-&gt;3-&gt;4-&gt;4-&gt;5</span><br><span class="line">输出: 1-&gt;2-&gt;5</span><br></pre></td></tr></table></figure><p><strong>示例 2:</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">输入: 1-&gt;1-&gt;1-&gt;2-&gt;3</span><br><span class="line">输出: 2-&gt;3</span><br></pre></td></tr></table></figure><p>代码</p><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Solution</span> &#123;</span><br><span class="line"><span class="keyword">public</span>:</span><br><span class="line">    <span class="function">ListNode* <span class="title">deleteDuplicates</span><span class="params">(ListNode* head)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">if</span>(!head || !head-&gt;next) <span class="keyword">return</span> head;</span><br><span class="line">        ListNode *root = <span class="keyword">new</span> <span class="built_in">ListNode</span>(INT_MAX);</span><br><span class="line">        root -&gt; next = head;</span><br><span class="line">        ListNode *pre = root;</span><br><span class="line">        <span class="keyword">while</span>(pre&amp;&amp;pre-&gt;next)&#123;</span><br><span class="line">            ListNode *pcur = pre-&gt;next;</span><br><span class="line">            <span class="keyword">if</span>(pcur-&gt;next==<span class="literal">nullptr</span>||pcur-&gt;next&amp;&amp;pcur-&gt;val!=pcur-&gt;next-&gt;val)</span><br><span class="line">                pre = pcur;</span><br><span class="line">            <span class="keyword">else</span>&#123;</span><br><span class="line">                <span class="keyword">while</span>(pcur-&gt;next&amp;&amp;pcur-&gt;next-&gt;val==pcur-&gt;val) </span><br><span class="line">                    pcur = pcur-&gt;next;</span><br><span class="line">                pre-&gt;next = pcur-&gt;next;</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> root -&gt; next;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><h2 id="四、回溯"><a href="#四、回溯" class="headerlink" title="四、回溯"></a>四、回溯</h2><h3 id="1-N皇后问题"><a href="#1-N皇后问题" class="headerlink" title="1. N皇后问题"></a>1. N皇后问题</h3><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br><span class="line">29</span><br><span class="line">30</span><br><span class="line">31</span><br><span class="line">32</span><br><span class="line">33</span><br><span class="line">34</span><br><span class="line">35</span><br><span class="line">36</span><br><span class="line">37</span><br><span class="line">38</span><br><span class="line">39</span><br><span class="line">40</span><br><span class="line">41</span><br><span class="line">42</span><br><span class="line">43</span><br><span class="line">44</span><br><span class="line">45</span><br><span class="line">46</span><br><span class="line">47</span><br><span class="line">48</span><br><span class="line">49</span><br><span class="line">50</span><br><span class="line">51</span><br><span class="line">52</span><br><span class="line">53</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Solution</span> &#123;</span><br><span class="line">unordered_set&lt;<span class="type">int</span>&gt; columns;</span><br><span class="line">unordered_set&lt;<span class="type">int</span>&gt; diagonals1;</span><br><span class="line">unordered_set&lt;<span class="type">int</span>&gt; diagonals2;</span><br><span class="line">vector&lt;vector&lt;string&gt;&gt; solutions;</span><br><span class="line"></span><br><span class="line"><span class="keyword">public</span>:</span><br><span class="line">    vector&lt;vector&lt;string&gt;&gt; <span class="built_in">solveNQueens</span>(<span class="type">int</span> n) &#123;</span><br><span class="line">        <span class="keyword">auto</span> queens = <span class="built_in">vector</span>&lt;<span class="type">int</span>&gt;(n, <span class="number">-1</span>);</span><br><span class="line">        <span class="built_in">backtrack</span>(queens, n, <span class="number">0</span>);</span><br><span class="line">        <span class="keyword">return</span> solutions;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function"><span class="type">void</span> <span class="title">backtrack</span><span class="params">(vector&lt;<span class="type">int</span>&gt; &amp;queens, <span class="type">int</span> n, <span class="type">int</span> row)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">if</span> (row == n) &#123;</span><br><span class="line">            vector&lt;string&gt; board = <span class="built_in">generateBoard</span>(queens, n);</span><br><span class="line">            solutions.<span class="built_in">push_back</span>(board);</span><br><span class="line">        &#125; <span class="keyword">else</span> &#123;</span><br><span class="line">            <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">0</span>; i &lt; n; i++) &#123;</span><br><span class="line">                <span class="keyword">if</span> (columns.<span class="built_in">find</span>(i) != columns.<span class="built_in">end</span>()) &#123;</span><br><span class="line">                    <span class="keyword">continue</span>;</span><br><span class="line">                &#125;</span><br><span class="line">                <span class="type">int</span> diagonal1 = row - i;</span><br><span class="line">                <span class="keyword">if</span> (diagonals1.<span class="built_in">find</span>(diagonal1) != diagonals1.<span class="built_in">end</span>()) &#123;</span><br><span class="line">                    <span class="keyword">continue</span>;</span><br><span class="line">                &#125;</span><br><span class="line">                <span class="type">int</span> diagonal2 = row + i;</span><br><span class="line">                <span class="keyword">if</span> (diagonals2.<span class="built_in">find</span>(diagonal2) != diagonals2.<span class="built_in">end</span>()) &#123;</span><br><span class="line">                    <span class="keyword">continue</span>;</span><br><span class="line">                &#125;</span><br><span class="line">                queens[row] = i;</span><br><span class="line">                columns.<span class="built_in">insert</span>(i);</span><br><span class="line">                diagonals1.<span class="built_in">insert</span>(diagonal1);</span><br><span class="line">                diagonals2.<span class="built_in">insert</span>(diagonal2);</span><br><span class="line">                <span class="built_in">backtrack</span>(queens, n, row + <span class="number">1</span>);</span><br><span class="line">                queens[row] = <span class="number">-1</span>;</span><br><span class="line">                columns.<span class="built_in">erase</span>(i);</span><br><span class="line">                diagonals1.<span class="built_in">erase</span>(diagonal1);</span><br><span class="line">                diagonals2.<span class="built_in">erase</span>(diagonal2);</span><br><span class="line">            &#125;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line"></span><br><span class="line">    <span class="function">vector&lt;string&gt; <span class="title">generateBoard</span><span class="params">(vector&lt;<span class="type">int</span>&gt; &amp;queens, <span class="type">int</span> n)</span> </span>&#123;</span><br><span class="line">        <span class="keyword">auto</span> board = <span class="built_in">vector</span>&lt;string&gt;();</span><br><span class="line">        <span class="keyword">for</span> (<span class="type">int</span> i = <span class="number">0</span>; i &lt; n; i++) &#123;</span><br><span class="line">            string row = <span class="built_in">string</span>(n, <span class="string">&#x27;.&#x27;</span>);</span><br><span class="line">            row[queens[i]] = <span class="string">&#x27;Q&#x27;</span>;</span><br><span class="line">            board.<span class="built_in">push_back</span>(row);</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> board;</span><br><span class="line">    &#125;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure><h2 id="五、数字游戏"><a href="#五、数字游戏" class="headerlink" title="五、数字游戏"></a>五、数字游戏</h2><h3 id="1-求平方和"><a href="#1-求平方和" class="headerlink" title="1 求平方和"></a>1 求平方和</h3><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">int</span> <span class="title">sqrt</span><span class="params">(<span class="type">int</span> x)</span> </span>&#123;</span><br><span class="line">    <span class="comment">// write code here</span></span><br><span class="line">    <span class="type">int</span> sum = <span class="number">0</span>;</span><br><span class="line">    <span class="keyword">for</span>(<span class="type">int</span> i = <span class="number">0</span>;i&lt;=x;i++)&#123;</span><br><span class="line">        <span class="type">int</span> odd = <span class="number">2</span>*i<span class="number">+1</span>;</span><br><span class="line">        sum+=odd;</span><br><span class="line">        <span class="keyword">if</span>(sum&gt;x)&#123;</span><br><span class="line">            <span class="keyword">return</span> i;</span><br><span class="line">        &#125;</span><br><span class="line">    &#125;</span><br><span class="line">    <span class="keyword">return</span> <span class="number">0</span>;        </span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h3 id="2-判断一个数是不是二的倍数"><a href="#2-判断一个数是不是二的倍数" class="headerlink" title="2 判断一个数是不是二的倍数"></a>2 判断一个数是不是二的倍数</h3><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">a % <span class="number">2</span> == <span class="number">0</span> 或者a &amp; <span class="number">0x0001</span> == <span class="number">0</span>。</span><br></pre></td></tr></table></figure><h3 id="3-不用-加减乘除做加法"><a href="#3-不用-加减乘除做加法" class="headerlink" title="3 不用+加减乘除做加法"></a>3 不用+加减乘除做加法</h3><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br></pre></td><td class="code"><pre><span class="line"><span class="function"><span class="type">int</span> <span class="title">Add</span><span class="params">(<span class="type">int</span> num1, <span class="type">int</span> num2)</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">    <span class="type">int</span> sum,carry;</span><br><span class="line">    <span class="keyword">do</span>&#123;</span><br><span class="line">        sum = num1^num2;</span><br><span class="line">        carry = (num1&amp;num2)&lt;&lt;<span class="number">1</span>;</span><br><span class="line">        num1 = sum;</span><br><span class="line">        num2 = carry;</span><br><span class="line">    &#125;<span class="keyword">while</span>(num2!=<span class="number">0</span>);</span><br><span class="line">    <span class="keyword">return</span> num1;</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure><h4 id="高楼扔鸡蛋"><a href="#高楼扔鸡蛋" class="headerlink" title="高楼扔鸡蛋"></a>高楼扔鸡蛋</h4><h5 id="887-鸡蛋掉落"><a href="#887-鸡蛋掉落" class="headerlink" title="887. 鸡蛋掉落"></a><a href="https://leetcode-cn.com/problems/super-egg-drop/">887. 鸡蛋掉落</a></h5><p>你将获得 <code>K</code> 个鸡蛋，并可以使用一栋从 <code>1</code> 到 <code>N</code> 共有 <code>N</code> 层楼的建筑。</p><p>每个蛋的功能都是一样的，如果一个蛋碎了，你就不能再把它掉下去。</p><p>你知道存在楼层 <code>F</code> ，满足 <code>0 &lt;= F &lt;= N</code> 任何从高于 <code>F</code> 的楼层落下的鸡蛋都会碎，从 <code>F</code> 楼层或比它低的楼层落下的鸡蛋都不会破。</p><p>每次<em>移动</em>，你可以取一个鸡蛋（如果你有完整的鸡蛋）并把它从任一楼层 <code>X</code> 扔下（满足 <code>1 &lt;= X &lt;= N</code>）。</p><p>你的目标是<strong>确切地</strong>知道 <code>F</code> 的值是多少。</p><p>无论 <code>F</code> 的初始值如何，你确定 <code>F</code> 的值的最小移动次数是多少？</p><h2 id="六、计算几何"><a href="#六、计算几何" class="headerlink" title="六、计算几何"></a>六、计算几何</h2><h3 id="1-给定三角形ABC和一点P-x-y-z-，判断点P是否在ABC内"><a href="#1-给定三角形ABC和一点P-x-y-z-，判断点P是否在ABC内" class="headerlink" title="1. 给定三角形ABC和一点P(x,y,z)，判断点P是否在ABC内"></a>1. 给定三角形ABC和一点P(x,y,z)，判断点P是否在ABC内</h3><p>根据面积法，如果P在三角形ABC内，那么三角形ABP的面积+三角形BCP的面积+三角形ACP的面积应该等于三角形ABC的面积。算法如下：</p><p>计算三角形面积</p><blockquote><figure class="highlight cpp"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br><span class="line">23</span><br><span class="line">24</span><br><span class="line">25</span><br><span class="line">26</span><br><span class="line">27</span><br><span class="line">28</span><br></pre></td><td class="code"><pre><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;iostream&gt;</span></span></span><br><span class="line"><span class="meta">#<span class="keyword">include</span> <span class="string">&lt;math.h&gt;</span></span></span><br><span class="line"><span class="keyword">using</span> <span class="keyword">namespace</span> std;</span><br><span class="line"><span class="meta">#<span class="keyword">define</span> ABS_FLOAT_0 0.0001</span></span><br><span class="line"><span class="keyword">struct</span> <span class="title class_">point_float</span></span><br><span class="line">&#123;</span><br><span class="line"><span class="type">float</span> x;</span><br><span class="line"><span class="type">float</span> y;</span><br><span class="line">&#125;;</span><br><span class="line"><span class="function"><span class="type">float</span> <span class="title">GetTriangleSquar</span><span class="params">(<span class="type">const</span> point_float pt0, <span class="type">const</span> point_float pt1, <span class="type">const</span> point_float pt2)</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line">point_float AB,   BC;</span><br><span class="line">AB.x = pt1.x - pt0.x;</span><br><span class="line">AB.y = pt1.y - pt0.y;</span><br><span class="line">BC.x = pt2.x - pt1.x;</span><br><span class="line">BC.y = pt2.y - pt1.y;</span><br><span class="line"><span class="keyword">return</span> <span class="built_in">fabs</span>((AB.x * BC.y - AB.y * BC.x)) / <span class="number">2.0f</span>;</span><br><span class="line">&#125;</span><br><span class="line"><span class="function"><span class="type">bool</span> <span class="title">IsInTriangle</span><span class="params">(<span class="type">const</span> point_float A, <span class="type">const</span> point_float B, <span class="type">const</span> point_float C, <span class="type">const</span> point_float D)</span></span></span><br><span class="line"><span class="function"></span>&#123;</span><br><span class="line"><span class="type">float</span> SABC, SADB, SBDC, SADC;</span><br><span class="line">SABC = <span class="built_in">GetTriangleSquar</span>(A, B, C);</span><br><span class="line">SADB = <span class="built_in">GetTriangleSquar</span>(A, D, B);</span><br><span class="line">SBDC = <span class="built_in">GetTriangleSquar</span>(B, D, C);</span><br><span class="line">SADC = <span class="built_in">GetTriangleSquar</span>(A, D, C);</span><br><span class="line"><span class="type">float</span> SumSuqar = SADB + SBDC + SADC;</span><br><span class="line"><span class="keyword">return</span> ((-ABS_FLOAT_0 &lt; (SABC - SumSuqar)) &amp;&amp; ((SABC - SumSuqar) &lt; ABS_FLOAT_0))；</span><br><span class="line">&#125;</span><br></pre></td></tr></table></figure></blockquote><h2 id="七、动态规划"><a href="#七、动态规划" class="headerlink" title="七、动态规划"></a>七、动态规划</h2><h3 id="474-一和零"><a href="#474-一和零" class="headerlink" title="474. 一和零"></a><a href="https://leetcode-cn.com/problems/ones-and-zeroes/">474. 一和零</a></h3><p>给你一个二进制字符串数组 <code>strs</code> 和两个整数 <code>m</code> 和 <code>n</code> 。</p><p>请你找出并返回 <code>strs</code> 的最大子集的大小，该子集中 <strong>最多</strong> 有 <code>m</code> 个 <code>0</code> 和 <code>n</code> 个 <code>1</code> 。</p><p>如果 <code>x</code> 的所有元素也是 <code>y</code> 的元素，集合 <code>x</code> 是集合 <code>y</code> 的 <strong>子集</strong> 。</p><p><strong>示例 1：</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">输入：strs = [&quot;10&quot;, &quot;0001&quot;, &quot;111001&quot;, &quot;1&quot;, &quot;0&quot;], m = 5, n = 3</span><br><span class="line">输出：4</span><br><span class="line">解释：最多有 5 个 0 和 3 个 1 的最大子集是 &#123;&quot;10&quot;,&quot;0001&quot;,&quot;1&quot;,&quot;0&quot;&#125; ，因此答案是 4 。</span><br><span class="line">其他满足题意但较小的子集包括 &#123;&quot;0001&quot;,&quot;1&quot;&#125; 和 &#123;&quot;10&quot;,&quot;1&quot;,&quot;0&quot;&#125; 。&#123;&quot;111001&quot;&#125; 不满足题意，因为它含 4 个 1 ，大于 n 的值 3 。</span><br></pre></td></tr></table></figure><p><strong>示例 2：</strong></p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">输入：strs = [&quot;10&quot;, &quot;0&quot;, &quot;1&quot;], m = 1, n = 1</span><br><span class="line">输出：2</span><br><span class="line">解释：最大的子集是 &#123;&quot;0&quot;, &quot;1&quot;&#125; ，所以答案是 2 。</span><br></pre></td></tr></table></figure><p> <strong>提示：</strong></p><ul><li><code>1 &lt;= strs.length &lt;= 600</code></li><li><code>1 &lt;= strs[i].length &lt;= 100</code></li><li><code>strs[i]</code> 仅由 <code>&#39;0&#39;</code> 和 <code>&#39;1&#39;</code> 组成</li><li><code>1 &lt;= m, n &lt;= 100</code></li></ul><h3 id="题解"><a href="#题解" class="headerlink" title="题解"></a>题解</h3><figure class="highlight c++"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br><span class="line">17</span><br><span class="line">18</span><br><span class="line">19</span><br><span class="line">20</span><br><span class="line">21</span><br><span class="line">22</span><br></pre></td><td class="code"><pre><span class="line"><span class="keyword">class</span> <span class="title class_">Solution</span> &#123;</span><br><span class="line"><span class="keyword">public</span>:</span><br><span class="line">    <span class="function"><span class="type">int</span> <span class="title">findMaxForm</span><span class="params">(vector&lt;string&gt;&amp; strs, <span class="type">int</span> m, <span class="type">int</span> n)</span> </span>&#123;</span><br><span class="line">        vector&lt;vector&lt;<span class="type">int</span>&gt;&gt; <span class="built_in">dp</span>(m<span class="number">+1</span>,<span class="built_in">vector</span>&lt;<span class="type">int</span>&gt;(n<span class="number">+1</span>,<span class="number">0</span>));</span><br><span class="line">        <span class="keyword">for</span>(<span class="keyword">auto</span> str:strs)</span><br><span class="line">        &#123;</span><br><span class="line">            <span class="type">int</span> zero = <span class="number">0</span>;</span><br><span class="line">            <span class="type">int</span> one = <span class="number">0</span>;</span><br><span class="line">            <span class="keyword">for</span>(<span class="type">int</span> i = <span class="number">0</span>;i&lt;str.<span class="built_in">length</span>();i++)</span><br><span class="line">            &#123;</span><br><span class="line">                zero += str[i]==<span class="string">&#x27;0&#x27;</span>?<span class="number">1</span>:<span class="number">0</span>;</span><br><span class="line">                one  += str[i]==<span class="string">&#x27;1&#x27;</span>?<span class="number">1</span>:<span class="number">0</span>;</span><br><span class="line">            &#125;</span><br><span class="line">            <span class="keyword">for</span>(<span class="type">int</span> i = m;i&gt;=zero;i--)</span><br><span class="line">                <span class="keyword">for</span>(<span class="type">int</span> j = n;j&gt;=one;j--)</span><br><span class="line">                &#123;</span><br><span class="line">                    dp[i][j] = <span class="built_in">max</span>(dp[i-zero][j-one]<span class="number">+1</span>,dp[i][j]);</span><br><span class="line">                &#125;</span><br><span class="line">        &#125;</span><br><span class="line">        <span class="keyword">return</span> dp[m][n];</span><br><span class="line">    &#125;</span><br><span class="line">&#125;;</span><br></pre></td></tr></table></figure>]]>
    </content>
    <id>https://grobenis.github.io/2020/09/11/2020-09-11-%E9%9D%A2%E8%AF%95%E7%BB%8F%E5%85%B8%E4%BB%A3%E7%A0%81%E9%A2%98/</id>
    <link href="https://grobenis.github.io/2020/09/11/2020-09-11-%E9%9D%A2%E8%AF%95%E7%BB%8F%E5%85%B8%E4%BB%A3%E7%A0%81%E9%A2%98/"/>
    <published>2020-09-11T01:44:02.000Z</published>
    <summary>
      <![CDATA[<p>本文旨在整理面试过程中经典的考题</p>
<p>本文整理面试中高频的经典代码题，覆盖字符串操作、排序与 Top K、链表、回溯、数字游戏、计算几何与动态规划等类别，每题附核心思路与参考实现，是面试前查漏补缺的实用清单。</p>
<blockquote>
<p><strong>经典题在手，面试心不慌</strong></p>
</blockquote>]]>
    </summary>
    <title>面试经典代码题</title>
    <updated>2026-08-25T13:18:00.582Z</updated>
  </entry>
  <entry>
    <author>
      <name>guoben</name>
    </author>
    <category term="科研" scheme="https://grobenis.github.io/categories/%E7%A7%91%E7%A0%94/"/>
    <category term="层次聚类" scheme="https://grobenis.github.io/tags/%E5%B1%82%E6%AC%A1%E8%81%9A%E7%B1%BB/"/>
    <content>
      <![CDATA[<p>本文主要为了了解聚类的分裂与合并。</p><p>本文介绍层次聚类的基本概念与树图、集合图等表达方式，重点讲解自底向上的合并与自顶向下的分裂两条实现路径，并讨论基于准则函数的逐步优化方法及类间距离度量的选取。</p><blockquote><p><strong>聚散之间，层次分明自现</strong></p></blockquote><span id="more"></span><h2 id="层次聚类"><a href="#层次聚类" class="headerlink" title="层次聚类"></a>层次聚类</h2><h3 id="概念"><a href="#概念" class="headerlink" title="概念"></a>概念</h3><p>层次聚类（Hierarchical Clustering）考虑把n个样本聚成c类的情况。首先把所有样本分成n类，每类正好含有一个样本。其次，我们将样本分为n-1类，接着是n-2类，这样下去知道所有样本被分为一类。我们成聚类数目$c=n-k+1$对应层次结构的第k层，因此第1层对应n个类而第n层对应一个类别。对层次结构的任意一层及其该层中的任意n个样本，如果该层中属于同一类，而且在更高的层次一直属于同一类，那么这样的序列称为层次聚类。</p><h3 id="层次聚类的表达方式"><a href="#层次聚类的表达方式" class="headerlink" title="层次聚类的表达方式"></a>层次聚类的表达方式</h3><ol><li>树图</li><li>集合图<ol><li>纯文本集合：$\{x_1,\{x_2,x_3\},\{x_4,x_5\}\}$</li><li>维恩图</li></ol></li></ol><p>其中，纯文本集合不能定量的体现相似性</p><h3 id="实现方法"><a href="#实现方法" class="headerlink" title="实现方法"></a>实现方法</h3><p>层次聚类方法主要通过两种途径进行实现：合并和分裂。</p><p>合并是自底向上，先使得所有样本归入一类，然后通过后续分裂，来增加类别的数目。</p><h4 id="一-合并"><a href="#一-合并" class="headerlink" title="一 合并"></a>一 合并</h4><p>自底向上，先使得样本各成一类，然后通过合并不同的类，来减少类别数目。</p><h5 id="基于合并的聚类算法"><a href="#基于合并的聚类算法" class="headerlink" title="基于合并的聚类算法"></a>基于合并的聚类算法</h5><p>一开始一共有c类</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">1 begin intialize: c,\hat&#123;c&#125; &lt;- n, D_i &lt;- &#123;x_i&#125;, i = 1,...,n</span><br><span class="line">2 do \hat&#123;c&#125;&lt;-\hat&#123;c&#125;-1</span><br><span class="line">3 求最接近的聚类，如D_i和D_j</span><br><span class="line">4合并D_i和D_j</span><br><span class="line">5 until c = \hat&#123;c&#125;</span><br><span class="line">6return c个聚类</span><br><span class="line">7 end</span><br></pre></td></tr></table></figure><p>对合并方法来说，从一个层次到另一个层次所需的计算比较简单。但是如果样本过多而期望的类别样本数目又很少，这种计算会被反复多次地执行。</p><p>不同类别之间的相似性的度量：<br>$$d_\min(D_i,D_j) = \min_{x\in D_i\\x'D_j}||x-x'||\\d_\max(D_i,D_j) = \max_{x\in D_i\\x'D_j}||x-x'||\\d_{avg}(D_i,D_j) = \frac{1}{n_in_j}\sum_{x\in D_i}\sum_{x\in D_j}||x-x'||\\d_{mean}(D_i,D_j) = ||m_i-m_j||$$<br>在上述的这些距离度量中，最近距离度量$d_\min(.,.)$和$d_\max(.,.)$ 代表了类与类之间距离的两个极端。就像所有利用最大值或最小值的算法，它们对噪声和孤立点非常敏感。用平静值代替它们可以改善这些问题。</p><h4 id="二-分裂"><a href="#二-分裂" class="headerlink" title="二 分裂"></a>二 分裂</h4><p>先将样本都归入一类，然后通过后续分裂，来增加类别数目。</p><h3 id="基于优化的层次聚类"><a href="#基于优化的层次聚类" class="headerlink" title="基于优化的层次聚类"></a>基于优化的层次聚类</h3><p>任选一种距离度量来表示类与类之间的距离时，没有考虑到是否能使聚类准则函数取极值。</p><p>因此可以稍作修改，获得一个极值化准则函数的“逐步优化的层次聚类”的算法。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">1 begin intialize: c,\hat&#123;c&#125; &lt;- n, D_i &lt;- &#123;x_i&#125;, i = 1,...,n</span><br><span class="line">2 do \hat&#123;c&#125;&lt;-\hat&#123;c&#125;-1</span><br><span class="line">3 寻找其合并类，将准则函数改变为最小的聚类，例如D_i和D_j</span><br><span class="line">4合并D_i和D_j</span><br><span class="line">5 until c = \hat&#123;c&#125;</span><br><span class="line">6return c个聚类</span><br><span class="line">7 end</span><br></pre></td></tr></table></figure><p>可以看出，基于$d_\max(.,.)$的聚类方法使得划分半径增长最慢，可以看作是一种逐步求精的例子。</p><p>还有一种基于误差平方和的准则函数$J_e$的方法，如果我们找到两个类，若它们的合并类造成$J_e$的增加凉最少，就要求距离<br>$$d_e(D_i,D_j) = \sqrt{\frac{n_in_j}{n_i+n_j}}||m_i-m_j||$$<br>最小，当挑选用来合并的聚类的时候，这个准则除了考虑了类与类之间的距离，还考虑了类中所含样本的个数。一般来说，基于$d_e(.,.)$的算法倾向于将孤立点或较小的类与较大的类合并。虽然最后的结果不一定能最小化$J_e$，但这个结果可以向进一步的迭代优化提供非常好的初始点。</p><h3 id="层次聚类和导出度量"><a href="#层次聚类和导出度量" class="headerlink" title="层次聚类和导出度量"></a>层次聚类和导出度量</h3><p>假定可以衡量数据集中的任意两个样本之间的相异程度，$\delta(x,x')$。而且$\delta(x,x')\geq 0 $，等号当且仅当$x=x'$时成立。那么合并聚类算法还是可以使用，只要理解两个最近的聚类就是最不相异的类。</p><p>如果定义两个聚类的相异度为：<br>$$\delta_\min(D_i,D_j) = \min_{x\in D_i\\x'\in D_j} \delta(x,x')\\\delta_\max(D_i,D_j) = \max_{x\in D_i\\x'\in D_j} \delta(x,x')$$<br>层次聚类算法就可以对给定的n个样本集导出距离函数。</p>]]>
    </content>
    <id>https://grobenis.github.io/2020/09/10/2020-09-10-%E5%B1%82%E6%AC%A1%E8%81%9A%E7%B1%BB/</id>
    <link href="https://grobenis.github.io/2020/09/10/2020-09-10-%E5%B1%82%E6%AC%A1%E8%81%9A%E7%B1%BB/"/>
    <published>2020-09-10T02:32:23.000Z</published>
    <summary>
      <![CDATA[<p>本文主要为了了解聚类的分裂与合并。</p>
<p>本文介绍层次聚类的基本概念与树图、集合图等表达方式，重点讲解自底向上的合并与自顶向下的分裂两条实现路径，并讨论基于准则函数的逐步优化方法及类间距离度量的选取。</p>
<blockquote>
<p><strong>聚散之间，层次分明自现</strong></p>
</blockquote>]]>
    </summary>
    <title>层次聚类</title>
    <updated>2026-08-25T13:18:00.542Z</updated>
  </entry>
  <entry>
    <author>
      <name>guoben</name>
    </author>
    <category term="数据库" scheme="https://grobenis.github.io/categories/%E6%95%B0%E6%8D%AE%E5%BA%93/"/>
    <category term="MySQL" scheme="https://grobenis.github.io/tags/MySQL/"/>
    <category term="数据库" scheme="https://grobenis.github.io/tags/%E6%95%B0%E6%8D%AE%E5%BA%93/"/>
    <content>
      <![CDATA[<p>本文学习自：数据库系统概论、MySQL必知必会</p><p>本文系统总结关系数据库的核心知识，覆盖数据库设计的六个阶段、三种数据模型、常用数据类型与范式，并详细讲解主键外键、索引、SQL 语法、各类查询（条件、投影、排序、分页、聚合、多表与连接查询）、增删改操作、MySQL 实用语句以及事务的 ACID 特性与四种隔离级别。</p><blockquote><p><strong>数据组织有序，查询方能高效可靠</strong></p></blockquote><span id="more"></span><p>SQL就是访问和处理关系数据库的计算机标准语言。也就是说，无论用什么编程语言（Java、Python、C++……）编写程序，只要涉及到操作关系数据库，比如，一个电商网站需要把用户和商品信息存入数据库，或者一个手机游戏需要把用户的道具、通关信息存入数据库，都必须通过SQL来完成。</p><h2 id="一-数据库概论"><a href="#一-数据库概论" class="headerlink" title="一 数据库概论"></a>一 数据库概论</h2><h3 id="一-数据库设计的六个阶段"><a href="#一-数据库设计的六个阶段" class="headerlink" title="一 数据库设计的六个阶段"></a>一 数据库设计的六个阶段</h3><ol><li><p>需求分析： 分析用户的需求，包括数据、功能和性能需求；</p></li><li><p>概念结构设计：主要采用E-R模型进行设计，包括画E-R图；</p></li><li><p>逻辑结构设计：通过将E-R图转换成表，实现从E-R模型到关系模型的转换，进行关系规范化；</p></li><li><p>数据库物理设计：主要是为所设计的数据库选择合适的存储结构和存储路径；</p></li><li><p>数据库的实施：包括编程、测试和试运行；</p></li><li><p>数据库运行和维护：系统的运行和数据库的日常维护</p></li></ol><h3 id="二-数据模型"><a href="#二-数据模型" class="headerlink" title="二 数据模型"></a>二 数据模型</h3><p>数据库按照数据结构来组织、存储和管理数据，实际上，数据库一共有三种模型：</p><ol><li><p>层次模型</p></li><li><p>网状模型</p></li><li><p>关系模型</p></li><li><p>层次模型就是以“上下级”的层次关系来组织数据的一种方式，层次模型的数据结构看起来就像一颗树：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">            ┌─────┐</span><br><span class="line">            │     │</span><br><span class="line">            └─────┘</span><br><span class="line">               │</span><br><span class="line">       ┌───────┴───────┐</span><br><span class="line">       │               │</span><br><span class="line">    ┌─────┐         ┌─────┐</span><br><span class="line">    │     │         │     │</span><br><span class="line">    └─────┘         └─────┘</span><br><span class="line">       │               │</span><br><span class="line">   ┌───┴───┐       ┌───┴───┐</span><br><span class="line">   │       │       │       │</span><br><span class="line">┌─────┐ ┌─────┐ ┌─────┐ ┌─────┐</span><br><span class="line">│     │ │     │ │     │ │     │</span><br><span class="line">└─────┘ └─────┘ └─────┘ └─────┘</span><br></pre></td></tr></table></figure></li><li><p>网状模型把每个数据节点和其他很多节点都连接起来，它的数据结构看起来就像很多城市之间的路网：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br></pre></td><td class="code"><pre><span class="line">     ┌─────┐      ┌─────┐</span><br><span class="line">   ┌─│     │──────│     │──┐</span><br><span class="line">   │ └─────┘      └─────┘  │</span><br><span class="line">   │    │            │     │</span><br><span class="line">   │    └──────┬─────┘     │</span><br><span class="line">   │           │           │</span><br><span class="line">┌─────┐     ┌─────┐     ┌─────┐</span><br><span class="line">│     │─────│     │─────│     │</span><br><span class="line">└─────┘     └─────┘     └─────┘</span><br><span class="line">   │           │           │</span><br><span class="line">   │     ┌─────┴─────┐     │</span><br><span class="line">   │     │           │     │</span><br><span class="line">   │  ┌─────┐     ┌─────┐  │</span><br><span class="line">   └──│     │─────│     │──┘</span><br><span class="line">      └─────┘     └─────┘</span><br></pre></td></tr></table></figure></li><li><p>关系模型把数据看作是一个二维表格，任何数据都可以通过行号+列号来唯一确定，它的数据模型看起来就是一个Excel表：</p><pre><code><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">┌─────┬─────┬─────┬─────┬─────┐</span><br><span class="line">│     │     │     │     │     │</span><br><span class="line">├─────┼─────┼─────┼─────┼─────┤</span><br><span class="line">│     │     │     │     │     │</span><br><span class="line">├─────┼─────┼─────┼─────┼─────┤</span><br><span class="line">│     │     │     │     │     │</span><br><span class="line">├─────┼─────┼─────┼─────┼─────┤</span><br><span class="line">│     │     │     │     │     │</span><br><span class="line">└─────┴─────┴─────┴─────┴─────┘</span><br></pre></td></tr></table></figure></code></pre></li></ol><h3 id="三-数据类型"><a href="#三-数据类型" class="headerlink" title="三 数据类型"></a>三 数据类型</h3><p>对于一个关系表，除了定义每一列的名称外，还需要定义每一列的数据类型。关系数据库支持的标准数据类型包括数值、字符串、时间等：</p><table><thead><tr><th align="left">名称</th><th align="left">类型</th><th align="left">说明</th></tr></thead><tbody><tr><td align="left">INT</td><td align="left">整型</td><td align="left">4字节整数类型，范围约+&#x2F;-21亿</td></tr><tr><td align="left">BIGINT</td><td align="left">长整型</td><td align="left">8字节整数类型，范围约+&#x2F;-922亿亿</td></tr><tr><td align="left">REAL</td><td align="left">浮点型</td><td align="left">4字节浮点数，范围约+&#x2F;-1038</td></tr><tr><td align="left">DOUBLE</td><td align="left">浮点型</td><td align="left">8字节浮点数，范围约+&#x2F;-10308</td></tr><tr><td align="left">DECIMAL(M,N)</td><td align="left">高精度小数</td><td align="left">由用户指定精度的小数，例如，DECIMAL(20,10)表示一共20位，其中小数10位，通常用于财务计算</td></tr><tr><td align="left">CHAR(N)</td><td align="left">定长字符串</td><td align="left">存储指定长度的字符串，例如，CHAR(100)总是存储100个字符的字符串</td></tr><tr><td align="left">VARCHAR(N)</td><td align="left">变长字符串</td><td align="left">存储可变长度的字符串，例如，VARCHAR(100)可以存储0~100个字符的字符串</td></tr><tr><td align="left">BOOLEAN</td><td align="left">布尔类型</td><td align="left">存储True或者False</td></tr><tr><td align="left">DATE</td><td align="left">日期类型</td><td align="left">存储日期，例如，2018-06-22</td></tr><tr><td align="left">TIME</td><td align="left">时间类型</td><td align="left">存储时间，例如，12:20:59</td></tr><tr><td align="left">DATETIME</td><td align="left">日期和时间类型</td><td align="left">存储日期+时间，例如，2018-06-22 12:20:59</td></tr></tbody></table><p>上面的表中列举了最常用的数据类型。很多数据类型还有别名，例如，<code>REAL</code>又可以写成<code>FLOAT(24)</code>。还有一些不常用的数据类型，例如，<code>TINYINT</code>（范围在0~255）。各数据库厂商还会支持特定的数据类型，例如<code>JSON</code>。</p><p>选择数据类型的时候，要根据业务规则选择合适的类型。通常来说，<code>BIGINT</code>能满足整数存储的需求，<code>VARCHAR(N)</code>能满足字符串存储的需求，这两种类型是使用最广泛的。</p><h3 id="四-关系数据库"><a href="#四-关系数据库" class="headerlink" title="四 关系数据库"></a>四 关系数据库</h3><h4 id="范式"><a href="#范式" class="headerlink" title="范式"></a>范式</h4><p>目前<a href="https://baike.baidu.com/item/%E5%85%B3%E7%B3%BB%E6%95%B0%E6%8D%AE%E5%BA%93">关系数据库</a>有六种范式：第一范式（1NF）、第二范式（2NF）、第三范式（3NF）、巴斯-科德范式（BCNF）、<a href="https://baike.baidu.com/item/%E7%AC%AC%E5%9B%9B%E8%8C%83%E5%BC%8F">第四范式</a>(4NF）和<a href="https://baike.baidu.com/item/%E7%AC%AC%E4%BA%94%E8%8C%83%E5%BC%8F">第五范式</a>（5NF，又称完美范式）。满足最低要求的范式是第一范式（1NF）。在第一范式的基础上进一步满足更多规范要求的称为第二范式（2NF），其余范式以次类推。一般来说，数据库只需满足第三范式(3NF）就行了。</p><h4 id="主流关系数据库"><a href="#主流关系数据库" class="headerlink" title="主流关系数据库"></a>主流关系数据库</h4><p>目前，主流的关系数据库主要分为以下几类：</p><ol><li>商用数据库，例如：<a href="https://www.oracle.com/">Oracle</a>，<a href="https://www.microsoft.com/sql-server/">SQL Server</a>，<a href="https://www.ibm.com/db2/">DB2</a>等；</li><li>开源数据库，例如：<a href="https://www.mysql.com/">MySQL</a>，<a href="https://www.postgresql.org/">PostgreSQL</a>等；</li><li>桌面数据库，以微软<a href="https://products.office.com/access">Access</a>为代表，适合桌面应用程序使用；</li><li>嵌入式数据库，以<a href="https://sqlite.org/">Sqlite</a>为代表，适合手机应用和桌面程序。</li></ol><h3 id="五-SQL（Structured-Query-Language）"><a href="#五-SQL（Structured-Query-Language）" class="headerlink" title="五 SQL（Structured Query Language）"></a>五 SQL（Structured Query Language）</h3><p>什么是SQL？SQL是结构化查询语言的缩写，用来访问和操作数据库系统。SQL语句既可以查询数据库中的数据，也可以添加、更新和删除数据库中的数据，还可以对数据库进行管理和维护操作。不同的数据库，都支持SQL，这样，我们通过学习SQL这一种语言，就可以操作各种不同的数据库。</p><p>虽然SQL已经被ANSI组织定义为标准，不幸地是，各个不同的数据库对标准的SQL支持不太一致。并且，大部分数据库都在标准的SQL上做了扩展。也就是说，如果只使用标准SQL，理论上所有数据库都可以支持，但如果使用某个特定数据库的扩展SQL，换一个数据库就不能执行了。例如，Oracle把自己扩展的SQL称为<code>PL/SQL</code>，Microsoft把自己扩展的SQL称为<code>T-SQL</code>。</p><p>现实情况是，如果我们只使用标准SQL的核心功能，那么所有数据库通常都可以执行。不常用的SQL功能，不同的数据库支持的程度都不一样。而各个数据库支持的各自扩展的功能，通常我们把它们称之为“方言”。</p><p>总的来说，SQL语言定义了这么几种操作数据库的能力：</p><p><strong>DDL：Data Definition Language</strong></p><p>DDL允许用户定义数据，也就是创建表、删除表、修改表结构这些操作。通常，DDL由数据库管理员执行。</p><p><strong>DML：Data Manipulation Language</strong></p><p>DML为用户提供添加、删除、更新数据的能力，这些是应用程序对数据库的日常操作。</p><p><strong>DQL：Data Query Language</strong></p><p>DQL允许用户查询数据，这也是通常最频繁的数据库日常操作。</p><h4 id="SQL语法特点"><a href="#SQL语法特点" class="headerlink" title="SQL语法特点"></a>SQL语法特点</h4><p>SQL语言关键字不区分大小写！！！但是，针对不同的数据库，对于表名和列名，有的数据库区分大小写，有的数据库不区分大小写。同一个数据库，有的在Linux上区分大小写，有的在Windows上不区分大小写。</p><p>所以，本教程约定：SQL关键字总是大写，以示突出，表名和列名均使用小写。</p><h2 id="二-关系模型"><a href="#二-关系模型" class="headerlink" title="二 关系模型"></a>二 关系模型</h2><p>关系数据库是建立在关系模型上的。而关系模型本质上就是若干个存储数据的二维表，可以把它们看作很多Excel表。</p><ul><li>表的每一行称为记录（Record），记录是一个逻辑意义上的数据。</li><li>表的每一列称为字段（Column），同一个表的每一行记录都拥有相同的若干字段。</li><li>字段定义了数据类型（整型、浮点型、字符串、日期等），以及是否允许为<code>NULL</code>。注意<code>NULL</code>表示字段数据不存在。一个整型字段如果为<code>NULL</code>不表示它的值为<code>0</code>，同样的，一个字符串型字段为<code>NULL</code>也不表示它的值为空串<code>&#39;&#39;</code>。</li></ul><ul><li>通常情况下，字段应该避免允许为NULL。不允许为NULL可以简化查询条件，加快查询速度，也利于应用程序读取数据后无需判断是否为NULL。</li></ul><p>和Excel表有所不同的是，关系数据库的表和表之间需要建立“一对多”，“多对一”和“一对一”的关系，这样才能够按照应用程序的逻辑来组织和存储数据。</p><p>在关系数据库中，关系是通过主键和外键来维护的</p><h3 id="主键"><a href="#主键" class="headerlink" title="主键"></a>主键</h3><p>在关系数据库中，一张表中的每一行数据被称为一条记录。一条记录就是由多个字段组成的。例如，<code>students</code>表的两行记录：</p><table><thead><tr><th align="left">id</th><th align="left">class_id</th><th align="left">name</th><th align="left">gender</th><th align="left">score</th></tr></thead><tbody><tr><td align="left">1</td><td align="left">1</td><td align="left">小明</td><td align="left">M</td><td align="left">90</td></tr><tr><td align="left">2</td><td align="left">1</td><td align="left">小红</td><td align="left">F</td><td align="left">95</td></tr></tbody></table><p>每一条记录都包含若干定义好的字段。同一个表的所有记录都有相同的字段定义。</p><p>对于关系表，有个很重要的约束，就是任意两条记录不能重复。不能重复不是指两条记录不完全相同，而是指能够通过某个字段唯一区分出不同的记录，这个字段被称为<em>主键</em>。</p><p>对主键的要求，最关键的一点是：记录一旦插入到表中，主键最好不要再修改，因为主键是用来唯一定位记录的，修改了主键，会造成一系列的影响。由于主键的作用十分重要，如何选取主键会对业务开发产生重要影响。如果我们以学生的身份证号作为主键，似乎能唯一定位记录。然而，身份证号也是一种业务场景，如果身份证号升位了，或者需要变更，作为主键，不得不修改的时候，就会对业务产生严重影响。</p><p>所以，选取主键的一个基本原则是：不使用任何业务相关的字段作为主键。</p><p>作为主键最好是完全业务无关的字段，我们一般把这个字段命名为<code>id</code>。常见的可作为<code>id</code>字段的类型有：</p><ol><li>自增整数类型：数据库会在插入数据时自动为每一条记录分配一个自增整数，这样我们就完全不用担心主键重复，也不用自己预先生成主键；</li><li>全局唯一GUID类型：使用一种全局唯一的字符串作为主键，类似<code>8f55d96b-8acc-4636-8cb8-76bf8abc2f57</code>。GUID算法通过网卡MAC地址、时间戳和随机数保证任意计算机在任意时间生成的字符串都是不同的，大部分编程语言都内置了GUID算法，可以自己预算出主键。</li></ol><p>对于大部分应用来说，通常自增类型的主键就能满足需求。我们在<code>students</code>表中定义的主键也是<code>BIGINT NOT NULL AUTO_INCREMENT</code>类型。</p><h4 id="联合主键"><a href="#联合主键" class="headerlink" title="联合主键"></a>联合主键</h4><p>关系数据库实际上还允许通过多个字段唯一标识记录，即两个或更多的字段都设置为主键，这种主键被称为联合主键。</p><p>对于联合主键，允许一列有重复，只要不是所有主键列都重复即可：</p><table><thead><tr><th align="left">id_num</th><th align="left">id_type</th><th align="left">other columns…</th></tr></thead><tbody><tr><td align="left">1</td><td align="left">A</td><td align="left">…</td></tr><tr><td align="left">2</td><td align="left">A</td><td align="left">…</td></tr><tr><td align="left">2</td><td align="left">B</td><td align="left">…</td></tr></tbody></table><p>如果我们把上述表的<code>id_num</code>和<code>id_type</code>这两列作为联合主键，那么上面的3条记录都是允许的，因为没有两列主键组合起来是相同的。</p><p>没有必要的情况下，我们尽量不使用联合主键，因为它给关系表带来了复杂度的上升。</p><h3 id="外键"><a href="#外键" class="headerlink" title="外键"></a>外键</h3><p>当我们用主键唯一标识记录时，我们就可以在<code>students</code>表中确定任意一个学生的记录：</p><table><thead><tr><th align="left">id</th><th align="left">name</th><th align="left">other columns…</th></tr></thead><tbody><tr><td align="left">1</td><td align="left">小明</td><td align="left">…</td></tr><tr><td align="left">2</td><td align="left">小红</td><td align="left">…</td></tr></tbody></table><p>我们还可以在<code>classes</code>表中确定任意一个班级记录：</p><table><thead><tr><th align="left">id</th><th align="left">name</th><th align="left">other columns…</th></tr></thead><tbody><tr><td align="left">1</td><td align="left">一班</td><td align="left">…</td></tr><tr><td align="left">2</td><td align="left">二班</td><td align="left">…</td></tr></tbody></table><p>但是我们如何确定<code>students</code>表的一条记录，例如，<code>id=1</code>的小明，属于哪个班级呢？</p><p>由于一个班级可以有多个学生，在关系模型中，这两个表的关系可以称为“一对多”，即一个<code>classes</code>的记录可以对应多个<code>students</code>表的记录。</p><p>为了表达这种一对多的关系，我们需要在<code>students</code>表中加入一列<code>class_id</code>，让它的值与<code>classes</code>表的某条记录相对应：</p><table><thead><tr><th align="left">id</th><th align="left">class_id</th><th align="left">name</th><th align="left">other columns…</th></tr></thead><tbody><tr><td align="left">1</td><td align="left">1</td><td align="left">小明</td><td align="left">…</td></tr><tr><td align="left">2</td><td align="left">1</td><td align="left">小红</td><td align="left">…</td></tr><tr><td align="left">5</td><td align="left">2</td><td align="left">小白</td><td align="left">…</td></tr></tbody></table><p>这样，我们就可以根据<code>class_id</code>这个列直接定位出一个<code>students</code>表的记录应该对应到<code>classes</code>的哪条记录。</p><p>例如：</p><ul><li>小明的<code>class_id</code>是<code>1</code>，因此，对应的<code>classes</code>表的记录是<code>id=1</code>的一班；</li><li>小红的<code>class_id</code>是<code>1</code>，因此，对应的<code>classes</code>表的记录是<code>id=1</code>的一班；</li><li>小白的<code>class_id</code>是<code>2</code>，因此，对应的<code>classes</code>表的记录是<code>id=2</code>的二班。</li></ul><p>在<code>students</code>表中，通过<code>class_id</code>的字段，可以把数据与另一张表关联起来，这种列称为<code>外键</code>。</p><p>外键并不是通过列名实现的，而是通过定义<strong>外键约束</strong>实现的：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">ALTER TABLE students</span><br><span class="line">ADD CONSTRAINT fk_class_id</span><br><span class="line">FOREIGN KEY (class_id)</span><br><span class="line">REFERENCES classes (id);</span><br></pre></td></tr></table></figure><p>其中，外键约束的名称<code>fk_class_id</code>可以任意，<code>FOREIGN KEY (class_id)</code>指定了<code>class_id</code>作为外键，<code>REFERENCES classes (id)</code>指定了这个外键将关联到<code>classes</code>表的<code>id</code>列（即<code>classes</code>表的主键）。</p><p>通过定义外键约束，关系数据库可以保证无法插入无效的数据。即如果<code>classes</code>表不存在<code>id=99</code>的记录，<code>students</code>表就无法插入<code>class_id=99</code>的记录。</p><p>由于外键约束会降低数据库的性能，大部分互联网应用程序为了追求速度，并不设置外键约束，而是仅靠应用程序自身来保证逻辑的正确性。这种情况下，<code>class_id</code>仅仅是一个普通的列，只是它起到了外键的作用而已。</p><p>要删除一个外键约束，也是通过<code>ALTER TABLE</code>实现的：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">ALTER TABLE students</span><br><span class="line">DROP FOREIGN KEY fk_class_id;</span><br></pre></td></tr></table></figure><p>注意：删除外键约束并没有删除外键这一列。删除列是通过<code>DROP COLUMN ...</code>实现的。</p><h4 id="多对多"><a href="#多对多" class="headerlink" title="多对多"></a>多对多</h4><p>通过一个表的外键关联到另一个表，我们可以定义出一对多关系。有些时候，还需要定义“多对多”关系。例如，一个老师可以对应多个班级，一个班级也可以对应多个老师，因此，班级表和老师表存在多对多关系。</p><p>多对多关系实际上是通过两个一对多关系实现的，即通过一个中间表，关联两个一对多关系，就形成了多对多关系：</p><h4 id="一对一"><a href="#一对一" class="headerlink" title="一对一"></a>一对一</h4><p>一对一关系是指，一个表的记录对应到另一个表的唯一一个记录。</p><h3 id="索引"><a href="#索引" class="headerlink" title="索引"></a>索引</h3><p>索引是关系数据库中对某一列或多个列的值进行预排序的数据结构。通过使用索引，可以让数据库系统不必扫描整个表，而是直接定位到符合条件的记录，这样就大大加快了查询速度。</p><h4 id="唯一索引"><a href="#唯一索引" class="headerlink" title="唯一索引"></a>唯一索引</h4><p>在设计关系数据表的时候，看上去唯一的列，例如身份证号、邮箱地址等，因为他们具有业务含义，因此不宜作为主键。</p><p>无论是否创建索引，对于用户和应用程序来说，使用关系数据库不会有任何区别。这里的意思是说，当我们在数据库中查询时，如果有相应的索引可用，数据库系统就会自动使用索引来提高查询效率，如果没有索引，查询也能正常执行，只是速度会变慢。因此，索引可以在使用数据库的过程中逐步优化。</p><h3 id="小结"><a href="#小结" class="headerlink" title="小结"></a>小结</h3><p>通过对数据库表创建索引，可以提高查询速度。</p><p>通过创建唯一索引，可以保证某一列的值具有唯一性。</p><p>数据库索引对于用户和应用程序来说都是透明的。</p><h2 id="三-查询数据"><a href="#三-查询数据" class="headerlink" title="三 查询数据"></a>三 查询数据</h2><h3 id="1-基本查询"><a href="#1-基本查询" class="headerlink" title="1 基本查询"></a>1 基本查询</h3><p>使用SELECT查询的基本语句<code>SELECT * FROM &lt;表名&gt;</code>可以查询一个表的所有行和所有列的数据。</p><p>SELECT查询的结果是一个二维表。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">SELECT * FROM &lt;表名&gt;</span><br></pre></td></tr></table></figure><p>知识点：SELECT语句的执行结果是<code>元组</code></p><p>元组（Tuple）：笛卡尔积中每一个元素<code>（d1，d2，…，dn）</code>叫作一个n元组（n-tuple）或简称元组。元组是关系数据库中的基本概念，关系是一张表，表中的每行就是一个元组，每列就是一个属性。</p><p>SELECT根据关系代数是进行投影操作，投影是将一个关系表中的属性投影出来</p><h3 id="2-条件查询"><a href="#2-条件查询" class="headerlink" title="2 条件查询"></a>2 条件查询</h3><p>使用<code>SELECT * FROM &lt;表名&gt;</code>可以查询到一张表的所有记录。但是，很多时候，我们并不希望获得所有记录，而是根据条件选择性地获取指定条件的记录，例如，查询分数在80分以上的学生记录。在一张表有数百万记录的情况下，获取所有记录不仅费时，还费内存和网络带宽。</p><p>SELECT语句可以通过<code>WHERE</code>条件来设定查询条件，查询结果是满足查询条件的记录。例如，要指定条件“分数在80分或以上的学生”，写成<code>WHERE</code>条件就是<code>SELECT * FROM students WHERE score &gt;= 80</code>。</p><p>其中，<code>WHERE</code>关键字后面的<code>score &gt;= 80</code>就是条件。<code>score</code>是列名，该列存储了学生的成绩，因此，<code>score &gt;= 80</code>就筛选出了指定条件的记录：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">SELECT * FROM students WHERE score &gt;= 80;</span><br></pre></td></tr></table></figure><h4 id="常用的条件表达式"><a href="#常用的条件表达式" class="headerlink" title="常用的条件表达式"></a>常用的条件表达式</h4><table><thead><tr><th align="left">条件</th><th align="left">表达式举例1</th><th align="left">表达式举例2</th><th align="left">说明</th></tr></thead><tbody><tr><td align="left">使用&#x3D;判断相等</td><td align="left">score &#x3D; 80</td><td align="left">name &#x3D; ‘abc’</td><td align="left">字符串需要用单引号括起来</td></tr><tr><td align="left">使用&gt;判断大于</td><td align="left">score &gt; 80</td><td align="left">name &gt; ‘abc’</td><td align="left">字符串比较根据ASCII码，中文字符比较根据数据库设置</td></tr><tr><td align="left">使用&gt;&#x3D;判断大于或相等</td><td align="left">score &gt;&#x3D; 80</td><td align="left">name &gt;&#x3D; ‘abc’</td><td align="left"></td></tr><tr><td align="left">使用&lt;判断小于</td><td align="left">score &lt; 80</td><td align="left">name &lt;&#x3D; ‘abc’</td><td align="left"></td></tr><tr><td align="left">使用&lt;&#x3D;判断小于或相等</td><td align="left">score &lt;&#x3D; 80</td><td align="left">name &lt;&#x3D; ‘abc’</td><td align="left"></td></tr><tr><td align="left">使用&lt;&gt;判断不相等</td><td align="left">score &lt;&gt; 80</td><td align="left">name &lt;&gt; ‘abc’</td><td align="left"></td></tr><tr><td align="left">使用LIKE判断相似</td><td align="left">name LIKE ‘ab%’</td><td align="left">name LIKE ‘%bc%’</td><td align="left">%表示任意字符，例如’ab%’将匹配’ab’，’abc’，’abcd’</td></tr></tbody></table><p>条件查询的语法就是：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">SELECT * FROM &lt;表名&gt; WHERE &lt;条件表达式&gt;</span><br></pre></td></tr></table></figure><p>条件表达式可以用<code>&lt;条件1&gt; AND &lt;条件2&gt;</code>表达满足条件1并且满足条件2。例如，符合条件“分数在80分或以上”，并且还符合条件“男生”，把这两个条件写出来：</p><ul><li>条件1：根据score列的数据判断：<code>score &gt;= 80</code>；</li><li>条件2：根据gender列的数据判断：<code>gender = &#39;M&#39;</code>，注意<code>gender</code>列存储的是字符串，需要用单引号括起来。</li></ul><p>就可以写出<code>WHERE</code>条件：<code>score &gt;= 80 AND gender = &#39;M&#39;</code>：</p><p>很显然<code>OR</code>条件要比<code>AND</code>条件宽松，返回的符合条件的记录也更多。</p><p>第三种条件是<code>NOT &lt;条件&gt;</code>，表示“不符合该条件”的记录。例如，写一个“不是2班的学生”这个条件，可以先写出“是2班的学生”：<code>class_id = 2</code>，再加上<code>NOT</code>：<code>NOT class_id = 2</code>：</p><p>上述<code>NOT</code>条件<code>NOT class_id = 2</code>其实等价于<code>class_id &lt;&gt; 2</code>，因此，<code>NOT</code>查询不是很常用。</p><p>要组合三个或者更多的条件，就需要用小括号<code>()</code>表示如何进行条件运算。例如，编写一个复杂的条件：分数在80以下或者90以上，并且是男生：</p><p>如果不加括号，条件运算按照<code>NOT</code>、<code>AND</code>、<code>OR</code>的优先级进行，即<code>NOT</code>优先级最高，其次是<code>AND</code>，最后是<code>OR</code>。加上括号可以改变优先级。</p><h3 id="3-投影查询"><a href="#3-投影查询" class="headerlink" title="3 投影查询"></a>3 投影查询</h3><ol><li><p>使用<code>SELECT * FROM &lt;表名&gt; WHERE &lt;条件&gt;</code>可以选出表中的若干条记录。我们注意到返回的二维表结构和原表是相同的，即结果集的所有列与原表的所有列都一一对应。</p></li><li><p>使用<code>SELECT 列1, 列2, 列3 FROM ...</code>时，还可以给每一列起个别名，这样，结果集的列名就可以与原表的列名不同。它的语法是<code>SELECT 列1 别名1, 列2 别名2, 列3 别名3 FROM ...</code>。</p></li><li><p>投影查询同样可以接<code>WHERE</code>条件，实现复杂的查询;</p></li></ol><h3 id="4-排序"><a href="#4-排序" class="headerlink" title="4 排序"></a>4 排序</h3><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">SELECT id, name, gender, score FROM students ORDER BY score DESC, gender;</span><br></pre></td></tr></table></figure><p>SELECT查询时，细心的读者可能注意到，查询结果集通常是按照<code>id</code>排序的，也就是根据主键排序。这也是大部分数据库的做法。如果我们要根据其他条件排序怎么办？可以加上<code>ORDER BY</code>子句。</p><ol><li>如果要反过来，按照成绩从高到底排序，我们可以加上<code>DESC</code>表示“倒序”：</li><li>如果<code>score</code>列有相同的数据，要进一步排序，可以继续添加列名。例如，使用<code>ORDER BY score DESC, gender</code>表示先按<code>score</code>列倒序，如果有相同分数的，再按<code>gender</code>列排序：</li><li>默认的排序规则是<code>ASC</code>：“升序”，即从小到大。<code>ASC</code>可以省略，即<code>ORDER BY score ASC</code>和<code>ORDER BY score</code>效果一样。</li></ol><p>使用<code>ORDER BY</code>可以对结果集进行排序；</p><p>可以对多列进行升序、倒序排序。</p><h3 id="5-分页查询"><a href="#5-分页查询" class="headerlink" title="5 分页查询"></a>5 分页查询</h3><h4 id="分页"><a href="#分页" class="headerlink" title="分页"></a>分页</h4><p>使用SELECT查询时，如果结果集数据量很大，比如几万行数据，放在一个页面显示的话数据量太大，不如分页显示，每次显示100条。</p><p>要实现分页功能，实际上就是从结果集中显示第1<del>100条记录作为第1页，显示第101</del>200条记录作为第2页，以此类推。</p><p>因此，分页实际上就是从结果集中“截取”出第M~N条记录。这个查询可以通过<code>LIMIT &lt;M&gt; OFFSET &lt;N&gt;</code>子句实现。我们先把所有学生按照成绩从高到低进行排序：</p><p>例子：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">SELECT id, name, gender, score FROM students ORDER BY score DESC;</span><br></pre></td></tr></table></figure><p>现在，我们把结果集分页，每页3条记录。要获取第1页的记录，可以使用<code>LIMIT 3 OFFSET 0</code>：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">SELECT id,name,gender,score</span><br><span class="line">FROM students</span><br><span class="line">ORDER BY score DESC</span><br><span class="line">LIMIT 3 OFFSET 0;</span><br></pre></td></tr></table></figure><p>上述查询<code>LIMIT 3 OFFSET 0</code>表示，对结果集从0号记录开始，最多取3条。注意SQL记录集的索引从0开始。</p><p>如果要查询第2页，那么我们只需要“跳过”头3条记录，也就是对结果集从3号记录开始查询，把<code>OFFSET</code>设定为3：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">SELECT id,name,gender,score</span><br><span class="line">FROM students</span><br><span class="line">ORDER BY score DESC</span><br><span class="line">LIMIT 3 OFFSET 3;</span><br></pre></td></tr></table></figure><p>注意：<code>OFFSET</code>超过了查询的最大数量并不会报错，而是得到一个空的结果集。</p><h4 id="注意"><a href="#注意" class="headerlink" title="注意"></a>注意</h4><p><code>OFFSET</code>是可选的，如果只写<code>LIMIT 15</code>，那么相当于<code>LIMIT 15 OFFSET 0</code>。</p><p>在MySQL中，<code>LIMIT 15 OFFSET 30</code>还可以简写成<code>LIMIT 30, 15</code>。</p><p>使用<code>LIMIT &lt;M&gt; OFFSET &lt;N&gt;</code>分页时，随着<code>N</code>越来越大，查询效率也会越来越低。</p><h4 id="小结-1"><a href="#小结-1" class="headerlink" title="小结"></a>小结</h4><p>使用<code>LIMIT &lt;M&gt; OFFSET &lt;N&gt;</code>可以对结果集进行分页，每次查询返回结果集的一部分；</p><p>分页查询需要先确定每页的数量和当前页数，然后确定<code>LIMIT</code>和<code>OFFSET</code>的值。</p><h3 id="6-聚合查询"><a href="#6-聚合查询" class="headerlink" title="6 聚合查询"></a>6 聚合查询</h3><p>对于统计总数、平均数这类计算，SQL提供了专门的聚合函数，使用聚合函数进行查询，就是聚合查询，它可以快速获得结果。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">SELECT COUNT(*) FROM students; #查询students表一共有多少条记录 COUNT(*)表示查询所有列的行数，要注意聚合的计算结果虽然是一个数字，但查询的结果仍然是一个二维表，只是这个二维表只有一行一列，并且列名是COUNT(*)。</span><br><span class="line">SELECT COUNT(*) num FROM students; #通常，使用聚合查询时，我们应该给列名设置一个别名，便于处理结果：</span><br><span class="line">SELECT COUNT(*) boys FROM students WHERE gender = &#x27;M&#x27;; #聚合查询同样可以使用WHERE条件，因此我们可以方便地统计出有多少男生、多少女生、多少80分以上的学生等：</span><br></pre></td></tr></table></figure><p>除了<code>COUNT()</code>函数外，SQL还提供了如下聚合函数：</p><table><thead><tr><th align="left">函数</th><th align="left">说明</th></tr></thead><tbody><tr><td align="left">SUM</td><td align="left">计算某一列的合计值，该列必须为数值类型</td></tr><tr><td align="left">AVG</td><td align="left">计算某一列的平均值，该列必须为数值类型</td></tr><tr><td align="left">MAX</td><td align="left">计算某一列的最大值</td></tr><tr><td align="left">MIN</td><td align="left">计算某一列的最小值</td></tr></tbody></table><p>注意，<code>MAX()</code>和<code>MIN()</code>函数并不限于数值类型。如果是字符类型，<code>MAX()</code>和<code>MIN()</code>会返回排序最后和排序最前的字符。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">SELECT AVG(score) average FROM students WHERE gender = &#x27;M&#x27;;要统计男生的平均成绩，我们用下面的聚合查询：</span><br><span class="line">SELECT AVG(score) average FROM students WHERE gender = &#x27;X&#x27;; #如果聚合查询的WHERE条件没有匹配到任何行，COUNT()会返回0，而SUM()、AVG()、MAX()和MIN()会返回NULL： 输出NULL</span><br></pre></td></tr></table></figure><h4 id="分组"><a href="#分组" class="headerlink" title="分组"></a>分组</h4><p>可以用<code>SELECT COUNT(*) num FROM students WHERE class_id = 1;</code>。如果要继续统计二班、三班的学生数量，难道必须不断修改<code>WHERE</code>条件来执行<code>SELECT</code>语句吗？对于聚合查询，SQL还提供了“分组聚合”的功能。我们观察下面的聚合查询：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">SELECT class_id, COUNT(*) num FROM students GROUP BY class_id;#对于聚合查询，SQL还提供了“分组聚合”的功能。</span><br><span class="line">SELECT name, class_id, COUNT(*) num FROM students GROUP BY class_id;#，GROUP BY子句指定了按class_id分组，因此，执行该SELECT语句时，会把class_id相同的列先分组，再分别计算，因此，得到了3行结果。</span><br></pre></td></tr></table></figure><p><code>GROUP BY</code>子句指定了按<code>class_id</code>分组，因此，执行该<code>SELECT</code>语句时，会把<code>class_id</code>相同的列先分组，再分别计算，因此，得到了3行结果。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">SELECT class_id, COUNT(*) num FROM students GROUP BY class_id;#执行这个查询，COUNT()的结果不再是一个，而是3个，这是因为，GROUP BY子句指定了按class_id分组，因此，执行该SELECT语句时，会把class_id相同的列先分组，再分别计算，因此，得到了3行结果。</span><br><span class="line">SELECT name, class_id, COUNT(*) num FROM students GROUP BY class_id;#结果集就可以一目了然地看出各个班级的学生人数。</span><br><span class="line">SELECT class_id, COUNT(*) num FROM students GROUP BY class_id;#我们再试试把name放入结果集：</span><br></pre></td></tr></table></figure><h3 id="7-多表查询"><a href="#7-多表查询" class="headerlink" title="7 多表查询"></a>7 多表查询</h3><p>SELECT查询不但可以从一张表查询数据，还可以从多张表同时查询数据。查询多张表的语法是：<code>SELECT * FROM &lt;表1&gt; &lt;表2&gt;</code>。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">SELECT * FROM students, classes; # 从students表和classes表的“乘积”，即查询数据，可以这么写；</span><br></pre></td></tr></table></figure><p>这种一次查询两个表的数据，查询的结果也是一个二维表，它是<code>students</code>表和<code>classes</code>表的“乘积”，即<code>students</code>表的每一行与<code>classes</code>表的每一行都两两拼在一起返回。结果集的列数是<code>students</code>表和<code>classes</code>表的列数之和，行数是<code>students</code>表和<code>classes</code>表的行数之积。</p><p>这种多表查询又称笛卡尔查询，使用笛卡尔查询时要非常小心，由于结果集是目标表的行数乘积，对两个各自有100行记录的表进行笛卡尔查询将返回1万条记录，对两个各自有1万行记录的表进行笛卡尔查询将返回1亿条记录。</p><p>你可能还注意到了，上述查询的结果集有两列<code>id</code>和两列<code>name</code>，两列<code>id</code>是因为其中一列是<code>students</code>表的<code>id</code>，而另一列是<code>classes</code>表的<code>id</code>，但是在结果集中，不好区分。两列<code>name</code>同理</p><p>要解决这个问题，我们仍然可以利用投影查询的“设置列的别名”来给两个表各自的<code>id</code>和<code>name</code>列起别名：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">SELECT</span><br><span class="line">    students.id sid,</span><br><span class="line">    students.name,</span><br><span class="line">    students.gender,</span><br><span class="line">    students.score,</span><br><span class="line">    classes.id cid,</span><br><span class="line">    classes.name cname</span><br><span class="line">FROM students, classes;</span><br></pre></td></tr></table></figure><p>注意，多表查询时，要使用<code>表名.列名</code>这样的方式来引用列和设置别名，这样就避免了结果集的列名重复问题。但是，用<code>表名.列名</code>这种方式列举两个表的所有列实在是很麻烦，所以SQL还允许给表设置一个别名，让我们在投影查询中引用起来稍微简洁一点：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br></pre></td><td class="code"><pre><span class="line">SELECT</span><br><span class="line">    s.id sid,</span><br><span class="line">    s.name,</span><br><span class="line">    s.gender,</span><br><span class="line">    s.score,</span><br><span class="line">    c.id cid,</span><br><span class="line">    c.name cname</span><br><span class="line">FROM students s, classes c;</span><br></pre></td></tr></table></figure><p>注意到<code>FROM</code>子句给表设置别名的语法是<code>FROM &lt;表名1&gt; &lt;别名1&gt;, &lt;表名2&gt; &lt;别名2&gt;</code>。这样我们用别名<code>s</code>和<code>c</code>分别表示<code>students</code>表和<code>classes</code>表。</p><p>多表查询也是可以添加<code>WHERE</code>条件的，我们来试试：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">SELECT </span><br><span class="line">    s.id sid,</span><br><span class="line">    s.name,</span><br><span class="line">    s.gender,</span><br><span class="line">    s.score,</span><br><span class="line">    c.id cid,</span><br><span class="line">    c.name cname</span><br><span class="line">FROM students s, classes c</span><br><span class="line">WHERE s.gender = &#x27;M&#x27; AND c.id = 1;</span><br></pre></td></tr></table></figure><p>这个查询的结果集每行记录都满足条件<code>s.gender = &#39;M&#39;</code>和<code>c.id = 1</code>。添加<code>WHERE</code>条件后结果集的数量大大减少了。</p><p>小结</p><ol><li>使用多表查询可以获取M x N行记录；</li><li>多表查询的结果集可能非常巨大，要小心使用。</li></ol><h3 id="8-连接查询"><a href="#8-连接查询" class="headerlink" title="8 连接查询"></a>8 连接查询</h3><p>连接查询是另一种类型的多表查询。连接查询对多个表进行JOIN运算，简单地说，就是先确定一个主表作为结果集，然后，把其他表的行有选择性地“连接”在主表结果集上。</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">SELECT s.id, s.name, s.class_id, s.gender, s.score FROM students s;#我们想要选出students表的所有学生信息，可以用一条简单的SELECT语句完成：</span><br><span class="line"></span><br></pre></td></tr></table></figure><p>假设我们希望结果集同时包含所在班级的名称，上面的结果集只有<code>class_id</code>列，缺少对应班级的<code>name</code>列。</p><p>现在问题来了，存放班级名称的<code>name</code>列存储在<code>classes</code>表中，只有根据<code>students</code>表的<code>class_id</code>，找到<code>classes</code>表对应的行，再取出<code>name</code>列，就可以获得班级名称。</p><p>这时，连接查询就派上了用场。我们先使用最常用的一种内连接——INNER JOIN来实现：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">SELECT s.id, s.name, s.class_id, c.name class_name, s.gender, s.score</span><br><span class="line">FROM students s</span><br><span class="line">INNER JOIN classes c</span><br><span class="line">ON s.class_id = c.id;</span><br></pre></td></tr></table></figure><p>注意INNER JOIN查询的写法是：</p><ol><li>先确定主表，仍然使用<code>FROM &lt;表1&gt;</code>的语法；</li><li>再确定需要连接的表，使用<code>INNER JOIN &lt;表2&gt;</code>的语法；</li><li>然后确定连接条件，使用<code>ON &lt;条件...&gt;</code>，这里的条件是<code>s.class_id = c.id</code>，表示<code>students</code>表的<code>class_id</code>列与<code>classes</code>表的<code>id</code>列相同的行需要连接；</li><li>可选：加上<code>WHERE</code>子句、<code>ORDER BY</code>等子句。</li></ol><p>使用别名不是必须的，但可以更好地简化查询语句。</p><p>那什么是内连接（INNER JOIN）呢？先别着急，有内连接（INNER JOIN）就有外连接（OUTER JOIN）。我们把内连接查询改成外连接查询，看看效果：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">SELECT s.id, s.name, s.class_id, c.name class_name, s.gender, s.score</span><br><span class="line">FROM students s</span><br><span class="line">RIGHT OUTER JOIN classes c</span><br><span class="line">ON s.class_id = c.id;</span><br></pre></td></tr></table></figure><h2 id="四-修改数据"><a href="#四-修改数据" class="headerlink" title="四 修改数据"></a>四 修改数据</h2><h3 id="1-增-INSERT"><a href="#1-增-INSERT" class="headerlink" title="1. 增 INSERT"></a>1. 增 INSERT</h3><p>用途：向数据库插入一条记录</p><p><code>INSERT</code>语句的基本语法是：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">INSERT INTO &lt;表名&gt; (字段1, 字段2, ...) VALUES (值1, 值2, ...);</span><br></pre></td></tr></table></figure><p>例如，我们向<code>students</code>表插入一条新记录，先列举出需要插入的字段名称，然后在<code>VALUES</code>子句中依次写出对应字段的值：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">INSERT INTO students (class_id, name, gender, score) VALUES (2, &#x27;大牛&#x27;, &#x27;M&#x27;, 80);</span><br><span class="line">-- 查询并观察结果:</span><br><span class="line">SELECT * FROM students;</span><br></pre></td></tr></table></figure><p>意到我们并没有列出<code>id</code>字段，也没有列出<code>id</code>字段对应的值，这是因为<code>id</code>字段是一个自增主键，它的值可以由数据库自己推算出来。此外，如果一个字段有默认值，那么在<code>INSERT</code>语句中也可以不出现。</p><p>要注意，字段顺序不必和数据库表的字段顺序一致，但值的顺序必须和字段顺序一致。也就是说，可以写<code>INSERT INTO students (score, gender, name, class_id) ...</code>，但是对应的<code>VALUES</code>就得变成<code>(80, &#39;M&#39;, &#39;大牛&#39;, 2)</code>。</p><p>还可以一次性添加多条记录，只需要在<code>VALUES</code>子句中指定多个记录值，每个记录是由<code>(...)</code>包含的一组值：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">INSERT INTO students (class_id, name, gender, score) VALUES</span><br><span class="line">  (1, &#x27;大宝&#x27;, &#x27;M&#x27;, 87),</span><br><span class="line">  (2, &#x27;二宝&#x27;, &#x27;M&#x27;, 81);</span><br><span class="line"></span><br><span class="line">SELECT * FROM students;</span><br></pre></td></tr></table></figure><ul><li>使用<code>INSERT</code>，我们就可以一次向一个表中插入一条或多条记录。</li></ul><p>​</p><h3 id="2-改-UPDATE"><a href="#2-改-UPDATE" class="headerlink" title="2. 改 UPDATE"></a>2. 改 UPDATE</h3><p>用途：使用UPDATE更新数据库中的记录</p><p>UPDATE语句的基本语法是：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">UPDATE &lt;表名&gt; SET 字段1=值1, 字段2=值2, ... WHERE ...;</span><br></pre></td></tr></table></figure><p>我们想更新<code>students</code>表<code>id=1</code>的记录的<code>name</code>和<code>score</code>这两个字段，先写出<code>UPDATE students SET name=&#39;大牛&#39;, score=66</code>，然后在<code>WHERE</code>子句中写出需要更新的行的筛选条件<code>id=1</code>：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">UPDATE students SET name=&#x27;大牛&#x27;, score=66 WHERE id=3;</span><br></pre></td></tr></table></figure><p>注意到<code>UPDATE</code>语句的<code>WHERE</code>条件和<code>SELECT</code>语句的<code>WHERE</code>条件其实是一样的，因此完全可以一次更新多条记录：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">UPDATE students SET name=&#x27;小牛&#x27;, score=77 WHERE id&gt;=5 AND id&lt;=7;</span><br></pre></td></tr></table></figure><p>在<code>UPDATE</code>语句中，更新字段时可以使用表达式。例如，把所有80分以下的同学的成绩加10分：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">UPDATE students SET score=score+10 WHERE score&lt;80;</span><br></pre></td></tr></table></figure><p>其中，<code>SET score=score+10</code>就是给当前行的<code>score</code>字段的值加上了10。</p><p>如果<code>WHERE</code>条件没有匹配到任何记录，<code>UPDATE</code>语句不会报错，也不会有任何记录被更新。例如：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">UPDATE students SET score=100 WHERE id=999;</span><br></pre></td></tr></table></figure><p>最后，要特别小心的是，<code>UPDATE</code>语句可以没有<code>WHERE</code>条件，例如：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">UPDATE students SET score=60;</span><br></pre></td></tr></table></figure><p>这时，整个表的所有记录都会被更新。所以，在执行<code>UPDATE</code>语句时要非常小心，最好先用<code>SELECT</code>语句来测试<code>WHERE</code>条件是否筛选出了期望的记录集，然后再用<code>UPDATE</code>更新。</p><h4 id="MySQL"><a href="#MySQL" class="headerlink" title="MySQL"></a>MySQL</h4><p>在使用MySQL这类真正的关系数据库时，<code>UPDATE</code>语句会返回更新的行数以及<code>WHERE</code>条件匹配的行数。</p><p>例如，更新<code>id=1</code>的记录时：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">mysql&gt; UPDATE students SET name=&#x27;大宝&#x27; WHERE id=1;</span><br><span class="line">Query OK, 1 row affected (0.00 sec)</span><br><span class="line">Rows matched: 1  Changed: 1  Warnings: 0</span><br></pre></td></tr></table></figure><p>MySQL会返回<code>1</code>，可以从打印的结果<code>Rows matched: 1 Changed: 1</code>看到。</p><p>当更新<code>id=999</code>的记录时：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">mysql&gt; UPDATE students SET name=&#x27;大宝&#x27; WHERE id=999;</span><br><span class="line">Query OK, 0 rows affected (0.00 sec)</span><br><span class="line">Rows matched: 0  Changed: 0  Warnings: 0</span><br></pre></td></tr></table></figure><p>MySQL会返回<code>0</code>，可以从打印的结果<code>Rows matched: 0 Changed: 0</code>看到。</p><h3 id="3-删-DELETE"><a href="#3-删-DELETE" class="headerlink" title="3. 删 DELETE"></a>3. 删 DELETE</h3><p>用途：删除数据库表中的记录</p><p><code>DELETE</code>语句的基本语法是：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">DELETE FROM &lt;表名&gt; WHERE ...;</span><br></pre></td></tr></table></figure><p>例如，我们想删除<code>students</code>表中<code>id=1</code>的记录，就需要这么写：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">DELETE FROM students WHERE id=1;</span><br></pre></td></tr></table></figure><p>注意到<code>DELETE</code>语句的<code>WHERE</code>条件也是用来筛选需要删除的行，因此和<code>UPDATE</code>类似，<code>DELETE</code>语句也可以一次删除多条记录：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">DELETE FROM students WHERE id&gt;=5 AND id&lt;=7;</span><br></pre></td></tr></table></figure><p>如果<code>WHERE</code>条件没有匹配到任何记录，<code>DELETE</code>语句不会报错，也不会有任何记录被删除。</p><p>最后，要特别小心的是，和<code>UPDATE</code>类似，不带<code>WHERE</code>条件的<code>DELETE</code>语句会删除整个表的数据：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">DELETE FROM students;</span><br></pre></td></tr></table></figure><p>这时，整个表的所有记录都会被删除。所以，在执行<code>DELETE</code>语句时也要非常小心，最好先用<code>SELECT</code>语句来测试<code>WHERE</code>条件是否筛选出了期望的记录集，然后再用<code>DELETE</code>删除。</p><p>在使用MySQL这类真正的关系数据库时，<code>DELETE</code>语句也会返回删除的行数以及<code>WHERE</code>条件匹配的行数。</p><p>例如，分别执行删除<code>id=1</code>和<code>id=999</code>的记录：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">mysql&gt; DELETE FROM students WHERE id=1;</span><br><span class="line">Query OK, 1 row affected (0.01 sec)</span><br><span class="line"></span><br><span class="line">mysql&gt; DELETE FROM students WHERE id=999;</span><br><span class="line">Query OK, 0 rows affected (0.01 sec)</span><br></pre></td></tr></table></figure><h2 id="五-MySQL"><a href="#五-MySQL" class="headerlink" title="五 MySQL"></a>五 MySQL</h2><p>进入命令</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">mysql -u root -p</span><br></pre></td></tr></table></figure><p>安装完MySQL后，除了MySQL Server，即真正的MySQL服务器外，还附赠一个MySQL Client程序。MySQL Client是一个命令行客户端，可以通过MySQL Client登录MySQL，然后，输入SQL语句并执行。</p><p>打开命令提示符，输入命令<code>mysql -u root -p</code>，提示输入口令。填入MySQL的root口令，如果正确，就连上了MySQL Server，同时提示符变为<code>mysql&gt;</code>：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br><span class="line">10</span><br><span class="line">11</span><br><span class="line">12</span><br><span class="line">13</span><br><span class="line">14</span><br><span class="line">15</span><br><span class="line">16</span><br></pre></td><td class="code"><pre><span class="line">┌────────────────────────────────────────────────────────┐</span><br><span class="line">│Command Prompt                                    - □ x │</span><br><span class="line">├────────────────────────────────────────────────────────┤</span><br><span class="line">│Microsoft Windows [Version 10.0.0]                      │</span><br><span class="line">│(c) 2015 Microsoft Corporation. All rights reserved.    │</span><br><span class="line">│                                                        │</span><br><span class="line">│C:\&gt; mysql -u root -p                                   │</span><br><span class="line">│Enter password: ******                                  │</span><br><span class="line">│                                                        │</span><br><span class="line">│Server version: 5.7                                     │</span><br><span class="line">│Copyright (c) 2000, 2018, ...                           │</span><br><span class="line">│Type &#x27;help;&#x27; or &#x27;\h&#x27; for help.                          │</span><br><span class="line">│                                                        │</span><br><span class="line">│mysql&gt;                                                  │</span><br><span class="line">│                                                        │</span><br><span class="line">└────────────────────────────────────────────────────────┘</span><br></pre></td></tr></table></figure><p>输入<code>exit</code>断开与MySQL Server的连接并返回到命令提示符。</p><p> MySQL Client的可执行程序是mysql，MySQL Server的可执行程序是mysqld。</p><p>MySQL Client和MySQL Server的关系如下：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">┌──────────────┐  SQL   ┌──────────────┐</span><br><span class="line">│ MySQL Client │───────&gt;│ MySQL Server │</span><br><span class="line">└──────────────┘  TCP   └──────────────┘</span><br></pre></td></tr></table></figure><p>在MySQL Client中输入的SQL语句通过TCP连接发送到MySQL Server。默认端口号是3306，即如果发送到本机MySQL Server，地址就是<code>127.0.0.1:3306</code>。</p><p>也可以只安装MySQL Client，然后连接到远程MySQL Server。假设远程MySQL Server的IP地址是<code>10.0.1.99</code>，那么就使用<code>-h</code>指定IP或域名：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">mysql -h 10.0.1.99 -u root -p</span><br></pre></td></tr></table></figure><h3 id="1-管理MYSQL"><a href="#1-管理MYSQL" class="headerlink" title="1 管理MYSQL"></a>1 管理MYSQL</h3><p>1: 创建表使用<code>CREATE TABLE</code>语句，而删除表使用<code>DROP TABLE</code>语句：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">mysql&gt; DROP TABLE students;</span><br><span class="line">Query OK, 0 rows affected (0.01 sec)</span><br></pre></td></tr></table></figure><p>2: 修改表就比较复杂。如果要给<code>students</code>表新增一列<code>birth</code>，使用：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ALTER TABLE students ADD COLUMN birth VARCHAR(10) NOT NULL;</span><br></pre></td></tr></table></figure><p>要修改<code>birth</code>列，例如把列名改为<code>birthday</code>，类型改为<code>VARCHAR(20)</code>：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ALTER TABLE students CHANGE COLUMN birth birthday VARCHAR(20) NOT NULL;</span><br></pre></td></tr></table></figure><p>3: 要删除列，使用：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">ALTER TABLE students DROP COLUMN birthday;</span><br></pre></td></tr></table></figure><h3 id="2-退出MySQL"><a href="#2-退出MySQL" class="headerlink" title="2 退出MySQL"></a>2 退出MySQL</h3><p>使用<code>EXIT</code>命令退出MySQL：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">mysql&gt; EXIT</span><br><span class="line">Bye</span><br></pre></td></tr></table></figure><p>注意<code>EXIT</code>仅仅断开了客户端和服务器的连接，MySQL服务器仍然继续运行。</p><h3 id="3-实用的SQL语句"><a href="#3-实用的SQL语句" class="headerlink" title="3 实用的SQL语句"></a>3 实用的SQL语句</h3><h4 id="A-插入或替换-REPLACE"><a href="#A-插入或替换-REPLACE" class="headerlink" title="A. 插入或替换 REPLACE"></a>A. 插入或替换 REPLACE</h4><p>如果我们希望插入一条新记录（INSERT），但如果记录已经存在，就先删除原记录，再插入新记录。此时，可以使用<code>REPLACE</code>语句，这样就不必先查询，再决定是否先删除再插入：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">REPLACE INTO students (id, class_id, name, gender, score) VALUES (1, 1, &#x27;小明&#x27;, &#x27;F&#x27;, 99);</span><br></pre></td></tr></table></figure><p>若<code>id=1</code>的记录不存在，<code>REPLACE</code>语句将插入新记录，否则，当前<code>id=1</code>的记录将被删除，然后再插入新记录。</p><h4 id="B-插入或更新-INSERT-INTO-ON-DUPLICATE-KEY-UPDATE"><a href="#B-插入或更新-INSERT-INTO-ON-DUPLICATE-KEY-UPDATE" class="headerlink" title="B. 插入或更新 INSERT INTO ON DUPLICATE KEY UPDATE"></a>B. 插入或更新 INSERT INTO ON DUPLICATE KEY UPDATE</h4><p>如果我们希望插入一条新记录（INSERT），但如果记录已经存在，就更新该记录，此时，可以使用</p><p><code>INSERT INTO ... ON DUPLICATE KEY UPDATE ...</code>语句：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">INSERT INTO students (id, class_id, name, gender, score) VALUES (1, 1, &#x27;小明&#x27;, &#x27;F&#x27;, 99) ON DUPLICATE KEY UPDATE name=&#x27;小明&#x27;, gender=&#x27;F&#x27;, score=99;</span><br></pre></td></tr></table></figure><p>若<code>id=1</code>的记录不存在，<code>INSERT</code>语句将插入新记录，否则，当前<code>id=1</code>的记录将被更新，更新的字段由<code>UPDATE</code>指定。</p><h4 id="C-插入或忽略-INSERT-IGNORE-INTO"><a href="#C-插入或忽略-INSERT-IGNORE-INTO" class="headerlink" title="C. 插入或忽略 INSERT IGNORE INTO"></a>C. 插入或忽略 INSERT IGNORE INTO</h4><p>如果我们希望插入一条新记录（INSERT），但如果记录已经存在，就啥事也不干直接忽略，此时，可以使用<code>INSERT IGNORE INTO ...</code>语句：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">INSERT IGNORE INTO students (id, class_id, name, gender, score) VALUES (1, 1, &#x27;小明&#x27;, &#x27;F&#x27;, 99);</span><br></pre></td></tr></table></figure><p>若<code>id=1</code>的记录不存在，<code>INSERT</code>语句将插入新记录，否则，不执行任何操作。</p><h4 id="D-快照-CREATE-TABLE-…-SELECT"><a href="#D-快照-CREATE-TABLE-…-SELECT" class="headerlink" title="D. 快照 CREATE TABLE … SELECT"></a>D. 快照 CREATE TABLE … SELECT</h4><p>如果想要对一个表进行快照，即复制一份当前表的数据到一个新表，可以结合<code>CREATE TABLE</code>和<code>SELECT</code>：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">-- 对class_id=1的记录进行快照，并存储为新表students_of_class1:</span><br><span class="line">CREATE TABLE students_of_class1 SELECT * FROM students WHERE class_id=1;</span><br></pre></td></tr></table></figure><p>新创建的表结构和<code>SELECT</code>使用的表结构完全一致。</p><h4 id="E-写入查询结果集"><a href="#E-写入查询结果集" class="headerlink" title="E. 写入查询结果集"></a>E. 写入查询结果集</h4><p>如果查询结果集需要写入到表中，可以结合<code>INSERT</code>和<code>SELECT</code>，将<code>SELECT</code>语句的结果集直接插入到指定表中。</p><p>例如，创建一个统计成绩的表<code>statistics</code>，记录各班的平均成绩：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br></pre></td><td class="code"><pre><span class="line">CREATE TABLE statistics (</span><br><span class="line">    id BIGINT NOT NULL AUTO_INCREMENT,</span><br><span class="line">    class_id BIGINT NOT NULL,</span><br><span class="line">    average DOUBLE NOT NULL,</span><br><span class="line">    PRIMARY KEY (id)</span><br><span class="line">);</span><br></pre></td></tr></table></figure><p>然后，我们就可以用一条语句写入各班的平均成绩：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">INSERT INTO statistics (class_id, average) SELECT class_id, AVG(score) FROM students GROUP BY class_id;</span><br></pre></td></tr></table></figure><p>确保<code>INSERT</code>语句的列和<code>SELECT</code>语句的列能一一对应，就可以在<code>statistics</code>表中直接保存查询的结果：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br><span class="line">8</span><br><span class="line">9</span><br></pre></td><td class="code"><pre><span class="line">&gt; SELECT * FROM statistics;</span><br><span class="line">+----+----------+--------------+</span><br><span class="line">| id | class_id | average      |</span><br><span class="line">+----+----------+--------------+</span><br><span class="line">|  1 |        1 |         86.5 |</span><br><span class="line">|  2 |        2 | 73.666666666 |</span><br><span class="line">|  3 |        3 | 88.333333333 |</span><br><span class="line">+----+----------+--------------+</span><br><span class="line">3 rows in set (0.00 sec)</span><br></pre></td></tr></table></figure><h4 id="F-强制使用指定索引"><a href="#F-强制使用指定索引" class="headerlink" title="F. 强制使用指定索引"></a>F. 强制使用指定索引</h4><p>在查询的时候，数据库系统会自动分析查询语句，并选择一个最合适的索引。但是很多时候，数据库系统的查询优化器并不一定总是能使用最优索引。如果我们知道如何选择索引，可以使用<code>FORCE INDEX</code>强制查询使用指定的索引。例如：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br></pre></td><td class="code"><pre><span class="line">&gt; SELECT * FROM students FORCE INDEX (idx_class_id) WHERE class_id = 1 ORDER BY id DESC;</span><br></pre></td></tr></table></figure><p>指定索引的前提是索引<code>idx_class_id</code>必须存在。</p><h4 id="G-导出"><a href="#G-导出" class="headerlink" title="G 导出"></a>G 导出</h4><p>1.导出整个 <a href="http://www.2cto.com/database/"><strong>数据库</strong></a></p><figure class="highlight cmd"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">mysqldump -u 用户名 -p 数据库名 &gt; 导出的文件名</span><br><span class="line"></span><br><span class="line">mysqldump -u wcnc -p smgp_apps_wcnc &gt; wcnc.sql</span><br></pre></td></tr></table></figure><p>2.导出一个表</p><figure class="highlight cmd"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br></pre></td><td class="code"><pre><span class="line">mysqldump -u 用户名 -p 数据库名 表名&gt; 导出的文件名</span><br><span class="line"></span><br><span class="line">mysqldump -u wcnc -p smgp_apps_wcnc users&gt; wcnc_users.sql</span><br></pre></td></tr></table></figure><p>3.导出一个数据库结构</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br></pre></td><td class="code"><pre><span class="line">mysqldump -u wcnc -p -d --add-drop-table smgp_apps_wcnc &gt;d:\wcnc_db.sql</span><br><span class="line">-d 没有数据 --add-drop-table 在每个create语句之前增加一个drop table</span><br></pre></td></tr></table></figure><h3 id="小结-2"><a href="#小结-2" class="headerlink" title="小结"></a>小结</h3><p>命令行程序<code>mysql</code>实际上是MySQL客户端，真正的MySQL服务器程序是<code>mysqld</code>，在后台运行。</p><h2 id="六-事务"><a href="#六-事务" class="headerlink" title="六 事务"></a>六 事务</h2><p>在执行SQL语句的时候，某些业务要求，一系列操作必须全部执行，而不能仅执行一部分。例如，一个转账操作：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br></pre></td><td class="code"><pre><span class="line">-- 从id=1的账户给id=2的账户转账100元</span><br><span class="line">-- 第一步：将id=1的A账户余额减去100</span><br><span class="line">UPDATE accounts SET balance = balance - 100 WHERE id = 1;</span><br><span class="line">-- 第二步：将id=2的B账户余额加上100</span><br><span class="line">UPDATE accounts SET balance = balance + 100 WHERE id = 2;</span><br></pre></td></tr></table></figure><p>这两条SQL语句必须全部执行，或者，由于某些原因，如果第一条语句成功，第二条语句失败，就必须全部撤销。</p><p>这种把多条语句作为一个整体进行操作的功能，被称为数据库<em>事务</em>。数据库事务可以确保该事务范围内的所有操作都可以全部成功或者全部失败。如果事务失败，那么效果就和没有执行这些SQL一样，不会对数据库数据有任何改动。</p><p>数据库事务具有ACID这4个特性：</p><ul><li>A： Atomic，原子性，将所有SQL作为原子工作单元执行，要么全部执行，要么全部不执行；</li><li>C： Consistent，一致性，事务完成后，所有数据的状态都是一致的，即A账户只要减去了100，B账户则必定加上了100；</li><li>I： Isolation，隔离性，如果有多个事务并发执行，每个事务作出的修改必须与其他事务隔离；</li><li>D： Duration，持久性，即事务完成后，对数据库数据的修改被持久化存储。</li></ul><p>对于单条SQL语句，数据库系统自动将其作为一个事务执行，这种事务被称为<em>隐式事务</em>。</p><p>要手动把多条SQL语句作为一个事务执行，使用<code>BEGIN</code>开启一个事务，使用<code>COMMIT</code>提交一个事务，这种事务被称为<em>显式事务</em>，例如，把上述的转账操作作为一个显式事务：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">BEGIN;</span><br><span class="line">UPDATE accounts SET balance = balance - 100 WHERE id = 1;</span><br><span class="line">UPDATE accounts SET balance = balance + 100 WHERE id = 2;</span><br><span class="line">COMMIT;</span><br></pre></td></tr></table></figure><p>很显然多条SQL语句要想作为一个事务执行，就必须使用显式事务。</p><p><code>COMMIT</code>是指提交事务，即试图把事务内的所有SQL所做的修改永久保存。如果<code>COMMIT</code>语句执行失败了，整个事务也会失败。</p><p>有些时候，我们希望主动让事务失败，这时，可以用<code>ROLLBACK</code>回滚事务，整个事务会失败：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br></pre></td><td class="code"><pre><span class="line">BEGIN;</span><br><span class="line">UPDATE accounts SET balance = balance - 100 WHERE id = 1;</span><br><span class="line">UPDATE accounts SET balance = balance + 100 WHERE id = 2;</span><br><span class="line">ROLLBACK;</span><br></pre></td></tr></table></figure><p>数据库事务是由数据库系统保证的，我们只需要根据业务逻辑使用它就可以。</p><p><strong>隔离级别</strong></p><p>对于两个并发执行的事务，如果涉及到操作同一条记录的时候，可能会发生问题。因为并发操作会带来数据的不一致性，包括脏读、不可重复读、幻读等。数据库系统提供了隔离级别来让我们有针对性地选择事务的隔离级别，避免数据不一致的问题。</p><p>SQL标准定义了4种隔离级别，分别对应可能出现的数据不一致的情况：</p><table><thead><tr><th align="left">Isolation Level</th><th align="left">脏读（Dirty Read）</th><th align="left">不可重复读（Non Repeatable Read）</th><th align="left">幻读（Phantom Read）</th></tr></thead><tbody><tr><td align="left">Read Uncommitted</td><td align="left">Yes</td><td align="left">Yes</td><td align="left">Yes</td></tr><tr><td align="left">Read Committed</td><td align="left">-</td><td align="left">Yes</td><td align="left">Yes</td></tr><tr><td align="left">Repeatable Read</td><td align="left">-</td><td align="left">-</td><td align="left">Yes</td></tr><tr><td align="left">Serializable</td><td align="left">-</td><td align="left">-</td><td align="left">-</td></tr></tbody></table><p>我们会依次介绍4种隔离级别的数据一致性问题。</p><h3 id="1-Read-Uncommitted"><a href="#1-Read-Uncommitted" class="headerlink" title="1 Read Uncommitted"></a>1 Read Uncommitted</h3><p>Read Uncommitted是隔离级别最低的一种事务级别。在这种隔离级别下，一个事务会读到另一个事务更新后但未提交的数据，如果另一个事务回滚，那么当前事务读到的数据就是脏数据，这就是脏读（Dirty Read）。</p><p>我们来看一个例子。</p><p>首先，我们准备好<code>students</code>表的数据，该表仅一行记录：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">mysql&gt; select * from students;</span><br><span class="line">+----+-------+</span><br><span class="line">| id | name  |</span><br><span class="line">+----+-------+</span><br><span class="line">|  1 | Alice |</span><br><span class="line">+----+-------+</span><br><span class="line">1 row in set (0.00 sec)</span><br></pre></td></tr></table></figure><p>然后，分别开启两个MySQL客户端连接，按顺序依次执行事务A和事务B：</p><table><thead><tr><th align="left">时刻</th><th align="left">事务A</th><th align="left">事务B</th></tr></thead><tbody><tr><td align="left">1</td><td align="left">SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;</td><td align="left">SET TRANSACTION ISOLATION LEVEL READ UNCOMMITTED;</td></tr><tr><td align="left">2</td><td align="left">BEGIN;</td><td align="left">BEGIN;</td></tr><tr><td align="left">3</td><td align="left">UPDATE students SET name &#x3D; ‘Bob’ WHERE id &#x3D; 1;</td><td align="left"></td></tr><tr><td align="left">4</td><td align="left"></td><td align="left">SELECT * FROM students WHERE id &#x3D; 1;</td></tr><tr><td align="left">5</td><td align="left">ROLLBACK;</td><td align="left"></td></tr><tr><td align="left">6</td><td align="left"></td><td align="left">SELECT * FROM students WHERE id &#x3D; 1;</td></tr><tr><td align="left">7</td><td align="left"></td><td align="left">COMMIT;</td></tr></tbody></table><p>当事务A执行完第3步时，它更新了<code>id=1</code>的记录，但并未提交，而事务B在第4步读取到的数据就是未提交的数据。</p><p>随后，事务A在第5步进行了回滚，事务B再次读取<code>id=1</code>的记录，发现和上一次读取到的数据不一致，这就是脏读。</p><p>可见，在Read Uncommitted隔离级别下，一个事务可能读取到另一个事务更新但未提交的数据，这个数据有可能是脏数据。</p><h3 id="2-Read-Committed"><a href="#2-Read-Committed" class="headerlink" title="2 Read Committed"></a>2 Read Committed</h3><p>在Read Committed隔离级别下，一个事务可能会遇到不可重复读（Non Repeatable Read）的问题。</p><p>不可重复读是指，在一个事务内，多次读同一数据，在这个事务还没有结束时，如果另一个事务恰好修改了这个数据，那么，在第一个事务中，两次读取的数据就可能不一致。</p><p>我们仍然先准备好<code>students</code>表的数据：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">mysql&gt; select * from students;</span><br><span class="line">+----+-------+</span><br><span class="line">| id | name  |</span><br><span class="line">+----+-------+</span><br><span class="line">|  1 | Alice |</span><br><span class="line">+----+-------+</span><br><span class="line">1 row in set (0.00 sec)</span><br></pre></td></tr></table></figure><p>然后，分别开启两个MySQL客户端连接，按顺序依次执行事务A和事务B：</p><table><thead><tr><th align="left">时刻</th><th align="left">事务A</th><th align="left">事务B</th></tr></thead><tbody><tr><td align="left">1</td><td align="left">SET TRANSACTION ISOLATION LEVEL READ COMMITTED;</td><td align="left">SET TRANSACTION ISOLATION LEVEL READ COMMITTED;</td></tr><tr><td align="left">2</td><td align="left">BEGIN;</td><td align="left">BEGIN;</td></tr><tr><td align="left">3</td><td align="left"></td><td align="left">SELECT * FROM students WHERE id &#x3D; 1;</td></tr><tr><td align="left">4</td><td align="left">UPDATE students SET name &#x3D; ‘Bob’ WHERE id &#x3D; 1;</td><td align="left"></td></tr><tr><td align="left">5</td><td align="left">COMMIT;</td><td align="left"></td></tr><tr><td align="left">6</td><td align="left"></td><td align="left">SELECT * FROM students WHERE id &#x3D; 1;</td></tr><tr><td align="left">7</td><td align="left"></td><td align="left">COMMIT;</td></tr></tbody></table><p>当事务B第一次执行第3步的查询时，得到的结果是<code>Alice</code>，随后，由于事务A在第4步更新了这条记录并提交，所以，事务B在第6步再次执行同样的查询时，得到的结果就变成了<code>Bob</code>，因此，在Read Committed隔离级别下，事务不可重复读同一条记录，因为很可能读到的结果不一致。</p><h3 id="3-Repeatable-Read"><a href="#3-Repeatable-Read" class="headerlink" title="3 Repeatable Read"></a>3 Repeatable Read</h3><p>在Repeatable Read隔离级别下，一个事务可能会遇到幻读（Phantom Read）的问题。</p><p>幻读是指，在一个事务中，第一次查询某条记录，发现没有，但是，当试图更新这条不存在的记录时，竟然能成功，并且，再次读取同一条记录，它就神奇地出现了。</p><p>我们仍然先准备好<code>students</code>表的数据：</p><figure class="highlight plaintext"><table><tr><td class="gutter"><pre><span class="line">1</span><br><span class="line">2</span><br><span class="line">3</span><br><span class="line">4</span><br><span class="line">5</span><br><span class="line">6</span><br><span class="line">7</span><br></pre></td><td class="code"><pre><span class="line">mysql&gt; select * from students;</span><br><span class="line">+----+-------+</span><br><span class="line">| id | name  |</span><br><span class="line">+----+-------+</span><br><span class="line">|  1 | Alice |</span><br><span class="line">+----+-------+</span><br><span class="line">1 row in set (0.00 sec)</span><br></pre></td></tr></table></figure><p>然后，分别开启两个MySQL客户端连接，按顺序依次执行事务A和事务B：</p><table><thead><tr><th align="left">时刻</th><th align="left">事务A</th><th align="left">事务B</th></tr></thead><tbody><tr><td align="left">1</td><td align="left">SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;</td><td align="left">SET TRANSACTION ISOLATION LEVEL REPEATABLE READ;</td></tr><tr><td align="left">2</td><td align="left">BEGIN;</td><td align="left">BEGIN;</td></tr><tr><td align="left">3</td><td align="left"></td><td align="left">SELECT * FROM students WHERE id &#x3D; 99;</td></tr><tr><td align="left">4</td><td align="left">INSERT INTO students (id, name) VALUES (99, ‘Bob’);</td><td align="left"></td></tr><tr><td align="left">5</td><td align="left">COMMIT;</td><td align="left"></td></tr><tr><td align="left">6</td><td align="left"></td><td align="left">SELECT * FROM students WHERE id &#x3D; 99;</td></tr><tr><td align="left">7</td><td align="left"></td><td align="left">UPDATE students SET name &#x3D; ‘Alice’ WHERE id &#x3D; 99;</td></tr><tr><td align="left">8</td><td align="left"></td><td align="left">SELECT * FROM students WHERE id &#x3D; 99;</td></tr><tr><td align="left">9</td><td align="left"></td><td align="left">COMMIT;</td></tr></tbody></table><p>事务B在第3步第一次读取<code>id=99</code>的记录时，读到的记录为空，说明不存在<code>id=99</code>的记录。随后，事务A在第4步插入了一条<code>id=99</code>的记录并提交。事务B在第6步再次读取<code>id=99</code>的记录时，读到的记录仍然为空，但是，事务B在第7步试图更新这条不存在的记录时，竟然成功了，并且，事务B在第8步再次读取<code>id=99</code>的记录时，记录出现了。</p><p>可见，幻读就是没有读到的记录，以为不存在，但其实是可以更新成功的，并且，更新成功后，再次读取，就出现了。</p><h3 id="4-Serializable"><a href="#4-Serializable" class="headerlink" title="4 Serializable"></a>4 Serializable</h3><p>Serializable 是最严格的隔离级别。在Serializable隔离级别下，所有事务按照次序依次执行，因此，脏读、不可重复读、幻读都不会出现。</p><p>虽然 Serializable 隔离级别下的事务具有最高的安全性，但是，由于事务是串行执行，所以效率会大大下降，应用程序的性能会急剧降低。如果没有特别重要的情景，一般都不会使用Serializable隔离级别。</p><p><strong>默认隔离级别</strong></p><p>如果没有指定隔离级别，数据库就会使用默认的隔离级别。在MySQL中，如果使用InnoDB，默认的隔离级别是Repeatable Read。</p>]]>
    </content>
    <id>https://grobenis.github.io/2020/08/31/2020-08-31-%E6%95%B0%E6%8D%AE%E5%BA%93%E6%80%BB%E7%BB%93/</id>
    <link href="https://grobenis.github.io/2020/08/31/2020-08-31-%E6%95%B0%E6%8D%AE%E5%BA%93%E6%80%BB%E7%BB%93/"/>
    <published>2020-08-31T13:18:32.000Z</published>
    <summary>
      <![CDATA[<p>本文学习自：数据库系统概论、MySQL必知必会</p>
<p>本文系统总结关系数据库的核心知识，覆盖数据库设计的六个阶段、三种数据模型、常用数据类型与范式，并详细讲解主键外键、索引、SQL 语法、各类查询（条件、投影、排序、分页、聚合、多表与连接查询）、增删改操作、MySQL 实用语句以及事务的 ACID 特性与四种隔离级别。</p>
<blockquote>
<p><strong>数据组织有序，查询方能高效可靠</strong></p>
</blockquote>]]>
    </summary>
    <title>数据库总结</title>
    <updated>2026-08-25T13:18:00.501Z</updated>
  </entry>
</feed>
