SuperMemo实践闭环(3)-批量挖空制卡的操作
ztj100 2024-12-07 18:49 22 浏览 0 评论
本文阐述了在不使用ImageOcclusionEditor的情况下,我们通过OpenCV实现批量图片遮挡的效果.
Occlusion脚本执行后的效果如图所示
一. 运行环境-配置Python/脚本/终端环境
PyCharm / Python3.9 / Open-CV
PyCharm / Python3.9 的配置不再详细讲解,你可以参考我之前的专栏文章.
PyCharm中使用Python3.9编译器
安装Open-CV的Python插件
pip3 install opencv-python
解决找不到CV2 Module的问题:
.py文件运行时找不到,要注意在运行环境配置中添加变量
解决运行时找不到CV2模块的问题
如果以上的方法还不能解决你的问题,请按这篇内容再配置一下:
终端单独运行找不到时,要确保你Python3.9的site-packages下存在cv2的模块
终端找不到CV2模块要确保.so存在
Likey@Laptop pythonProject % python3 ./occlusionCard.py "你的图片路径"
脚本生成的最终SM样式网页
二. 脚本内容 - 如下脚本生成多个Occlusion图片至图床(使用了PicGo)并返回多个图片链接
OpenCV-Occlusion 脚本提供如下,具体操作步骤为: 1.截图后对要制卡区绘制矩形遮挡并保存(注意保证矩形区透明度75%以上,而且能看到遮挡处的内容), 2.直接执行上面那条脚本:(如下代码区也有使用方法的说明) 3.如果遮挡生成的图片不对请酌情调整contourAreaValue参数
#!/usr/bin/python3# -*- coding: utf-8 -*-import numpy as npimport cv2 as cvimport osimport sysimport requestsimport jsonfrom pprint import pprint################################## 实现(opencv)批量生成挖空卡片,调用picGo上传并返回链接# 作者:一只小胖子# 版本:V0.1# 知乎:https://www.zhihu.com/people/lxf-8868# 使用方法:# 1.使用snipaste截图,并用矩形工具描出实心遮挡区(注意:透明度为75%以上)# 2.运行python3 occlusionCard.py "你的图片路径(包括后缀名)"# 设置轮廓面积值,按效果自己调整,一般是1500-4000之间contourAreaValue = 2500################################## 设置putText函数字体font = cv.FONT_HERSHEY_SIMPLEX# 计算两边夹角额cos值def angle_cos(p0, p1, p2): d1, d2 = (p0 - p1).astype('float'), (p2 - p1).astype('float') return abs(np.dot(d1, d2) / np.sqrt(np.dot(d1, d1) * np.dot(d2, d2)))# 合并图片def merge_img(image1, image2): h1, w1, c1 = image1.shape h2, w2, c2 = image2.shape if c1 != c2: print("channels NOT match, cannot merge") return else: if w1 > w2: tmp = np.zeros([h2, w1 - w2, c1]) image3 = np.hstack([image2, tmp]) image3 = np.vstack([image1, image3]) elif w1 == w2: image3 = np.hstack([image1, image2]) else: tmp = np.zeros([h1, w2 - w1, c2]) image3 = np.hstack([image1, tmp]) image3 = np.vstack([image3, image2]) return image3# 查找矩形轮廓def find_squares(filepath, flag): img = cv.imread(filepath) # 读取图片对象 gray = cv.cvtColor(img, cv.COLOR_BGR2GRAY) gray = cv.GaussianBlur(gray, (3, 3), 1) # 1 ret, th1 = cv.threshold(gray, 128, 255, 0) # cv.THRESH_OTSU) # 0,255,cv.THRESH_BINARY | cv.THRESH_OTSU 127, 255,0 # 开闭运算去除噪点 kernel = cv.getStructuringElement(cv.MORPH_ELLIPSE, (3, 3)) th1 = cv.morphologyEx(th1, cv.MORPH_OPEN, kernel) th1 = cv.morphologyEx(th1, cv.MORPH_CLOSE, kernel) binary = cv.Canny(th1, 50, 100) contours, _hierarchy = cv.findContours(binary, cv.RETR_EXTERNAL, cv.CHAIN_APPROX_SIMPLE) # cv.RETR_EXTERNAL cv.RETR_TREE print("轮廓数量:%d" % len(contours)) # 轮廓遍历 roi_list = [] contours_2 = [] for cnt in contours: # cnt_len = cv.arcLength(cnt, True) # 计算轮廓周长 # cnt = cv.approxPolyDP(cnt, 0.02 * cnt_len, True) # 多边形逼近 # # 条件判断逼近边的数量是否为4,轮廓面积是否大于1000,检测轮廓是否为凸的 # if cv.contourArea(cnt) > 4000 and cv.isContourConvex(cnt): if cv.contourArea(cnt) > contourAreaValue: # 获取外接矩形的值 x, y, w, h = cv.boundingRect(cnt) roi_list.append((x, y, w, h, cnt)) contours_2.append({"h": h, "cnt": cnt}) print("发现挖空[" + str(len(roi_list)) + "]处, contours数为[" + str(len(contours_2)) + "]") squares_len = len(roi_list) filenameAtr = filepath.rsplit("/") for roi_idx in range(len(roi_list)): index = 0 if flag == 1: # 正面 (将回答问题为红色遮挡,已回答的显示答案,其它为蓝色遮挡) img = cv.imread(filepath) for roi_list_ in roi_list: (x, y, w, h, cnt_obj) = roi_list_ M = cv.moments(cnt_obj) # 计算轮廓的矩 cx = int(M['m10'] / M['m00']) cy = int(M['m01'] / M['m00'] + h * 0.5 + 20) # 轮廓重心下移0.5倍高度 cv.putText(img, ("#%d" % index), (cx, cy), font, 0.8, (0, 128, 0), 2, cv.LINE_AA) # 抗锯齿 cv.drawContours(img, contours, 0, (255, 0, 255), 2) if index < roi_idx: index = index + 1 continue elif index == roi_idx: idx_red_col = (0, 0, 255) # 红色 else: idx_red_col = (255, 0, 0) # 蓝色 cv.rectangle(img, (x, y), (x + w, y + h), idx_red_col, -1) # -1 2 -1为填充 index = index + 1 cv_filename = filenameAtr[0] + str(roi_idx) + "_A_" + filenameAtr[-1] print(cv_filename) # cv.imshow(cv_filename, img) cv.imwrite(cv_filename, img) elif flag == 0: # 反面 img2 = cv.imread(filepath) for roi_list_ in roi_list: (x, y, w, h, cnt_obj) = roi_list_ M = cv.moments(cnt_obj) # 计算轮廓的矩 cx = int(M['m10'] / M['m00']) cy = int(M['m01'] / M['m00'] + h * 0.5 + 20) # 轮廓重心下移0.5倍高度 cv.putText(img2, ("#%d" % index), (cx, cy), font, 0.8, (0, 128, 0), 2, cv.LINE_AA) # 抗锯齿 cv.drawContours(img2, contours, 0, (255, 0, 255), 2) if index <= roi_idx: # 已回答和正要回答的显示答案 index = index + 1 continue else: idx_red_col = (255, 0, 0) # 蓝色 cv.rectangle(img2, (x, y), (x + w, y + h), idx_red_col, -1) # -1 2 -1为填充 index = index + 1 cv_filename = filenameAtr[0] + str(roi_idx) + "_B_" + filenameAtr[-1] print(cv_filename) # cv.imshow(cv_filename, img2) cv.imwrite(cv_filename, img2) # cnt_len = cv.arcLength(cnt, True) # 计算轮廓周长 # cnt = cv.approxPolyDP(cnt, 0.02 * cnt_len, True) # 多边形逼近 # # 条件判断逼近边的数量是否为4,轮廓面积是否大于1000,检测轮廓是否为凸的 # if len(cnt) == 4 and cv.contourArea(cnt) > 1000 and cv.isContourConvex(cnt): # M = cv.moments(cnt) # 计算轮廓的矩 # cx = int(M['m10'] / M['m00']) # cy = int(M['m01'] / M['m00']) # 轮廓重心 # # cnt = cnt.reshape(-1, 2) # max_cos = np.max([angle_cos(cnt[i], cnt[(i + 1) % 4], cnt[(i + 2) % 4]) for i in range(4)]) # # 只检测矩形(cos90° = 0) # if max_cos < 0.1: # # True # # 检测四边形(不限定角度范围) # # if True: # # if True: # index = index + 1 # cv.putText(img, ("#%d" % index), (cx, cy), font, 0.7, (255, 0, 255), 2) # squares.append(cnt) return squares_len# 逻辑执行入口def main(img_file_path): file_dir_path = os.path.dirname(img_file_path) + "/" file_img_name = os.path.basename(img_file_path) file_upload_list = [] # 待上传的图片列表 print("1.开始生成正面图像") squares_len = find_squares(img_file_path, 1) print("2.开始生成反面图像") squares_len = find_squares(img_file_path, 0) print("3.开始合成生成的图象") for file_obj_idx in range(squares_len): # print("{}{}{}{}".format(file_dir_path, file_obj_idx, "_A_", file_img_name)) # print("{}{}{}{}".format(file_dir_path, file_obj_idx, "_B_", file_img_name)) view1 = cv.imread("{}{}{}{}".format(file_dir_path, file_obj_idx, "_A_", file_img_name)) view2 = cv.imread("{}{}{}{}".format(file_dir_path, file_obj_idx, "_B_", file_img_name)) # 迭加图片模式 # cv.addWeighted(view1, alpha, src2, beta, gamma, dst=None, dtype=None) # alpha/beta 对应两张图片的透明度, 0是完全透明 1是完全不透明 # overlapping = cv.addWeighted(view1, 0.8, view2, 0.2, 0) # 合并的文件名 file_out_name = "{}{}_QA_{}".format(file_dir_path, file_obj_idx, file_img_name) print(file_out_name) # 只上传合并后的QA图片 file_upload_list.append(file_out_name) # 水平或垂直合并图片 view3 = merge_img(view1, view2) # cv.imshow('view3', view3) cv.imwrite(file_out_name, view3) # # Exit if ESC pressed # # cv.waitKey() k = cv.waitKey(100) & 0xff # 100ms if k == 27: cv.destroyAllWindows() # exit() # 获取图片存储目录待上传图片 print(' 图片处理生成至: {} 结束!'.format(file_dir_path)) print("开始上传图片,请确保你已提前配置好picGo上传环境...") # # 获取传入路径下的: 当前目录, 子目录列表, 文件列表 # for f_path, dir_names, f_names in os.walk(file_dir_path): # # 正常的图片文件,关键字查询过滤文件名 # f_names = [f_name for f_name in f_names if not f_name.startswith(".") and f_name.__contains__(".jpg") # and os.path.isfile(f_name) and str(f_name).__contains__("#")] # # 得到全路径信息 # f_names = [os.path.join(f_path, f_name) for f_name in f_names] # break # 只处理根目录文件 print("待处理上传图片列表: {}".format(file_upload_list)) url = "http://127.0.0.1:36677/upload" payload = json.dumps({ "list": file_upload_list }) headers = { 'Content-Type': 'application/json' } response = requests.request("POST", url, headers=headers, data=payload) print("PicGo上传返回结果:") print(response) obj_json_list = json.loads(response.text) pprint(obj_json_list) # 获取图片的URL链接 if "result" in obj_json_list.keys(): url_for_sm18 = [] # 格式化成SuperMemo网页样式 for url_ in obj_json_list["result"]: url_str = "{} <img src="{}"/> <hr> ".format(file_img_name, url_) url_for_sm18.append(url_str) print("上传成功,返回SuperMemo样式:") # $fileName <img src="$url"/> <hr> print(" ".join(url_for_sm18)) else: print("上传失败,请检查是否重复上传!")# ------------开始调用方法-------------# 通过终端传参调用img_path = str(sys.argv[1])print("开始处理传入的图片:{}".format(img_path))main(img_file_path=img_path)# pycharm直接运行# if __name__ == '__main__':# # print(__doc__)# img_path = "/Users/Likey/PycharmProjects/pythonProject/pic/33.jpg"# main(img_path)
如果你用的是代码,下面的第三步骤可以不用再做了,代码已经实现了这个功能,第三步骤适合不用代码,直接手工操作的场景,总体上会麻烦一点....,建议你直接用代码的方式更方便.
三. Occlusion图片批量上传服务器并返回链接(这一步可以用上面第二步的代码替换了....)
(软件自带了一个网页链接,但这里我们要另外配置一个自定义链接用于获取批量的图片网址.)
先自定义返回图片的链接格式
通过以上的脚本生成了多个挖空Occlusion图片后,我们按需选中多个图片拖至PicGo软件的主界面进行上传,上传完后通过自定义的网页形式,批量获取图片网页链接.
主界面批量上传多图至服务端获取批量多图的自定义格式链接
# 多图的自定义返回链接 QA_2_444 <img src="https://gitee.com/lxf-8868/picgo_repo1/raw/master/img/QA_2_444.jpg"/> <hr> QA_1_444 <img src="https://gitee.com/lxf-8868/picgo_repo1/raw/master/img/QA_1_444.jpg"/> <hr> QA_6_444 <img src="https://gitee.com/lxf-8868/picgo_repo1/raw/master/img/QA_6_444.jpg"/> <hr> QA_7_444 <img src="https://gitee.com/lxf-8868/picgo_repo1/raw/master/img/QA_7_444.jpg"/> <hr> 20210228124446 <img src="https://gitee.com/lxf-8868/picgo_repo1/raw/master/img/20210228124446.png"/> <hr>
四. 按以上方式通过代码或者手工获取到多个链接后,我们就可以直接在SuperMemo中处理了.
导入批量图片链接并分割为子问答批量转换子问答及添加模版多图片链接制卡的最终效果
本文结束....
我是一只热爱学习的小胖子,如果你也热爱学习,并且对SuperMemo感兴趣,欢迎转发和评论!
相关推荐
- 离谱!写了5年Vue,还不会自动化测试?
-
前言大家好,我是倔强青铜三。是一名热情的软件工程师,我热衷于分享和传播IT技术,致力于通过我的知识和技能推动技术交流与创新,欢迎关注我,微信公众号:倔强青铜三。Playwright是一个功能强大的端到...
- package.json 与 package-lock.json 的关系
-
模块化开发在前端越来越流行,使用node和npm可以很方便的下载管理项目所需的依赖模块。package.json用来描述项目及项目所依赖的模块信息。那package-lock.json和...
- Github 标星35k 的 SpringBoot整合acvtiviti开源分享,看完献上膝盖
-
前言activiti是目前比较流行的工作流框架,但是activiti学起来还是费劲,还是有点难度的,如何整合在线编辑器,如何和业务表单绑定,如何和系统权限绑定,这些问题都是要考虑到的,不是说纯粹的把a...
- Vue3 + TypeScript 前端研发模板仓库
-
我们把这个Vue3+TypeScript前端研发模板仓库的初始化脚本一次性补全到可直接运行的状态,包括:完整的目录结构所有配置文件研发规范文档示例功能模块(ExampleFeature)...
- Vue 2迁移Vue 3:从响应式到性能优化
-
小伙伴们注意啦!Vue2已经在2023年底正式停止维护,再不升级就要面临安全漏洞没人管的风险啦!而且Vue3带来的性能提升可不是一点点——渲染速度快40%,内存占用少一半,更新速度直接翻倍!还在...
- VUE学习笔记:声明式渲染详解,对比WEB与VUE
-
声明式渲染是指使用简洁的模板语法,声明式的方式将数据渲染进DOM系统。声明式是相对于编程式而言,声明式是面向对象的,告诉框架做什么,具体操作由框架完成。编程式是面向过程思想,需要手动编写代码完成具...
- 苏州web前端培训班, 苏州哪里有web前端工程师培训
-
前端+HTML5德学习内容:第一阶段:前端页面重构:PC端网站布局、HTML5+CSS3基础项目、WebAPP页面布局;第二阶段:高级程序设计:原生交互功能开发、面向对象开发与ES5/ES6、工具库...
- 跟我一起开发微信小程序——扩展组件的代码提示补全
-
用户自定义代码块步骤:1.HBuilderX中工具栏:工具-代码块设置-vue代码块2.通过“1”步骤打开设置文件...
- JimuReport 积木报表 v1.9.3发布,免费可视化报表
-
项目介绍积木报表JimuReport,是一款免费的数据可视化报表,含报表、大屏和仪表盘,像搭建积木一样完全在线设计!功能涵盖:数据报表、打印设计、图表报表、门户设计、大屏设计等!...
- 软开企服开源的无忧企业文档(V2.1.3)产品说明书
-
目录1....
- 一款面向 AI 的下一代富文本编辑器,已开源
-
简介AiEditor是一个面向AI的下一代富文本编辑器。开箱即用、支持所有前端框架、支持Markdown书写模式什么是AiEditor?AiEditor是一个面向AI的下一代富文本编辑...
- 玩转Markdown(2)——抽象语法树的提取与操纵
-
上一篇玩转Markdown——数据的分离存储与组件的原生渲染发布,转眼已经鸽了大半年了。最近在操纵mdast生成md文件的时候,心血来潮,把玩转Markdown(2)给补上了。...
- DeepseekR1+ollama+dify1.0.0搭建企业/个人知识库(入门避坑版)
-
找了网上的视频和相关文档看了之后,可能由于版本不对或文档格式不对,很容易走弯路,看完这一章,可以让你少踩三天的坑。步骤和注意事项我一一列出来:1,前提条件是在你的电脑上已配置好ollama,dify1...
- 升级JDK17的理由,核心是降低GC时间
-
升级前后对比升级方法...
- 一个vsCode格式化插件_vscode格式化插件缩进量
-
ESlint...
你 发表评论:
欢迎- 一周热门
-
-
MySQL中这14个小玩意,让人眼前一亮!
-
旗舰机新标杆 OPPO Find X2系列正式发布 售价5499元起
-
【VueTorrent】一款吊炸天的qBittorrent主题,人人都可用
-
面试官:使用int类型做加减操作,是线程安全吗
-
C++编程知识:ToString()字符串转换你用正确了吗?
-
【Spring Boot】WebSocket 的 6 种集成方式
-
PyTorch 深度学习实战(26):多目标强化学习Multi-Objective RL
-
pytorch中的 scatter_()函数使用和详解
-
与 Java 17 相比,Java 21 究竟有多快?
-
基于TensorRT_LLM的大模型推理加速与OpenAI兼容服务优化
-
- 最近发表
- 标签列表
-
- idea eval reset (50)
- vue dispatch (70)
- update canceled (42)
- order by asc (53)
- spring gateway (67)
- 简单代码编程 贪吃蛇 (40)
- transforms.resize (33)
- redisson trylock (35)
- 卸载node (35)
- np.reshape (33)
- torch.arange (34)
- npm 源 (35)
- vue3 deep (35)
- win10 ssh (35)
- vue foreach (34)
- idea设置编码为utf8 (35)
- vue 数组添加元素 (34)
- std find (34)
- tablefield注解用途 (35)
- python str转json (34)
- java websocket客户端 (34)
- tensor.view (34)
- java jackson (34)
- vmware17pro最新密钥 (34)
- mysql单表最大数据量 (35)