问题
案例:B 站UP主 蛋蛋yong闯小B站 的实战案例
对于同一组图片,本地识别出来的图片获得 x 距离,和网页提交的距离不一样!
本地识别:得到距离是 165
*
网页上:得到的距离是 128
解决方法
研究了好几天,一直解决不了,最后还是靠 AI 解决的,所以下面直接给出 AI 的分析
结论
本地图片识别得到的值,更接近“图上缺口的视觉位置”。
网页最终提交给验证码校验接口的 dx,并不是这个视觉位置本身,而是经过以下因素换算后的值:
- 滑块小图在图内有固定初始偏移
left: 10px
- 前端拖动时,图上小滑块的位置使用了
speed 比例
speed 在代码中会随机取 0.9、1.1、1.2 之一
所以:
图上缺口视觉位置 = 10 + dx * speed
dx = (图上缺口视觉位置 - 10) / speed
这意味着:
165 和 128 不一致,不一定是识别错了
- 更可能是“视觉坐标”和“提交坐标”本来就不同
speed 会参与校验上报
前端上报轨迹数据时,也会带上 speed:
{
"x": B,
"y": T,
"speed": t.speed,
"dt": _dx.dt
}
这进一步说明 speed 是这个验证码逻辑里的有效参数,不是纯前端展示细节。
对 165 和 128 的解释
如果本地图片识别得到的 165 是图上的视觉位置,那么根据上面的关系:
dx = (165 - 10) / speed
若本次 speed = 1.2,则:
dx = (165 - 10) / 1.2
= 155 / 1.2
= 129.17
这个结果和网页上的 128 已经非常接近。
考虑到下面这些因素,出现 1 像素左右的差值是正常的:
- 图片识别算法本身会有 1 到数像素误差
- 页面上取值可能经过
Math.round、parseInt
- 人眼在截图中量出来的位置不一定是精确像素
- 浏览器渲染和截图时可能存在轻微缩放
因此,165 和 128 之间的差距,主要不是因为图片识别失败,而是因为坐标系不同。
最终判断
本地识别出来的 165 更像是图上的视觉位置;
网页实际校验使用的 128 更像是经过 left: 10px 和 speed 反算后的拖动距离。
所以,这个问题的本质更接近:
而不是:
小结
- 其他参数按照视频基本都可以搞出来。
- 这个站点的轨迹校验不严格,网上随便生成都可以
- 若是出现二次验证则是 IP 问题
-
识别代码也是 B 站 UP 主提供的
# ============================================================
# 最优滑块识别算法(FFT加速版)
# ============================================================
# 基于测试结果:
# - 准确度:与原始方法完全一致(212px, score=0.5516)
# - 速度:10.3倍提升(381ms -> 37ms)
# - 结论:单纯FFT加速是最优方案,多尺度反而降低准确度
# ============================================================
import numpy as np
import matplotlib.pyplot as plt
from pathlib import Path
from scipy.ndimage import gaussian_filter, sobel
from scipy.signal import fftconvolve
# ============================================================
# 辅助函数:兼容PNG/WebP的图像加载
# ============================================================
def load_image(img_path):
"""
加载图像,优先使用matplotlib,失败时回退到Pillow。
这样可以兼容“扩展名是.png,但实际内容是WebP”这类情况,
同时把整型图像统一归一化到0-1范围,尽量保持原有PNG读取行为。
"""
try:
return plt.imread(img_path)
except FileNotFoundError:
raise
except Exception:
try:
from PIL import Image
except ImportError as exc:
raise RuntimeError(
"当前图片无法由matplotlib直接读取,请安装Pillow以兼容PNG/WebP读取:pip install pillow"
) from exc
with Image.open(img_path) as img:
img_array = np.array(img)
if np.issubdtype(img_array.dtype, np.integer):
max_value = np.iinfo(img_array.dtype).max
img_array = img_array.astype(np.float32) / max_value
return img_array
# ============================================================
# 辅助函数:RGB/RGBA转灰度图
# ============================================================
def rgb2gray(img):
"""
将RGB或RGBA图像转换为灰度图,并提取Alpha透明度通道
Args:
img: numpy数组格式的图像
Returns:
(gray, alpha): 灰度图像数组和透明度通道(如果有)
"""
if len(img.shape) == 3:
if img.shape[2] == 4:
gray = np.dot(img[..., :3], [0.2989, 0.5870, 0.1140])
alpha = img[..., 3]
return gray, alpha
else:
return np.dot(img[..., :3], [0.2989, 0.5870, 0.1140]), None
return img, None
# ============================================================
# 核心优化:FFT加速的NCC计算
# ============================================================
def compute_ncc_scores_fft(edge_bg, edge_template_masked, mask):
"""
使用FFT加速计算所有位置的NCC分数
原理:
传统方法:for循环逐个位置计算,O(N*M) 复杂度
FFT方法:利用卷积定理一次性计算所有位置,O(N*logN) 复杂度
结果:完全一致,但速度提升10倍以上
Args:
edge_bg: 背景图的边缘特征
edge_template_masked: 应用掩码后的模板边缘特征
mask: 掩码(标记有效区域)
Returns:
ncc_scores: 每个x位置的NCC分数(1D数组)
"""
# --------------------------------------------------------
# 步骤1:计算掩码内的有效像素数和模板统计量
# --------------------------------------------------------
masked_pixels = edge_template_masked[mask]
if len(masked_pixels) == 0:
return np.zeros(edge_bg.shape[1] - edge_template_masked.shape[1] + 1)
# 模板的均值和方差
mean_t = np.mean(masked_pixels)
template_centered = edge_template_masked - mean_t * mask.astype(float)
sum_sq_t = np.sum((masked_pixels - mean_t) ** 2)
if sum_sq_t == 0:
return np.zeros(edge_bg.shape[1] - edge_template_masked.shape[1] + 1)
# --------------------------------------------------------
# 步骤2:使用FFT计算互相关(分子部分)
# --------------------------------------------------------
# fftconvolve利用快速傅里叶变换加速卷积计算
# mode='valid'表示只计算完全重叠的部分
# 翻转模板是因为卷积需要翻转操作
cross_corr = fftconvolve(edge_bg, template_centered[::-1, ::-1], mode='valid')
# --------------------------------------------------------
# 步骤3:使用FFT计算滑动窗口的局部统计量(分母部分)
# --------------------------------------------------------
# 计算每个窗口位置的像素和与平方和
edge_bg_sq = edge_bg ** 2
# 使用卷积快速计算滑动窗口的和(等价于积分图方法)
window_sum = fftconvolve(edge_bg, mask[::-1, ::-1].astype(float), mode='valid')
window_sq_sum = fftconvolve(edge_bg_sq, mask[::-1, ::-1].astype(float), mode='valid')
# 计算每个窗口的像素数(掩码内的有效像素)
n_pixels = np.sum(mask)
# --------------------------------------------------------
# 步骤4:计算NCC分数
# --------------------------------------------------------
# 计算窗口的均值和方差
window_mean = window_sum / n_pixels
window_var = window_sq_sum / n_pixels - window_mean ** 2
window_var = np.maximum(window_var, 0) # 避免浮点误差导致的负数
sum_sq_p = window_var * n_pixels
# 计算分母(标准差的乘积)
denominator = np.sqrt(sum_sq_p * sum_sq_t)
# 计算NCC分数(避免除以0)
ncc_scores = np.zeros_like(cross_corr)
valid_mask = denominator > 1e-8
# 只在有效位置计算NCC
if np.any(valid_mask):
# 如果是2D结果,取每列的最大值(假设高度对齐)
if len(cross_corr.shape) == 2:
cross_corr_1d = np.max(cross_corr, axis=0)
denominator_1d = np.max(denominator, axis=0)
valid_mask_1d = denominator_1d > 1e-8
ncc_scores = np.zeros_like(cross_corr_1d)
ncc_scores[valid_mask_1d] = cross_corr_1d[valid_mask_1d] / denominator_1d[valid_mask_1d]
else:
ncc_scores[valid_mask] = cross_corr[valid_mask] / denominator[valid_mask]
return ncc_scores
# ============================================================
# 亚像素插值(高斯拟合)
# ============================================================
def subpixel_refinement(ncc_scores, best_int):
"""
使用高斯拟合进行亚像素插值,提高精度
Args:
ncc_scores: NCC分数数组
best_int: 整数精度的峰值位置
Returns:
best_subpixel: 亚像素精度的位置
"""
positions = len(ncc_scores)
# 检查是否有左右邻居点(边界位置无法插值)
if 0 < best_int < positions - 1:
# 获取三个点的分数
s_prev = ncc_scores[best_int - 1]
s_curr = ncc_scores[best_int]
s_next = ncc_scores[best_int + 1]
# 使用二次拟合(与原算法保持一致)
denom = 2 * (s_prev - 2 * s_curr + s_next)
if denom != 0:
# 计算亚像素偏移量
frac = (s_prev - s_next) / denom
# 限制偏移量在合理范围内
frac = np.clip(frac, -0.5, 0.5)
return best_int + frac
return float(best_int)
# ============================================================
# 主函数:最优滑块距离识别算法
# ============================================================
def detect_slider_distance(bg_path, slider_path, axis=0, sigma=1, alpha_thresh=0.5):
"""
最优滑块距离识别算法(FFT加速版)
特点:
✅ 准确度:与原始方法完全一致
✅ 速度:10倍以上提升(381ms -> 37ms)
✅ 稳定性:经过实际测试验证
Args:
bg_path: 背景图路径(带有缺口的完整图片)
slider_path: 滑块图路径(需要匹配的小块图片)
axis: 边缘检测方向,0表示垂直边缘(默认)
sigma: 高斯模糊的标准差(默认1)
alpha_thresh: Alpha通道阈值(默认0.5)
Returns:
(distance, score):
distance - 滑块应该移动到的x坐标位置(整数)
score - 最佳匹配位置的NCC分数(0-1之间)
"""
# ============================================================
# 步骤1:加载图像并转换为灰度图
# ============================================================
bg_img = load_image(bg_path)
template_img = load_image(slider_path)
bg, _ = rgb2gray(bg_img)
template, alpha = rgb2gray(template_img)
# ============================================================
# 步骤2:创建掩码
# ============================================================
if alpha is not None:
mask = alpha > alpha_thresh
else:
mask = np.ones(template.shape, dtype=bool)
# ============================================================
# 步骤3:边缘检测
# ============================================================
# 高斯模糊降噪
bg_blur = gaussian_filter(bg, sigma=sigma)
template_blur = gaussian_filter(template, sigma=sigma)
# Sobel边缘检测(垂直边缘)
edge_bg = np.abs(sobel(bg_blur, axis=axis))
edge_template = np.abs(sobel(template_blur, axis=axis))
# 应用掩码到模板
edge_template_masked = edge_template * mask.astype(float)
# ============================================================
# 步骤4:FFT加速的NCC匹配(核心优化)
# ============================================================
ncc_scores = compute_ncc_scores_fft(edge_bg, edge_template_masked, mask)
# ============================================================
# 步骤5:找到最佳匹配位置
# ============================================================
best_int = np.argmax(ncc_scores)
max_score = ncc_scores[best_int]
# ============================================================
# 步骤6:亚像素插值
# ============================================================
best_subpixel = subpixel_refinement(ncc_scores, best_int)
# ============================================================
# 步骤7:返回结果
# ============================================================
return int(best_subpixel), max_score
# ============================================================
# 对外封装:只返回滑块移动距离
# ============================================================
def get_slider_move_distance(bg_path, slider_path, axis=0, sigma=1, alpha_thresh=0.5):
"""
传入背景图路径和滑块图路径,只返回滑块移动距离。
Args:
bg_path: 背景图路径
slider_path: 滑块图路径
axis: 边缘检测方向,默认0
sigma: 高斯模糊标准差,默认1
alpha_thresh: Alpha通道阈值,默认0.5
Returns:
distance: 滑块移动距离(整数像素)
"""
distance, _ = detect_slider_distance(
bg_path,
slider_path,
axis=axis,
sigma=sigma,
alpha_thresh=alpha_thresh,
)
return distance
# ============================================================
# 可视化辅助函数
# ============================================================
def draw_result(bg_path, distance, output_path='result.png', color='red', linewidth=2):
"""
在背景图上绘制识别结果
Args:
bg_path: 背景图路径
distance: 识别出的距离
output_path: 输出图片路径
color: 线条颜色
linewidth: 线条宽度
"""
bg = plt.imread(bg_path)
fig, ax = plt.subplots(
figsize=(bg.shape[1] / 100, bg.shape[0] / 100),
dpi=100,
)
fig.subplots_adjust(0, 0, 1, 1)
ax.imshow(bg)
ax.axvline(x=distance, color=color, linewidth=linewidth)
ax.axis('off')
plt.savefig(output_path, dpi=100, pad_inches=0)
plt.close()
print(f"结果图已保存: {output_path}")
# ============================================================
# 使用示例
# ============================================================
if __name__ == '__main__':
import time
base_dir = Path(__file__).resolve().parent
bg_path = base_dir / "result.png"
slider_path = base_dir / "slide.png"
output_path = base_dir / "result_marked.png"
print("=" * 70)
print("最优滑块识别算法(FFT加速版)")
print("=" * 70)
try:
# 计时
start_time = time.time()
# 识别距离
distance, score = detect_slider_distance(bg_path, slider_path)
# 计算耗时
elapsed_time = (time.time() - start_time) * 1000
# 输出结果
print("\n识别成功")
print(f" 背景图: {bg_path.name}")
print(f" 滑块图: {slider_path.name}")
print(f" 滑块移动距离: {distance} px")
print(f" 匹配分数: {score:.4f}")
print(f" 识别耗时: {elapsed_time:.2f} ms")
# 可视化结果(可选)
draw_result(bg_path, distance, output_path)
print(f" 标注结果: {output_path.name}")
print("\n" + "=" * 70)
print("说明")
print(" 已使用脚本所在目录中的 result.png 和 slide.png 完成识别")
print(" 为避免覆盖原图,标注图输出为 result_marked.png")
print("=" * 70)
except FileNotFoundError as exc:
print("\n错误:找不到图片文件")
print(f" 缺失文件: {exc.filename}")
print(f" 当前脚本目录: {base_dir}")
except Exception as e:
print(f"\n错误:{e}")