news 2026/9/28 1:56:31

基于Python与OpenCV的围棋棋盘棋子识别:从定位到颜色判别的完整实现

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
基于Python与OpenCV的围棋棋盘棋子识别:从定位到颜色判别的完整实现

简介:基于Python与OpenCV搭建的围棋棋盘棋子识别项目,融合图像处理与计算机视觉技术,专为毕业设计、课程设计及项目开发场景准备。项目针对重难点进行拆解,包含棋盘定位、棋子检测与黑白分类等关键模块,从图像采集到结果输出形成完整流程;既有可直接运行的Python源码,也有详细说明文档,适合计算机视觉方向的学生参考与二次扩展。压缩包共40个文件,其中37个图像样本用于测试与验证,2个Python脚本分别承担主程序和辅助测试,1个Markdown说明文档介绍使用方式;整体约4.83MB,目录结构清晰,便于对照学习。目前已有280人浏览/学习。源码经过严格测试,可在此基础上延伸实现棋子计数、落子识别等高级功能;文档梳理了识别流程与使用方法,配合多样图片样本,能帮助快速理解OpenCV在围棋场景中的落地方式,也便于直接用于课程汇报或毕设演示。

1. 基于 Python 与 OpenCV 的围棋棋盘棋子识别,到底在解决什么问题

拿到这个标题时,我第一反应是这是个典型的“图像处理综合项目”:把一张围棋棋盘的俯拍照片,变成一行行结构化数据——棋盘上哪里落了一颗黑子,哪里落了一颗白子。基于 Python 与 OpenCV 的围棋棋盘棋子识别,核心就两件事:定位棋盘网格,再识别网格交叉点上的棋子颜色。它的适用范围很明确:毕业设计、课程设计、入门级图像处理项目开发。相比用深度学习检测棋子,这条纯 OpenCV 的路线有天然优势——每步都有中间结果可看、参数可调、答辩时能讲清楚原理,而且在一台普通笔记本上跑实时视频流也毫无压力。

我自己做过类似的棋类识别项目,最深的体会是:这题目难度不在“会不会用函数”,而在“参数和边界条件”。棋盘反光、棋子阴影、透视变形,随便一个都能让识别率掉一半。所以这篇笔记不打算铺开讲 API,而是按“棋盘定位 → 棋子检测 → 坐标映射 → 颜色判断 → 踩坑排查”这条完整流程,给出能直接跑的代码和参数依据。

2. 方案选型与棋盘定位:先让程序找到棋盘,再谈棋子识别

2.1 为什么选传统 CV 方案,而不是直接上深度学习

做围棋棋子识别,常见的技术路线有三条:纯传统 CV(OpenCV 的直线检测、圆检测)、传统 CV 定位棋盘 + 小分类器判棋子、端到端的目标检测(YOLO 等)。我一般会建议学生在课程设计和毕业设计阶段优先选第一条,理由很实际:

深度学习方法在“棋子检测”这个任务上精度确实更高,但你需要一块能跑训练的显卡、一份围棋棋子的标注数据集,还要花时间调训练参数。而传统 CV 方案的数据集就是你拿手机拍几张照片,甚至可以用程序合成带棋盘的图片来调通流程。从答辩角度讲,霍夫变换检测直线、霍夫梯度法检测圆,每一个环节都是图像处理课程里学过的知识点,老师问起来你有东西可讲。深度学习的黑匣子反而容易在答辩时露怯。

我并不是说深度学习方案不行。如果你后续想往工程化方向走,或者要处理视频流中的运动模糊、遮挡,那换 YOLO 是合理的。但作为“毕业设计&课程设计&项目开发”这个定位,先用传统 CV 把完整流程跑通,再在某个环节替换成深度学习做对比实验,是性价比最高的做法。

2.2 棋盘定位实现:从轮廓检测到透视校正

棋子识别的前提是棋盘网格要“正”。现实中拍棋盘很难做到完全俯拍,多少带点透视,导致棋盘上的圆看起来是椭圆。所以流程第一步,是把图片中的棋盘区域抠出来,做透视变换矫正成正方形。

import cv2 import numpy as np src = cv2.imread("go_board.jpg") gray = cv2.cvtColor(src, cv2.COLOR_BGR2GRAY) blur = cv2.GaussianBlur(gray, (5, 5), 0) edges = cv2.Canny(blur, 50, 150) contours, _ = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) contours = sorted(contours, key=cv2.contourArea, reverse=True) board_pts = None for cnt in contours[:5]: peri = cv2.arcLength(cnt, True) approx = cv2.approxPolyDP(cnt, 0.02 * peri, True) if len(approx) == 4: board_pts = approx.reshape(4, 2).astype(np.float32) break if board_pts is None: raise ValueError("没有找到四边形棋盘区域")

这段代码做了四件事:转灰度、高斯模糊降噪、Canny 边缘检测、找最大外轮廓。核心参数有两个。Canny 的两个阈值 50 和 150,表示梯度强度低于 50 的像素直接丢弃,高于 150 的认定为强边缘,介于两者之间且与强边缘连接的才保留。光照强、阴影重时,把低阈值调到 80 左右能减少棋盘格内木纹产生的碎边缘。approxPolyDP的0.02 * peri是逼近精度,数值越小多边形越接近原始轮廓,数值越大越容易把弯曲边缘拟合成直线。识别不到的常见原因是棋盘边缘和背景对比度太低,这种情况先加大 Canny 低阈值,再看轮廓排序逻辑是否把桌面边框当成了棋盘。

拿到棋盘四角后,下一步是透视校正。这里有个关键坑:approxPolyDP返回的四个点顺序不固定,可能是左上、右上、右下、左下,也可能是右下、左上、左下、右上。直接把四点和目标矩形四点对齐,会出现棋盘被旋转或镜像的怪结果。我通常先按“左上 → 右上 → 右下 → 左下”的顺序排序:

def order_points(pts): s = pts.sum(axis=1) diff = np.diff(pts, axis=1).ravel() tl = pts[np.argmin(s)] br = pts[np.argmax(s)] tr = pts[np.argmin(diff)] bl = pts[np.argmax(diff)] return np.array([tl, tr, br, bl], dtype=np.float32) board_pts = order_points(board_pts) W, H = 760, 760 dst_pts = np.array([[0, 0], [W - 1, 0], [W - 1, H - 1], [0, H - 1]], dtype=np.float32) M = cv2.getPerspectiveTransform(board_pts, dst_pts) warped = cv2.warpPerspective(src, M, (W, H))

排序原理很简单:sum(axis=1)最小的是左上角,最大的是右下角;np.diff是 x 减 y 的差值,左上角 x 小于 y,差值最小,所以取argmin那个点作为右上角。透视变换矩阵M把任意四边形的棋盘区域映射到一个 760×760 的正方形画布,输出尺寸建议设成能被 18 整除的数,这样后面算网格间距时不会出现小数点错误。这里W-1、H-1是为了避免像素索引越界,属于 OpenCV 绘图的常见细节。

2.3 棋盘网格提取:霍夫直线检测与轮廓法的取舍

棋盘定位还有一种常见做法:不找四边形轮廓,而是用霍夫直线检测找横竖网格线,再求直线交点。对于背景干净、棋盘线清晰的图片,霍夫直线法很直接;但如果桌面上有纹路、棋盘周围有文字,检测出的直线会混入大量噪声,过滤起来非常麻烦。

我的做法是把两种方法结合:先用轮廓法拿到棋盘四边并做透视校正,在校正后的正方形图上再跑霍夫直线检测来获取精确的网格间距。这样霍夫直线只需要在棋盘内部工作,干扰少得多。轮廓法负责“粗定位”,直线法负责“细定位”,各干各的活,调试也方便。

3. 棋子识别:HoughCircles 的三组核心参数与漏检补检

3.1 HoughCircles 参数解析

棋盘矫正成正方形后,棋子检测就变成在固定区域内找圆。OpenCV 里最常用的是HoughCircles,但很多初学者在这里翻车:同样的代码,换张角度稍微偏一点的照片就一个圆都检测不到。问题基本出在参数上,尤其是param2和minDist。

roi_gray = cv2.cvtColor(warped, cv2.COLOR_BGR2GRAY) roi_gray = cv2.medianBlur(roi_gray, 5) circles = cv2.HoughCircles( roi_gray, cv2.HOUGH_GRADIENT, dp=1, minDist=25, param1=120, param2=28, minRadius=8, maxRadius=20 ) if circles is not None: circles = np.round(circles[0]).astype(int) for x, y, r in circles: cv2.circle(warped, (x, y), r, (0, 255, 0), 2)

这里有一个影响成败的细节:HoughCircles之前要先用medianBlur而不是GaussianBlur。中值滤波对椒盐噪声和棋子表面的高光点有奇效,能直接干掉一批虚假圆,高斯滤波做不到这点。

参数方面,dp=1表示累加器分辨率与原图一致,调成 2 会让检测变快但小圆容易丢。minDist=25是两个圆心之间的最小距离,棋盘上相邻棋子的间距通常在 35 像素左右(760 像素画布除 18),设 25 足够防止同一个棋子被检测成多个圆。param1=120是 Canny 高阈值,传给内部边缘检测用。param2=28是圆心累加器阈值,是“一个圆至少需要多少边缘点支持”的判定值,调小可以检出更多圆,但误检也会变多。minRadius和maxRadius要根据棋盘尺寸和棋子大小比例来定,通常把棋子直径设为两相邻网格间距的 0.8 倍左右。

这套参数在室内均匀光照下,识别率能到 90% 以上。如果你拍的图是斜 30 度角,透视校正后棋子会轻微变形,可以把param2降到 22,同时把minRadius调小一号,让检测更激进,后面再用轮廓圆度过滤噪声。

3.2 漏检补检:轮廓圆度过滤与固定半径模板匹配

HoughCircles 不是万能的,棋盘上棋子排布紧密时,minDist设大了漏检,设小了把相邻棋子连成一个大圆。另一种更稳的做法是:先用findContours找所有封闭轮廓,再用圆度指标过滤。

contours, _ = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE) for cnt in contours: area = cv2.contourArea(cnt) if area < 80 or area > 600: continue perimeter = cv2.arcLength(cnt, True) circularity = 4 * np.pi * area / (perimeter * perimeter) if circularity > 0.8: (x, y), radius = cv2.minEnclosingCircle(cnt) cv2.circle(warped, (int(x), int(y)), int(radius), (0, 255, 0), 2)

圆度的计算公式是4π面积/周长平方,正圆的圆度等于 1,形状越不规则值越小。取 0.8 作为阈值,能过滤掉棋盘网格交叉线产生的三角形小块,以及棋子阴影形成的不规则轮廓。面积阈值 80 到 600 需要按实际画布尺寸缩放,760×760 的画布上棋子半径约 12 像素,面积约 450,留出上下余量是合理的。

实际项目中,我会让 HoughCircles 和轮廓圆度检测同时跑,两份结果做并集。HoughCircles 在棋子边缘模糊时容易漏检,轮廓法在这种情况下反而更稳,因为 Canny 边缘对棋子内部的纹理不敏感。并集之后再去重(两个圆心的距离小于 8 像素视为同一个),漏检率能降到 5% 以内。

4. 坐标映射与棋子颜色判断:从像素到十九路盘面

4.1 像素坐标转棋盘点位

检测到棋子圆心后,下一个核心问题是如何把像素坐标对应到围棋 19×19 盘面的交叉点上。围棋的走棋记录通常写成“S16”“C7”这种字母加数字的坐标,课程设计大多不需要做这么完整,但至少要把 (x, y) 像素坐标换算成行列号。

由于前面已经做了透视校正,棋盘被映射成了一个规则的 760×760 正方形,网格间距是(760 / 18)像素。棋盘点位计算的代码很简单:

def pixel_to_grid(x, y, n=19, margin=20): # margin 是棋盘最外边线到画布边缘的留白 if x < margin or x > W - margin or y < margin or y > H - margin: return -1, -1 step_x = (W - 2 * margin) / (n - 1) step_y = (H - 2 * margin) / (n - 1) col = int((x - margin) / step_x + 0.5) row = int((y - margin) / step_y + 0.5) return row, col

这里有个新手很容易搞混的地方:n=19但这个 19 是交叉点数,不是格子数。19 路棋盘有 19 个交叉点,间距数量是 18,所以分母是n-1而不是n。如果不小心把分母写成 19,整盘棋子的坐标都会偏移半格,越靠边越明显,这是我做项目时真实踩过的坑。

margin的取值要跟透视变换时棋盘四角的选取对应。如果在提取棋盘轮廓时,四边形刚好卡在整块木板的外边线上,那四边最外侧的交叉点距离边缘还有一小段距离,margin 取 20 像素是合理的。如果四边形紧贴最外侧交叉点,margin 应该设成 0。我建议在预处理阶段就固定“轮廓取外框、margin 取 20”的策略,避免每次都重新量。

绘制网格点可以直观检查映射是否正确:

for row in range(19): for col in range(19): x = margin + col * (W - 2 * margin) / 18 y = margin + row * (H - 2 * margin) / 18 cv2.circle(warped, (int(x), int(y)), 3, (255, 0, 0), -1)

把检测到的棋子圆心和最近网格点的距离打印出来,如果大部分距离小于 5 像素,说明定位环节是准的。如果系统性偏移半个格距,优先怀疑n-1写错,其次是margin值不对。

4.2 黑白子判定:局部掩膜均值比全局阈值更抗光照干扰

棋子颜色判断看起来简单——取圆心像素的灰度值,深色是黑子,浅色是白子。但真实拍摄场景里,棋盘木纹颜色有深有浅,白子受反光影响可能局部发灰,黑子在强光下也可能拍出亮斑。用全图统一阈值切,很快会翻车。

我一般用局部掩膜方式:以圆心为中心、半径略小于棋子半径的圆内,取所有像素的加权亮度均值,再和一个亮度阈值比较。

def stone_color(warped_bgr, center_x, center_y, radius=12): mask = np.zeros((H, W), dtype=np.uint8) cv2.circle(mask, (int(center_x), int(center_y)), radius, 255, -1) mean_val = cv2.mean(warped_bgr, mask)[:3] brightness = 0.114 * mean_val[0] + 0.587 * mean_val[1] + 0.299 * mean_val[2] return "black" if brightness < 100 else "white"

亮度公式用的是 OpenCV 内置的 RGB 转灰度权重,B 通道权重 0.114、G 通道 0.587、R 通道 0.299。人为调用这个公式而不是直接cv2.cvtColor,是为了只计算掩膜内部区域的均值,cvtColor是对整张图操作。

阈值 100 是一个经验值,它在 0 到 255 的亮度标尺中间偏暗。室内光照均匀时,黑子亮度通常在 50 到 80,白子在 180 到 220,中间有足够余量。但如果你使用的是深色木质棋盘,空交叉点的木纹亮度可能在 90 到 110,这时黑子和空点容易混淆。我的处理办法是把“棋子是否存在”和“棋子颜色”做成两个判断:先用掩膜区域的标准差判断有没有棋子(标准差大说明该区域有高对比度的边缘,属于棋子边界),再判断颜色。空交叉点区域亮度均匀、标准差小,有棋子的区域标准差大。

还有一个在强反光下更稳的技巧:把 BGR 图转成 HSV,只看 V(明度)通道。白子的 V 值即使反光也保持在 200 以上,黑子的 V 值即使被环境光照亮也难超过 120,两个类别的分布重叠更小。

5. 常见问题与避坑排查:环境、定位、检测三个层面的真实翻车记录

5.1 环境配置:装好 OpenCV 却 import 失败

现象:pip install opencv-python安装成功,但在 VSCode 里运行import cv2报ModuleNotFoundError: No module named 'cv2'。

原因:十个里有七个是 Python 解释器选错。电脑上装了 Anaconda、系统 Python、VSCode 内置虚拟环境,pip 装到的是某个特定环境的 site-packages,而 VSCode 右下角选中的解释器是另一个。还有两成情况是 pip 与 python 版本不对应,用python -m pip install opencv-python才能保证装到当前 python 对应的包目录里。

解决:在 VSCode 里按Ctrl+Shift+P,输入 “Python: Select Interpreter”,选择你 pip 对应环境。然后在终端跑python -c "import cv2; print(cv2.__version__)",能输出版本号就说明环境通了。我自己习惯新建一个 conda 环境专门跑图像处理项目,把opencv-python、numpy、matplotlib一次装齐,避免多个项目互相污染依赖。

5.2 棋盘定位:findContours 返回值报错或轮廓取错

现象:老代码contours, hierarchy = cv2.findContours(...)报not enough values to unpack (expected 3, got 2)。

原因:这是 OpenCV 3.x 到 4.x 的经典破坏性变更。旧版findContours返回三个值,新版本只返回两个。网上大量博客的代码是照着旧版写的,直接复制就容易撞上。

解决:统一用新写法contours, _ = cv2.findContours(edges, cv2.RETR_EXTERNAL, cv2.CHAIN_APPROX_SIMPLE),其中cv2.RETR_EXTERNAL表示只取最外层轮廓,这个标志位在棋盘定位场景下是刻意选的——它能把棋盘内部纵横网格线产生的子轮廓全部排除,避免轮廓排序时抓到一个小网格而不是整个棋盘。排序按area倒序后取前几个候选,因为有时桌面上的纸片轮廓比棋盘大,直接取第一个会锁定错误目标。

另一个常见问题是桌面上有圆形纹理或者木桌边缘也被检成四边形。我的兜底策略是同时计算候选四边形的长宽比和面积占整图比例,棋盘的长宽比一般接近 1,面积占比通常在 30% 以上,两个条件都满足才接受。

5.3 棋子检测:HoughCircles 漏检、误检与颜色误判

现象:棋盘中间和天元位置的棋子都能检测到,但棋盘四角和边缘的棋子频繁漏检,或者把数据棋子相互粘连成一个椭圆。

原因:透视校正后的棋盘边角区域通常有明显畸变残留,尤其当原始照片是从斜上方 45 度拍的时候。边角棋子成像时已经接近椭圆,HoughCircles 的梯度累加器对这种变形的圆敏感度降低。另外拼接在一起的同色棋子之间边缘梯度弱,两个相邻圆心的距离小于minDist时,累加器会把它们当作同一个圆心。

解决:两个方向配合。第一,拍照时尽量让手机正对棋盘中心,减少透视角度,这是最省事的办法。第二,检测策略上把minDist设为相邻棋子间距的 0.6 到 0.7 倍,给轻微变形的圆留余量;边角区域单独切 ROI,对每个 ROI 单独跑HoughCircles并用更低的param2阈值,检测完再合并去重。颜色误判的翻车则大多发生在暖色灯光下,白子被照成淡黄色,RGB 三个通道的均值整体偏高,按单一亮度阈值切还勉强能用,但如果是日光灯加窗外自然光的混合光源,最好用 HSV 的 V 通道配合色相做二次判断。

6. 让结果可验证、可展示:评估脚本与SGF导出

课程设计和项目开发的区别在于,后者需要让用户相信你的识别结果是可信的。我做完识别后不会只看反馈图,而是写一个评估脚本:对着人工标注的答案逐点比对,输出准确率、漏检率、误检率三个数字。这个数字在验收和答辩时特别好用,比“看起来识别得不错”有说服力得多。

评估脚本的做法是:把每张测试图的真实棋谱存成一个文本文件,每行一个坐标点加颜色,比如3,15,b、7,10,w。程序跑完识别后,把所有识别结果也存成同样格式,两个文件做集合对比。

def evaluate(truth_path, pred_path): def load(path): points = set() with open(path, "r") as f: for line in f: line = line.strip() if not line: continue parts = line.split(",") points.add((int(parts[0]), int(parts[1]), parts[2])) return points truth = load(truth_path) pred = load(pred_path) hit = len(truth & pred) precision = hit / len(pred) if pred else 0 recall = hit / len(truth) if truth else 0 return precision, recall

这里的truth & pred利用的是 Python 集合的交集运算。要是更严格一点,棋子检测存在“圆找到了但坐标偏了一格”的情况,此时按精确坐标比对会误判为漏检,可以在比较前把每个预测坐标映射到最近的网格点上,再做匹配。

更进一步,我会把识别结果导出为 SGF 格式。SGF 是围棋棋谱的标准文件格式,几乎所有围棋软件都能直接打开。导出格式很简洁:

def export_sgf(moves, output_path): # moves 元素格式: (row, col, color),color 为 'b' 或 'w' with open(output_path, "w") as f: f.write("(;GM[1]SZ[19]\n") for i, (row, col, color) in enumerate(moves, start=1): # A 对应第一列,行列从 0 开始 col_char = chr(ord('A') + col) f.write(f";{color}[{col_char}{19 - row}]") if i % 5 == 0: f.write("\n") f.write(")\n")

SGF 坐标的字母序列里 I 不存在(为了不和数字 1 混淆),但 0-18 列映射到 A-T 后会跳过 I。如果项目做到了这一步,等于把“识别”和“应用”打通了——识别出的棋盘点位可以导入任何围棋软件复盘,这已经超出普通课程设计的要求,作为项目开发的亮点是足够的。

我自己的习惯是:每调完一组参数,就把测试图和标注结果一起存档,命名带上参数摘要,比如param2_28_mindist_25.jpg。回看时能直观看到参数对结果的影响,也方便向别人说明“我调参是有依据的”,而不是拍脑袋。做这类图像项目,最怕的就是陷入“调一个参数、全图变个样”的玄学状态,把每一次实验都留档,等于给自己一颗后悔药。希望这篇笔记能帮你把这条识别流水线跑通,少走我走过的弯路。

本文还有配套的精品资源,点击获取

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/28 1:56:30

绝缘子污染物图像分类实战:CNN迁移学习与避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/28 1:55:52

网站制作沈阳新手必看:图解步骤破解域名服务器配置难题

网站制作沈阳新手必看:图解步骤破解域名服务器配置难题 做网站最怕什么?不是代码写不出来,也不是设计不好看,而是最后一步,域名解析和服务器配置把你整懵了。很多沈阳本地的老板或者刚入行的新人,卡在“域名买好了怎么连上服务器”、“SSL证书怎么装”、“备案流程卡在哪”这几个环节,直接导致项目延期。…

作者头像 李华
网站建设 2026/9/28 1:55:39

济南公司注册网站多少钱?揭秘3个避坑点

济南公司注册网站多少钱?揭秘3个避坑点 网站做好了没人访问,这钱花得真冤。很多济南老板问,做一个正规的公司注册展示站到底多少钱,答案往往被中间商加了层皮。 别被“千元套餐”忽悠了,那是模板站,SEO几乎为零。真正的价值在于代码质量与服务器配置。 需求拆解:你缺的不是钱是流量…

作者头像 李华
网站建设 2026/9/28 1:55:33

宁波seo推荐避坑指南:3步搞定服务器域名不迷路

宁波seo推荐避坑指南:3步搞定服务器域名不迷路 域名解析报错,服务器连接超时,后台配置改了三遍还是打不开网页。是不是觉得脑子像一团浆糊?别慌,这正是很多宁波本地创业团队和中小企业主在建站初期最常踩的坑。我见过太多人花大价钱买了高配服务器,结果因为不懂DNS解析或SSL证书配置,导致网站在谷歌和百度…

作者头像 李华
网站建设 2026/9/28 1:55:26

CANoe LIN网络搭建实战:LDF导入、调度表配置与避坑指南

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/28 1:55:23

兰州网站建设与优化报价全拆解,教你怎么选不踩坑

兰州网站建设与优化报价全拆解,教你怎么选不踩坑 网站突然打不开,或者打开后满屏乱码、弹窗广告,后台登录密码改不过去?这种“被黑挂马”的恐慌,很多兰州本地企业主都经历过。这时候你才发现,当初为了省几百块选的廉价建站方案,现在成了最大的安全隐患。面对这种情况,除了紧急止损,更该反思的是:在兰州做网站,到…

作者头像 李华