update at 2025-10-25 19:23:02
This commit is contained in:
160
spec.md
160
spec.md
@@ -1,140 +1,42 @@
|
||||
## 项目定位
|
||||
|
||||
## 需求
|
||||
基于sharp图像处理库,实现图片中的滑块精确识别。
|
||||
豆瓣滑块验证码检测器使用 Node.js + Sharp,对静态截图中的滑块缺口进行自动定位。当前版本聚焦豆瓣样式的双滑块图片,要求识别 1~2 个滑块并输出带标注的结果图。
|
||||
|
||||
## 滑块形状图片和标注图片
|
||||
滑块抠图放在images/slider目录中,这些图片是滑块的形状。注意:方形滑块的某些边可能有半圆形凹陷或凸起。
|
||||
标注滑块的图片放在images/target中,图中标有target文字的框框是目标。注意:要识别的不是target框,而是target所框的滑块。
|
||||
## 数据输入
|
||||
|
||||
## 程序识别标准
|
||||
images/douban-target下放置手工红框标注的图片,**目标在红色框框内**,作为算法生成结果的比对。
|
||||
如果同一张图片程序生成框和人工框匹配,通过测试,否则继续优化。
|
||||
注意:不是下方绿色的滑块,是红框内的滑块。
|
||||
- `images/douban/`:待检测图片,命令行默认扫描该目录。
|
||||
- `images/douban-target/`:对应图片的人工红框标注,用于准确性验证。
|
||||
- `images/output/`:检测结果输出目录(自动生成,蓝框标注)。
|
||||
- 自定义目录可通过 `npm run detect -- --pic-dir=<relative-path>` 指定。
|
||||
|
||||
## 豆瓣滑块截图
|
||||
images/douban目录放置豆瓣网滑块真实截图。
|
||||
完成代码后,使用该目录的图片中做验证,并用蓝色方框框出滑块准确位置,输出到images/output目录。
|
||||
## 检测要求
|
||||
|
||||
## 思路
|
||||
先识别douban-target中的红框手工标注的位置,作为基准,检验output中的标注与基准比较,来确定是否正确识别
|
||||
douban-target中的图片是手工标注,所以允许output标注结果与基准有少量偏差,比如10px
|
||||
1. 支持 1~2 个滑块的定位,允许图片存在缺失或额外背景元素。
|
||||
2. 输出的边界框应与人工标注的中心点偏差 ≤ 10 px,IoU ≥ 0.1 视为可接受匹配。
|
||||
3. 允许算法返回多个候选框;验证阶段会进行一对一匹配并统计准确率、召回率。
|
||||
4. 结果需在原图上以蓝色矩形绘制并写入 `images/output/`。
|
||||
|
||||
改进思路:
|
||||
1. 不能根据亮度,滑块的亮度不确定。
|
||||
2. 图片中滑块只有2个
|
||||
3. 滑块形状是正方形,其中2个边,有半圆凹陷或凸起。
|
||||
最佳方法是边缘检测。滑块边框色调基本一致。
|
||||
## 算法概览
|
||||
|
||||
1. **预处理**:同时读取原始图与归一化图,生成原始 `RawImage`、增强版图像。
|
||||
2. **候选生成**(`src/detection/candidate-search.ts`):
|
||||
- 多阈值暗/亮区域扫描 → 连通域过滤
|
||||
- Sobel 边缘图 → 形态学闭运算补全边界
|
||||
- 颜色量化 + LAB 色差检测 → 识别低对比度候选
|
||||
- IoU 去重后,依据正方形程度、颜色一致性、内部边缘稀疏度、外部梯度平滑度重新评分。
|
||||
3. **框体精炼**:使用边缘投影收紧候选框边界。
|
||||
4. **自学习扩展**(`src/detector-self-learning.ts`):将最佳候选作为模板,在同一水平带上搜索第二个滑块。
|
||||
5. **结果绘制**:`SliderDetector.annotate` 在输出图片上渲染所有有效框体。
|
||||
|
||||
## 网友思路,滑块预处理
|
||||
## 验证流程
|
||||
|
||||
先提取一下滑块的轮廓,抖音的滑块特征很明显,可以不用cv2.Canny来提取边缘特征。
|
||||
- CLI (`src/cli.ts`) 会加载 `ground-truth.json`,对默认目录进行逐图检测。
|
||||
- 使用 `SliderValidator` 计算匹配情况,终端输出每张图与总体的准确率、召回率。
|
||||
- 若指定 `--pic-dir`,仍会先运行 ground-truth 评估(若文件存在),再处理目标目录。
|
||||
|
||||
具体步骤如下:
|
||||
- 去除外围透明像素点(滑块外层的像素点的a值都是0)
|
||||
- 将图片转成灰度图并进行二值化操作(0和255)
|
||||
- 只保留二值化为255的像素点
|
||||
- 去除多余噪声
|
||||
|
||||
### 读取rgba格式的滑块
|
||||
import cv2
|
||||
input_img = cv2.imread("slide.png", cv2.IMREAD_UNCHANGED)
|
||||
将透明值为0的像素点设置为纯黑色
|
||||
|
||||
### 取透明维度的值
|
||||
alpha_channel = input_img[:, :, 3]
|
||||
|
||||
### 只使用rgb三个维度的值
|
||||
rgb_image = input_img[:, :, :3]
|
||||
rgb_image[alpha_channel == 0] = [0, 0, 0]
|
||||
提取白色边缘并设置成黑色,将其他像素点设置为白色
|
||||
|
||||
gray = cv2.cvtColor(rgb_image, cv2.COLOR_BGR2GRAY)
|
||||
_, thresholded = cv2.threshold(gray, 240, 255, cv2.THRESH_BINARY)
|
||||
white_img = np.ones_like(rgb_image) * 255
|
||||
white_img[thresholded == 255] = [0, 0, 0]
|
||||
|
||||
### 去除噪声(判断某个黑色像素点周围3x3范围内有多少个黑色像素点,少于阈值认为是噪声)
|
||||
|
||||
def count_black_neighbors_by_cv2(gray_image):
|
||||
if gray_image.ndim == 3:
|
||||
gray_image = cv2.cvtColor(gray_image, cv2.COLOR_BGR2GRAY)
|
||||
_, binary_image = cv2.threshold(gray_image, 240, 255, cv2.THRESH_BINARY_INV)
|
||||
binary_image = binary_image // 255
|
||||
kernel = np.ones((3, 3), dtype=np.uint8)
|
||||
kernel[1, 1] = 0
|
||||
black_neighbors = cv2.filter2D(binary_image, -1, kernel)
|
||||
# 设置边缘为0
|
||||
black_neighbors[:, 0] = 0
|
||||
black_neighbors[:, 109] = 0
|
||||
return black_neighbors
|
||||
|
||||
当然也可以通过遍历来实现,这样更容易理解点
|
||||
|
||||
def count_black_neighbors_by_range(gray_image):
|
||||
# 将图像转换为灰度图
|
||||
if len(gray_image.shape) == 3:
|
||||
gray_image = cv2.cvtColor(gray_image, cv2.COLOR_BGR2GRAY)
|
||||
# 二值化图像
|
||||
_, binary_image = cv2.threshold(gray_image, 240, 255, cv2.THRESH_BINARY_INV)
|
||||
binary_image = binary_image // 255
|
||||
# 创建一个与输入图像大小相同的全零数组
|
||||
black_neighbors = np.zeros_like(binary_image)
|
||||
|
||||
# 遍历图像中的3x3邻域,计算每个像素
|
||||
neighbor_offsets = [(-1, -1), (-1, 0), (-1, 1),
|
||||
(0, -1), (0, 1),
|
||||
(1, -1), (1, 0), (1, 1)]
|
||||
|
||||
# 遍历每个像素
|
||||
rows, cols = binary_image.shape
|
||||
for row in range(1, rows - 1):
|
||||
for col in range(1, cols - 1):
|
||||
# 当它本身不是黑色像素点的时候,就不计算
|
||||
if binary_image[row, col] != 1:
|
||||
continue
|
||||
count = 0
|
||||
for offset in neighbor_offsets:
|
||||
neighbor_row, neighbor_col = row + offset[0], col + offset[1]
|
||||
if binary_image[neighbor_row, neighbor_col] == 1:
|
||||
count += 1
|
||||
black_neighbors[row, col] = count
|
||||
|
||||
return black_neighbors
|
||||
|
||||
black_neighbors = count_black_neighbors_by_range(white_img)
|
||||
output = np.ones_like(rgb_image) * 255
|
||||
output[black_neighbors > 4] = 0
|
||||
|
||||
|
||||
好了,现在可以把上面看到的内容忘掉了,因为在实际识别的时候用不到(我发现不做处理比做处理识别的准确率要高很多),直接识别准确率甚至接近百分百了。
|
||||
|
||||
### 下面是识别的完整代码
|
||||
|
||||
import os
|
||||
import cv2
|
||||
|
||||
def get_slide_distance(bg_path, slide_path):
|
||||
'''
|
||||
识别滑块具体位置,返回位置比例: 位置/图片宽度
|
||||
使用的时候再乘以实际图片宽度即可
|
||||
'''
|
||||
bg_img = cv2.imread(bg_path)
|
||||
sd_img = cv2.imread(slide_path)
|
||||
bg_gray = cv2.cvtColor(bg_img, cv2.COLOR_BGR2GRAY)
|
||||
bg_gray = cv2.GaussianBlur(bg_gray, (5, 5), 0)
|
||||
bg_edge = cv2.Canny(bg_gray, 30, 100)
|
||||
rgb_bg_gray = cv2.cvtColor(bg_edge, cv2.COLOR_GRAY2RGB)
|
||||
|
||||
sd_gray = cv2.cvtColor(sd_img, cv2.COLOR_BGR2GRAY)
|
||||
sd_gray = cv2.GaussianBlur(sd_gray, (5, 5), 0)
|
||||
sd_edge = cv2.Canny(sd_gray, 30, 100)
|
||||
rgb_sd_gray = cv2.cvtColor(sd_edge, cv2.COLOR_GRAY2RGB)
|
||||
result = cv2.matchTemplate(rgb_bg_gray, rgb_sd_gray, cv2.TM_CCORR_NORMED)
|
||||
_, _, _, max_loc = cv2.minMaxLoc(result)
|
||||
cv2.rectangle(bg_img, (max_loc[0], max_loc[1]), (max_loc[0]+110, max_loc[1] + 110),
|
||||
(0, 255, 0), 2)
|
||||
result_path = os.path.join(os.path.dirname(bg_path), "result.png")
|
||||
cv2.imwrite(result_path, bg_img)
|
||||
return max_loc[0]/bg_gray.shape[1]
|
||||
## 开发约定
|
||||
|
||||
- 代码组织遵循模块划分:`detector.ts`(主检测器)、`detection/`(候选策略)、`utils/`(基础工具)、`validator.ts`(验证)。
|
||||
- 新增检测策略时建议在 `candidate-search.ts` 中扩展,对应评分指标需保持可调节权重。
|
||||
- 所有新增图像应放入 `images/douban` / `images/douban-target` 并更新 `ground-truth.json`。
|
||||
- 运行 `npm run build` 进行类型检查;`npm run detect` 验收算法表现。
|
||||
|
||||
Reference in New Issue
Block a user