<?xml version="1.0" encoding="UTF-8"?><rss xmlns:dc="http://purl.org/dc/elements/1.1/" xmlns:content="http://purl.org/rss/1.0/modules/content/" xmlns:atom="http://www.w3.org/2005/Atom" version="2.0"><channel><title><![CDATA[我的飛鳥集]]></title><description><![CDATA[代码与诗的世界]]></description><link>https://fmcf.cc</link><image><url>https://api.fmcf.cc/api/v2/objects/avatar/q4yxd5smqxro8lfm69.png</url><title>我的飛鳥集</title><link>https://fmcf.cc</link></image><generator>Magneto</generator><lastBuildDate>Tue, 07 Apr 2026 20:01:13 GMT</lastBuildDate><atom:link href="https://fmcf.cc/feed" rel="self" type="application/rss+xml"/><pubDate>Tue, 07 Apr 2026 20:01:13 GMT</pubDate><language><![CDATA[zh-CN]]></language><item><title><![CDATA[无 OpenCV 对图像逆透视]]></title><description><![CDATA[<link rel="preload" as="image" href="https://api.fmcf.cc/api/v2/objects/image/uhx3x6a0jfstuwmki1.png"/><link rel="preload" as="image" href="https://api.fmcf.cc/api/v2/objects/image/uet59g3hxdgx72k9b0.png"/><link rel="preload" as="image" href="https://api.fmcf.cc/api/v2/objects/image/yx8z9fr269y4ikwkq4.png"/><div><blockquote>该渲染由 MX-SPACE API 生成，可能存在排版问题，最佳体验请前往：<a href="https://fmcf.cc/posts/technology/inverse_perspective">https://fmcf.cc/posts/technology/inverse_perspective</a></blockquote><div><h2 id="">前言</h2><p>伴随着无人驾驶技术的发展，越来越多人接触到了更新的科技，也越来越好奇计算机世界到底是如何实现自动驾驶，而自动驾驶系统附带的某些功能又是如何实现的也引起了很多人的好奇。这次将讲述并且实践系统中 <strong>「360° 倒车影像」</strong>背后的算法逻辑。</p><h2 id="">逆透视变换</h2><p>在画面获取时，整车会调用多个摄像头，<strong>拼</strong>成一幅「360°全景照片」</p><p>而形成俯视视角的 <strong>「360° 倒车影像」</strong> 要经过一层数学运算，也就是<strong>逆透视变换</strong>，简称IPM。</p><p>在这个领域，有很多种IPM变换方式，比如「对应点对单应变换方法」、「简化相机模型逆透视变换」，但都利用了矩阵的变换法则。</p><h3 id="">对应点对单应变换方法</h3><p>这个变化方式相对简单，不过多描述。</p><p>输入至少四个对应点对，不能有三点及以上共线，不需要知道摄相机参数或者平面位置的任何信息，利用点对，求解透视变换矩阵，其中矩阵是一个三阶方阵，因此可以<strong>构建一个线性方程求解</strong>。如果大于四个点，可以使用 $ransac$ 方法求解，而选点的方法通常采用手动选取，一般是选择消失点。</p>
<p>$$</p><p> \begin{bmatrix} t<em>i x&#x27;</em>i \ t<em>i y&#x27;</em>i \ t<em>i \end{bmatrix} = map_matrix \cdot \begin{bmatrix} x</em>i \ y_i \ 1 \end{bmatrix}</p><p>$$</p><p>$$</p>
<p>dst(i) = (x&#x27;<em>i, y&#x27;</em>i), 
 src(i) = (x<em>i, y</em>i), 
 i = 0, 1, 2, 3</p><p>$$</p>
<p><img src="https://api.fmcf.cc/api/v2/objects/image/uhx3x6a0jfstuwmki1.png" height="320" width="884"/></p><p>这个变化在代码上实现相对简单，可以相对容易实现 IPM 变换，这里就不再过多赘述，也不提供代码示例。</p><h3 id="-ipm">简化相机模型 IPM方法</h3><p>这是这次重点要解析的变换方法，这个算法的本质是利用相机成像过程中的各种坐标之间的转换关系，再对它进行抽象和简化，最终得到的世界坐标。</p><p>然后再建立<strong>世界坐标</strong>与<strong>图像坐标</strong>的对应关系，使用该关系进行<strong>数学变换</strong>得来。</p><p><img src="https://api.fmcf.cc/api/v2/objects/image/uet59g3hxdgx72k9b0.png"/></p><p>不同于某些复杂、冗长的计算公式，这里我们依旧采用坐标运算。针对这种 IPM 计算方法，我们需要先测量摄像头实际参数。</p><p>这里仰角 $θ$ 为 $23°$，中心高度 $H$ 为 $37 cm$，视点到视平面距离 $d$ 为 $87 cm$，然后求世界坐标 $P_W$。</p><p>设摄像头图像坐标为 $\boldsymbol{P_G} = (x, y, z, 1)$，并由世界坐标与图像坐标的关系建立矩阵方程，</p><p>$$
\boldsymbol{P<em>G} = \boldsymbol{P</em>W} 
\begin{bmatrix} 
1 &amp; 0 &amp; 0 &amp; 0 \ 
0 &amp; 1 &amp; 0 &amp; 0 \ 
0 &amp; 0 &amp; 1 &amp; 0 \ 
0 &amp; 0 &amp; -H &amp; 1 
\end{bmatrix} 
\begin{bmatrix} 
\cos\theta &amp; 0 &amp; -\sin\theta &amp; 0 \ 
0 &amp; 1 &amp; 0 &amp; 0 \ 
\sin\theta &amp; 0 &amp; \cos\theta &amp; 0 \ 
0 &amp; 0 &amp; 0 &amp; 1 
\end{bmatrix} 
\cdot 
\begin{bmatrix} 
1 &amp; 0 &amp; 0 &amp; \frac{1}{d} \ 
0 &amp; 1 &amp; 0 &amp; 0 \ 
0 &amp; 0 &amp; 1 &amp; 0 \ 
0 &amp; 0 &amp; 0 &amp; 1 
\end{bmatrix}. 
\tag{1}
$$</p><p>将图像坐标代入式$(1)$，求得世界坐标的矩阵，即</p><p>$$
\boldsymbol{P_W} = \left[ \frac{x\cos\theta - z\sin\theta}{1 - \frac{x}{d}} \quad \frac{y}{1 - \frac{x}{d}} \quad \frac{x\sin\theta + z\cos\theta - \frac{H}{d}}{1 - \frac{x}{d}} \quad 1 \right]. \tag{2}
$$</p><p>令 $A = H\cos\theta$ 以及 $B = -d$ 还有 $C = d\sin\theta - \frac{H}{d}$ 以及 $D = \cos\theta$ 还有 $E = d\sin\theta$，由几何关系可知 $x\sin\theta + z\cos\theta - \frac{H}{d} = 0$。求得 $\boldsymbol{P_W}$ 最简形式为</p><p>$$
\boldsymbol{P_W} = \left[ \frac{A + Bz}{C + Dz} \quad \frac{Ey}{C + Dz} \quad 0 \quad 1 \right]. \tag{3}
$$</p><p>最后，处理图像。由于所处理的图像是二位平面图，故图像深度恒为0。根据$(3)$，只需要把数组的横纵坐标带入，即可求得世界坐标下坐标值，即 IPM 后的俯视图。
<img src="https://api.fmcf.cc/api/v2/objects/image/yx8z9fr269y4ikwkq4.png"/></p>
<pre class="language-cpp lang-cpp"><code class="language-cpp lang-cpp">#include &lt;cmath&gt;
#include &lt;cstdint&gt;
#include &lt;vector&gt;
#include &lt;algorithm&gt;

namespace ipm
{
    // =========================
    // 基础数据结构
    // =========================

    struct Vec3
    {
        double x;
        double y;
        double z;
    };

    struct GroundPoint
    {
        double X;     // 世界坐标 X（左右）
        double Y;     // 世界坐标 Y（前后）
        bool valid;   // 是否与地面有有效交点
    };

    struct CameraParam
    {
        // 焦距（像素单位）
        // 如果你只有一个 d，可以令 fx = fy = d
        double fx;
        double fy;

        // 主点（通常是图像中心）
        double cx;
        double cy;

        // 相机离地高度，单位例如 cm
        double H;

        // 相机向下俯角（弧度）
        double pitch;
    };

    struct IPMParam
    {
        // 输出俯视图尺寸
        int outWidth;
        int outHeight;

        // 世界坐标范围（单位与 H 一致，例如 cm）
        // X: 左右范围
        // Y: 前后范围
        double minX;
        double maxX;
        double minY;
        double maxY;
    };

    // =========================
    // 工具函数
    // =========================

    inline double clampDouble(double v, double lo, double hi)
    {
        return (v &lt; lo) ? lo : ((v &gt; hi) ? hi : v);
    }

    inline uint8_t clampToByte(double v)
    {
        if (v &lt; 0.0) return 0;
        if (v &gt; 255.0) return 255;
        return static_cast&lt;uint8_t&gt;(v + 0.5);
    }

    // 绕 X 轴旋转：把相机坐标系下的方向，转到世界坐标系
    // 这里假定：
    // - 世界 Z 轴向上
    // - 相机光轴默认朝世界 Y 正方向
    // - pitch &gt; 0 表示相机向下俯视
    //
    // 为了和图像坐标（v向下）匹配，构造一个工程上常用的映射：
    //
    // 相机系射线 rc = [x, y, 1]
    // 先映射到“未俯仰时”的世界方向：
    //   x -&gt; Xw
    //   y -&gt; -Zw
    //   z -&gt; Yw
    //
    // 再绕世界 X 轴旋转 pitch
    //
    inline Vec3 cameraRayToWorldRay(const Vec3&amp; rc, double pitch)
    {
        // 未俯仰时的世界方向
        // 相机右 -&gt; 世界右
        // 相机下 -&gt; 世界负上
        // 相机前 -&gt; 世界前
        const double X0 = rc.x;
        const double Y0 = rc.z;
        const double Z0 = -rc.y;

        const double c = std::cos(pitch);
        const double s = std::sin(pitch);

        // 绕 X 轴旋转
        Vec3 rw;
        rw.x = X0;
        rw.y = c * Y0 - s * Z0;
        rw.z = s * Y0 + c * Z0;

        return rw;
    }

    // =========================
    // 像素点 -&gt; 地面世界坐标
    // =========================
    //
    // 输入像素点 (u, v)，计算它在地面 Z=0 上对应的世界点 (X, Y)
    //
    // 注意：
    // 1. 如果这条射线朝天或者平行地面，则 invalid
    // 2. fx, fy 用像素单位
    // 3. H 的单位决定输出世界坐标单位
    //
    inline GroundPoint imagePixelToGround(
        double u,
        double v,
        const CameraParam&amp; cam)
    {
        // 1) 像素坐标 -&gt; 相机归一化坐标
        Vec3 rc;
        rc.x = (u - cam.cx) / cam.fx;
        rc.y = (v - cam.cy) / cam.fy;
        rc.z = 1.0;

        // 2) 相机射线 -&gt; 世界射线
        Vec3 rw = cameraRayToWorldRay(rc, cam.pitch);

        // 3) 相机中心在世界坐标中的位置
        // Cw = (0, 0, H)
        // 射线方程：P(t) = Cw + t * rw
        //
        // 与地面 Zw = 0 相交：
        // H + t * rw.z = 0  =&gt;  t = -H / rw.z
        //
        GroundPoint gp{};
        gp.valid = false;

        // 射线没有指向地面，或者几乎平行地面
        if (std::abs(rw.z) &lt; 1e-12)
            return gp;

        const double t = -cam.H / rw.z;

        // 只接受“向前”的交点
        if (t &lt;= 0.0)
            return gp;

        gp.X = t * rw.x;
        gp.Y = t * rw.y;
        gp.valid = true;
        return gp;
    }

    // =========================
    // 世界坐标 -&gt; 俯视图像素
    // =========================
    //
    // 把地面点 (X, Y) 映射到输出俯视图中的 (bx, by)
    //
    // 输出图约定：
    // - 左边是 minX，右边是 maxX
    // - 上边是 maxY（更远处）
    // - 下边是 minY（更近处）
    //
    inline bool groundToBirdPixel(
        double X, double Y,
        const IPMParam&amp; ipmParam,
        double&amp; bx, double&amp; by)
    {
        if (X &lt; ipmParam.minX || X &gt; ipmParam.maxX ||
            Y &lt; ipmParam.minY || Y &gt; ipmParam.maxY)
        {
            return false;
        }

        const double xRatio =
            (X - ipmParam.minX) / (ipmParam.maxX - ipmParam.minX);

        const double yRatio =
            (Y - ipmParam.minY) / (ipmParam.maxY - ipmParam.minY);

        // X 从左到右
        bx = xRatio * (ipmParam.outWidth - 1);

        // 希望“远处在图像上方”
        by = (1.0 - yRatio) * (ipmParam.outHeight - 1);

        return true;
    }

    // =========================
    // 双线性采样（灰度图）
    // =========================
    inline uint8_t bilinearSampleGray(
        const uint8_t* src,
        int width,
        int height,
        int stride,
        double u,
        double v)
    {
        if (u &lt; 0.0 || v &lt; 0.0 || u &gt; width - 1.0 || v &gt; height - 1.0)
            return 0;

        const int x0 = static_cast&lt;int&gt;(std::floor(u));
        const int y0 = static_cast&lt;int&gt;(std::floor(v));
        const int x1 = std::min(x0 + 1, width - 1);
        const int y1 = std::min(y0 + 1, height - 1);

        const double dx = u - x0;
        const double dy = v - y0;

        const double p00 = src[y0 * stride + x0];
        const double p10 = src[y0 * stride + x1];
        const double p01 = src[y1 * stride + x0];
        const double p11 = src[y1 * stride + x1];

        const double v0 = p00 * (1.0 - dx) + p10 * dx;
        const double v1 = p01 * (1.0 - dx) + p11 * dx;
        const double val = v0 * (1.0 - dy) + v1 * dy;

        return clampToByte(val);
    }

    // =========================
    // 双线性采样（RGB 三通道）
    // 每像素 3 字节，RGBRGB...
    // =========================
    inline void bilinearSampleRGB(
        const uint8_t* src,
        int width,
        int height,
        int stride,
        double u,
        double v,
        uint8_t outRGB[3])
    {
        if (u &lt; 0.0 || v &lt; 0.0 || u &gt; width - 1.0 || v &gt; height - 1.0)
        {
            outRGB[0] = outRGB[1] = outRGB[2] = 0;
            return;
        }

        const int x0 = static_cast&lt;int&gt;(std::floor(u));
        const int y0 = static_cast&lt;int&gt;(std::floor(v));
        const int x1 = std::min(x0 + 1, width - 1);
        const int y1 = std::min(y0 + 1, height - 1);

        const double dx = u - x0;
        const double dy = v - y0;

        const uint8_t* p00 = src + y0 * stride + x0 * 3;
        const uint8_t* p10 = src + y0 * stride + x1 * 3;
        const uint8_t* p01 = src + y1 * stride + x0 * 3;
        const uint8_t* p11 = src + y1 * stride + x1 * 3;

        for (int c = 0; c &lt; 3; ++c)
        {
            const double v0 = p00[c] * (1.0 - dx) + p10[c] * dx;
            const double v1 = p01[c] * (1.0 - dx) + p11[c] * dx;
            const double val = v0 * (1.0 - dy) + v1 * dy;
            outRGB[c] = clampToByte(val);
        }
    }

    // =========================
    // 俯视图像素 -&gt; 世界坐标
    // =========================
    //
    // 这是做“逆映射”的关键：
    // 对输出俯视图的每个像素，先求它在世界地面的点，
    // 再反算它在原图中的位置，最后从原图采样。
    //
    inline void birdPixelToGround(
        double bx,
        double by,
        const IPMParam&amp; ipmParam,
        double&amp; X,
        double&amp; Y)
    {
        const double xRatio = bx / (ipmParam.outWidth - 1);
        const double yRatio = 1.0 - by / (ipmParam.outHeight - 1);

        X = ipmParam.minX + xRatio * (ipmParam.maxX - ipmParam.minX);
        Y = ipmParam.minY + yRatio * (ipmParam.maxY - ipmParam.minY);
    }

    // =========================
    // 世界地面点 -&gt; 原图像素
    // =========================
    //
    // 已知世界点 (X, Y, 0)，反投影到输入图像，便于做逆映射采样。
    //
    inline bool groundToImagePixel(
        double X,
        double Y,
        const CameraParam&amp; cam,
        double&amp; u,
        double&amp; v)
    {
        // 世界点 Pw = (X, Y, 0)
        // 相机中心 Cw = (0, 0, H)
        // 世界方向向量 d_w = Pw - Cw = (X, Y, -H)
        const double dwx = X;
        const double dwy = Y;
        const double dwz = -cam.H;

        // 需要把世界方向转回相机方向
        // cameraRayToWorldRay 里用的是：Rw = Rx(pitch) * base
        // 因此这里做逆旋转：Rx(-pitch)
        const double c = std::cos(cam.pitch);
        const double s = std::sin(cam.pitch);

        // 先逆旋转到未俯仰状态
        const double X0 = dwx;
        const double Y0 = c * dwy + s * dwz;
        const double Z0 = -s * dwy + c * dwz;

        // 再映射回相机坐标
        // base: [X0, Y0, Z0] = [xc, zc, -yc]
        const double xc = X0;
        const double yc = -Z0;
        const double zc = Y0;

        // 在相机后方，无效
        if (zc &lt;= 1e-12)
            return false;

        u = cam.fx * (xc / zc) + cam.cx;
        v = cam.fy * (yc / zc) + cam.cy;
        return true;
    }

    // =========================
    // 灰度图 IPM
    // =========================
    //
    // src: 输入灰度图
    // dst: 输出灰度图，需由外部分配 outHeight * dstStride 字节
    //
    inline void warpIPMGray(
        const uint8_t* src,
        int srcWidth,
        int srcHeight,
        int srcStride,
        uint8_t* dst,
        int dstStride,
        const CameraParam&amp; cam,
        const IPMParam&amp; ipmParam)
    {
        for (int by = 0; by &lt; ipmParam.outHeight; ++by)
        {
            uint8_t* dstRow = dst + by * dstStride;

            for (int bx = 0; bx &lt; ipmParam.outWidth; ++bx)
            {
                // 1) 输出俯视图像素 -&gt; 世界地面点
                double X, Y;
                birdPixelToGround(static_cast&lt;double&gt;(bx),
                                  static_cast&lt;double&gt;(by),
                                  ipmParam, X, Y);

                // 2) 世界地面点 -&gt; 原图像素
                double u, v;
                if (!groundToImagePixel(X, Y, cam, u, v))
                {
                    dstRow[bx] = 0;
                    continue;
                }

                // 3) 双线性采样
                dstRow[bx] = bilinearSampleGray(src, srcWidth, srcHeight, srcStride, u, v);
            }
        }
    }

    // =========================
    // RGB 图 IPM
    // =========================
    //
    // src: 输入 RGB 图，按 RGBRGB... 排列
    // dst: 输出 RGB 图，按 RGBRGB... 排列
    //
    inline void warpIPMRGB(
        const uint8_t* src,
        int srcWidth,
        int srcHeight,
        int srcStride,
        uint8_t* dst,
        int dstStride,
        const CameraParam&amp; cam,
        const IPMParam&amp; ipmParam)
    {
        for (int by = 0; by &lt; ipmParam.outHeight; ++by)
        {
            uint8_t* dstRow = dst + by * dstStride;

            for (int bx = 0; bx &lt; ipmParam.outWidth; ++bx)
            {
                double X, Y;
                birdPixelToGround(static_cast&lt;double&gt;(bx),
                                  static_cast&lt;double&gt;(by),
                                  ipmParam, X, Y);

                double u, v;
                if (!groundToImagePixel(X, Y, cam, u, v))
                {
                    uint8_t* p = dstRow + bx * 3;
                    p[0] = p[1] = p[2] = 0;
                    continue;
                }

                uint8_t rgb[3];
                bilinearSampleRGB(src, srcWidth, srcHeight, srcStride, u, v, rgb);

                uint8_t* p = dstRow + bx * 3;
                p[0] = rgb[0];
                p[1] = rgb[1];
                p[2] = rgb[2];
            }
        }
    }

}
</code></pre></div><p style="text-align:right"><a href="https://fmcf.cc/posts/technology/inverse_perspective#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://fmcf.cc/posts/technology/inverse_perspective</link><guid isPermaLink="true">https://fmcf.cc/posts/technology/inverse_perspective</guid><dc:creator><![CDATA[Magneto]]></dc:creator><pubDate>Mon, 30 Mar 2026 13:32:49 GMT</pubDate></item><item><title><![CDATA[融合大核归纳偏置与正交空间感知的轻量化特征重构范式]]></title><description><![CDATA[<link rel="preload" as="image" href="https://api.fmcf.cc/api/v2/objects/icon/4hn0zyzw8s1t1nvdwh.png"/><link rel="preload" as="image" href="https://api.fmcf.cc/api/v2/objects/icon/1awb669njidb8pu0t7.png"/><link rel="preload" as="image" href="https://api.fmcf.cc/api/v2/objects/icon/mpw39c01qeq4uc1ct2.png"/><link rel="preload" as="image" href="https://api.fmcf.cc/api/v2/objects/icon/4qolhpe34p4600huc4.png"/><div><blockquote>该渲染由 MX-SPACE API 生成，可能存在排版问题，最佳体验请前往：<a href="https://fmcf.cc/posts/life/CoordDWConvPro">https://fmcf.cc/posts/life/CoordDWConvPro</a></blockquote><div><p>如果你还在为边缘计算下&quot;既要又要&quot;而犯愁，那么这篇文章或许值得一看。</p><h1 id="abstract">Abstract</h1><p>在轻量级卷积神经网络的设计中，如何在有限的 FLOPs 下平衡“局部感受野”与“全局空间感知”始终是一个核心难题。</p><p>传统的 $3 \times 3$ 卷积受限于感受野大小，而常规的 SE-Block 注意力机制则因全局池化操作导致了空间位置信息的坍塌，为此特地开发了一种新型算子。该结构创新性地融合了 $5 \times 5$ 大核深度卷积 与 坐标注意力机制（Coordinate Attention），并通过 强制残差策略与 GroupNorm 优化，成功构建了一种对硬件友好且具备鲁棒位置编码能力的特征提取范式。</p><h1 id="1">1.设计动机与理论背景</h1><p>在分析代码之前，我们需要理解该模块试图解决的三个核心痛点：</p><ul><li>有效感受野（ERF）的局限性：传统轻量级网络过度依赖 $3 \times 3$ 卷积堆叠。根据研究，深层网络的实际有效感受野往往呈高斯分布且随深度衰减，难以捕捉大尺度语义目标。</li><li>空间语义的错位（Spatial Misalignment）：标准的 SE 模块通过 Global Average Pooling 将特征图压缩为 $1 \times 1 \times C$，虽然强化了通道依赖，却彻底丢失了物体的空间坐标信息。</li><li><p>微批次统计的不稳定性（Micro-Batch Instability）：在端侧设备进行迁移学习或微调时，受限于显存，Batch Size 往往极小（如 2 或 4），此时 BatchNorm 的统计量估计将产生巨大偏差，导致训练发散。</p><p>而本次构建的注意力融合卷积核正是基于上述理论背景提出的解决方案。</p></li></ul><h1 id="2">2.核心架构拆解</h1><p>该模块并非简单的层级堆叠，而是一个精心设计的特征重构闭环。以下结合代码逻辑，分步骤进行深度剖析：</p><h2 id="21">2.1大核深度卷积带来的归纳偏置</h2><p>代码实现：</p><pre class="language-python lang-python"><code class="language-python lang-python">self.dw_conv = nn.Conv2d(c1, c1, kernel_size=5, stride=s, padding=2, groups=c1, bias=False)
</code></pre><p>设计：将卷积核从 $3 \times 3$ 提升至 $5 \times 5$。从信息论角度看，这增加了单个神经元的“可视区域”。</p><p>理论优势： $5 \times 5$  卷积的感受野面积是 $3 \times 3$ 的 $25/9 \approx 2.78$ 倍。在轻量级网络（如 MobileNetV3）中，这种大核深度卷积 能有效模拟 Transformer 中的 Token Mixer 行为，增强对纹理和形状的捕捉能力，且 NCNN 等推理框架对 $5 \times 5$ DW 算子已具备极高的 Winograd 算法优化支持。</p><br/><h2 id="22-">2.2 正交特征分解与坐标注意力</h2><p>这是本模块的“灵魂”。不同于 SE 的全局池化，该模块利用两个正交的 1D Global Pooling 操作将空间信息分解。</p>
<h3 id="-i">步骤 I：正交投影</h3><pre class="language-python lang-python"><code class="language-python lang-python">x_h = self.pool_h(feat)  # Output: (N, C, H, 1)
x_w = self.pool_w(feat)  # Output: (N, C, 1, W)
</code></pre><ul><li>数学表征：输入张量 $X$ 被分别沿水平坐标 $X$ 和垂直坐标 $Y$ 进行聚合。这种操作生成了两个方向感知特征图，使得网络能够捕捉沿着一个空间方向的长距离依赖，同时保存另一个方向的精确位置信息。</li></ul><h3 id="-ii">步骤 II：跨维度交互与降维</h3><pre class="language-python lang-python"><code class="language-python lang-python">y = torch.cat([x_h, x_w], dim=2)
y = self.conv_pool(y)
y = self.gn(y) # GroupNorm for stability
</code></pre><ul><li><p>优化策略： 此处引入了 <code>reduction=16</code> 的瓶颈层以降低模型复杂度。</p></li><li><p>改进： GroupNorm 的引入是点睛之笔。在注意力分支的中间层，特征通道被压缩，且往往伴随着极小的 Batch Size。GN 将通道分组进行归一化，其统计量不依赖于 Batch Size，从而解决了在微调任务中 BN 层导致的“统计量漂移”问题。</p></li></ul><h3 id="-iii">步骤 III：注意力重校准</h3><pre class="language-python lang-python"><code class="language-python lang-python">a_h = self.conv_h(x_h).sigmoid()
a_w = self.conv_w(x_w).sigmoid()
out = identity_feat * a_w * a_h
</code></pre><ul><li>特征融合：
最终的输出特征图是通过原始特征与两个方向的注意力图进行 Hadamard Product 得到的。这相当于为特征图上的每一个像素点 $(i, j)$ 赋予了一个基于全局上下文计算的“重要性权重”。</li></ul><h2 id="23-">2.3 强制残差流</h2><pre class="language-python lang-python"><code class="language-python lang-python">if self.use_res:
    return x + out
</code></pre><ul><li>梯度流保护： 注意力机制本质上是一种“Soft Gating”。在训练初期，注意力权重可能接近于零。强制残差连接构建了一条 恒等映射 通路，确保了在最坏情况下（注意力层失效），该模块退化为标准的卷积层，从而保证了深层网络梯度的有效反向传播，避免了梯度消失。</li></ul><h2 id="3-">3. 详细执行流与张量演化</h2><p>为了更清晰地展示该模块内部的数据流转，我们将 Forward 过程形式化为以下详细步骤：</p><ol start="1"><li>空间特征提取：</li></ol><ul><li><p>输入 $X \in \mathbb{R}^{N \times C_1 \times H \times W}$。</p></li><li><p>经过 $5 \times 5$ DWConv $\rightarrow$ $1 \times 1$ PWConv $\rightarrow$ BN $\rightarrow$ Hardswish。</p></li><li><p>输出中间特征 $F \in \mathbb{R}^{N \times C_2 \times H \times W}$。</p></li></ul><ol start="2"><li><p>坐标信息编码：</p><ul><li>H-Pooling: 将 $F$ 压缩为 $Z^h \in \mathbb{R}^{N \times C_2 \times H \times 1}$。</li><li>W-Pooling: 将 $F$ 压缩为 $Z^w \in \mathbb{R}^{N \times C_2 \times 1 \times W}$。</li></ul></li><li><p>变换与激活 ：</p></li></ol><ul><li>拼接 $Z^h$ 与 $Z^w$ 并通过 $1 \times 1$ 卷积降维至 $C_{mid}$。</li><li>应用 GroupNorm(1, mip) 进行归一化（此处 Group=1 等效于 LayerNorm，但针对通道维度）。</li><li>应用 Non-linear 激活函数。</li></ul><ol start="4"><li>解码与重加权：</li></ol><ul><li>将特征张量重新切分为空间感知的权重向量 $A^h$ 和 $A^w$。</li><li>$Y = F \odot A^h \odot A^w$ （其中 $\odot$ 表示广播机制下的逐元素相乘）。</li></ul><ol start="5"><li>特征重构 (Reconstruction)：</li></ol><ul><li>最终输出 $O = X + Y$ (若满足残差条件)。</li></ul><h2 id="4-">4. 实验验证与数据可视化</h2><p>为了验证 该卷积核 在真实场景中的有效性，我们在受控环境下进行了严格的对比实验。</p><p><strong>实验设置：</strong></p><ul><li><p>数据集：自定义检测数据集（包含警棍、手电筒、刀具等高相似度类别）。</p></li><li><p>训练策略：SGD 优化器，Cosine LR 调度，训练周期为 5000 Epochs（以确保模型完全收敛）。</p></li></ul><p><strong>Baseline：</strong>仅将本模块替换为标准的 3x3 DWConv，其余网络架构保持完全一致。</p><h3 id="41--trade-off-analysis">4.1 总体性能评估：计算量与精度的权衡 (Trade-off Analysis)</h3><p><img src="https://api.fmcf.cc/api/v2/objects/icon/4hn0zyzw8s1t1nvdwh.png" alt="COMMON" height="1200" width="2400"/>
<img src="https://api.fmcf.cc/api/v2/objects/icon/1awb669njidb8pu0t7.png" alt="ENHANCE"/>
<strong>数据解读</strong>： 如上表所示，该卷积核 在参数量几乎不增加、FLOPs 仅增加 0.05G（几乎可忽略不计）的前提下，实现了 mAP@50-95 惊人的 10% 绝对增长。这证明了该模块并非通过简单的堆砌参数换取性能，而是通过更高效的空间特征建模提升了模型的表征能力。</p><h3 id="42-">4.2 难例挖掘与细粒度分类</h3><p>在测试中“类间相似性” 是最大的挑战。</p><p>例如，长条形的“警棍”与“手电筒”在低分辨率下极难区分。</p><p>我们提取了模型在这些特定类别上的 Top-1 准确率 (Accuracy) 进行对比分析：
<img src="https://api.fmcf.cc/api/v2/objects/icon/mpw39c01qeq4uc1ct2.png" alt="COMMON"/>
<img src="https://api.fmcf.cc/api/v2/objects/icon/4qolhpe34p4600huc4.png" alt="ENHANCE"/></p><h2 id="5-conclusion">5. Conclusion</h2><p>该算子展示了一种极具前瞻性的轻量级网络设计思路。</p><ol start="1"><li>通过 $5 \times 5$ 卷积 引入了更强的空间归纳偏置。</li><li>通过 坐标注意力 解决了标准 CNN 缺乏位置感知能力的问题。</li><li>通过 GroupNorm 和 Hardswish 展现了优秀的工程落地意识，使其在 小样本微调 和端侧推理 场景下具有极高的实用价值。</li></ol><p>该模块不仅是一个即插即用的组件，更为后续的轻量级检测网络设计提供了一个标准的空间-通道解耦 范式。</p></div><p style="text-align:right"><a href="https://fmcf.cc/posts/life/CoordDWConvPro#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://fmcf.cc/posts/life/CoordDWConvPro</link><guid isPermaLink="true">https://fmcf.cc/posts/life/CoordDWConvPro</guid><dc:creator><![CDATA[Magneto]]></dc:creator><pubDate>Sun, 18 Jan 2026 04:50:08 GMT</pubDate></item><item><title><![CDATA[童年回忆]]></title><description><![CDATA[<div><blockquote>该渲染由 MX-SPACE API 生成，可能存在排版问题，最佳体验请前往：<a href="https://fmcf.cc/notes/19">https://fmcf.cc/notes/19</a></blockquote><div><p>《七子之歌》和《北京欢迎你》是我童年记忆最深的两首歌，至今会把我仍会哼唱这两首歌，它们像一根丝线，牵引我回到童年。</p><p>2008年，盛大的奥运对于小小的我不能理解它的意义，我只知道是我的父亲去了北京与大家共襄盛举，是“我家大门常打开，开怀容纳天地”的余音绕梁，是大家都爱着自己，是每一个身边的人脸上都洋溢着幸福的笑容。我至今想来还是后悔在开幕式的时候睡着了，错过了那个最充满希望的一次奥运。</p><p>再长大一岁，是母亲每个夜晚的哭泣，父母亲带我去到很多医院求医，他们跑遍大江南北，带我去北京上海，最后在西京医院治好了，想来这或许也是他们后来在陕西定居的原因吧。第一次住进二三十层的高楼正是在医院的病房，父亲指着楼下川流不息的车流说“你看那些车像不像蚂蚁一样”，是啊人生如蝼蚁，可我今天还是想要追寻蝼蚁的意义。手术的事情我一点也记不起来了，全麻醉是什么样的感觉我也不记得。医生嘱咐以后几年都不能剧烈活动，可是我还是跑跳着走出医院，父母亲没有阻拦我只是笑，小时候只是乐于终于离开一个困住我的地方，现在仔细想来是他们悬在心里的那颗石头终于落地了。</p><p>再长大，那个时候我的家境很好，父亲只是问我喜欢哪所学校，就把我送去了那所学校上学，我在学校音乐课上一遍一遍唱“你可知Macau不是我真姓，我离开你太久了母亲”，小孩子是不知道什么是祖国统一的，只知道我离不开我的母亲，那个因我而日夜哭泣的母亲。</p><p>母亲不会带我吃肯德基，但是每到春节回家总是可以吃到每年唯一一次肯德基，小时候的我能吃到肯德基就是最大最大的满足，我可以开心一整个星期。现在想来，童年的自己真的很好满足。</p><p>童年很幸运，很幸福。幸运那时候大家都很富有，幸运那时候家里很富有才能治好我的病，幸运我有这样爱我的父母亲，幸运有一群爱我的家人，幸运那个时代每个人的脸上都洋溢着希望，正是因为这样的幸运我才会幸福，正是以前的幸运和幸福我才怀念，怀念的是那个可以很简单就可以满足的自己，最天真和无邪的自己。</p><p>母亲总说“不要看你爸没个正形，他可是一步一步走出大山，白手起家才有今天这样的成就的。”</p><p>再到后来的世界就变了，复杂、猜忌、质疑，父亲因为做项目被骗血本无归，后来又趟了一滩浑水，背了几千万的债务，那些所谓的“朋友”也不再帮助父亲，甚至吓跑了我哥他女朋友一家人，我们一家人过上了拮据的日子。好在家里的大家都齐心协力，用十年时间走出阴影，只是父亲不再年轻。</p><p>我有叛逆，但他们溢于言表的爱和童年那段幸福快乐的时光，和在那时不愿放弃我的父母亲，让我爱着这个家，爱这个世界，支撑我在日后最艰难的日子里一步一步走出阴影，和父亲一样。</p><p>我爱这个世界，想温柔地爱所有人。</p></div><p style="text-align:right"><a href="https://fmcf.cc/notes/19#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://fmcf.cc/notes/19</link><guid isPermaLink="true">https://fmcf.cc/notes/19</guid><dc:creator><![CDATA[Magneto]]></dc:creator><pubDate>Mon, 22 Dec 2025 16:20:02 GMT</pubDate></item><item><title><![CDATA[春门鼹影]]></title><description><![CDATA[<div><blockquote>该渲染由 MX-SPACE API 生成，可能存在排版问题，最佳体验请前往：<a href="https://fmcf.cc/posts/life/Chunmen_mole_shadow">https://fmcf.cc/posts/life/Chunmen_mole_shadow</a></blockquote><div><p>这是一首诗 送给一个人</p><pre class=""><code class="">鼹鼠迈进春的门廊
你正梳着苇草的发梢  
河面的波涛，都长出童话的绒毛
  
絮是柳林不肯收信的邮票
  影是野树精心编织的舞蹈
    
摊开掌心——  
  那颗玻璃珠似的月亮，
  映出甲虫壳里沉睡的梦想
  
爱人啊，航线是水鼠未唱完的歌谣  
  在所有风声经过的地方  
  请与我歌唱
  陶罐里会长出铃铛，壶中总有花茶香</code></pre></div><p style="text-align:right"><a href="https://fmcf.cc/posts/life/Chunmen_mole_shadow#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://fmcf.cc/posts/life/Chunmen_mole_shadow</link><guid isPermaLink="true">https://fmcf.cc/posts/life/Chunmen_mole_shadow</guid><dc:creator><![CDATA[Magneto]]></dc:creator><pubDate>Sat, 20 Dec 2025 14:39:21 GMT</pubDate></item><item><title><![CDATA[双系统趣事]]></title><description><![CDATA[<div><blockquote>该渲染由 MX-SPACE API 生成，可能存在排版问题，最佳体验请前往：<a href="https://fmcf.cc/notes/18">https://fmcf.cc/notes/18</a></blockquote><div><p>用双系统 Windows 和 Ubuntu 的同学其实遇到过一个很有意思的问题，双系统时间不同步，从 Linux 切换回 Windows 后恰好慢了 8 个小时</p><p>其实这是因为 Windows 和 Linux 对硬件时钟（RTC）时间的解读方式不同。
我们的主板上会有一颗时钟芯片，存储着物理世界的时间，但
Windows 认为硬件时钟里存储的时间应该是本地时间（比如，你的时区是东八区，硬件时钟就应该是北京时间）。
Linux/Ubuntu 认为硬件时钟里存储的时间应该是 UTC（协调世界时）时间。</p><p>假设你的时区是 东八区（UTC+8，北京时间）。真实世界的时间是 北京时间（本地时间）下午4点（16:00），对应的 UTC 时间就是 上午8点（08:00）。</p>
<h3 id="-windows-">第一步：在 Windows 中</h3><p>1.正在使用 Windows，系统时间正确显示为 16:00（下午4点）。</p><p>2.Windows 认为硬件时钟应该存储本地时间。所以，它会把 16:00这个时间写入硬件时钟。</p><h3 id="-ubuntu">第二步：重启，进入 Ubuntu</h3><p>1.启动电脑，进入 Ubuntu。</p><p>2.Ubuntu 去读取硬件时钟，它默认认为硬件时钟上存储的是 UTC 时间。于是它读到了 16:00。</p><p>3.Ubuntu 心想：“哦，硬件时钟上说 UTC 时间是 16:00。而我所在的时区是 UTC+8，那么本地时间应该是 UTC 时间加上 8小时。”</p><p>4.计算：16:00（它以为的UTC） + 8小时 = 第二天凌晨00:00。</p><p>5.<strong>Ubuntu 自动通过互联网进行网络时间校准（NTP）。当系统启动联网后，它发现：“咦？我根据硬件时钟算出来的本地时间（00:00）和网络标准时间（16:00）对不上，差了8小时。”</strong></p><p>于是，Ubuntu 做了一件聪明事：</p><ul><li>它自动把系统时间纠正为了正确的网络时间（16:00）。</li><li>同时，为了保持未来的一致性，它用这个纠正后的 UTC 去<strong>更新硬件时钟</strong>。</li><li>此时，硬件时钟里的数字被 Ubuntu 从 16:00改为了 08:00。</li></ul><h3 id="-windows">第三步：重启，切回 Windows（时间错误）</h3><p>再次重启电脑，回到 Windows。</p><ul><li>Windows 去读取硬件时钟，它依然认为硬件时钟上存储的是本地时间。于是它读到了 08:00。</li><li>当 Windows 无法在启动时自动联网校准时，则需要手动校准，因此直接把这个时间显示为本地时间上午8点。</li></ul></div><p style="text-align:right"><a href="https://fmcf.cc/notes/18#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://fmcf.cc/notes/18</link><guid isPermaLink="true">https://fmcf.cc/notes/18</guid><dc:creator><![CDATA[Magneto]]></dc:creator><pubDate>Sat, 01 Nov 2025 11:54:57 GMT</pubDate></item><item><title><![CDATA[又一列车]]></title><description><![CDATA[<div><blockquote>该渲染由 MX-SPACE API 生成，可能存在排版问题，最佳体验请前往：<a href="https://fmcf.cc/notes/16">https://fmcf.cc/notes/16</a></blockquote><p>这列车出现在海岸，自京城而来，身披大地母亲由北至南的尘土，风尘仆仆。
大地与海洋这两位母亲，护送它前往天涯海角……</p><p style="text-align:right"><a href="https://fmcf.cc/notes/16#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://fmcf.cc/notes/16</link><guid isPermaLink="true">https://fmcf.cc/notes/16</guid><dc:creator><![CDATA[Magneto]]></dc:creator><pubDate>Tue, 07 Oct 2025 01:59:56 GMT</pubDate></item><item><title><![CDATA[Build Gaussian Splatting Trainer for Ubuntu 24 & New CUDA SDK Version]]></title><description><![CDATA[<div><blockquote>该渲染由 MX-SPACE API 生成，可能存在排版问题，最佳体验请前往：<a href="https://fmcf.cc/posts/technology/Build-Gaussian-Splatting">https://fmcf.cc/posts/technology/Build-Gaussian-Splatting</a></blockquote><div><p>::: gallery
https://api.fmcf.cc/api/v2/objects/icon/3yy0ne2i6e03ayluxk.webp
:::</p><br/><p><a href="https://github.com/graphdeco-inria/gaussian-splatting">https://github.com/graphdeco-inria/gaussian-splatting</a></p><br/><center>YouTube Video:</center><p><a href="https://www.youtube.com/watch?v=T_kXY43VZnk">https://www.youtube.com/watch?v=T_kXY43VZnk</a></p><h2 id="preface">Preface</h2><h3 id="introduction">Introduction</h3><p>3D 高斯溅射是时下最新的一项计算机视觉/计算机图形学/深度学习技术，它是“神经渲染”领域的最新的成果。它引入了三项关键技术，使得在保证画质的同时，也能实现实用的训练时间，并能在 1080p 分辨率下达到实时渲染</p><h3 id="fundamental-theories">Fundamental Theories</h3><p><strong>技术核心</strong></p><ul><li><p>NeRF</p><ul><li>传统NeRF是把场景表示为一个连续函数 $F(\mathbf{x}, \mathbf{d})$，输入3D空间点 $\mathbf{x}$ 和视角方向 $\mathbf{d}$</li><li>但NeRF渲染需要体积积分，训练和推理都很慢。</li></ul></li><li><p>SfM</p><ul><li>并没有直接在稠密体素上优化，而是先通过COLMAP/SfM得到稀疏点云。</li><li>每个点都变成一个&quot;高斯球体&quot;来近似场景。</li></ul></li><li><p>Gaussian Splatting</p><ul><li>每个场景点被建模为一个3D Gaussian Ellipsoid，它们带有协方差矩阵 $\Sigma$</li><li>渲染时并未逐点绘制，而是把这些 Ellipsoid 投射到2D图像上，得到平滑的 Splat</li></ul></li><li><p>Differentiable Rasterization</p><ul><li>渲染可微，使得通过反向传播来优化位置、形状、颜色、透明度等参数</li><li>用的是类似 OpenGL 的快速 Rasterizer</li></ul></li></ul><p><strong>基础数学模型</strong></p><ul><li>单个高斯定义</li></ul><p>一个高斯分布中心在 $\mu \in \mathbb{R}^{3}$，协方差矩阵 $\Sigma \in \mathbb{R}^{3 \times 3}$：</p><p>$$ G(\mathbf{x}) = \exp\left(-\frac{1}{2}(\mathbf{x} - \mu)^T \Sigma^{-1}(\mathbf{x} - \mu)\right) $$</p><p>这里：</p><ul><li>$\mu$：高斯的中心（3D点的位置）</li><li>$\Sigma$：协方差矩阵，控制形状（球形/椭球）</li><li><p>$G(\mathbf{x})$：某点在该高斯下的&quot;贡献&quot;</p><hr/></li><li><p>投影到 2D 图像平面</p></li></ul><p>通过相机投影矩阵 $\Pi$，将 $\mu$ 和 $\Sigma$ 映射到图像平面：</p><p>$$ \mu<em>{2D} = \Pi(\mu), \quad \Sigma</em>{2D} = J \Sigma J^{T} $$</p><p>其中 $J$ 是投影的雅可比矩阵。</p><p>这使得 3D 高斯在屏幕上变成一个 2D 高斯斑点（一个模糊的小椭圆）。</p><hr/><ul><li>颜色与透明度合成</li></ul><p>每个高斯还带颜色 $\mathbf{c}$ 和透明度 $\alpha$。</p><p>图像上的最终像素值是多个高斯 splat 的加权混合：</p><p>$$ C(\mathbf{p}) = \sum<em>{i} T</em>{i} \alpha<em>{i} \mathbf{c}</em>{i} $$</p><p>其中 $T<em>{i} = \prod</em>{j&lt;i} (1-\alpha_{j})$ 表示前面高斯的透射率（类似体积渲染公式）。</p><hr/><ul><li>优化目标</li></ul><p>与真实照片对比，最小化差异（MSE、SSIM、LPIPS 等损失）。</p><p>通过反向传播，更新 $\mu,\Sigma,\alpha,\mathbf{c}$ 等参数，使得渲染结果与真实图像一致。</p><h2 id="build-step">Build Step</h2><h3 id="device--system-preparation">Device &amp; System Preparation</h3><ul><li>NVIDIA GPU &amp; GPU Drive</li><li>ANY X86_64 CPU（High computing power better）</li><li>Ubuntu 24</li><li><a href="https://www.anaconda.com">Anaconda</a> OR <a href="https://www.anaconda.com/docs/getting-started/miniconda/main">Miniconda</a></li><li><strong>Python 3.9</strong>（important）</li></ul><h3 id="about-cuda-version">About CUDA Version</h3><ul><li>Use <strong>CUDA Version  == 12.8</strong></li></ul><p>The default CUDA  version on the Ubuntu24 system is 12.9, and latest version is 13.</p><p>However, using newer versions of CUDA Toolkit + PyTorch may cause various issues, such as requiring higher Python versions that are not supported by Gaussian Splatting.</p><h3 id="install">Install</h3><p>Review the <a href="https://github.com/graphdeco-inria/gaussian-splatting/blob/main/environment.yml">Environment.yml</a> <strong>before starting</strong>, as it contains all the modules required for use.</p><p>If you are using servers or computers located within China, please configure a network proxy or set up a download mirror in advance to avoid download failures.</p><p>Please ensure that your system has Git installed before executing the command.</p><p><strong><em>Step 1: Git Clone</em></strong></p><pre class="language-shell lang-shell"><code class="language-shell lang-shell">git clone https://github.com/graphdeco-inria/gaussian-splatting --recursive
</code></pre>
<p>Note:<code>--recursive</code> must be included because the project has submodules.</p><p><strong><em>Step 2: Create Conda Environment</em></strong></p><pre class="language-shell lang-shell"><code class="language-shell lang-shell">conda create -n gaussian_splattingpython=3.9 ipython
conda activate gaussian_splatting
</code></pre>
<p><strong><em>Step 3: Install CUDA Tookit</em></strong></p><p>Please check if your CUDA Toolkit version is 12.8. If so, proceed to execute the <em>final
command</em> in the <strong><em>&quot;Third Step.&quot;</em></strong></p><pre class="language-shell lang-shell"><code class="language-shell lang-shell">nvcc --version
</code></pre><p>Note: The CUDA version displayed by the <code>nvidia-smi</code> command refers to the driver version, not the CUDA Toolkit version.</p><p>If not, you will need to uninstall the current version and reinstall it. This guide will skip the uninstallation steps and focus solely on the installation process for version 12.8.</p><pre class="language-shell lang-shell"><code class="language-shell lang-shell">wget https://developer.download.nvidia.com/compute/cuda/repos/ubuntu2404/x86_64/cuda-keyring_1.1-1_all.deb
sudo dpkg -i cuda-keyring_1.1-1_all.deb
sudo apt-get update
sudo apt-get -y install cuda-toolkit-12-8
</code></pre>
<pre class="language-shell lang-shell"><code class="language-shell lang-shell">conda install -c nvidia cuda-toolkit=12.4
</code></pre><p>Conda will install CUDA Toolkit 12.4, which remains backward-compatible with CUDA 12.8 instructions.</p><p><strong><em>Step 4: Install the C++ Compilation Environment</em></strong></p><pre class="language-shell lang-shell"><code class="language-shell lang-shell">sudo apt-get update 
sudo apt install build-essential ninja-build
</code></pre>
<p><strong><em>Step 5: Install Additional Dependencies</em></strong></p><pre class="language-shell lang-shell"><code class="language-shell lang-shell">conda install -c conda-forge plyfile
conda install tqdm
</code></pre><p>and important step:</p><pre class="language-shell lang-shell"><code class="language-shell lang-shell">pip install torch==2.7.1 torchvision==0.22.1 torchaudio==2.7.1 --index-url https://download.pytorch.org/whl/cu128
</code></pre><p>Installation will be <em>aborted</em> if your Python version is not 3.9.</p><pre class="language-shell lang-shell"><code class="language-shell lang-shell">pip install opencv-python joblib
</code></pre><p>Finally, ensure <code>numpy==1.22</code>. If not, please uninstall the current numpy using the <code>pip</code> and reinstall version 1.22.</p><p><strong><em>Step 6: Modify Modules</em></strong></p><p>First, navigate to and enter your project directory. For example, my directory is <code>~/Code/gaussian-splatting</code>.</p><pre class="language-shell lang-shell"><code class="language-shell lang-shell">cd ~/Code/gaussian-splatting
</code></pre><p>and edit file to include float.h: <code>#include &lt;float.h&gt;</code></p><pre class="language-shell lang-shell"><code class="language-shell lang-shell">vim submodules/simple-knn/simple-knn.cu
</code></pre><p><strong><em>Step 7: Compile Modules</em></strong></p><pre class="language-shell lang-shell"><code class="language-shell lang-shell">pip install submodules/diff-gaussian-rasterization 
pip install submodules/simple-knn
pip install submodules/fused-ssim
</code></pre>
<p><strong><em>Final Step</em></strong></p><p>After saving the dataset, you can execute the command to begin training.</p><pre class="language-shell lang-shell"><code class="language-shell lang-shell">python train.py -s ./xxx
</code></pre></div><p style="text-align:right"><a href="https://fmcf.cc/posts/technology/Build-Gaussian-Splatting#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://fmcf.cc/posts/technology/Build-Gaussian-Splatting</link><guid isPermaLink="true">https://fmcf.cc/posts/technology/Build-Gaussian-Splatting</guid><dc:creator><![CDATA[Magneto]]></dc:creator><pubDate>Sun, 24 Aug 2025 05:38:29 GMT</pubDate></item><item><title><![CDATA[我独自等待]]></title><description><![CDATA[<div><blockquote>该渲染由 MX-SPACE API 生成，可能存在排版问题，最佳体验请前往：<a href="https://fmcf.cc/notes/15">https://fmcf.cc/notes/15</a></blockquote><div><p>“祝福未来的你幸福，以后结婚记得给我发请帖~（微笑）再见~”</p><p>  今天是你的生日，一年前也是，我清晰的记得，一举一动、一颦一笑都深深刻在我的心底，因这件事吵架，更因这件事离别，谁曾想这是最后一次见面，或许你已经开始奔赴新的生活，但我依旧妄图在这里独自等待。</p><p>  你是我的天使，我的希望，曾经。当我曾坠入谷底，是你一点一点帮助我，我喜欢坐在你身边，听你讲故事，看着你弹琴，也喜欢和你在一起散步，畅想我们的未来。你也是我的挚友，我们在最美的中学时代相遇，陪伴彼此走过点滴，计算机和摄影是你曾最爱的话题，我追逐你的脚步，一点又一滴，彼此进步，勇往直前。</p><p>  你是我的梦魇，我的绝望，现在。你最终还是将我推入深渊，我一次又一次恳求你，渴求你不要扔掉我，即使是远远的看着你，你也狠心把我抛弃。我从未如此爱一个人，甚达后无来者之境地，我未曾想过如此深厚的友谊甚至是爱情，最终要如此收尾。</p><p>  我曾认为时间会冲淡一切，但在爱你这件事上，仍未改变。我常常夜里梦见你，你在我的梦里还是那样温柔，热情的与我相拥。</p><p>  “我还以为再也见不到你了！抱抱！！（扑上去）”，可再也见不到了，这是梦，即使不愿醒来，不愿相信，但梦醒了，我还是要面对如此的事实。</p><p>  生日快乐，允中。即使你可能再也不会，也不愿看到我的祝福。</p></div><p style="text-align:right"><a href="https://fmcf.cc/notes/15#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://fmcf.cc/notes/15</link><guid isPermaLink="true">https://fmcf.cc/notes/15</guid><dc:creator><![CDATA[Magneto]]></dc:creator><pubDate>Sun, 17 Aug 2025 12:16:20 GMT</pubDate></item><item><title><![CDATA[使用 Ncatbot 构建多模态对话插件，对接 OpenAI 与 Ollama 的实现]]></title><description><![CDATA[<div><blockquote>该渲染由 MX-SPACE API 生成，可能存在排版问题，最佳体验请前往：<a href="https://fmcf.cc/posts/technology/ncatbot-multimodal-plugin">https://fmcf.cc/posts/technology/ncatbot-multimodal-plugin</a></blockquote><div><h2 id="0-">0. 前言</h2><p>项目已在 Github 上开源，这里是一些基础的技术讲解。</p><p><a href="https://github.com/ouyangyanhuo/ModelChat">https://github.com/ouyangyanhuo/ModelChat</a></p>
<h2 id="1-">1. 结构设计：插件化工程</h2><p>在项目中，采用插件化架构进行系统构建，利用 Ncatbot 的插件模式，可以很好的将主程序与功能隔离开。</p><p>插件架构的优势在于<strong>模块边界清晰、依赖隔离度高、系统可拓展性强</strong>。标准的插件化极大程度上将代码之间解耦，无论是开发还是部署都是十分友好的。</p><p>项目系统目录结构如下：</p><pre class=""><code class="">ModuleChat/
├── main.py            # 插件主入口，负责指令注册与调度逻辑
├── chat.py            # 模型适配层，封装本地和云端模型的调用
├── config.yml         # 配置文件，集中控制模型参数与启用选项
├── requirements.txt   # 依赖库
└── cache/
    └── history.json  # 聊天历史记忆文件
</code></pre>
<p><code>chat.py</code> 是系统的核心模块，主程序 <code>main.py</code> 负责接收、解析指令，并且路由消息到模型模块处理。在开发中这是十分友好的一种结构，使得专注于单个功能模块的开发与调试，极大降低了维护复杂度。</p><p>而配置项集中于 <code>config.yml</code>，进一步提升了灵活性与环境适配能力。</p><p>采用临时 json 文件的方式，记录指令的调用与回复，并且传入 API 接口，可以使得大模型可以获得一定程度上的短期记忆功能，但一定程度上来说，这对系统并不友好，我认为更优解是采用数据库的形式，但使用数据库很大程度上增大了系统的复杂性，所以使用 json 文件是一个很好的代替选择。</p><h2 id="2-">2. 插件主程序：指令解耦与路由中枢</h2><p><code>main.py</code> 是插件的主入口，通过 <code>register_user_func</code> 方法注册两个指令：<code>/chat</code> 和 <code>/clear chat_history</code>，分别对应聊天功能和历史记录清除。</p><p>此外，主程序支持对图像消息的自动识别，提取其中的图像 URL 并传递给 <code>chat_model_instance.recognize_image</code> 方法，自动获取视觉描述。</p><pre class="language-python lang-python"><code class="language-python lang-python">            if image_url and self.chat_model.get(&#x27;enable_vision&#x27;, True) and not self.chat_model.get(&#x27;use_local_model&#x27;):
                # 使用图像识别功能
                image_description = await chat_model_instance.recognize_image(image_url)
                user_input = f&quot;用户发送了一张图片，图片描述是：{image_description}。用户说：{user_input}&quot;
            elif image_url and not self.chat_model.get(&#x27;enable_vision&#x27;, True):
                # 图像识别功能未开启,但检测是否是本地模型
                if self.chat_model.get(&#x27;use_local_model&#x27;):
                    user_input = f&quot;用户发送了一张图片，但用户使用的是本地模型，无法进行图像识别。用户说：{user_input}&quot;
                else:
                    user_input = f&quot;用户发送了一张图片，但图像识别功能未开启。用户说：{user_input}&quot;
</code></pre>
<p>获取到视觉描述后，再转到语言大模型来进行输出，其实这是一种很好的解决方法，在当前使用场景下，更多的是需要对图片进行识别后对内容进行分析处理，而不是处理图像本身，这可以很大程度上减少 API 调用，提高缓存命中率，减少 TOKEN 使用，从而降低 API 调用成本，并且可以使用云端大模型来进行识别后，再交给本地大模型回答，近一步压缩成本。</p><p>错误处理方面，<code>try...except</code> 包裹了整个聊天逻辑，避免了图像解码失败或 API 异常造成主流程崩溃，保持插件健壮性。</p><p>整合来看，<code>main.py</code> 是典型的“轻控制器”模式，仅协调各组件而不承担业务逻辑细节，使整个插件具备良好的工程可读性。</p><h2 id="3-">3. 模型适配模块：多模型封装与语义一致性</h2><p><code>chat.py</code> 是插件的核心逻辑。它负责处理模型调用、聊天历史记忆、图像识别等任务。为了兼容多种模型接口（如 OpenAI API 与 Ollama 本地服务），采用了统一封装接口的策略，使外部调用者无需关注模型细节，只需通过 <code>useCloudModel()</code> 或 <code>useLocalModel()</code> 两个方法即可完成对话。</p><pre class="language-python lang-python"><code class="language-python lang-python">    async def useLocalModel(self, msg: BaseMessage, user_input: str):
        &quot;&quot;&quot;使用本地模型处理消息&quot;&quot;&quot;
        try:
            # 构建消息列表，包含历史记录
            messages = self._build_messages(user_input, msg.user_id if hasattr(msg, &#x27;user_id&#x27;) else None)
            response: ChatResponse = chat(
                model=self.config[&#x27;model&#x27;],
                messages=messages
            )
            reply = response.message.content.strip()

            # 保存当前对话到历史记录
            if hasattr(msg, &#x27;user_id&#x27;):
                self._update_user_history(msg.user_id, {&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: user_input})
                self._update_user_history(msg.user_id, {&quot;role&quot;: &quot;assistant&quot;, &quot;content&quot;: reply})
        except Exception as e:
            reply = f&quot;请求出错了：{str(e)}&quot;
        return reply
</code></pre><p>值得注意的是，由于 OpenAI 接口调用和 Ollama 调用存在些许不同，并且某些模型的参数并不完全，所以相比起来，使用 OpenAI 接口，调用云端模型其实可以获得更好的体验，例如我们可以控制模型的 temperature 使它更具想象力亦或者是更注重实时，减少幻觉。</p><p>所有用户历史被存储在 <code>cache/history.json</code> 文件中，这是一种持久化保存的方案，并且可以具备一定程度的可追溯性，历史记录通过 <code>_update_user_history</code> 方法动态更新，控制在配置文件设定的最大轮数之内。这种方式防止了上下文过大带来的性能问题，同时确保模型能理解连续上下文，提升回答质量，使得即使对接接口也可以拥有近似记忆的能力。</p><p>类中还集成了 OpenAI 的图像识别模型，通过 <code>_build_vision_messages</code> 构建多模态消息结构。并且设计上我将图像处理、消息构造、异常处理、调用模型等函数分离，使得开发中可以更快定位问题所在，开源后也更利于其他开发者阅读。</p><h2 id="4-openai">4. 云端模型对接（OpenAI）：标准化封装</h2><p>云端模型的调用主要通过 <code>openai</code> 官方库封装，利用 <code>chat.completions.create</code> 方法完成上下文构建与回复生成。在每一次调用中都通过 <code>_build_messages()</code> 方法构造完整的对话上下文，加入系统提示词和使用 <code>cache/history.json</code> 保存的历史记录，实现多轮记忆式对话。</p><pre class="language-python lang-python"><code class="language-python lang-python">    def _build_messages(self, user_input: str, user_id: str = None):
        &quot;&quot;&quot;构建消息列表&quot;&quot;&quot;
        messages = []
        
        # 添加系统提示词
        system_prompt = self.config.get(&#x27;system_prompt&#x27;, &quot;你是一名聊天陪伴机器人&quot;)
        messages.append({&quot;role&quot;: &quot;system&quot;, &quot;content&quot;: system_prompt})

        if user_id:
            history = self._get_user_history(user_id)
            messages.extend(history)
        
        # 添加当前用户输入
        messages.append({&quot;role&quot;: &quot;user&quot;, &quot;content&quot;: user_input})
        return messages
</code></pre>
<p>调用逻辑中封装了 <code>temperature</code> 参数，支持通过配置文件灵活控制模型输出的随机性。</p><p>当遇到 Bug report 时，统一使用 return 将报错反馈到用户面，这样可以减少大量的报错开发处理，并且将常见的由于配置失误导致的问题，更清晰的反馈出来，即<strong>统一模型+规则处理</strong>的两种方法来反馈运行时遇到的问题。</p><pre class="language-python lang-python"><code class="language-python lang-python">                if &quot;401&quot; in str(fallback_error) or &quot;Unauthorized&quot; in str(fallback_error):
                    raise Exception(&quot;模型API认证失败，请检查配置文件&quot;)
                raise Exception(f&quot;图像识别出错: {str(e)}, 备用方法也失败: {str(fallback_error)}&quot;)
</code></pre>
<p>返回结果后，会将本轮问答同步到用户历史缓存中，并保存到本地文件中，确保下一轮能正常取回上下文。这样可以降低内存依赖、增强缓存命中率，同时为后续调试与行为复现提供依据。</p><h2 id="5-ollama">5. 本地模型调用（Ollama）：轻量化推理与接口统一</h2><p>本地模型的调用通过 <code>ollama.chat()</code> 完成，复用了 <code>_build_messages()</code> 的上下文构建逻辑，以确保调用逻辑与云端一致，保持接口一致性。</p><p>这种本地推理机制的优点非常明显：在无网络或私有部署环境下依旧能使用智能对话功能，极大增强了插件的部署灵活性和安全性。即便是在隐私敏感场景中，也可以做到本地化部署与运行。</p><p>设计上保持本地与云端调用接口一致（都封装为 <code>use*Model()</code>），外部调用方无需判断模型来源，从而降低复杂度。除此之外，同样实现了历史记录更新、异常捕获机制，使本地模型具备与云端一致的功能完整度与稳定性。</p><h2 id="6-">6. 图像识别处理逻辑：多模态输入的语义增强策略</h2><p>图像识别功能是本插件的一大亮点。插件支持识别图像消息并通过 OpenAI 视觉模型进行处理。整个流程如下：</p><ul><li><ol start="1"><li><p>从图像消息中提取 URL；</p><pre class="language-python lang-python"><code class="language-python lang-python">for segment in msg.message:
   if isinstance(segment, dict) and segment.get(&quot;type&quot;) == &quot;image&quot;:
       image_url = segment.get(&quot;data&quot;, {}).get(&quot;url&quot;)
       break
</code></pre>
</li></ol></li><li><ol start="2"><li>通过 HTTP 请求获取图像内容并进行 Base64 编码；</li></ol></li><li><ol start="3"><li>构造视觉输入格式（包括 <code>image_url</code> 和 <code>text prompt</code>）；</li></ol></li></ul><pre class="language-python lang-python"><code class="language-python lang-python">            response = requests.get(image_url)
            response.raise_for_status()
            return base64.b64encode(response.content).decode(&#x27;utf-8&#x27;)
</code></pre>
<ul><li><ol start="4"><li>调用视觉模型完成图像描述；</li></ol></li></ul><pre class="language-python lang-python"><code class="language-python lang-python">            # 获取并编码图片
            image_data = self._encode_image_from_url(image_url)
            
            # 构建消息
            messages = self._build_vision_messages(image_data, prompt)
            
            # 调用视觉模型
            response = self.vision_client.chat.completions.create(
                model=self.config.get(&#x27;vision_model&#x27;),
                messages=messages,
                temperature=self.config.get(&#x27;model_temperature&#x27;, 0.6),
                stream=False,
                max_tokens=2048
            )
</code></pre>
<ul><li><ol start="5"><li>将图像描述拼接到用户输入中，提升上下文语义完整性。</li></ol></li></ul><p>这一机制有效解决了图文混合输入场景中的信息不对称问题，同时通过分级调节调用，可以仅利用云端高算力处理复杂问题，再交给本地处理简单化后的问题，极大程度上减少了 TOKEN 使用率。</p><p>异常处理方面，我们设计了两级降级策略：如主调用失败则尝试纯文本 fallback prompt；若仍失败，则提示用户检查 API 密钥或模型状态。这种容错设计使插件能在部分失败时依旧保持服务不中断。</p><h2 id="7-">7. 聊天历史系统：记忆窗口控制</h2><p>聊天历史存储在 <code>cache/history.json</code> 文件中，按用户维度管理。该设计使系统可同时服务多用户，并为每个用户维护独立上下文。通过 <code>_get_user_history</code> 和 <code>_update_user_history</code> 方法，插件能自动在每轮对话中注入历史信息，实现类“记忆式”问答体验。</p><p>我们对历史记录长度做了窗口限制（默认10轮），以控制上下文规模并避免模型处理压力过大、过度消耗 TOKEN。缓存更新为同步写入的操作，确保在系统崩溃、断电等异常情况下不会造成信息丢失。</p><pre class="language-python lang-python"><code class="language-python lang-python">    async def clear_user_history(self, user_id: str):
        &quot;&quot;&quot;清除指定用户的历史记录&quot;&quot;&quot;
        user_id = str(user_id)
        if user_id in self.history:
            del self.history[user_id]
            self._save_history()
            reply = &quot;已清空聊天记录&quot;
        else:
            reply = &quot;没有找到用户的聊天记录&quot;
        return reply
</code></pre>
<p>此外，还支持通过指令 <code>/clear chat_history</code> 主动清除用户历史，为隐私或重新对话提供了便利。这种机制让插件既具备持久性，又保留用户主动控制空间。</p><h2 id="8-debug--log">8. DEBUG &amp; LOG</h2><p>在调试处打断点、print 标志 这都是一个很好的测试习惯，我还从微信开发学到了一招 —— print(&quot;FUCK&quot;)，在长期运行时偶尔会出现崩溃的情况，这时候事实上可以在 log 中输出指定的字符，当查看日志定位问题的时候，可以直接搜索字符串来迅速定位，FUCK 无疑是一种有趣的方式。</p></div><p style="text-align:right"><a href="https://fmcf.cc/posts/technology/ncatbot-multimodal-plugin#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://fmcf.cc/posts/technology/ncatbot-multimodal-plugin</link><guid isPermaLink="true">https://fmcf.cc/posts/technology/ncatbot-multimodal-plugin</guid><dc:creator><![CDATA[Magneto]]></dc:creator><pubDate>Mon, 04 Aug 2025 09:25:02 GMT</pubDate></item><item><title><![CDATA[一直切，你的刀还会利吗？]]></title><description><![CDATA[<div><blockquote>该渲染由 MX-SPACE API 生成，可能存在排版问题，最佳体验请前往：<a href="https://fmcf.cc/notes/14">https://fmcf.cc/notes/14</a></blockquote><div><p>民航局发布禁止无 3C 充电宝带上飞机是一件十分正常的事情，毕竟飞机在飞行过程中发生爆炸事件是非常恐怖的事情。</p><p>地方的高铁站跟着凑什么热闹？? 甚至铁路局都说过很多次，不会禁止充电宝上高铁，今天依旧看到有高铁站在拦截无 3C 充电宝。</p><p>有些人就这么害怕承担责任吗？就这么害怕自己的乌纱帽掉下来吗？</p><p>一直切，你的刀还会利吗？</p></div><p style="text-align:right"><a href="https://fmcf.cc/notes/14#comments">看完了？说点什么呢</a></p></div>]]></description><link>https://fmcf.cc/notes/14</link><guid isPermaLink="true">https://fmcf.cc/notes/14</guid><dc:creator><![CDATA[Magneto]]></dc:creator><pubDate>Mon, 07 Jul 2025 05:21:21 GMT</pubDate></item></channel></rss>