从乐谱图像提取横竖线并细化边缘

扫描乐谱中的五线谱、符干和其他直线具有明显方向。与其先寻找复杂的识别模型,不如让一个细长的结构元素沿指定方向“筛选”像素。OpenCV 教程用腐蚀和膨胀分别提取水平与垂直结构,再对粗糙边缘做局部平滑,展示了一个完整的形态学图像处理流程。

本文依据 OpenCV 4.13.0 的 Extract horizontal and vertical lines by using morphological operations 全文整理。原作者为 Theodore Tsesmelis,C++ 示例源码署名 OpenCV team。原教程标注适用于 OpenCV ≥ 3.0;本文按固定 4.13.0 文档及同名仓库标签核对 C++、Java、Python 三份实现,核对日期为 2026 年 10 月 5 日。

教程的目标是用 erode、dilate 和 getStructuringElement 从乐谱中分离横竖线。输出是线条图与平滑后的图像,没有完成音高、节奏、音符类别或 OCR 识别。这里没有运行图像处理代码;原创算法示意图与原教程结果图分别标注,不冒充本次实测输出。

形态学提线示意:灰度图反色并自适应二值化后分为水平和垂直两个分支,分别使用宽除以30乘1与1乘高除以30的核执行先腐蚀后膨胀;垂直结果反色后提边、膨胀掩膜并局部平滑。
原创算法示意图,展示真实参数与数据流;不是原图处理结果或软件截图。

形态学操作比较的是核覆盖范围中的像素

形态学是一组以结构元素为依据的图像操作。对于输出图中的每个像素,算法查看输入图中相应位置及其邻域,再根据核的形状与大小计算结果。选择不同结构元素,就能让操作对特定形状更敏感。

膨胀取结构元素覆盖的邻域像素最大值。二值图中,只要核所覆盖的有效位置有一个前景像素,输出就成为前景;以白色为前景时,它表现为对象边界向外扩张。腐蚀则取邻域最小值,白色前景会缩小,窄小或不连续结构可能被去掉。原教程分别展示二值和灰度示例;对多通道图像,这类数值运算按通道处理,不能把“颜色最大”理解为人眼感知的最亮颜色。

结构元素是由 0 和 1 表示的邻域形状,1 对应参与运算的位置。核通常远小于原图,锚点决定当前被处理像素对应核中的哪个位置。常见形状包括直线、菱形、圆盘与周期性线条。原文用 7×7 菱形展示中心锚点;本例则采用两个矩形核,一个横向细长,一个纵向细长。核越接近想保留对象的方向与尺度,筛选效果就越有针对性。

以下原教程图像保持原始像素;原文的 src.png 与 gray.png 内容一致,合并展示并保留两个阶段的说明。本文原创算法示意图另行标注。

二值图膨胀:核覆盖邻域取最大值
二值图膨胀:核覆盖邻域取最大值。来源:Theodore Tsesmelis / OpenCV 4.13.0 原教程;保留原图,不是本次复现实验。
灰度图膨胀:核覆盖邻域取最大值
灰度图膨胀:核覆盖邻域取最大值。来源:Theodore Tsesmelis / OpenCV 4.13.0 原教程;保留原图,不是本次复现实验。
二值图腐蚀:核覆盖邻域取最小值
二值图腐蚀:核覆盖邻域取最小值。来源:Theodore Tsesmelis / OpenCV 4.13.0 原教程;保留原图,不是本次复现实验。
灰度图腐蚀:核覆盖邻域取最小值
灰度图腐蚀:核覆盖邻域取最小值。来源:Theodore Tsesmelis / OpenCV 4.13.0 原教程;保留原图,不是本次复现实验。
7×7 菱形结构元素及中心锚点
7×7 菱形结构元素及中心锚点。来源:Theodore Tsesmelis / OpenCV 4.13.0 原教程;保留原图,不是本次复现实验。

准备输入与运行环境

输入应使用教程自身的乐谱图 src.png(固定 4.13.0 标签)。原网页的下载链接指向会继续变化的 4.x 分支,因此这里给出固定标签,便于与代码核对。不要随便换一张类似乐谱后声称复现了原例,也不要假设 Python 轮子会自动带齐教程图片。

本文采用的 Python 复现基线是 CPython 3.11、opencv-python==4.13.0.92 与 numpy==2.2.6,使用 CPU 标准包。它是待实际复现的环境约定,本次未安装这些依赖。标准、contrib、headless 包共用 cv2 命名空间,一般应选一种;本文代码调用 imshow,需要图形会话,不能直接把 headless 包当作等价替换。可用轮子仍取决于 Python、操作系统和处理器架构,包的最低 Python 元数据并不意味着每个平台都有预编译文件。

python -m pip install "opencv-python==4.13.0.92" "numpy==2.2.6"
python morph_lines_detection.py /path/to/src.png

C++ 版本需要对应的 OpenCV 头文件和库;Java 版本需要相匹配的 Java 绑定及本地库。三个版本都逐阶段打开窗口,等待按键后销毁该阶段窗口。远程无桌面的机器需要另行设计保存图像的输出方式,本文没有把 GUI 样例伪装成无界面服务。

从输入到二值图

读取并检查图像

Python 用 cv.imread(path, cv.IMREAD_COLOR) 读取输入,失败时返回 None;C++ 与 Java 检查 Mat.empty()。C++ 原例使用 CommandLineParser 与 samples::findFile,默认名称是 notes.png,但网页提供的示例图叫 src.png。因此应明确传入图片路径,而不是依赖文件名恰好存在。

以颜色方式读取会得到 BGR 图。三个版本随后将三通道输入转换为灰度;如果本来已经是灰度,则直接使用。显示阶段分别命名为 src 与 gray。原例的灰度分支是教学性防守写法,并不意味着 IMREAD_COLOR 会保留原输入的单通道形式。

原教程乐谱输入(原文灰度阶段图与此文件字节一致)
原教程乐谱输入(原文灰度阶段图与此文件字节一致)。来源:Theodore Tsesmelis / OpenCV 4.13.0 原教程;保留原图,不是本次复现实验。

反色后做自适应阈值

黑字白底的乐谱先反色,让希望保留的线条成为亮前景,再执行局部均值阈值:

gray = cv.bitwise_not(gray)
bw = cv.adaptiveThreshold(
    gray, 255, cv.ADAPTIVE_THRESH_MEAN_C,
    cv.THRESH_BINARY, 15, -2
)

最大输出值为 255;局部窗口是 15×15,方法为邻域均值,常量 C 为 -2。OpenCV 的阈值为局部均值减去 C,所以这里相当于均值加 2;不能把负号抄丢。自适应阈值使用 8 位单通道输入,窗口大小要求是大于 1 的奇数。参数针对原例选定,不是所有扫描分辨率与光照条件的通用答案。

原教程反色并自适应阈值后的二值图
原教程反色并自适应阈值后的二值图。来源:Theodore Tsesmelis / OpenCV 4.13.0 原教程;保留原图,不是本次复现实验。

把横向与纵向处理分开

从二值图复制出两个独立缓冲区 horizontal 与 vertical。C++ 与 Java 使用 clone();Python 使用 np.copy()。不能把两个变量都简单指向同一可变数组,再以为它们是彼此独立的输出。

水平结构元素

原例把图像宽度整数除以 30,得到水平核的长度,高度固定为 1:

horizontal_size = horizontal.shape[1] // 30
horizontal_structure = cv.getStructuringElement(
    cv.MORPH_RECT, (horizontal_size, 1)
)
horizontal = cv.erode(horizontal, horizontal_structure)
horizontal = cv.dilate(horizontal, horizontal_structure)

先腐蚀、后用同一个核膨胀,是开运算的组合。第一步淘汰在水平方向上不足以容纳整个核的细小结构;第二步让保留下来的结构重新延展。它通常保留较长的横线,并抑制不符合这个方向和尺度的对象,而不是无条件保留“所有水平像素”。

原教程水平线结构元素及锚点示意
原教程水平线结构元素及锚点示意。来源:Theodore Tsesmelis / OpenCV 4.13.0 原教程;保留原图,不是本次复现实验。
原教程水平分支结果:主要保留五线谱横线
原教程水平分支结果:主要保留五线谱横线。来源:Theodore Tsesmelis / OpenCV 4.13.0 原教程;保留原图,不是本次复现实验。

垂直结构元素

垂直分支同样用图像高度整数除以 30,核宽为 1:

vertical_size = vertical.shape[0] // 30
vertical_structure = cv.getStructuringElement(
    cv.MORPH_RECT, (1, vertical_size)
)
vertical = cv.erode(vertical, vertical_structure)
vertical = cv.dilate(vertical, vertical_structure)

两个核都使用 OpenCV 的宽、高顺序,而 NumPy 的 shape 是行、高度在前,列、宽度在后。把顺序混淆,会把希望提取的方向反过来。图像任一相关尺寸小于 30 时,整数除法还会得到 0,导致无效核;本文配套代码增加了输入尺寸检查,但实际核长仍需根据扫描分辨率、线宽、倾斜和对象尺寸调整。

横线与竖线图是两个独立结果。后续平滑处理沿着垂直分支进行;原例没有把水平结果与垂直结果合并为完整的音符识别模型,也没有做乐谱几何校正。旋转或透视变形明显时,轴对齐核的效果会下降。

原教程垂直线结构元素及锚点示意
原教程垂直线结构元素及锚点示意。来源:Theodore Tsesmelis / OpenCV 4.13.0 原教程;保留原图,不是本次复现实验。
原教程垂直分支结果:抑制横线,保留音符相关竖直结构
原教程垂直分支结果:抑制横线,保留音符相关竖直结构。来源:Theodore Tsesmelis / OpenCV 4.13.0 原教程;保留原图,不是本次复现实验。

细化边缘:只在边界附近替换为平滑值

原教程观察到提取后的音符相关结构边缘较粗糙,于是把 vertical 反色,再用一个更小的局部阈值窗口产生边缘区域掩膜。这里的“提边”是该教程采用的阈值技巧,并非调用 Canny。

完整处理顺序为:反色垂直结果;用 3×3 自适应阈值找出边缘区域;用 2×2 全 1 核膨胀掩膜;复制垂直图并做 2×2 均值模糊;最后只在掩膜非零位置,将原图像素替换为平滑结果。

vertical = cv.bitwise_not(vertical)
edges = cv.adaptiveThreshold(
    vertical, 255, cv.ADAPTIVE_THRESH_MEAN_C,
    cv.THRESH_BINARY, 3, -2
)
edges = cv.dilate(edges, np.ones((2, 2), np.uint8))
smooth = cv.blur(np.copy(vertical), (2, 2))
rows, cols = np.where(edges != 0)
vertical[rows, cols] = smooth[rows, cols]

C++ 和 Java 用 smooth.copyTo(vertical, edges) 完成带掩膜复制,Python 用 np.where 取出同一组坐标。最终不是把整幅图都替换成模糊图,非掩膜位置保持不变。这有助于在平滑边缘时保留其他区域,但模糊之后像素不一定仍只有 0 与 255,因此不能自动把最终输出当成严格二值图。

原教程掩膜局部平滑结果,并非本次运行输出
原教程掩膜局部平滑结果,并非本次运行输出。来源:Theodore Tsesmelis / OpenCV 4.13.0 原教程;保留原图,不是本次复现实验。

三种语言的完整程序

以下三份程序都保留原例的全部处理阶段及窗口显示。为明确失败行为,均新增“宽和高至少为 30”的检查;Python 入口改为把 main 返回值传给进程退出状态;C++ 显式补上 imgcodecs.hpp 头文件。除这些已标明的静态修订外,算法、阈值、核尺寸规则与最终掩膜复制流程保持原样。本次没有运行或编译它们。

Python

"""
@file morph_lines_detection.py
@brief Use morphology transformations for extracting horizontal and vertical lines sample code
"""
import numpy as np
import sys
import cv2 as cv


def show_wait_destroy(winname, img):
    cv.imshow(winname, img)
    cv.moveWindow(winname, 500, 0)
    cv.waitKey(0)
    cv.destroyWindow(winname)

def main(argv):
    # [load_image]
    # Check number of arguments
    if len(argv) < 1:
        print ('Not enough parameters')
        print ('Usage:\nmorph_lines_detection.py < path_to_image >')
        return -1

    # Load the image
    src = cv.imread(argv[0], cv.IMREAD_COLOR)

    # Check if image is loaded fine
    if src is None:
        print ('Error opening image: ' + argv[0])
        return -1

    # 中文整理新增:防止尺寸除以 30 后得到零尺寸核。
    if min(src.shape[:2]) < 30:
        print("Image width and height must be at least 30 pixels")
        return 1

    # Show source image
    cv.imshow("src", src)
    # [load_image]
    # [gray]
    # Transform source image to gray if it is not already
    if len(src.shape) != 2:
        gray = cv.cvtColor(src, cv.COLOR_BGR2GRAY)
    else:
        gray = src

    # Show gray image
    show_wait_destroy("gray", gray)
    # [gray]
    # [bin]
    # Apply adaptiveThreshold at the bitwise_not of gray, notice the ~ symbol
    gray = cv.bitwise_not(gray)
    bw = cv.adaptiveThreshold(gray, 255, cv.ADAPTIVE_THRESH_MEAN_C, \
                                cv.THRESH_BINARY, 15, -2)
    # Show binary image
    show_wait_destroy("binary", bw)
    # [bin]

    # [init]
    # Create the images that will use to extract the horizontal and vertical lines
    horizontal = np.copy(bw)
    vertical = np.copy(bw)
    # [init]
    # [horiz]
    # Specify size on horizontal axis
    cols = horizontal.shape[1]
    horizontal_size = cols // 30

    # Create structure element for extracting horizontal lines through morphology operations
    horizontalStructure = cv.getStructuringElement(cv.MORPH_RECT, (horizontal_size, 1))

    # Apply morphology operations
    horizontal = cv.erode(horizontal, horizontalStructure)
    horizontal = cv.dilate(horizontal, horizontalStructure)
    # Show extracted horizontal lines
    show_wait_destroy("horizontal", horizontal)
    # [horiz]

    # [vert]
    # Specify size on vertical axis
    rows = vertical.shape[0]
    verticalsize = rows // 30

    # Create structure element for extracting vertical lines through morphology operations
    verticalStructure = cv.getStructuringElement(cv.MORPH_RECT, (1, verticalsize))
    # Apply morphology operations
    vertical = cv.erode(vertical, verticalStructure)
    vertical = cv.dilate(vertical, verticalStructure)

    # Show extracted vertical lines
    show_wait_destroy("vertical", vertical)
    # [vert]

    # [smooth]
    # Inverse vertical image
    vertical = cv.bitwise_not(vertical)
    show_wait_destroy("vertical_bit", vertical)
    '''
    Extract edges and smooth image according to the logic
    1. extract edges
    2. dilate(edges)
    3. src.copyTo(smooth)
    4. blur smooth img
    5. smooth.copyTo(src, edges)
    '''

    # Step 1
    edges = cv.adaptiveThreshold(vertical, 255, cv.ADAPTIVE_THRESH_MEAN_C, \
                                cv.THRESH_BINARY, 3, -2)
    show_wait_destroy("edges", edges)
    # Step 2
    kernel = np.ones((2, 2), np.uint8)
    edges = cv.dilate(edges, kernel)
    show_wait_destroy("dilate", edges)

    # Step 3
    smooth = np.copy(vertical)

    # Step 4
    smooth = cv.blur(smooth, (2, 2))

    # Step 5
    (rows, cols) = np.where(edges != 0)
    vertical[rows, cols] = smooth[rows, cols]

    # Show final result
    show_wait_destroy("smooth - final", vertical)
    # [smooth]

    return 0

if __name__ == "__main__":
    sys.exit(main(sys.argv[1:]))

C++

/**
 * @file Morphology_3(Extract_Lines).cpp
 * @brief Use morphology transformations for extracting horizontal and vertical lines sample code
 * @author OpenCV team
 */
#include <opencv2/core.hpp>
#include <opencv2/imgproc.hpp>
#include <opencv2/highgui.hpp>
#include <opencv2/imgcodecs.hpp>
#include <iostream>

void show_wait_destroy(const char* winname, cv::Mat img);

using namespace std;
using namespace cv;
int main(int argc, char** argv)
{
    //! [load_image]
    CommandLineParser parser(argc, argv, "{@input | notes.png | input image}");
    Mat src = imread( samples::findFile( parser.get<String>("@input") ), IMREAD_COLOR);
    if (src.empty())
    {
        cout << "Could not open or find the image!\n" << endl;
        cout << "Usage: " << argv[0] << " <Input image>" << endl;
        return -1;
    }

    // 中文整理新增:避免零尺寸结构元素。
    if (src.cols < 30 || src.rows < 30) {
        std::cerr << "Image width and height must be at least 30 pixels\n";
        return 1;
    }
    // Show source image
    imshow("src", src);
    //! [load_image]
    //! [gray]
    // Transform source image to gray if it is not already
    Mat gray;

    if (src.channels() == 3)
    {
        cvtColor(src, gray, COLOR_BGR2GRAY);
    }
    else
    {
        gray = src;
    }

    // Show gray image
    show_wait_destroy("gray", gray);
    //! [gray]

    //! [bin]
    // Apply adaptiveThreshold at the bitwise_not of gray, notice the ~ symbol
    Mat bw;
    adaptiveThreshold(~gray, bw, 255, ADAPTIVE_THRESH_MEAN_C, THRESH_BINARY, 15, -2);
    // Show binary image
    show_wait_destroy("binary", bw);
    //! [bin]

    //! [init]
    // Create the images that will use to extract the horizontal and vertical lines
    Mat horizontal = bw.clone();
    Mat vertical = bw.clone();
    //! [init]

    //! [horiz]
    // Specify size on horizontal axis
    int horizontal_size = horizontal.cols / 30;
    // Create structure element for extracting horizontal lines through morphology operations
    Mat horizontalStructure = getStructuringElement(MORPH_RECT, Size(horizontal_size, 1));

    // Apply morphology operations
    erode(horizontal, horizontal, horizontalStructure, Point(-1, -1));
    dilate(horizontal, horizontal, horizontalStructure, Point(-1, -1));

    // Show extracted horizontal lines
    show_wait_destroy("horizontal", horizontal);
    //! [horiz]
    //! [vert]
    // Specify size on vertical axis
    int vertical_size = vertical.rows / 30;

    // Create structure element for extracting vertical lines through morphology operations
    Mat verticalStructure = getStructuringElement(MORPH_RECT, Size(1, vertical_size));

    // Apply morphology operations
    erode(vertical, vertical, verticalStructure, Point(-1, -1));
    dilate(vertical, vertical, verticalStructure, Point(-1, -1));
    // Show extracted vertical lines
    show_wait_destroy("vertical", vertical);
    //! [vert]

    //! [smooth]
    // Inverse vertical image
    bitwise_not(vertical, vertical);
    show_wait_destroy("vertical_bit", vertical);

    // Extract edges and smooth image according to the logic
    // 1. extract edges
    // 2. dilate(edges)
    // 3. src.copyTo(smooth)
    // 4. blur smooth img
    // 5. smooth.copyTo(src, edges)
    // Step 1
    Mat edges;
    adaptiveThreshold(vertical, edges, 255, ADAPTIVE_THRESH_MEAN_C, THRESH_BINARY, 3, -2);
    show_wait_destroy("edges", edges);

    // Step 2
    Mat kernel = Mat::ones(2, 2, CV_8UC1);
    dilate(edges, edges, kernel);
    show_wait_destroy("dilate", edges);

    // Step 3
    Mat smooth;
    vertical.copyTo(smooth);

    // Step 4
    blur(smooth, smooth, Size(2, 2));

    // Step 5
    smooth.copyTo(vertical, edges);
    // Show final result
    show_wait_destroy("smooth - final", vertical);
    //! [smooth]

    return 0;
}

void show_wait_destroy(const char* winname, cv::Mat img) {
    imshow(winname, img);
    moveWindow(winname, 500, 0);
    waitKey(0);
    destroyWindow(winname);
}

Java

/**
 * @file Morphology_3.java
 * @brief Use morphology transformations for extracting horizontal and vertical lines sample code
 */

import org.opencv.core.*;
import org.opencv.highgui.HighGui;
import org.opencv.imgcodecs.Imgcodecs;
import org.opencv.imgproc.Imgproc;

class Morphology_3Run {

    public void run(String[] args) {
        //! [load_image]
        // Check number of arguments
        if (args.length == 0){
            System.out.println("Not enough parameters!");
            System.out.println("Program Arguments: [image_path]");
            System.exit(-1);
        }

        // Load the image
        Mat src = Imgcodecs.imread(args[0]);

        // Check if image is loaded fine
        if( src.empty() ) {
            System.out.println("Error opening image: " + args[0]);
            System.exit(-1);
        }
        // 中文整理新增:避免零尺寸结构元素。
        if (src.cols() < 30 || src.rows() < 30) {
            System.err.println("Image width and height must be at least 30 pixels");
            System.exit(1);
        }
        // Show source image
        HighGui.imshow("src", src);
        //! [load_image]

        //! [gray]
        // Transform source image to gray if it is not already
        Mat gray = new Mat();

        if (src.channels() == 3)
        {
            Imgproc.cvtColor(src, gray, Imgproc.COLOR_BGR2GRAY);
        }
        else
        {
            gray = src;
        }

        // Show gray image
        showWaitDestroy("gray" , gray);
        //! [gray]
        //! [bin]
        // Apply adaptiveThreshold at the bitwise_not of gray
        Mat bw = new Mat();
        Core.bitwise_not(gray, gray);
        Imgproc.adaptiveThreshold(gray, bw, 255, Imgproc.ADAPTIVE_THRESH_MEAN_C, Imgproc.THRESH_BINARY, 15, -2);

        // Show binary image
        showWaitDestroy("binary" , bw);
        //! [bin]
        //! [init]
        // Create the images that will use to extract the horizontal and vertical lines
        Mat horizontal = bw.clone();
        Mat vertical = bw.clone();
        //! [init]

        //! [horiz]
        // Specify size on horizontal axis
        int horizontal_size = horizontal.cols() / 30;
        // Create structure element for extracting horizontal lines through morphology operations
        Mat horizontalStructure = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size(horizontal_size,1));

        // Apply morphology operations
        Imgproc.erode(horizontal, horizontal, horizontalStructure);
        Imgproc.dilate(horizontal, horizontal, horizontalStructure);

        // Show extracted horizontal lines
        showWaitDestroy("horizontal" , horizontal);
        //! [horiz]
        //! [vert]
        // Specify size on vertical axis
        int vertical_size = vertical.rows() / 30;

        // Create structure element for extracting vertical lines through morphology operations
        Mat verticalStructure = Imgproc.getStructuringElement(Imgproc.MORPH_RECT, new Size( 1,vertical_size));

        // Apply morphology operations
        Imgproc.erode(vertical, vertical, verticalStructure);
        Imgproc.dilate(vertical, vertical, verticalStructure);
        // Show extracted vertical lines
        showWaitDestroy("vertical", vertical);
        //! [vert]

        //! [smooth]
        // Inverse vertical image
        Core.bitwise_not(vertical, vertical);
        showWaitDestroy("vertical_bit" , vertical);

        // Extract edges and smooth image according to the logic
        // 1. extract edges
        // 2. dilate(edges)
        // 3. src.copyTo(smooth)
        // 4. blur smooth img
        // 5. smooth.copyTo(src, edges)
        // Step 1
        Mat edges = new Mat();
        Imgproc.adaptiveThreshold(vertical, edges, 255, Imgproc.ADAPTIVE_THRESH_MEAN_C, Imgproc.THRESH_BINARY, 3, -2);
        showWaitDestroy("edges", edges);

        // Step 2
        Mat kernel = Mat.ones(2, 2, CvType.CV_8UC1);
        Imgproc.dilate(edges, edges, kernel);
        showWaitDestroy("dilate", edges);

        // Step 3
        Mat smooth = new Mat();
        vertical.copyTo(smooth);
        // Step 4
        Imgproc.blur(smooth, smooth, new Size(2, 2));

        // Step 5
        smooth.copyTo(vertical, edges);

        // Show final result
        showWaitDestroy("smooth - final", vertical);
        //! [smooth]

        System.exit(0);
    }

    private void showWaitDestroy(String winname, Mat img) {
        HighGui.imshow(winname, img);
        HighGui.moveWindow(winname, 500, 0);
        HighGui.waitKey(0);
        HighGui.destroyWindow(winname);
    }
}
public class Morphology_3 {
    public static void main(String[] args) {
        // Load the native library.
        System.loadLibrary(Core.NATIVE_LIBRARY_NAME);
        new Morphology_3Run().run(args);
    }
}

如何逐步核对结果

运行原例时,应依次观察 src、gray、binary、horizontal、vertical、vertical_bit、edges、dilate 和 smooth - final。如果二值图已经漏掉细线,后续形态学操作不会恢复原本丢失的信息;如果横线图混入大量非目标对象,应先检查方向和核长;如果垂直结构过度消失,应检查高度尺度与阈值,而不是只加大最后的模糊。

读取外部图像时还应控制文件来源、像素尺寸和内存消耗;示例的 None / empty() 检查只覆盖读入失败,不代表已完成不可信文件处理的完整防护。这里没有网络请求、系统命令或凭据,但不能据此宣称代码“没有漏洞”。

原教程的理论图、乐谱原图和各阶段结果图已从 OpenCV 4.13.0 固定标签保留。它们是原作者提供的图像,并非本次运行结果。

原作者:Theodore Tsesmelis;相关代码:OpenCV team 及贡献者。OpenCV 4.13.0 仓库根许可为 Apache License 2.0,完整许可文本附于文末,源码来源保留;不把仓库总许可自动扩大为所有第三方图片或视频的许可。中文翻译及编辑标注:未完纪,2026-10-05。

版权与许可全文

以下保留本页涉及的来源材料或示例代码的版权、许可条件与免责声明;各自适用范围依原声明。中文翻译及编辑标注:未完纪,2026-10-05。

LICENSE

                                 Apache License
                           Version 2.0, January 2004
                        http://www.apache.org/licenses/

   TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION

   1. Definitions.

      "License" shall mean the terms and conditions for use, reproduction,
      and distribution as defined by Sections 1 through 9 of this document.

      "Licensor" shall mean the copyright owner or entity authorized by
      the copyright owner that is granting the License.

      "Legal Entity" shall mean the union of the acting entity and all
      other entities that control, are controlled by, or are under common
      control with that entity. For the purposes of this definition,
      "control" means (i) the power, direct or indirect, to cause the
      direction or management of such entity, whether by contract or
      otherwise, or (ii) ownership of fifty percent (50%) or more of the
      outstanding shares, or (iii) beneficial ownership of such entity.

      "You" (or "Your") shall mean an individual or Legal Entity
      exercising permissions granted by this License.

      "Source" form shall mean the preferred form for making modifications,
      including but not limited to software source code, documentation
      source, and configuration files.

      "Object" form shall mean any form resulting from mechanical
      transformation or translation of a Source form, including but
      not limited to compiled object code, generated documentation,
      and conversions to other media types.

      "Work" shall mean the work of authorship, whether in Source or
      Object form, made available under the License, as indicated by a
      copyright notice that is included in or attached to the work
      (an example is provided in the Appendix below).

      "Derivative Works" shall mean any work, whether in Source or Object
      form, that is based on (or derived from) the Work and for which the
      editorial revisions, annotations, elaborations, or other modifications
      represent, as a whole, an original work of authorship. For the purposes
      of this License, Derivative Works shall not include works that remain
      separable from, or merely link (or bind by name) to the interfaces of,
      the Work and Derivative Works thereof.

      "Contribution" shall mean any work of authorship, including
      the original version of the Work and any modifications or additions
      to that Work or Derivative Works thereof, that is intentionally
      submitted to Licensor for inclusion in the Work by the copyright owner
      or by an individual or Legal Entity authorized to submit on behalf of
      the copyright owner. For the purposes of this definition, "submitted"
      means any form of electronic, verbal, or written communication sent
      to the Licensor or its representatives, including but not limited to
      communication on electronic mailing lists, source code control systems,
      and issue tracking systems that are managed by, or on behalf of, the
      Licensor for the purpose of discussing and improving the Work, but
      excluding communication that is conspicuously marked or otherwise
      designated in writing by the copyright owner as "Not a Contribution."

      "Contributor" shall mean Licensor and any individual or Legal Entity
      on behalf of whom a Contribution has been received by Licensor and
      subsequently incorporated within the Work.

   2. Grant of Copyright License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      copyright license to reproduce, prepare Derivative Works of,
      publicly display, publicly perform, sublicense, and distribute the
      Work and such Derivative Works in Source or Object form.

   3. Grant of Patent License. Subject to the terms and conditions of
      this License, each Contributor hereby grants to You a perpetual,
      worldwide, non-exclusive, no-charge, royalty-free, irrevocable
      (except as stated in this section) patent license to make, have made,
      use, offer to sell, sell, import, and otherwise transfer the Work,
      where such license applies only to those patent claims licensable
      by such Contributor that are necessarily infringed by their
      Contribution(s) alone or by combination of their Contribution(s)
      with the Work to which such Contribution(s) was submitted. If You
      institute patent litigation against any entity (including a
      cross-claim or counterclaim in a lawsuit) alleging that the Work
      or a Contribution incorporated within the Work constitutes direct
      or contributory patent infringement, then any patent licenses
      granted to You under this License for that Work shall terminate
      as of the date such litigation is filed.

   4. Redistribution. You may reproduce and distribute copies of the
      Work or Derivative Works thereof in any medium, with or without
      modifications, and in Source or Object form, provided that You
      meet the following conditions:

      (a) You must give any other recipients of the Work or
          Derivative Works a copy of this License; and

      (b) You must cause any modified files to carry prominent notices
          stating that You changed the files; and

      (c) You must retain, in the Source form of any Derivative Works
          that You distribute, all copyright, patent, trademark, and
          attribution notices from the Source form of the Work,
          excluding those notices that do not pertain to any part of
          the Derivative Works; and

      (d) If the Work includes a "NOTICE" text file as part of its
          distribution, then any Derivative Works that You distribute must
          include a readable copy of the attribution notices contained
          within such NOTICE file, excluding those notices that do not
          pertain to any part of the Derivative Works, in at least one
          of the following places: within a NOTICE text file distributed
          as part of the Derivative Works; within the Source form or
          documentation, if provided along with the Derivative Works; or,
          within a display generated by the Derivative Works, if and
          wherever such third-party notices normally appear. The contents
          of the NOTICE file are for informational purposes only and
          do not modify the License. You may add Your own attribution
          notices within Derivative Works that You distribute, alongside
          or as an addendum to the NOTICE text from the Work, provided
          that such additional attribution notices cannot be construed
          as modifying the License.

      You may add Your own copyright statement to Your modifications and
      may provide additional or different license terms and conditions
      for use, reproduction, or distribution of Your modifications, or
      for any such Derivative Works as a whole, provided Your use,
      reproduction, and distribution of the Work otherwise complies with
      the conditions stated in this License.

   5. Submission of Contributions. Unless You explicitly state otherwise,
      any Contribution intentionally submitted for inclusion in the Work
      by You to the Licensor shall be under the terms and conditions of
      this License, without any additional terms or conditions.
      Notwithstanding the above, nothing herein shall supersede or modify
      the terms of any separate license agreement you may have executed
      with Licensor regarding such Contributions.

   6. Trademarks. This License does not grant permission to use the trade
      names, trademarks, service marks, or product names of the Licensor,
      except as required for reasonable and customary use in describing the
      origin of the Work and reproducing the content of the NOTICE file.

   7. Disclaimer of Warranty. Unless required by applicable law or
      agreed to in writing, Licensor provides the Work (and each
      Contributor provides its Contributions) on an "AS IS" BASIS,
      WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
      implied, including, without limitation, any warranties or conditions
      of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
      PARTICULAR PURPOSE. You are solely responsible for determining the
      appropriateness of using or redistributing the Work and assume any
      risks associated with Your exercise of permissions under this License.

   8. Limitation of Liability. In no event and under no legal theory,
      whether in tort (including negligence), contract, or otherwise,
      unless required by applicable law (such as deliberate and grossly
      negligent acts) or agreed to in writing, shall any Contributor be
      liable to You for damages, including any direct, indirect, special,
      incidental, or consequential damages of any character arising as a
      result of this License or out of the use or inability to use the
      Work (including but not limited to damages for loss of goodwill,
      work stoppage, computer failure or malfunction, or any and all
      other commercial damages or losses), even if such Contributor
      has been advised of the possibility of such damages.

   9. Accepting Warranty or Additional Liability. While redistributing
      the Work or Derivative Works thereof, You may choose to offer,
      and charge a fee for, acceptance of support, warranty, indemnity,
      or other liability obligations and/or rights consistent with this
      License. However, in accepting such obligations, You may act only
      on Your own behalf and on Your sole responsibility, not on behalf
      of any other Contributor, and only if You agree to indemnify,
      defend, and hold each Contributor harmless for any liability
      incurred by, or claims asserted against, such Contributor by reason
      of your accepting any such warranty or additional liability.

   END OF TERMS AND CONDITIONS

   APPENDIX: How to apply the Apache License to your work.

      To apply the Apache License to your work, attach the following
      boilerplate notice, with the fields enclosed by brackets "[]"
      replaced with your own identifying information. (Don't include
      the brackets!)  The text should be enclosed in the appropriate
      comment syntax for the file format. We also recommend that a
      file or class name and description of purpose be included on the
      same "printed page" as the copyright notice for easier
      identification within third-party archives.

   Copyright [yyyy] [name of copyright owner]

   Licensed under the Apache License, Version 2.0 (the "License");
   you may not use this file except in compliance with the License.
   You may obtain a copy of the License at

       http://www.apache.org/licenses/LICENSE-2.0

   Unless required by applicable law or agreed to in writing, software
   distributed under the License is distributed on an "AS IS" BASIS,
   WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
   See the License for the specific language governing permissions and
   limitations under the License.
© 版权声明
THE END
喜欢就支持一下吧
点赞0 分享
评论 抢沙发

请登录后发表评论

    暂无评论内容