最近在复习（学习）Vue.js 相关的知识，其中路由Router和参数传递是一个十分重要且较难的知识。于是花时间整理了一下下，希望对自己有用~1. 通过 name 传递参数export default new Router({ routes: [ { path: '/', name: 'index', component: index },{ path: '/page', name: 'page', component: page } ] });我们常把页面的路由配置在 /src/router 文件夹下的 index.js 文件中，如上为配置的内容。这其中的 name 属性是可以作为参数传递的，在模版中直接只用 {{$route.name}} 接收，即可以在模版中显示。2. 通过 < router-link > 中的 to 传递参数使用 < router-link to=" /page "> page < /router-link > 可以实现页面的跳转，这里面 to 是可以带上参数跳转的，我们需要在给 to 进行绑定，即 :to(v-bind:to)<router-link :to="{name:'page',params:{data:'我是App.vue传至page.vue的参数'}}" >page</router-link>这其中需要注意的是要给 to 加上绑定，后面跟的是对象形式的字符串。其中的 name 是我们在路由配置文件（上图图二）中配置的 name 两者要对应。params 即是我们要传的参数，它可以是多个值。随后在对应的模版页面中（这里为 page.vue）进行接收。{{$route.params.data}} 3. 在业务逻辑中实现页面跳转并传参当在业务逻辑中需要实现页面跳转经常能够使用到编程式导航：this.$router.go(1)，进入下一级this.$router.back(-1)，返回上一级，通俗的说就是前进与后退。this.$router.push(’/page’)，跳转到 page.vue那么我们应该如何传递参数呢？例如，我们在判断完某个条件后需要跳转到 page 页并且需要传递 age 和 address 两个参数过去，我们的操作方法是：App.vue page 我们在需要的地方加上点击事件：toPage(){ this.$router.push({ name: 'page', params: { age: 22, address: 'China' } }) }随后在 methods 中写入该事件，需要注意的是这里面的 name 同样是要与在配置路由时配置的 name 属性保持一致。params 是我们传入的参数，可以是常量也可以是变量。page.vue在 page.vue 中接收参数，可以在生命周期的 mounted() 接收并且放入 data ，再在模版上显示出来。 {{myage}}-{{myaddress}} 4. 利用 url 传参在项目开发过程中常常通过 url 进行参数传递，在 vue-cli 中该如何操作呢？在配置路由处以冒号形式，在 url 后方加上参数（/src/router/index.js）{ path: '/page/:pageId/:pageTitle', name: 'page', component: page }在<router-link></router-link> 的 to 属性中加入参数（App.vue） <router-link to="/" >index</router-link> <router-link to="/page/1/标题" >page</router-link> <router-view/> 在 page.vue 中接收参数 id: {{$route.params.pageId}} 标题： {{$route.params.pageTitle}} 5. 父组件与子组件之间传参项目中组件是经常被使用到的，那么父组件与子组件之间的联动是非常重要的。父传子先建立一个组件并且使用，在 components 文件夹下新建 component.vue 文件作为组件。 我的组件 {{msg}}-{{content}} 随后在 page.vue 中引入该组件，在 中 import 且引用，随后在模版中使用，如下： <children></children> 要传递参数，可以在 <children> </children>标签中加入要传入的参数，并且在组件（component.vue）中使用 props 接收再使用插值在模版中显示。同时我们也可以给要传的参数绑定起来，如下第二种写法，在参数前加上冒号（v-bind）这样我们就能在 data 中编辑我们的参数了。// page.vue <children msg="我是信息" content="我是内容"></children> <children :msg="message" :content="content"></children> // component.vue 我的组件 {{msg}}-{{content}} 子传父子组件再传值回父组件，在函数中使用 this.$emit()// component.vue 在 page.vue 的 <children> 标签中加入 @changeData，即可在点击子组件之后在父组件出接收到传过来的参数<children :msg="msg" :content="content" @changeData="dataChange"></children> 最后在附加一点，在父组件中该如何调用子组件中的函数呢，这里需要用到 ref 属性，添加在我们的 <children> 标签中// page.vue click <children ref="child" :msg="msg" :content="content" @changeData="dataChange"></children>// page.vue // 重复部分省略 ... methods: { ifClick(){ this.$refs.child.isClick(); } } // component.vue methods: { isClick(){ console.log("父组件点击了"); } }
众所周知，Cloudflare是一个好东西，其免费的CDN加速和Workers功能，令许多事方便了。这里，我们将使用Cloudflare Workers，反向代理部分国外网站，实现加速或免FQ访问。[card title="Cloudflare Workers简介" color="success"]Cloudflare Workers是一种可以运行JavaScript脚本的边缘节点，可以实现无服务器直接运行JavaScript代码。[/card]步骤1.登录你的Cloudflare账号，并进入到Workers界面(若从未使用过Workers，则需要先设置自定义workers域名，这里略去，格式为xxx.yourname.workers.dev)：2.新建一个Worker，修改此Worker名称，并将下列JavaScript代码粘贴到代码框中，并保存。[quote color="danger"]注意：你需要将upstream和upstream_mobile修改为你需要反代的网站域名。[/quote]// Website you intended to retrieve for users. const upstream = 'example.com' // Custom pathname for the upstream website. const upstream_path = '/' // Website you intended to retrieve for users using mobile devices. const upstream_mobile = 'example.com' // Countries and regions where you wish to suspend your service. const blocked_region = ['KP', 'SY', 'PK', 'CU'] // IP addresses which you wish to block from using your service. const blocked_ip_address = ['0.0.0.0', '127.0.0.1'] // Whether to use HTTPS protocol for upstream address. const https = true // Whether to disable cache. const disable_cache = false // Replace texts. const replace_dict = { '$upstream': '$custom_domain' } addEventListener('fetch', event => { event.respondWith(fetchAndApply(event.request)); }) async function fetchAndApply(request) { const region = request.headers.get('cf-ipcountry').toUpperCase(); const ip_address = request.headers.get('cf-connecting-ip'); const user_agent = request.headers.get('user-agent'); let response = null; let url = new URL(request.url); let url_hostname = url.hostname; if (https == true) { url.protocol = 'https:'; } else { url.protocol = 'http:'; } if (await device_status(user_agent)) { var upstream_domain = upstream; } else { var upstream_domain = upstream_mobile; } url.host = upstream_domain; if (url.pathname == '/') { url.pathname = upstream_path; } else { url.pathname = upstream_path + url.pathname; } if (blocked_region.includes(region)) { response = new Response('Access denied: WorkersProxy is not available in your region yet.', { status: 403 }); } else if (blocked_ip_address.includes(ip_address)) { response = new Response('Access denied: Your IP address is blocked by WorkersProxy.', { status: 403 }); } else { let method = request.method; let request_headers = request.headers; let new_request_headers = new Headers(request_headers); new_request_headers.set('Host', upstream_domain); new_request_headers.set('Referer', url.protocol + '//' + url_hostname); let original_response = await fetch(url.href, { method: method, headers: new_request_headers }) let original_response_clone = original_response.clone(); let original_text = null; let response_headers = original_response.headers; let new_response_headers = new Headers(response_headers); let status = original_response.status; if (disable_cache) { new_response_headers.set('Cache-Control', 'no-store'); } new_response_headers.set('access-control-allow-origin', '*'); new_response_headers.set('access-control-allow-credentials', true); new_response_headers.delete('content-security-policy'); new_response_headers.delete('content-security-policy-report-only'); new_response_headers.delete('clear-site-data'); if(new_response_headers.get("x-pjax-url")) { new_response_headers.set("x-pjax-url", response_headers.get("x-pjax-url").replace("//" + upstream_domain, "//" + url_hostname)); } const content_type = new_response_headers.get('content-type'); if (content_type != null && content_type.includes('text/html') && content_type.includes('UTF-8')) { original_text = await replace_response_text(original_response_clone, upstream_domain, url_hostname); } else { original_text = original_response_clone.body } response = new Response(original_text, { status, headers: new_response_headers }) } return response; } async function replace_response_text(response, upstream_domain, host_name) { let text = await response.text() var i, j; for (i in replace_dict) { j = replace_dict[i] if (i == '$upstream') { i = upstream_domain } else if (i == '$custom_domain') { i = host_name } if (j == '$upstream') { j = upstream_domain } else if (j == '$custom_domain') { j = host_name } let re = new RegExp(i, 'g') text = text.replace(re, j); } return text; } async function device_status(user_agent_info) { var agents = ["Android", "iPhone", "SymbianOS", "Windows Phone", "iPad", "iPod"]; var flag = true; for (var v = 0; v < agents.length; v++) { if (user_agent_info.indexOf(agents[v]) > 0) { flag = false; break; } } return flag; }3.最后，在浏览器中通过你所设置的Workers链接访问就可以啦~[quote color="info"]例如：修改后的Workers名称为test，访问链接为test.yourname.workers.dev[/quote][quote color="danger"]注意：此方法只能反向代理部分国外网站，一些网站并不支持反代，例如Twitter。同时，此方法仅仅适用于部分加载源比较单一的网站，此方法也无法实现被反代网站账户的登录。[/quote]最后附上博主搭建的部分反代网站，供小伙伴们使用喔~：Github：https://github.moejs.workers.dev/维基百科：https://wiki.moejs.workers.dev/Telegram：https://telegram.moejs.workers.dev/
1. Oracle和MySQL的区别？1、Oracle是大型数据库，而MySQL是中小型数据库。但是MySQL是开源的，但是Oracle是收费的，而且比较贵。2、Oracle的内存占有量非常大，而mysql非常小3、MySQL支持主键自增长，指定主键为auto increment，插入时会自动增长。Oracle主键一般使用序列。4、MySQL字符串可以使用双引号包起来，而Oracle只可以单引号5、MySQL分页用limit关键字，而Oracle使用rownum字段表明位置，而且只能使用小于，不能使用大于。6、Oracle在处理长字符串的时候,长度是小于等于4000个字节,如果要插入更长的字符串,考虑用CLOB类型,插入修改记录前要做进行修改和 长度的判断,如果为空,如果长度超出返回操作处理.（CLOB类型是内置类型，它一般都作为某一行中的一列,有些数据库也有别名）7、MySQL中0、1判断真假，Oracle中true false8、MySQL中命令默认commit,但是Oracle需要手动提交9、MySQL在windows环境下大小写不敏感 在unix,linux环境下区分大小写，Oracle不区分2. Oracle的行转列行转列 pivot 列转换 unpivot pivot（聚合函数 for 列名 in（类型））3. sql的执行顺序开始->FROM子句->WHERE子句->GROUP BY子句->HAVING子句->ORDER BY子句->SELECT子句->LIMIT子句->最终结果4. oracle实现分组查询roup by 要分组的列名 对数据集合处理的函数，可以处理多行数据--5个分组函数--count --计数函数，用于数量的统计--sum 求和函数，对数据进行求和--avg 求评平均，对一组数据求平均值--min ,求最小值--max ,求最大值5. 左连接怎么使用的select * from table_1 left join table_2 on table_1.id = table_2.id6. 索引怎么用，在一张表上创建很多索引可以吗，为什么？create index 索引名 on 表名（列名） tablespace 表空间名可以。因为.......(待补充)7. 如果想查某个时间段的数据，该怎么查？select * from table_1 where date between 时间段1 AND 时间段28. 数据库怎么优化查询？1.尽量少用in操作符2.尽量用not exists或者外连接替代not in操作3.尽量不使用<>或者!=运算符4.在设计表的时候，把索引列设置为not null5.尽量不使用通配符%或者_作为查询字符串的第一个字符6.where字句避免在索引列上计算使用7.用>=代替>8.利用SGA共享池，避开parse阶段9.where后面的条件，表连接的语句在前，可以过滤掉最大数量记录的条件居后10.使用表的别名，并将之作为每列的前缀11.显式或者隐式运算的字段不能索引12.用Union all 代替union13.通过改变Oracle的SGA的大小9. 索引的实现方式btree索引 hash索引 R-tree索引 full-text 全文索引10. MySQL的索引是是用什么数据结构，并介绍内部如何实现？btree结构:又叫多路平衡搜索数,一颗m叉的btree特性如下1.树中每个节点最多包含m个孩子2.除跟节点与叶子节点外,每个节点 只是有[ceil(m/2)]个孩子3.若个节点不是 叶子节点,则至少有两个孩子4.所有的叶子节点都在同一层5.每个非叶子节点由n个key与n+1个指针组成,其中[ceil(m/2)-1]<=n<=m-111. 索引会对更新造成影响吗，为什么会。因为虽然索引大大提高了查询效率,同时也降低了更新表的速度12. 视图索引的创建create unique index index_name on test(id);13. 视图为什么比一般的语句好用？因为是一种虚拟存在的表，不在数据中实际存在。可以简化查询14. MySQL存储引擎有哪些，默认用哪个myisam和innodb 默认innodb15. 创建存储过程create procedure 存储过程名(参数列表)BEGIN 存储过程体(一组合法的sql语句)END16. 给出一个内查询的例子select xxx from table_1 where id in (select id from table_2 where name = 'jack');17. 数据库查询优化同8.18. 介绍一下innodb 和MyIsam，他们的区别INNODB存储引擎是mysql的默认存储引擎,提供了具有提交,回滚,崩溃恢复能力的事务安全。但是对于MYLSAM的存储引擎,INNODB写的处理效率差一些,并且会占用更多的磁盘空间以保留数据和索引.MyIsam不支持事务,也不支持外键,其优势就是访问速度快。对事务的完整性没有要求或者以select,为主的应用基本都可以使用这个引擎来创建表.19. 数据库索引怎么使用？分类？在哪些地方使用过？create [unique|fulltext] INDEX index_name [USING index_type] on tb_name;索引的分类 :单值索引:即一个索引只包含单个列一个表可以有多个单列索引唯一索引:索引的列的值必须唯一,但允许有空值复合索引:即一个索引包含多个列在大量数据中查找等操作的时候，可以使用索引提高检索效率。20. 事务，事务的特性？事务就是用户定义的一系列执行SQL语句的操作, 这些操作要么完全地执行，要么完全地都不执行， 它是一个不可分割的工作执行单元。原子性，一致性，隔离性，持久性21. truncate、delete、drop的区别？1.delete可以加where ,truncate不可以2.truncate删除,效率高一点点3.加入要删除的表中有自增长列如果用delete删除后,在插入数据,自增长的列值从断点 开始而truncate删除后,再插入数据 自增长的列的值从1开始.4.truncate删除没有返回值的,delete是有返回值5.truncate删除不能回滚,delete删除可以回滚6.drop语句将表所占用的空间全释放掉。22. oracle提供的内置函数？字符函数：length() 返回str长度trim() 去除字符串两边空格lower() 转小写upper() 转大写concat() 拼接两个字符串instr(str,find_str) 查询find_str在str是否存在substr() 截取字符串转换函数：1、to_number(str)：将字符类型转换成number类型。2、to_char(time,format)：将time时间类型按照format的格式转换成字符类型。3、to_date(str,format)：将str类型的时间按照format的格式转换成date时间类型数值型函数1、round(num,length)：num小数保留length位长度,并且四舍五入。2、ceil(num)：返回向上取整num的结果。3、floor(num)：返回向下取整num的结果。23. 连接表的时候是怎么操作的select * from table_1 join table_2 on table_1.id=table_2.id where 限制条件24. 有两个数组 任意类型 怎么最快做到查询其中有哪些数据是重复的（不知道）25. mysql怎么做全连接select 查询列表from 表1 别名 [full outer]join 表2 别名on 连接条件[where 筛选条件]26. 如何实现多条修改可以使用while循环语句，或者IF--THEN语句结合。27. 分页组件怎么是实现，在sql中怎么实现在Oracle中，使用rownum进行分页，在MySQL中，使用limit关键字。28. C3P连接池的使用和好处ComboPooledDataSource source = new ComboPooledDataSource(); source.setDriverClass("com.mysql.jdbc.Driver"); source.setJdbcUrl("jdbc:mysql://127.0.0.1:3306/aa?useUnicode=true&characterEncoding=utf-8"); source.setUser("root"); source.setPassword("000"); //获取链接 System.out.println(source.getConnection());好处：C3P0连接池不仅可以自动清理不再使用的Connection，还可以自动清理Statement和ResultSet。29. 手机端数据库的了解主要有5种。BerkeleyDB Couchbase-lite LevelDB SQLite UnQlite30. oracle实例和用户的关系用户是在oracle实例下建立的。不同实例可以建相同名字的用户。31. JDBC怎么连接数据库 //1.加载驱动程序 Class.forName("com.mysql.jdbc.Driver"); //2.获得数据库链接 Connection conn=DriverManager.getConnection(URL, USER, PASSWORD); //3.通过数据库的连接操作数据库，实现增删改查（使用Statement类） Statement st=conn.createStatement(); ResultSet rs=st.executeQuery("select * from user"); //4.处理数据库的返回结果(使用ResultSet类) while(rs.next()){ System.out.println(rs.getString("user_name")+" "+rs.getString("user_password")); } //关闭资源 rs.close(); st.close(); conn.close();32. MySQL创建和删除表的关键字创建：create 删除：drop33. 比较表和视图的区别是一种虚拟存在的表，不在数据中实际存在。视图可以简化查询。34. oracle中的五个约束条件是什么？主键约束 唯一约束 检查约束 默认约束 外键约束35. 事物的传播属性有哪几种？分别有什么作用（Spring）1、mandatory 表示当前方法必须在事务中进行，当前事务不存在，则抛出异常2、required （默认的）当前方法必须在事务中进行，如果当前事务存在，则在当前事务中执行，如果不存在当前事务，则启动一个新的事务。3、required_new 这个方法要求在新的事务中执行，会启动一个新的事务，如果存在当前事务，则该事务会先被挂起。4、support 该方法可以不在事务中，但是如果在存在当前事务，那就在当前事务中执行。5、never 给方法不可以在事务中执行，如果存在当前事务则抛出异常。6、not_support 该方法不可以在事务中执行，如果存在当前事务，则该事务会被挂起。7、nest，嵌套事务。如果当前事务不存在，则nest和requied相同。
闲话少絮。开始正题——OpenCV的车流量统计。调试平台OpenCV 4.2VS 2019汽车识别原理——背景/前景分割算法如今，检测和提取车辆时候，常用的方法有MOG2算法和KNN算法。MOG算法是以高斯混合模型（GMM）为基础的背景/前景分割算法。它是以2004年和2006年Z.Zivkovic的两篇文章为基础的。这个算法的一个特点是它为每一个像素选择一个合适数目的高斯分布。其主要原理为：在一个固定位置和角度固定的视频或图像中，提取分割图像或视频中运动的成分。此算法使用背景建模的方式，将整张图片或一帧视频分为前景和后景。此算法运行时，会将动态的前景与静止的后景相减，得出结果即为徐提取的运动物体的图像。K最近邻算法（KNN）是属于机器学习的一种算法。其主要原理为：给定一个已训练数据集，对新的输入实例，在训练数据集中找到与该实例最邻近（注：衡量邻近的标准以具体选取的某个特征而言，例如下面示意图中使用的特征为欧式距离）的K个实例，这K个实例的多数属于某个类，则判定该输入实例同属此类。如下图所示：训练者取k值，计算以欧氏距离k为半径的圆内其他类别的个数，图中中心小红点以k为半径的圆内三角形个数最多，则判定中心小红点为三角形。整体结构整体流程框架图本程序的主要运行流程为：程序运行开始，首先从文件中获取上次保存的光流量检测矩形框数据（顶点坐标和矩形的长宽）；其后，分别初始化背景提取对象，使用MOG2和KNN两种算法。与此同时，建立一个鼠标回调函数，用于捕获鼠标左键（绘制矩形框）、中键（取消操作）和右键（保存矩形框数据到文件）的操作。此后，程序进入主循环状态。程序循环从视频中获取一帧的图像，先进行压缩处理，以提高后续运算速度。之后，将这一帧图片从RGB转为灰度图片。为了祛除灰度后可能出现的小毛刺杂点，再进行平滑滤波处理。此后，分别通过MOG2和KNN算法提取前景，并将提取后的视频显示出来。与此同时，获取每个矩形框中积分后的亮度和。最后，两个算法中，分别将实时得到的亮度和与所预设的阈值进行比较，当满足条件后，便认为一辆汽车通过矩形框，使得计数器加一。如此重复，统计整个视频中的车流量。主要参数int detectTHD此参数为预设的亮度阈值。确定是否有汽车经过检测框中，其需要联合上一帧积分亮度和本次积分亮度后综合做出决定。cv::Size newSize(frame.cols / 2, frame.rows / 2);此参数可以储存一帧视频缩小后的大小。cv::Mat showMat;此参数用于储存最终显示的图像矩阵。主要函数static void onMouse(int event, int x, int y, int flags, void\*)此函数主要用于检测鼠标左键、滑轮（中键）和右键的一些操作，用于绘制矩形检测框。cv::resize(frame, newframe, newSize);此函数主要用于缩小原视频比例，提高计算机运算速度。bgMOG2-\>apply(greyFrame, mog2RES, update_bg_model ? -1 : 0);以及bgKNN-\>apply(greyFrame, KNN, update_bg_model ? -1 : 0);apply函数主要用于两种算法的前景提取。此后，前景提取后的这帧视频保存于bgMOG2和bgKNN之中。cv::rectangle(showMat, myLanneRect.at(k), cv::Scalar(255, 255, 255), 3);此函数主要用于前景提取后在其上面绘制矩形框。cv::integral(subMat, sumMat, CV_32S);计算车道矩形框亮度积分图。cv::putText();此函数可以按照要求，在视频图像上显示文字和统计数字。测试结果功能测试进行车流量统计前，应当首先根据矩形框中出现车辆时，估算其亮度平均值作为亮度阈值，使其设定为一个较为合适的值，增强检测的灵敏性。由下图可知，当程序运行后，通过手动标记车道检测区域，得到2个矩形检测区。每次获得矩形中的积分亮度结果，当上从亮度结果大于阈值，且此次亮度结果小于阈值时，使车辆统计结果加一。算法处理时间由测试结果可知，相较于MOG2算法，KNN算法在运算处理上花费更多的时间，KNN算法时间约为MOG2时间的2倍。同时，算法处理总时间也相对较长，除了两个算法所带来的开销外，仍有其他附加代码所花费的时间。缩放图片前后比较程序中，可以使用cv::resize();函数进行图片的压缩，缩放图片对于检测速度有较大的影响。不使用缩放时，将视频中原始一帧的图像进行计算，经测试发现，处理速度很慢，显示的图片有明显的脱帧和卡顿现象，CPU占用率相当高，与压缩后（上图）相比，无论使用MOG2还是KNN算法，其处理时间均成倍增加。下图为不使用缩小图片尺寸条件下的处理时间。矩形检测区亮度阈值的影响阈值设置的合理性也是车流量检测准确性的一个重要指标。过高或过低的阈值均不能很好地反映车辆的经过和实现的统计。阈值过低，将会把视频中环境干扰噪声和其他运动对象（如三轮车和行人）当做汽车统计，使得统计结果偏大；同理，阈值过高，将很难检测到车辆的通过，当车辆进过矩形检测区时，无法实现车辆的统计。本程序中，经过检测矩形框内平均值的大致估算，将阈值detectTHD设置为900000。下图中显示了当阈值设定过小时的状况。右端矩形框（右车道）经过了一辆电动车，程序误认为汽车，并错误地将统计结果L1的值从1加为2。同理，当阈值设置过大，也会造成统计的不准确。下图显示了当阈值设定过大时的状况。可以发现，即使是车辆经过了矩形框，车辆统计变量L0和L1仍为0。结论由以上分析可知，汽车流量的统计可以借助设定矩形区域内的亮度阈值来确定。为识别运动对象（汽车）的状态，可使用背景提取算法，如本软件中使用到的MOG2和KNN算法。通过比较不同算法间的处理时间，我们应当合理选择一种耗时短且提取车辆准确性高的一种算法。此外，识别统计车流量较为重要的一环便是设置合理的亮度阈值，亮度阈值设置的合理性直接关系到车流量统计准确性。附：程序源代码此项目Github地址： https://github.com/cwxyr/traffic-detection#include "stdafx.h" #include <Windows.h> #include <string> #include <opencv2/core.hpp> #include <opencv2/imgproc.hpp> #include <opencv2/highgui.hpp> #include <opencv2/features2d.hpp> #include <opencv2/video.hpp> #ifdef _DEBUG #pragma comment(lib, "opencv_world420d.lib") #else #pragma comment(lib, "opencv_world420.lib") #endif //===【鼠标事件回调函数】=== int detectTHD = 900000; //亮度阈值：有车辆经过的； std::vector<int> myLanneLightSum_Last; //车道亮度和：上一帧的 std::vector<int> myLanneVihicleCnt; //车道车辆计数器 std::vector<cv::Rect> myLanneRect; //车道矩形框；显示为红色； std::vector<cv::Point> myMousePoints; //鼠标点向量；显示为蓝色； int myMouseEventBusy = 0; //鼠标回调事件忙:简单的资源锁 static void onMouse(int event, int x, int y, int flags, void*) { myMouseEventBusy = 1; cv::Point mPoint; cv::Rect mRect; switch (event) { case cv::EVENT_LBUTTONDOWN: //左键按下:增加myMousePoints中的点数 mPoint = cv::Point(x, y); myMousePoints.push_back(mPoint); //将当前鼠标点推送到向量中； if (myMousePoints.size() > 4) myMousePoints.erase(myMousePoints.begin()); //保证myMousePoints向量中节点数不大于4； break; case cv::EVENT_RBUTTONDOWN: //右键键按下：将myMousePoints中的4个点推送到矩形框向量myLanneRect if (myMousePoints.size() == 4) { int Xmin = 100000; int Ymin = 100000; int Xmax = 0; int Ymax = 0; for (int k = 0; k < 4; k++) { Xmin = std::min(Xmin, myMousePoints.at(k).x); Ymin = std::min(Ymin, myMousePoints.at(k).y); Xmax = std::max(Xmax, myMousePoints.at(k).x); Ymax = std::max(Ymax, myMousePoints.at(k).y); }//for k <<< === 用四个点构成矩形框的参数 mRect = cv::Rect(Xmin, Ymin, Xmax - Xmin, Ymax - Ymin); //构成矩形框 myLanneRect.push_back(mRect); myLanneLightSum_Last.push_back(0); myLanneVihicleCnt.push_back(0); myMousePoints.clear(); //清除鼠标点向量 }///if break; case cv::EVENT_MBUTTONDOWN: //中间键键按下：删除myMousePoints中的一个点；myMousePoints为空时，删除myLanneRect中的节点； printf("EVENT_MBUTTONDOWN\n"); if (myMousePoints.size() > 0) myMousePoints.pop_back(); else { myLanneRect.pop_back(); myLanneLightSum_Last.pop_back(); myLanneVihicleCnt.pop_back(); } break; }/////switch myMouseEventBusy = 0; return; } int main(int argc, char* argv[]) { char errorMSG[256]; char curPathName[384] = ""; char curModulerPath[384] = ""; GetCurrentDirectory(383, curModulerPath); printf("Line39: curModulerPath = %s\n", curModulerPath); //=======读取标记的矩形框文件内容到myLanneRect：======= #ifndef READ_RECT_FILE FILE *pFILE = fopen("MarkRect.txt", "r"); if (pFILE != NULL) { cv::Rect mRect; while (fgets(errorMSG, 255, pFILE) != NULL) { int rtn = sscanf(errorMSG, "%d %d %d %d", &mRect.x, &mRect.y, &mRect.width, &mRect.height); if (rtn == 4) { myLanneRect.push_back(mRect); myLanneLightSum_Last.push_back(0); myLanneVihicleCnt.push_back(0); } } fclose(pFILE); }///if #endif // !READ_RECT_FILE std::string imgName = "video-02.mp4"; char FilePath[384]; if (strlen(curPathName) > 0) sprintf(FilePath, "%s\\%s", curPathName, imgName.c_str()); //图片文件路径 else sprintf(FilePath, "%s", imgName.c_str()); //图片文件路径 //==【01】== 打开视频文件或摄像头 cv::VideoCapture cap; //VideoCapture类实例化，使用缺省摄像头 if (0 && "UsingCam") cap.open(0); else cap.open(FilePath); if (!cap.isOpened()) // check if we succeeded { printf("error#73: 打开设备或文件失败，检查是否存在！回车退出！\n路径=%s\n", FilePath); fgets(FilePath, 127, stdin); return -1; } cv::Mat frame, newframe, greyFrame, floatFrame, lastFrame, frame2, mog2RES, KNN, out_frame, avgFrame; std::vector<cv::Mat> diffIMGvec; //==【02】== 创建运动视频背景提取对象：用于分离背景和运动对象 cv::Ptr<cv::BackgroundSubtractorMOG2> bgMOG2 = cv::createBackgroundSubtractorMOG2(); cv::Ptr<cv::BackgroundSubtractorKNN> bgKNN = cv::createBackgroundSubtractorKNN(); bgMOG2->setVarThreshold(30); bool update_bg_model = true; //==【03】== 命名几个显示窗口 cv::namedWindow("RawWnd", cv::WINDOW_NORMAL); cv::setMouseCallback("RawWnd", onMouse, &newframe); //设置鼠标事件回调函数（"RawWnd"窗口的）：同时传递彩色图像指针； cv::namedWindow("Out_KNN", cv::WINDOW_NORMAL); cv::namedWindow("Out_MOG2", cv::WINDOW_NORMAL); int frameNums = 0; for (;;) { frame.rows = 0; double t1 = (double)cv::getCPUTickCount(); //开始统计时间 cap.read(frame); if (frame.rows == 0) break; cv::Size newSize(frame.cols / 2, frame.rows / 2); //压缩图像，将其尺寸缩小 cv::resize(frame, newframe, newSize); cv::cvtColor(newframe, greyFrame, cv::COLOR_RGB2GRAY); //转换为灰度图 cv::blur(greyFrame, greyFrame, cv::Size(3, 3)); //使用平滑运算 double t2 = (double)cv::getCPUTickCount(); bgMOG2->apply(greyFrame, mog2RES, update_bg_model ? -1 : 0); //使用MOG2算法提取前景 double t3 = (double)cv::getCPUTickCount(); //获取处理时间 double t4 = (double)cv::getCPUTickCount(); bgKNN->apply(greyFrame, KNN, update_bg_model ? -1 : 0); //使用KNN算法提取前景 double t5 = (double)cv::getCPUTickCount(); //获取处理时间 printf("MOG2 Time = %.3fms\n", 1e0 * (t3 - t2) / (double)cv::getTickFrequency()); printf("KNN Time = %.3fms\n", 1e0 * (t5 - t4) / (double)cv::getTickFrequency()); printf("Total Time = %.3fms\n", 1e0 * (t5 - t1) / (double)cv::getTickFrequency()); printf("--------------------\n"); if (!mog2RES.empty()) //计算MOG2算法下矩形框的积分亮度值 { cv::Mat showMat; mog2RES.copyTo(showMat); if (myMouseEventBusy == 0) { for (int k = 0; k < myLanneRect.size(); k++) { cv::rectangle(showMat, myLanneRect.at(k), cv::Scalar(255, 255, 255), 3); cv::Mat subMat = mog2RES(myLanneRect.at(k)); //再MOG2的前景提取结果中，取车道标记矩形框区域为subMat矩阵 cv::Mat sumMat; //积分图 == subMat的积分矩阵 cv::integral(subMat, sumMat, CV_32S); //设置积分矩阵的数据类型为uint； int sumValue = (int)sumMat.at<int>((int)sumMat.rows - 1, (int)sumMat.cols - 1); //获取积分图右下角的值，就是矩形框内亮度和； sprintf(errorMSG, "sum = %d;", sumValue); cv::putText(showMat, errorMSG, cv::Point(myLanneRect.at(k).x, myLanneRect.at(k).y + 4), 0.2, 1, cv::Scalar(255, 0, 0), 2);//显示矩形框内的亮度和； }//for k }////if cv::imshow("Out_MOG2", showMat); } if (!KNN.empty()) //计算KNN算法下矩形框的积分亮度值 { cv::Mat showMat; KNN.copyTo(showMat); if (myMouseEventBusy == 0) { for (int k = 0; k < myLanneRect.size(); k++) { cv::rectangle(showMat, myLanneRect.at(k), cv::Scalar(255, 255, 255), 3); cv::Mat subMat = KNN(myLanneRect.at(k)); //再KNN的前景提取结果中，取车道标记矩形框区域为subMat矩阵 cv::Mat sumMat; //积分图 == subMat的积分矩阵 cv::integral(subMat, sumMat, CV_32S); //设置积分矩阵的数据类型为uint； int sumValue = (int)sumMat.at<int>((int)sumMat.rows - 1, (int)sumMat.cols - 1); //获取积分图右下角的值，就是矩形框内亮度和； sprintf(errorMSG, "sum = %d;", sumValue); cv::putText(showMat, errorMSG, cv::Point(myLanneRect.at(k).x, myLanneRect.at(k).y + 4), 0.2, 1, cv::Scalar(255, 0, 0), 2);//显示矩形框内的亮度和； }//for k }////if imshow("Out_KNN", showMat); } //===>>> 显示原始图像：显示车道标记信息 + 矩形框内亮度和 + 车流量统计 #ifndef SHOW_RAW_MAT cv::Mat showMat; newframe.copyTo(showMat); //矩阵复制 sprintf(errorMSG, "mL=add Point; mR=add Rect; mM=delete Point;"); cv::putText(showMat, errorMSG, cv::Point(8, 32), 0.2, 1, cv::Scalar(255, 0, 0), 2);//显示提示信息； //==>> 显示车道矩形框为红色 + 车流量统计 + 车流量显示 if (myMouseEventBusy == 0) { for (int k = 0; k < myLanneRect.size(); k++) { cv::rectangle(showMat, myLanneRect.at(k), cv::Scalar(0, 0, 255), 3); cv::Mat subMat = mog2RES(myLanneRect.at(k)); //再MOG2的前景提取结果中，取车道标记矩形框区域为subMat矩阵 cv::Mat sumMat; //积分图 == subMat的积分矩阵 cv::integral(subMat, sumMat, CV_32S); //设置积分矩阵的数据类型为int，计算车道矩形框内亮度积分图； int sumValue = (int)sumMat.at<int>((int)sumMat.rows - 1, (int)sumMat.cols - 1); //获取积分图右下角的值，就是矩形框内亮度和； sprintf(errorMSG, "sum = %d;", sumValue); cv::putText(showMat, errorMSG, cv::Point(myLanneRect.at(k).x, myLanneRect.at(k).y + 4), 0.2, 1, cv::Scalar(255, 255, 0), 2);//显示矩形框内的亮度和； //===>>> 车流量统计： if (myLanneLightSum_Last.at(k) > detectTHD && sumValue <= detectTHD) { //:: 车辆通过了矩形框：上一帧亮度和大于阈值，本帧亮度和小于阈值；车辆计数器自加； myLanneVihicleCnt.at(k)++; myLanneLightSum_Last.at(k) = sumValue; } else myLanneLightSum_Last.at(k) = sumValue; //存储当前亮度和到myLanneLightSum_Last }//for k //===>> 车流量统计结果显示 cv::Mat topareaMat = showMat(cv::Rect(0, 0, showMat.cols, 75)); //最顶部48行置0； topareaMat *= 255; std::string strVihicleCnt = "VihicleCnt: "; for (int k = 0; k < myLanneRect.size(); k++) { sprintf(errorMSG, "L%d = %d;", k, myLanneVihicleCnt.at(k)); strVihicleCnt += errorMSG; } cv::putText(showMat, strVihicleCnt.c_str(), cv::Point(8, 64), 0.2, 1, cv::Scalar(0, 0, 255), 2); //流量统计显示到彩色图片上 }////if //==>> 显示正在标记的坐标点为蓝色： if (myMouseEventBusy == 0) { for (int k = 1; k < myMousePoints.size(); k++) { cv::line(showMat, myMousePoints.at(k - 1), myMousePoints.at(k), cv::Scalar(255, 0, 0), 15); }//for k if(myMousePoints.size() == 4) cv::line(showMat, myMousePoints.at(0), myMousePoints.at(3), cv::Scalar(255, 0, 0), 2); }////if imshow("RawWnd", showMat); #endif // SHOW_RAW_MAT int keycode = cv::waitKey(100); //等待100ms if (keycode == 'q') break; else if (keycode == ' ') { update_bg_model = !update_bg_model; printf("Learn background is in state = %d\n", update_bg_model); } else if (keycode == 'w') { //写文件：记录标记的矩形框到文件中： #ifndef WRITE_RECT_FILE FILE *pFILE = fopen("MarkRect.txt", "w"); if (pFILE != NULL) { for (int k = 0; k < myLanneRect.size(); k++) { fprintf(pFILE, "%d %d %d %d\n", myLanneRect.at(k).x, myLanneRect.at(k).y, myLanneRect.at(k).width, myLanneRect.at(k).height); } fclose(pFILE); }///if #endif // !WRITE_RECT_FILE } frameNums++; Sleep(50); }//for cap.release(); return 0; }
利用OpenCV对图像和视频进行载入和访问
运行平台OpenCV 4.2VS 2019图像在内存中的存储和操作从图像数据性质推理图像在内存中的存储方式：图像数据是多通道：1/3/4通道； 像素数据类型：1位，uchar，short，int，float，double等；OpenCV中的C++ Mat类：class CV_EXPORTS Mat { public: // …大量方法… ... /*! includes several bit-fields: - the magic signature - continuity flag - depth - number of channels */ int flags; //! the array dimensionality, >= 2 int dims; //! the number of rows and columns or (-1, -1) when the array has more than 2 dimensions int rows, cols; //! pointer to the data uchar* data; // other members ... };图像在内存中的基本要素：①列数cols = 宽度，行数rows = 高度，通道数（彩色空间类型），位深度（数据类型：8位无符号8U，32位浮点32F等）； ②像素存储区（uchar* data;）创建Mat矩阵--通过构造函数：Mat img(Size(320,240), CV_8UC3);`宽度(320)，高度(240)，数据类型8U(8位无符号)，3通道创建Mat矩阵--通过create函数：Mat img; img .create(Size(320,240), CV_32FC3);//32位浮点型创建载入图像文件：Mat img = imread("lena.jpg", CV_LOAD_IMAGE_COLOR);像素数据在内存中的组织方式：PS:内存是顺序排列的字节阵列通过Mat方法访问图像像素3通道24位深度（表达每个通道占用8位）彩色图像像素的访问：cv::Vec3b pixlsValue; //==表达这是一个3字节像素数据节点; pixlsValue = img.at<cv::Vec3b>(y, x); //y表达行号；x表达列号；尖括号<...>中的内容是C++模板，指定图像矩阵的数据类型；像素值的修改方法：pixlsValue.val[0] = xxx; pixlsValue.val[1] = xxx; pixlsValue.val[2] = xxx;修改后的像素值返回到存储器：img.at<cv::Vec3b>(y, x) = pixlsValue；其中，y和x为像素坐标;通过C++指针方法访问图像像素图像行列数Rows和Cols，数据首地址 uchar* data，通道数chaN，每行总字节数byteR可通过矩阵类接口获得；每个像素的字节数byteN= 位深度/8；位深度24位彩色图像每个像素占用3字节；float类型单通道图像每个像素占用4字节；...每行总字节数byteR = 每像素字节数 * 列数 = byteN * Cols;第row行第col列像素向量的地址偏移量（相对像素数据首字节）：pixlAddr = row * byteR + col * byteN ；像素向量首字节地址为 (uchar *)&data [pixlAddr];如果是uchar型图像，则像素首地址为：uchar *pixlPTR = (uchar*) data [pixlAddr];如果是float型图像，则像素首地址为：float *pixlPTR = (float*) data [pixlAddr];对于3通道8位（位深度24位）彩色图像(RGB)，则3个彩色通道像素存储位置为：pixlPTR[0] = B 、pixlPTR[1] = G 、pixlPTR[2] = R；通过实验可以证明，使用C++指针访问像素，时间开销远低于调用cv::Mat.at()方法；图像像素访问例程#include "stdafx.h" #include <Windows> #include <opencv2/core.hpp> #include <opencv2/imgproc.hpp> #include <opencv2/highgui.hpp> #include <opencv2/features2d.hpp> #ifdef _DEBUG #pragma comment(lib, "opencv_world420d.lib") #else #pragma comment(lib, "opencv_world420.lib") #endif int main(int argc, char* argv[]) { char curPathName[384] = ""; char FilePath[384] = "lena.jpg"; if (strlen(curPathName) > 0) sprintf(FilePath, "%s\\lena.jpg", curPathName); //图片文件路径 cv::Mat readImg = cv::imread(FilePath, cv::IMREAD_COLOR); //读取彩色图片: opencv420的IMREAD_COLOR与之前版本不同 if (readImg.empty()) { printf("error#41: 未能读取到图片，检查文件是否存在！回车退出！\n图片路径=%s\n", FilePath); fgets(FilePath, 127, stdin); return -1; } cv::imshow("readImg", readImg);//显示图片，窗口标题为readImg cv::Mat imgFloat; readImg.convertTo(imgFloat, CV_32F); //在C++中，访问矩阵中的像素有两种方法：通过矩阵方法和通过指针；通过指针是最快的访问方法 cv::Vec3b colorValue; uchar greyValue; int channelNUMS = readImg.channels(); uchar *pDATA = readImg.data; //为了通过指针访问存储区，我们获取存储区的首指针 int dataSTEP = readImg.step1(); //注意，当图像列数cols=512，3通道时，dataSTEP值为512 * 3 = 1536； int dataSTEP2 = imgFloat.step1(); //注意，当图像列数cols=512，3通道时，dataSTEP值为512 * 3 = 1536； int64 timeSTART = cv::getTickCount(); for (int i = 0; i< imgFloat.rows / 2; i++) { for (int j = 0; j< readImg.cols; j++) { //我们使用条件编译开关来控制使用那一段代码 #ifndef ACCESS_IAMGE_BY_PTR //使用Mat的At()方法访问像素：花费时间几乎是指针方法的10倍； switch (channelNUMS) { case 1: readImg.at<uchar>(i, j) = 255 - readImg.at<uchar>(i, j); break; case 3: colorValue = readImg.at<cv::Vec3b>(i, j); colorValue.val[0] = 255 - colorValue.val[0]; colorValue.val[1] = 255 - colorValue.val[1]; colorValue.val[2] = 255 - colorValue.val[2]; readImg.at<cv::Vec3b>(i, j) = colorValue; break; } #else //使用指针方法访问像素：花费时间几乎是Mat的At()方法的1/10倍； if (pDATA[i * dataSTEP * sizeof(uchar) + j * channelNUMS + 0] > 200 && pDATA[i * dataSTEP * sizeof(uchar) + j * channelNUMS + 1] > 200 && pDATA[i * dataSTEP * sizeof(uchar) + j * channelNUMS + 2] > 200) { pDATA[i * dataSTEP * sizeof(uchar) + j * channelNUMS + 0] = 64; pDATA[i * dataSTEP * sizeof(uchar) + j * channelNUMS + 1] = 64; pDATA[i * dataSTEP * sizeof(uchar) + j * channelNUMS + 2] = 0; } else { pDATA[i * dataSTEP * sizeof(uchar) + j * channelNUMS + 0] = 255 - pDATA[i * dataSTEP * sizeof(uchar) + j * channelNUMS + 0]; pDATA[i * dataSTEP * sizeof(uchar) + j * channelNUMS + 1] = 255 - pDATA[i * dataSTEP * sizeof(uchar) + j * channelNUMS + 1]; pDATA[i * dataSTEP * sizeof(uchar) + j * channelNUMS + 2] = 255 - pDATA[i * dataSTEP * sizeof(uchar) + j * channelNUMS + 2]; } #endif ACCESS_IAMGE_BY_PTR } } int64 timeEND = cv::getTickCount(); //处理结束时间 double usingTime = (double)(timeEND - timeSTART) / (double)cv::getTickFrequency();//统计矩阵操作花费的时间，单位ms； printf("INFO#128: 图像处理耗时：%.6f 秒\n", usingTime); cv::imshow("Nagtive Film", readImg); cv::imwrite("Nagtive_Film.bmp", readImg); //保存处理后的图像为bmp文件 cv::waitKey();//等待按下键盘，这是程序退出前让显示窗得以被显示的方法！ return 0; }VS 2019实际运行效果如下：视频图像的特点视频源：摄像头和视频文件；视频图像格式：AVI、VCD、SVCD、DVD、MPG、WMV、RM、RMVB、MOV、MP4、MPEG4、3GP、H264等等。视频图像在硬盘中存储要求更高压缩比，压缩、解压缩处理的计算开销较大；无论什么格式视频图像，载入到内存后将被转换为像素矩阵，并存储到内存中。此时，像素访问方法将与静止图像没有差异。通过C++调用OpenCV库载入图像这里使用C++和OpenCV库将视频载入内存，并将视频图像记录到另一个视频文件中可以发现：视频是一个图像序列；图像序列中的每幅图像仍以像素矩阵存储在内存中，访问方法与静止图像相同。使用OpenCV记录的视频文件，仅仅存储了经过编码后的视频流数据，还不符合视频文件格式标准，不能使用常规的播放器来播放，需要使用一些特殊的视频播放器（例如VLC）来播放。//#include "stdafx.h" #include <stdio.h> #include <iostream> // for standard I/O #include <Windows.h> #include <opencv2/core.hpp> #include <opencv2/imgproc.hpp> #include <opencv2/highgui.hpp> #include <opencv2/features2d.hpp> #include <opencv2/video.hpp> #ifdef _DEBUG0 #pragma comment(lib, "opencv_world420d.lib") #else #pragma comment(lib, "opencv_world420.lib") #endif int main(int argc, char **argv) { char curPathName[384] = ""; //==【01】== 打开视频文件或摄像头 char FilePath[384] = "HighWay_Video_001.Avi"; if (strlen(curPathName) > 0) sprintf(FilePath, "%s\\HighWay_Video_001.Avi", curPathName); //图片文件路径 //==【01】== 视频输入选择和打开：摄像头或视频文件 cv::String path = FilePath; if(argc > 1) path = argv[1]; //可通过命令行第二个参数指定视频文件名 //path = ""; //====>>>>如果启用此行，将从摄像头中读取视频图像<<<<==== cv::VideoCapture cap; //VideoCapture类实例化，使用缺省摄像头 if(path.length() == 0) cap.open(0); //打开第一个摄像头 else cap.open(path); //打开视频文件 if(!cap.isOpened()) // check if we succeeded return -1; cv::Mat frame, greyFrame; //==【02】== 视频记录类实例化 cv::VideoWriter outputVideo; // Open the output int saveAVIenable = 0; //非0时记录视频 if(saveAVIenable){ bool rtn = outputVideo.open("Result003.avi", cv::VideoWriter::fourcc('M', 'J', 'P', 'G'), 10, cv::Size((int)cap.get(cv::CAP_PROP_FRAME_WIDTH), (int)cap.get(cv::CAP_PROP_FRAME_HEIGHT) ) , true); if(!rtn) return -2; } //==【03】== 创建一个运动视频背景提取对象：分离背景和运动对象： 暂时未使用！ cv::Ptr<cv::BackgroundSubtractorMOG2> bgsubtractor= cv::createBackgroundSubtractorMOG2(); //==【04】== 命名两个显示窗口 cv::namedWindow("SRC", 0); cv::namedWindow("GREY", 0); //==【05】== 循环读取视频 int frameNums = 0; for(;;) { double t1 = (double)cv::getCPUTickCount(); cap.read(frame); //读取一个视频帧 if(frame.empty()) break; if(saveAVIenable) outputVideo.write(frame); //写视频文件 //==>> 取视频帧frame的一个矩形区，转换为灰度图像，存储到greyFrame cv::cvtColor(frame(cv::Rect(0, 150, frame.cols, frame.rows - 150)), greyFrame, cv::COLOR_RGB2GRAY); //==>> 将图像greyFrame的长宽分别缩小到1/2 cv::resize(greyFrame, greyFrame, cv::Size(greyFrame.cols/2, greyFrame.rows/2)); //==>> 使用7*7卷积窗，平滑灰度图像greyFrame cv::blur(greyFrame, greyFrame, cv::Size(7,7)); // ==>> 计算处理时间 double t2 = (double)cv::getCPUTickCount(); printf("Line74 Video Play Delta Time = %.3fms\n", 1e0 * (t2 - t1)/(double)cv::getTickFrequency()); // ==>> 显示处理结果 imshow("SRC", frame); if(!greyFrame.empty()) imshow("GREY", greyFrame); // ==>> 按下键盘q退出 int keycode = cv::waitKey(30); if( keycode == 'q') break; frameNums++; }//for if(saveAVIenable) outputVideo.release(); cap.release(); return 0; }VS 2019实际运行效果如下：
学做图书管理系统--使用pyodbc和PyQt5连接前台GUI和后台SQL Server数据库
在连接数据库之前，需要确保电脑上有以下环境和软件：（必备）任意一种数据库（这里选择SQL Server）（必备）Python 3，可以在Python的官网下载对应的版本（必备）任意一种代码编辑器或IDE（本教程使用的VS Code和SSMS两种，也可使用Python自带的IDLE或者Pycharm）（可选）使用Python包管理器Anaconda，或者使用Python自带的pip安装pyodbc首先，使用组合键Win + R调出运行窗口，输入cmd，打开命令行。在命令行（CMD）界面，如使用了Anaconda，则需要激活conda环境（如没有Anaconda，则可以跳过这一步）：activate当光标前面出现了(base) C:\Users\用户名>，则说明进入了conda的base环境。输入如下任意其中一种命令：pip install pyodbc #常规方法，pip安装 conda install pyodbc #如果安装了Anaconda，则可以使用conda命令当出现提示确认是否安装，输入y，则可以等待安装完成。安装SQL Server的ODBC驱动程序和命令行工具进入微软的此页面下载安装ODBC驱动程序进入微软的此页面下载安装命令行工具在命令行输入如下命令登录SQL Server：sqlcmd -S localhost -U sa -P your_password 1> #已登录成功
学做图书管理系统--使用SQL Server创建并插入数据，建立触发器和存储过程
学做图书管理系统--利用Python的PyQt5编写简单的GUI图形界面
自己使用到的工具VS CodePyQt5依赖包Qt DesignerPython几种常见的GUI库TkinterTkinter是Python自带的量轻级的简单GUI库，使用也较为简单。但是Tkinter之适用于基础和小型程序的开发，界面的美观程度也相对有限。在网上找到了比较全面介绍Tkinter的使用教程：https://www.cnblogs.com/shwee/p/9427975.htmlhttps://www.runoob.com/python/python-gui-tkinter.htmlPyQt5PyQt5算是比较成熟的GUI库了，PyQt从著名的由C++编写的Qt库移植封装到Python而来。同时，利用PyQt5开发GUI比较美观，同时利用Qt Designer可以拖动一些控件，实现GUI可视化的快速开发，可通过pyuic5工具转化为.py文件。在这里，自己选择了PyQt5开发前端界面。安装PyQt5和Qt Designer安装过程较为简单，只需在命令行窗口输入两行命令（在国内可以使用清华大学tuna镜像站，下载依赖包更快）：pip install PyQt5 -i https://pypi.tuna.tsinghua.edu.cn/simple #安装PyQt5 pip install PyQt5-tools -i https://pypi.tuna.tsinghua.edu.cn/simple # 安装Qt Designer如图：PS：若使用Anaconda，会省去很多环境配置步骤，这里只需安装PyQt5-tools即可，命令同上。如果想使用Anaconda，这里有详尽的配置及使用方法：Anaconda详细安装及使用教程（带图文）安装成功后，Qt Designer会放在Python（或Anaconda）安装路径，即安装路径\Python37\Lib\site-packages\pyqt5-tools\Qt\bin或者安装路径\Anaconda3\Lib\site-packages\pyqt5-tools\Qt\bin中，找到designer.exe运行即可。PS：为了今后打开方便，可以创建一个快捷方式哦~Qt Designer界面如下：其中： 左栏为一些常用的一些控件，可以拖放到窗口里面；右栏为不同的控件的值的设定，选中对应的控件便可以编辑。关于Qt Designer的使用方法，可以自行找找度娘，使用方法很多啦~~这里放一个教程：PyQt5速成教程导出为.py文件当我们按照自己的需求，设计好了相关的界面后，便可以导入成.py文件啦~设计保存后，桌面上会有一个.ui文件，这个就是Qt Designer生成的XML格式的文件。这里，我们使用pyuic5这个插件转换文件格式。转到这个文件夹目录后，在命令行窗口输入如下格式命令：pyuic5 - o 目标文件名.py 源文件名.ui如我的这样做的：此时，会发现，文件夹多了一个转换后的同名.py文件：打开SetupUi.py文件，发现里面代码很长，但是已经自动生成了，这样少了很多敲代码的步骤呢~# -*- coding: utf-8 -*- # Form implementation generated from reading ui file 'SetupUi.ui' # # Created by: PyQt5 UI code generator 5.13.0 # # WARNING! All changes made in this file will be lost! from PyQt5 import QtCore, QtGui, QtWidgets class Ui_mainWindow(object): def setupUi(self, mainWindow): mainWindow.setObjectName("mainWindow") mainWindow.resize(561, 491) mainWindow.setAnimated(False) self.centralwidget = QtWidgets.QWidget(mainWindow) self.centralwidget.setObjectName("centralwidget") self.verticalLayout = QtWidgets.QVBoxLayout(self.centralwidget) self.verticalLayout.setObjectName("verticalLayout") self.tabWidget = QtWidgets.QTabWidget(self.centralwidget) self.tabWidget.setObjectName("tabWidget") self.tab = QtWidgets.QWidget() self.tab.setObjectName("tab") self.formLayout = QtWidgets.QFormLayout(self.tab) self.formLayout.setObjectName("formLayout") self.label1 = QtWidgets.QLabel(self.tab) self.label1.setObjectName("label1") self.formLayout.setWidget(0, QtWidgets.QFormLayout.FieldRole, self.label1) self.graphicsView = QtWidgets.QGraphicsView(self.tab) brush = QtGui.QBrush(QtGui.QColor(0, 0, 0)) brush.setStyle(QtCore.Qt.NoBrush) self.graphicsView.setBackgroundBrush(brush) self.graphicsView.setObjectName("graphicsView") self.formLayout.setWidget(3, QtWidgets.QFormLayout.FieldRole, self.graphicsView) self.buttonBox = QtWidgets.QDialogButtonBox(self.tab) self.buttonBox.setStatusTip("") self.buttonBox.setAccessibleName("") self.buttonBox.setOrientation(QtCore.Qt.Horizontal) self.buttonBox.setStandardButtons(QtWidgets.QDialogButtonBox.Close|QtWidgets.QDialogButtonBox.Ok) self.buttonBox.setCenterButtons(False) self.buttonBox.setObjectName("buttonBox") self.formLayout.setWidget(4, QtWidgets.QFormLayout.FieldRole, self.buttonBox) self.frame = QtWidgets.QFrame(self.tab) self.frame.setFrameShape(QtWidgets.QFrame.StyledPanel) self.frame.setFrameShadow(QtWidgets.QFrame.Raised) self.frame.setObjectName("frame") self.formLayout.setWidget(5, QtWidgets.QFormLayout.LabelRole, self.frame) self.tabWidget.addTab(self.tab, "") self.tab_2 = QtWidgets.QWidget() self.tab_2.setObjectName("tab_2") self.tabWidget.addTab(self.tab_2, "") self.verticalLayout.addWidget(self.tabWidget) mainWindow.setCentralWidget(self.centralwidget) self.menubar = QtWidgets.QMenuBar(mainWindow) self.menubar.setGeometry(QtCore.QRect(0, 0, 561, 23)) self.menubar.setObjectName("menubar") self.menu_options = QtWidgets.QMenu(self.menubar) self.menu_options.setObjectName("menu_options") self.menu_about = QtWidgets.QMenu(self.menubar) self.menu_about.setObjectName("menu_about") mainWindow.setMenuBar(self.menubar) self.statusbar = QtWidgets.QStatusBar(mainWindow) self.statusbar.setObjectName("statusbar") mainWindow.setStatusBar(self.statusbar) self.action = QtWidgets.QAction(mainWindow) self.action.setCheckable(False) self.action.setWhatsThis("") self.action.setShortcutContext(QtCore.Qt.WindowShortcut) self.action.setObjectName("action") self.action_2 = QtWidgets.QAction(mainWindow) self.action_2.setShortcutContext(QtCore.Qt.ApplicationShortcut) self.action_2.setMenuRole(QtWidgets.QAction.QuitRole) self.action_2.setObjectName("action_2") self.menu_options.addAction(self.action_2) self.menu_about.addAction(self.action) self.menubar.addAction(self.menu_options.menuAction()) self.menubar.addAction(self.menu_about.menuAction()) self.retranslateUi(mainWindow) self.tabWidget.setCurrentIndex(0) QtCore.QMetaObject.connectSlotsByName(mainWindow) def retranslateUi(self, mainWindow): _translate = QtCore.QCoreApplication.translate mainWindow.setWindowTitle(_translate("mainWindow", "MainWindow")) self.tabWidget.setToolTip(_translate("mainWindow", " ")) self.label1.setText(_translate("mainWindow", "学生图书管理系统")) self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab), _translate("mainWindow", "Tab 1")) self.tabWidget.setTabText(self.tabWidget.indexOf(self.tab_2), _translate("mainWindow", "Tab 2")) self.menu_options.setTitle(_translate("mainWindow", "选项")) self.menu_about.setTitle(_translate("mainWindow", "关于")) self.action.setText(_translate("mainWindow", "关于作者")) self.action_2.setText(_translate("mainWindow", "退出")) 使用VS Code编写基础程序使用VS Code打开这个文件夹，秉承界面和其他操作分离的原则，我们可以新建一个.py文件，如命名为ConnetDatabase.py，从而调用刚才生成的界面模块，新建一个MyWindow的子类，继承于QMainWindow和Ui_mainWindow父类，并初始化，编写构造函数即可。最后，创建一个名为myWin的对象，启动即可。import sys from PyQt5.QtWidgets import QApplication, QMainWindow #导入PyQt相关模块 from SetupUi import * #导入之前新生成的窗口模块 class MyWindow(QMainWindow, Ui_mainWindow): def __init__(self, parent=None): super(MyWindow, self).__init__(parent) self.setupUi(self) if __name__ == '__main__': app = QApplication(sys.argv) myWin = MyWindow() #创建对象 myWin.show() #显示窗口 sys.exit(app.exec_()) 运行后的效果如图：至此，基本的窗口创建成功，便可以开始写后端的具体代码实现了~参考资料八款常用的 Python GUI 开发框架推荐Python制作小软件——1. 安装并使用PyQt5进行界面设计Qt官网——Getting to Know Qt DesignerQt官网——Qt Designer ManualPyQt5快速入门（二）PyQt5信号槽机制【第一节】PyQt5简介
[转]计算机四级网络工程师思维导图--计算机网络部分
计算机四级网络工程师考试快开始了。 这里总结和整理了相关的常考重点的思维导图，挺实用的，希望对大家有用哦~此部分为计算机网络部分~PS:思维导图来自微信公众号“方大熊”，自己做了稍微的整理，希望大家喜欢呀~图片高清，但文字较小也很长，可以点击图片后放大学习哦~一、网络技术基础二、局域网基础三、Internet基础感谢你的耐心阅读呀(๑◡๑)，总结不易，收藏点个赞呗~你的支持就是我前进的动力呀~
[转]计算机四级网络工程师思维导图--操作系统部分
计算机四级网络工程师考试快开始了。 这里总结和整理了相关的常考重点的思维导图，挺实用的，希望对大家有用哦~此部分为操作系统部分~PS:思维导图来自微信公众号“方大熊”，自己做了稍微的整理，希望大家喜欢呀~图片高清，但文字较小也很长，可以点击图片后放大学习哦~一、操作系统原理二、操作系统运行机制三、进程线程模型四、并发与同步五、内存管理六、文件管理七、I/O设备管理八、死锁感谢你的耐心阅读呀(๑◡๑)，总结不易，收藏点个赞呗~你的支持就是我前进的动力呀~