7AX0Uf.jpg

声明

本文章中所有内容仅供学习交流使用,不用于其他任何目的,不提供完整代码,抓包内容、敏感网址、数据接口等均已做脱敏处理,严禁用于商业用途和非法用途,否则由此产生的一切后果均与作者无关!

本文章未经许可禁止转载,禁止任何修改后二次传播,擅自使用本文讲解的技术而导致的任何意外,作者均不负责,若有侵权,请在公众号【K哥爬虫】联系作者立即删除!

前言

最近查看 QQ 群消息,无意间看到了粉丝们关于 opencv 的相关讨论,有热心的群友给出了大致的解决方向。同时也有很多星球伙伴,在星球分享关于验证码识别的相关知识,学习交流的氛围很好。本文就针对提问和已经存在的主题做一期总结与答疑,也是丰富验证码识别类型的相关文章:

7Av2wV.jpg

分析目标

  • 地址 1:aHR0cHM6Ly96bmZtLmJhaXdhbmcuY29tLw==
  • 地址 2:aHR0cHM6Ly93d3cuZGluZ3hpYW5nLWluYy5jb20vYnVzaW5lc3MvY2FwdGNoYQ==
  • 暂定某东旋转验证码

初识乾坤

首先我们来分析一下粉丝提到的站点 1,该站的查询接口返回的验证码图片如下:

7AfGmj.jpg

上图分别是一张缺口图与一张完整的图像,这种类型在滑块中还是比较好处理的,完全可以利用 absdiff 来完成。在图像处理和计算机视觉领域,计算图像之间的差异是一项基本的任务。这种差异可以帮助我们识别图像中的变化、运动对象或者进行图像配准等。在 OpenCV 库中,absdiff 函数提供了一种高效的方式来计算两个图像之间的绝对差值:

特性描述
函数名称cv2.absdiff()
参数src1src2:两张要进行比较的图像,必须具有相同的尺寸和通道数。
返回值返回一张新的图像,其中每个像素值是 src1src2 在对应位置的绝对差异。
图像类型支持灰度图像和彩色图像(RGB)。对于彩色图像,分别计算每个颜色通道的绝对差异。
常见用途1. 背景减除:检测视频中的运动物体;2. 图像对比:比较两张图像是否相似;3. 图像变化检测:找出图像间的变化。
应用场景1. 运动检测:通过计算背景和当前帧之间的差异检测前景物体;2. 监控:背景与前景变化检测;3. 图像注册与对比。
性能特点快速计算两张图像之间的像素差异,适用于图像对比、背景减除等任务。

所以我们先将两张图像转为灰度,然后计算绝对差值,结果如下:

7Aaqmj.jpg

之后,使用 Canny 边缘检测提取边缘,最终使用轮廓查找,找到符合条件的轮廓即可,完整代码如下:

import cv2
import numpy as np


def detect_slider_gap_with_canny(original_path, with_hole_path):
    """
    使用 Canny 边缘检测法检测滑块验证码缺口位置,并返回滑块需要移动的 x 坐标值。

    参数:
        original_path (str): 原图路径(无缺口)。
        with_hole_path (str): 带缺口的图片路径。

    返回:
        int: 滑块需要移动的 x 坐标值。
    """
    # 读取图片(灰度模式)
    original = cv2.imread(original_path, cv2.IMREAD_GRAYSCALE)
    with_hole = cv2.imread(with_hole_path, cv2.IMREAD_GRAYSCALE)

    # 检查两张图片是否大小一致
    if original.shape != with_hole.shape:
        raise ValueError("两张图片的尺寸不一致,请检查输入图片!")

    # 计算绝对差值
    diff = cv2.absdiff(original, with_hole)
    cv2.imshow("diff",diff)
    cv2.waitKey(0)

    # 使用 Canny 边缘检测提取边缘
    edges = cv2.Canny(diff, threshold1=50, threshold2=150)

    cv2.imshow("edges", edges)
    cv2.waitKey(0)

    # 查找边缘图中的轮廓
    contours, _ = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

    # 遍历轮廓,找到缺口的 x 坐标
    for contour in contours:
        # 获取轮廓的边界矩形
        x, y, w, h = cv2.boundingRect(contour)

        # 假设缺口的宽度和高度有一定限制,筛选可能的缺口
        if 20 < w < 100 and 20 < h < 100:  # 根据具体验证码调整范围
            return x

    # 如果未检测到缺口
    raise ValueError("未能检测到缺口,请检查输入图片!")


# 示例用法
if __name__ == "__main__":
    original_path = "1.png"  # 原图路径
    with_hole_path = "2.png"  # 带缺口的图片路径

    try:
        slider_x = detect_slider_gap_with_canny(original_path, with_hole_path)
        print(f"滑块需要移动的 x 值: {slider_x}")
    except Exception as e:
        print(f"错误: {e}")

渐入佳境

给粉丝答疑完之后,我们再来看看星球成员最近分享的,稍微复杂点的案例,是关于差异点击类型的验证码。该验证码是基于给定的图像中,选择其中不同类型的图案或文字:

7ADa4j.gif

该类型的验证码如下图所示:

7ADOk5.jpg

主要是利用 PCA 特征降维和余弦计算,关键代码如下:

def find_anomalous_image(images):
    # 提取所有图像的特征
    features = [extract_features(img) for img in images]

    # 设置 PCA 的 n_components 为样本数和特征数的最小值
    n_components = min(len(features), len(features[0]))
    pca = PCA(n_components=n_components)
    reduced_features = pca.fit_transform(features)

    # 计算余弦相似度
    similarity_matrix = cosine_similarity(reduced_features)
    average_similarity = np.mean(similarity_matrix, axis=1)

    # 找到平均相似度最低的图像索引
    anomalous_index = np.argmin(average_similarity)
    return anomalous_index

那么,借此思路我们同样也可以用其他办法来解决,既然图案有差异,那么我们可以通过模板匹配的结果来计算得分,同样还是遍历每个图案,求自身与其他图案的模板的得分平均值,最终还是利用 np.argmin 去得到平均相似度最低的字符索引,即可得到答案:

import cv2
import ddddocr
import numpy as np


# 初始化 ddddocr 检测器
det = ddddocr.DdddOcr(det=True, show_ad=False)

def cv_show(img):
    cv2.imshow("img", img)
    cv2.waitKey(0)
    cv2.destroyAllWindows()

def preprocess_image(image):
    """
    对图像进行预处理:灰度化和二值化。
    """
    gray = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    _, binary = cv2.threshold(gray, 127, 255, cv2.THRESH_BINARY)
    return binary

def extract_cropped_regions(image_path):
    """
    从验证码图像中裁剪检测到的字符区域。

    :param image_path: 验证码图像路径
    :return: 裁剪后的字符图像列表和检测框坐标
    """
    original_image = cv2.imread(image_path)
    with open(image_path, 'rb') as f:
        image_data = f.read()
    poses = det.detection(image_data)  # 检测字符位置

    cropped_images = []
    for bbox in poses:
        x_min, y_min, x_max, y_max = bbox
        cropped = original_image[y_min:y_max, x_min:x_max]
        cropped_images.append((cropped, bbox))

    return cropped_images

def match_cropped_regions(cropped_images, threshold=0.8):
    """
    使用模板匹配比较裁剪的字符图像,找到异常字符。

    :param cropped_images: 裁剪的字符图像和其对应的检测框
    :param threshold: 模板匹配的相似度阈值
    :return: 异常字符的索引
    """
    num_images = len(cropped_images)
    similarity_scores = np.zeros((num_images, num_images))

    for i, (img1, _) in enumerate(cropped_images):
        for j, (img2, _) in enumerate(cropped_images):
            if i != j:
                # 预处理图像
                img1_processed = preprocess_image(img1)
                img2_processed = preprocess_image(img2)

                # 计算模板匹配得分
                result = cv2.matchTemplate(img1_processed, img2_processed, cv2.TM_CCOEFF_NORMED)
                # print(result)
                similarity_scores[i, j] = np.max(result)

    # 计算每个字符与其他字符的平均相似度
    print(similarity_scores)
    average_similarity = np.mean(similarity_scores, axis=1)
    print(average_similarity)

    # 找到平均相似度最低的字符索引
    anomalous_index = np.argmin(average_similarity)
    return anomalous_index

def main():
    # 输入验证码图像路径
    captcha_image_path = "1.png"  # 替换为你的验证码路径

    # 裁剪字符区域
    cropped_images = extract_cropped_regions(captcha_image_path)

    # cv_show(cropped_images)

    # 找到异常字符
    anomalous_index = match_cropped_regions(cropped_images)

    # 可视化结果
    original_image = cv2.imread(captcha_image_path)
    for i, (_, bbox) in enumerate(cropped_images):
        x_min, y_min, x_max, y_max = bbox
        color = (0, 255, 0) if i != anomalous_index else (0, 0, 255)  # 异常字符用红框标记
        cv2.rectangle(original_image, (x_min, y_min), (x_max, y_max), color, 2)

    # 保存并显示结果
    output_path = "output.png"
    cv2.imwrite(output_path, original_image)
    print(f"结果已保存到: {output_path}")

    # 显示结果
    cv2.imshow("Matched Result", original_image)
    cv2.waitKey(0)
    cv2.destroyAllWindows()


if __name__ == "__main__":
    main()

依此类推,利用孪生 SiameseResNet50 网络,同样也可以不炼丹就解决该类型验证码, ResNet50 已经在大量的图像数据集(如 ImageNet)上训练过,预训练的 ResNet50 能提供非常好的性能,尤其是在图像分类、特征提取和其他计算机视觉任务中:

import cv2
import ddddocr

import torch
import torch.nn as nn
import torchvision.transforms as transforms
import torchvision.models as models

import numpy as np

# 使用 ddddocr 进行文字检测
det = ddddocr.DdddOcr(det=True, show_ad=False)

# 定义孪生网络模型
class SiameseResNet50(nn.Module):
    def __init__(self):
        super(SiameseResNet50, self).__init__()
        # 加载预训练的ResNet50模型
        resnet50 = models.resnet50(pretrained=True)
        # 去掉最后的全连接层
        self.resnet50 = nn.Sequential(*list(resnet50.children())[:-1])
        self.fc = nn.Sequential(
            nn.Linear(resnet50.fc.in_features, 512),
            nn.ReLU(),
            nn.Linear(512, 256),
        )

    def forward_one(self, x):
        # 提取图像的特征
        x = self.resnet50(x)
        x = x.view(x.size(0), -1)  # Flatten
        x = self.fc(x)
        return x

    def forward(self, x1, x2):
        output1 = self.forward_one(x1)
        output2 = self.forward_one(x2)
        return output1, output2


# 提取图像特征
def extract_features(image, model, transform):
    image = cv2.resize(image, (224, 224))  # 调整为ResNet50的输入大小
    image = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)  # 转换为RGB格式
    image = transform(image).unsqueeze(0)  # 应用变换并添加batch维度
    model.eval()
    with torch.no_grad():
        feature = model.forward_one(image).numpy()
    return feature


# 找到异常图像
def find_anomalous_image(images, model, transform):
    # 提取所有图像的特征
    features = [extract_features(img, model, transform) for img in images]
    features = np.vstack(features)

    # 计算特征之间的欧几里得距离
    distance_matrix = np.linalg.norm(features[:, np.newaxis] - features[np.newaxis, :], axis=2)
    average_distance = np.mean(distance_matrix, axis=1)
    print(average_distance)

    # 找到具有最大平均距离的图像
    anomalous_index = np.argmax(average_distance)
    return anomalous_index


def main():
    # 加载预训练模型
    model = SiameseResNet50()
    # 定义图像变换
    transform = transforms.Compose([
        transforms.ToPILImage(),
        transforms.ToTensor(),
        transforms.Normalize((0.5, 0.5, 0.5), (0.5, 0.5, 0.5)),  # Normalize RGB图像
    ])

    # 读取输入图像
    image_path = "1.png"  # 替换为您的图像路径
    original_image = cv2.imread(image_path)
    with open(image_path, 'rb') as f:
        image_data = f.read()
    poses = det.detection(image_data)

    # 裁剪检测到的区域
    cropped_images = []
    for bbox in poses:
        x_min, y_min, x_max, y_max = bbox
        cropped = original_image[y_min:y_max, x_min:x_max]
        cropped_images.append(cropped)

    # 找到最异常的图像
    anomalous_index = find_anomalous_image(cropped_images, model, transform)

    # 输出异常图像的坐标
    print(f"与其他不同的图像坐标为: {poses[anomalous_index]}")

    # 绘制矩形框,跳过最异常的图像
    for i, bbox in enumerate(poses):
        x_min, y_min, x_max, y_max = bbox
        color = (0, 255, 0) if i == anomalous_index else (0, 0, 255)  # 绿色表示异常,红色表示其他
        cv2.rectangle(original_image, (x_min, y_min), (x_max, y_max), color, 2)

    # 保存结果图像
    output_path = "output.png"
    cv2.imwrite(output_path, original_image)
    print(f"结果已保存到: {output_path}")


if __name__ == "__main__":
    main()

崭露头角

经过上文几轮介绍,我们已经拿下 2 种类型验证码的识别,这俩种应该算比较简单的,那么对于个别类型的验证码如果单单通过降维来提取主干特征,筛选正确答案,是远远不能满足要求的。比如上文的差异点击升级以后,便是字体风格类型验证码的识别,想从主干特征几乎一模一样的字体中筛选出正确答案,我们对特征的提取是需要叠加的,还要考虑文字的字体、笔画以及大小等等因素的影响,该类型的验证码如下图所示:

7ADXks.jpg

可以看到,此类验证码对于特征的提取,肯定不是单纯的模板匹配或者直接相似度就能解决的。换汤不换药,我们首先还是将图像转为灰度图,然后创建一个特征容器 features,用来存储特征集合。

提取轮廓面积与周长

_, binary_image = cv2.threshold(inverted_image, 0, 255, cv2.THRESH_BINARY + cv2.THRESH_OTSU)
contours, _ = cv2.findContours(binary_image, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE)

# 轮廓面积与周长
if contours:
    largest_contour = max(contours, key=cv2.contourArea)
    contour_area = cv2.contourArea(largest_contour)
    contour_perimeter = cv2.arcLength(largest_contour, True)
    features.extend([contour_area, contour_perimeter])

    # 轮廓宽高比
    _, _, width, height = cv2.boundingRect(largest_contour)
    aspect_ratio = width / height
    features.append(aspect_ratio)
else:
    features.extend([0, 0, 0])  # 如果没有找到轮廓,添加 0

笔画宽度提取

这类验证码最重要的特征就是笔画了,主要提取宽度均值和宽度标准差:

kernel = np.ones((3, 3), np.uint8)
dilated_image = cv2.dilate(binary_image, kernel, iterations=1)
eroded_image = cv2.erode(binary_image, kernel, iterations=1)
stroke_width_map = dilated_image - eroded_image
stroke_width_mean = np.mean(stroke_width_map)
stroke_width_std = np.std(stroke_width_map)
features.extend([stroke_width_mean, stroke_width_std])

骨架提取

skeleton_image = skeletonize(binary_image > 0)  # 先二值化,再进行骨架化
skeleton_length = np.sum(skeleton_image)
features.append(skeleton_length)

灰度统计与局部梯度

梯度统计主要用来求边缘强度,不同风格的字体边缘特征有时候有明显差别:

# 5. 灰度统计特征
mean_intensity = np.mean(gray_image)  # 灰度均值
std_intensity = np.std(gray_image)  # 灰度标准差
features.extend([mean_intensity, std_intensity])

# 6. 局部梯度分析(提取边缘强度)
gradient_x = cv2.Sobel(inverted_image, cv2.CV_64F, 1, 0, ksize=3)
gradient_y = cv2.Sobel(inverted_image, cv2.CV_64F, 0, 1, ksize=3)
gradient_magnitude = np.sqrt(gradient_x ** 2 + gradient_y ** 2)
gradient_mean = np.mean(gradient_magnitude)
gradient_std = np.std(gradient_magnitude)
features.extend([gradient_mean, gradient_std])

局部特征

局部特征主要用来提取字体形状和结构:

# 7. 局部特征(将图像分割为网格)
grid_size = 8  # 网格大小
cell_height, cell_width = resized_image.shape[0] // grid_size, resized_image.shape[1] // grid_size
local_grid_features = []

# 遍历每个网格,计算每个小区域的均值
for i in range(grid_size):
    for j in range(grid_size):
        cell = resized_image[i * cell_height:(i + 1) * cell_height, j * cell_width:(j + 1) * cell_width]
        local_grid_features.append(np.mean(cell))  # 计算网格单元的均值
        
features.extend(local_grid_features)

最终特征合并,计算相似性矩阵,回归上文相似度计算,继续计算均值,找到平均相似度最低的索引:

similarity_matrix = cosine_similarity(features)

# 计算每个图像与其他图像的平均相似度
average_similarity = np.mean(similarity_matrix, axis=1)
print("平均相似度:", average_similarity)

# 找到平均相似度最低的图像索引
anomalous_index = np.argmin(average_similarity)

最终效果如下:

7AOqZ4.jpg

行云流水

走到这里,已经对 3 种验证码进行了处理,最后我们来用 cv 处理一下旋转验证码,这种方法对于中小型网站,是足够使用的,相反对于 AI 类型的验证码也是一种处理办法,对于 AI 生成的旋转验证码,模型通常没有很好的泛性进行适配,如果模型可以一劳永逸,那么风控频繁的更新将毫无意义:

7A1zS7.jpg

感知哈希 (pHash) + 汉明距离

以某度验证码为例,图库有限的情况下,这不疑也是一种解决办法,处理思路就是通过将图片的主干特征提取出来,计算平均值,生成二进制哈希值,代码如下:

def phash(image):
    resized = cv2.resize(image, (32, 32), interpolation=cv2.INTER_AREA)
    gray = cv2.cvtColor(resized, cv2.COLOR_BGR2GRAY)
    dct = cv2.dct(np.float32(gray))
    dct_low = dct[:8, :8]  # 提取左上角低频部分
    mean_val = np.mean(dct_low)
    hash_str = ''.join(['1' if x > mean_val else '0' for x in dct_low.flatten()])
    return hash_str

通过读取转正的图像,将哈希值以列表的形式读出,最终存储在序列化文件中,格式如下:

['1010100001000000101000000010000010000000100000001000000000000000', '1011101000110010111000001010010010000001000000001000000000100010', '1010101100000000111010001000000010000000000000000000000000000000', '1110100000001100101000001000000010000000100000000000000000010000', '1010001010100010101000000000001000000010000010001000001000001000', '1110001100110000101110001100000010000000000000101000000000000000']

那么哈希值的对比就需要用到汉明距离了。

汉明距离的基本原理

汉明距离是用于衡量两个等长字符串之间的相似程度的指标,它表示两个字符串对应位上不同字符的个数。用于计算两个哈希值的相似性。距离越小,图像越相似,反之则差异越大:

from scipy.spatial.distance import hamming

hash1 = "1100101011110000"
hash2 = "1100101011010000"
distance = hamming(list(hash1), list(hash2))  # 汉明距离
print(f"汉明距离: {distance}")

最终通过将待旋转的图片经过 360 度旋转计算哈希值,最后找到最相似的角度,即可完成旋转验证码的识别:

import cv2
import numpy as np

def rotate_image(image, angle):
    h, w = image.shape[:2]
    center = (w // 2, h // 2)
    matrix = cv2.getRotationMatrix2D(center, angle, 1.0)
    return cv2.warpAffine(image, matrix, (w, h))

def find_best_angle(target_image, reference_image, step=1):
    target_hash = phash(target_image)
    best_angle, min_distance = 0, float('inf')
    for angle in range(0, 360, step):
        rotated = rotate_image(reference_image, angle)
        rotated_hash = phash(rotated)
        distance = hamming(list(target_hash), list(rotated_hash))
        if distance < min_distance:
            best_angle, min_distance = angle, distance
    return best_angle

个别角度可能顺时针、逆时针不同,需要用 360 - 计算出来的角度

更详细的代码可以参考热心网友已经整理好的 GitHub,方法大同小异:

相关链接:https://github.com/decodecaptcha/Rotate-Captcha-Angle-Prediction

SIFT 特征匹配 + 仿射变换

SIFT 的基本原理

SIFT 是一种经典的特征提取算法,能提取图像的关键点及其局部特征,具有旋转不变性尺度不变性,适用于旋转验证码中图像特征的匹配。

在标注阶段,首先需要准备一些已知的正确图像(即“转正”图像),这些图像的旋转角度是已知的。然后,通过计算这些图像的直方图特征,并将其保存到 pkl 文件 中,以便在后续的预测阶段使用:

import cv2
import pickle
import numpy as np

def calculate_histogram(image):
    # 计算图像的灰度直方图
    grayscale_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY)
    hist = cv2.calcHist([grayscale_image], [0], None, [256], [0, 256])
    # 归一化直方图
    cv2.normalize(hist, hist, alpha=0, beta=1, norm_type=cv2.NORM_MINMAX)
    return hist

def save_histograms(images, output_file):
    histograms = {'baidu': {}, 'allimg': {}}

    for idx, img_path in enumerate(images):
        img = cv2.imread(img_path)  # 读取图像
        hist = calculate_histogram(img)  # 计算图像的灰度直方图
        histograms['baidu'][f"image_{idx}"] = hist  # 将直方图保存到 'baidu' 键下
        histograms['allimg'][f"image_{idx}"] = img  # 将图像数据存储到 'allimg' 键下

    # 将数据保存到 pkl 文件
    with open(output_file, 'wb') as file:
        pickle.dump(histograms, file)

    print(f"saved to {output_file}")

# 示例:保存直方图数据和图像集合
image_paths = ['5_288.jpeg', '6_268.jpeg']  # 示例图片路径
output_file = 'baidu.pkl'  # 输出文件路径
save_histograms(image_paths, output_file)

之后读取未转正的图像,计算直方图,找到已经储存到 pkl 文件中最合适的图像,可以用 cv2.compareHist() 方法来计算图像之间的直方图相似度,找到相似的图像以后利用 SIFT 进行特征匹配和仿射变换来计算旋转角度。

流程如下:

import cv2
import time
import pickle
import numpy as np


# 从文件加载直方图模型
def load_histograms_from_file(file_path):
    with open(file_path, 'rb') as file:
        histograms_data = pickle.load(file)
    return histograms_data

# 特征匹配计算图像的旋转角度
def compute_rotation_angle_by_features(reference_img, query_img):
    # 将图像转换为灰度图
    query_gray = cv2.cvtColor(query_img, cv2.COLOR_BGR2GRAY)
    reference_gray = cv2.cvtColor(reference_img, cv2.COLOR_BGR2GRAY)

    # 使用 ORB 特征提取器代替 SIFT(ORB 更加高效)
    orb = cv2.ORB_create()

    # 提取特征点和描述符
    kp1, des1 = orb.detectAndCompute(query_gray, None)
    kp2, des2 = orb.detectAndCompute(reference_gray, None)

    # 使用暴力匹配器进行特征点匹配
    bf = cv2.BFMatcher(cv2.NORM_HAMMING, crossCheck=True)
    matches = bf.match(des1, des2)
    matches = sorted(matches, key=lambda x: x.distance)

    # 提取匹配点
    src_pts = np.float32([kp1[m.queryIdx].pt for m in matches]).reshape(-1, 1, 2)
    dst_pts = np.float32([kp2[m.trainIdx].pt for m in matches]).reshape(-1, 1, 2)

    # 计算变换矩阵,这里使用 RANSAC 方法剔除错误匹配
    matrix, _ = cv2.estimateAffinePartial2D(src_pts, dst_pts, method=cv2.RANSAC, ransacReprojThreshold=5.0)

    if matrix is not None:
        # 提取旋转角度
        angle = np.arctan2(matrix[0, 1], matrix[0, 0]) * (180 / np.pi)
    else:
        angle = None
        print("未能估计变换矩阵。")

    return angle

# 根据旋转角度旋转图像
def rotate_image(img, angle):
    height, width = img.shape[:2]
    # 计算旋转矩阵
    rotation_matrix = cv2.getRotationMatrix2D((width / 2, height / 2), angle, 1)

    # 进行旋转变换
    rotated_img = cv2.warpAffine(img, rotation_matrix, (width, height), flags=cv2.INTER_CUBIC)
    return rotated_img

# 使用直方图匹配估计图像的旋转角度
def estimate_image_angle(histograms, image_collection, query_img):
    start_time = time.time()

    # 计算查询图像的灰度直方图
    query_gray = cv2.cvtColor(query_img, cv2.COLOR_BGR2GRAY)
    query_hist = cv2.calcHist([query_gray], [0], None, [256], [0, 256])
    cv2.normalize(query_hist, query_hist, 0, 1, cv2.NORM_MINMAX)

    best_match_score = -1
    best_match_key = None

    # 查找与查询图像最相似的图像
    for key, hist in histograms.items():
        similarity = cv2.compareHist(query_hist, hist, cv2.HISTCMP_CORREL)
        if similarity > best_match_score:
            best_match_score = similarity
            best_match_key = key

    print(f"找到最匹配的图像: {best_match_key}")

    # 使用 'best_match_key' 作为索引,从 'image_collection' 字典中获取图像
    best_match_image = image_collection[best_match_key]

    # 计算最佳匹配图像与查询图像之间的旋转角度
    rotation_angle = compute_rotation_angle_by_features(best_match_image, query_img)
    end_time = time.time()

    print(f"执行时间: {end_time - start_time:.2f} 秒,旋转角度: {rotation_angle:.2f} 度。")

    # 显示最匹配的图像
    cv2.imshow("Best Match Image", best_match_image)
    cv2.waitKey(0)
    cv2.destroyAllWindows()

    return rotation_angle


# 加载直方图模型和图像集合
histograms_data = load_histograms_from_file('baidu.pkl')
histogram_model = histograms_data['baidu']
image_dataset = histograms_data['allimg']
query_image_path = '5.jpeg'  # 示例查询图像路径

query_img = cv2.imread(query_image_path)

# 估计查询图像的旋转角度
estimated_angle = estimate_image_angle(histogram_model, image_dataset, query_img)

最终结果如下:

7ArkpL.jpg

更细致的处理方法还可以创建掩码,将中心图抠出来,这样会更加准确,可以根据这个思路去设计标注工具,基本很快就能完成对抗,持续对抗是一个不错的选择。这部分标注好的数据集大概 5000 个,后续我会上传到知识星球中,仅供学习交流。


K哥爬虫
166 声望141 粉丝

Python网络爬虫、JS 逆向等相关技术研究与分享。