-
一、前言1.1 功能简介随着物联网技术的快速发展,无线通信模块成为了连接物理世界与数字世界的桥梁。在众多无线通信解决方案中,SIM系列模块以其紧凑的设计、良好的网络兼容性以及丰富的功能特性而受到广泛欢迎。SIM系列模块不仅支持基础的GSM服务如语音通话和短信收发,还能够通过GPRS进行数据传输,非常适合应用于各种智能设备和远程监控系统中。对于开发者而言,如何有效地利用SIM系列模块实现特定功能成为了一个重要技术点。其中,使用串行通信接口(即串口)发送AT指令来控制模块是实现这一目标的关键手段之一。AT指令集提供了一套标准化的方法,让工程师可以通过简单的文本命令与模块交互,从而执行复杂任务。本文将以SIM800C为例,详细介绍如何通过串口AT指令对SIM800C进行调试,实现打电话和接听电话这两个常见应用场景。通过对相关AT指令的学习及实践操作,将能够掌握如何设置并测试SIM800C的基本通话功能。1.2 拨打电话功能的应用场景这里以SIM800C模块为例进行介绍。在物联网(IoT)应用中,SIM800C模块的拨打电话功能可以实现多种实用的功能,尤其是在需要远程控制、监控或紧急响应的情况下。以下是几个具体的应用场景:(1)远程控制:通过拨打预设的电话号码,用户可以从远处激活或控制设备。例如,在智能家居系统中,可以通过电话呼叫来打开或关闭家中的灯光、调整空调温度或是启动安防系统。(2)安全报警:当监测到异常情况时,如入侵警报、火灾预警等,SIM800C可以自动拨打预先设置好的紧急联系人或服务中心电话,及时通报情况并请求帮助。这种方式能够迅速传达警情,提高响应速度。(3)状态查询:某些情况下,用户可能希望直接与远端设备进行互动以获取实时信息。比如农业环境监测站,农民可以通过拨打特定号码来接收土壤湿度、气温等数据报告。(4)语音提示:结合语音合成技术,SIM800C还可以用来发送语音消息给用户。这在一些公共服务领域特别有用,例如公共交通车辆到达提醒、天气预报更新等。(5)医疗辅助:对于老年人护理或者慢性病患者管理来说,SIM800C可以被集成进穿戴式健康监测器内。一旦检测到生命体征异常,设备会自动拨打医生或家属的电话,确保病人能够得到及时救助。(6)工业自动化:在复杂的工业环境中,维护人员可能需要定期检查机器运行状况。利用SIM800C建立一个简易的语音通信通道后,工作人员就可以随时随地拨入系统听取设备状态汇报了。(7)物流跟踪:货物运输途中,通过安装带有SIM800C模块的追踪装置,物流公司能够更加方便地与司机保持联系,甚至可以在必要时主动发起通话询问具体情况。借助SIM800C的拨打电话功能,开发者能够在多种IoT应用场景下构建出高效且可靠的通讯解决方案。1.3 SIM900A与SIM800C模块介绍SIM900A和SIM800C都是由SIMCom公司设计生产的GSM/GPRS通信模块,它们广泛应用于各种物联网(IoT)设备中,提供语音通话、短信服务以及数据传输功能。这些模块基于成熟可靠的GSM技术,适用于全球范围内的多种应用场合。SIM900A,它是一款四频GSM/GPRS模块,支持850MHz、900MHz、1800MHz和1900MHz频率,这意味着它可以适应世界上几乎所有地区的GSM网络。SIM900A拥有紧凑的设计,尺寸为24 x 24 x 3 mm,非常适合集成到空间有限的项目中。该模块通过标准AT指令集进行控制,允许开发者轻松实现电话拨打、短信收发及GPRS连接等功能。此外,SIM900A还具备低功耗模式,在不活跃时可以节省电池寿命,这对于依赖电池供电的远程监控系统尤其重要。SIM800C,这是SIMCom推出的另一款高性能GSM/GPRS模块,同样支持四频GSM网络,并且在某些方面对SIM900A进行了改进或扩展。例如,SIM800C不仅保持了小巧的体积,还在性能上有所提升,特别是在处理速度和稳定性方面。与SIM900A一样,SIM800C也遵循标准AT命令集,使得迁移现有应用程序变得相对容易。值得注意的是,SIM800C增加了对更多高级功能的支持,如增强型音频接口,这使其成为需要高质量语音通信的应用的理想选择。此外,SIM800C还优化了电源管理机制,进一步降低了待机状态下的能耗。除了上述特性外,无论是SIM900A还是SIM800C,都提供了丰富的外部接口选项,包括UART串口、GPIO引脚、ADC输入等,方便用户根据实际需求定制开发。同时,两者均兼容广泛的SIM卡类型,从普通SIM卡到更先进的USIM卡均可使用。这些特点使得SIM900A和SIM800C能够满足不同层次的应用需求,从简单的信息传递到复杂的物联网解决方案。SIM900A和SIM800C作为SIMCom旗下广受欢迎的GSM/GPRS模块系列成员,各自以其独特的优势服务于不同的市场细分领域。无论是在智能家居、工业自动化、移动支付终端还是车辆追踪系统等领域,这些模块都能够提供稳定可靠的无线通信能力,助力于构建更加智能互联的世界。随着技术不断进步,未来我们还可以期待看到更多具有创新特性的新型号推出,以更好地应对日益增长的市场需求。1.4 原理图三、模块调试3.1 工具软件下载项目设计里用到的工具软件,STM32的源码工程,都可以在这里下载。 https://pan.quark.cn/s/145a9b3f7f533.2 准备好模块(1)ATK-SIM800C GSM/GPRS 模块一个(2)直流稳压电源1个(推荐12V 1A电源)(3)中国移动/联通GSM SIM卡一张(未停机,开通GPRS业务)(4)耳机一副(带麦克风功能,用于通话测试)3.3 串口调试助手的设置模块接好线,连接上电脑之后,发送AT命令测试模块是否正常(注意 勾选新行)。说明: 第一次发送AT 过去时,模块会匹配波特率,模块会自适应波特率; 第二次发送AT就会返回OK。AT OK3.4 初始化配置下面是上电初始的设置指令。【1】测试模块是否正常 发送: AT 模块会返回: OK【2】 设置TTS来电有铃声 发送: AT+CTTSRING=0 模块会返回: OK【3】设置TTS声音大小、语调配置 发送: AT+CTTSPARAM=20,0,50,70,0 模块会返回: OK【4】设置来电显示 发送: AT+CLIP=1 模块会返回: OK【5】设置被叫号码显示 发送: AT+COLP=1 模块会返回: OK3.5 拨打电话的测试流程下面是介绍关于电话相关的指令。【1】拨打电话 ATD指令用于拨打任意电话号码,格式为:ATD【号码】; 末尾的’;’一定要加上,否则不能 成功拨号,如发送:ATD10086;,即可实现拨打10086。发送: ATD10086; 模块会返回: +COLP: 表示拨号成功。如果返回NO CARRIER、NO ANSWER、ERROR都表示错误。【2】应答电话 ATA指令,用于应答电话,当收到来电的时候,给模块发送:ATA,即可接听来电。 发送: ATA 模块会返回: OK如果模块收到+CLIP:就表示收到来电,可以发送ATA接听电话。【3】挂断电话 ATH指令,用于挂断电话,要想结束正在进行的通话,只需给模块发送:ATH,即可挂断。 发送: ATH 模块会返回: OK四、代码实现下面是通过STM32完成对SIM800C模块控制完成短信发送、电话拨打等操作。4.1 底层的命令发送接口//SIM800C发送命令后,检测接收到的应答 //str:期待的应答结果 //返回值:0,没有得到期待的应答结果 //其他,期待应答结果的位置(str的位置) u8* sim800c_check_cmd(u8 *str) { char *strx=0; if(USART2_RX_STA&0X8000) //接收到一次数据了 { USART2_RX_BUF[USART2_RX_STA&0X7FFF]=0;//添加结束符 strx=strstr((const char*)USART2_RX_BUF,(const char*)str); } return (u8*)strx; } //向SIM800C发送命令 //cmd:发送的命令字符串(不需要添加回车了),当cmd<0XFF的时候,发送数字(比如发送0X1A),大于的时候发送字符串. //ack:期待的应答结果,如果为空,则表示不需要等待应答 //waittime:等待时间(单位:10ms) //返回值:0,发送成功(得到了期待的应答结果) // 1,发送失败 u8 sim800c_send_cmd(u8 *cmd,u8 *ack,u16 waittime) { u8 res=0; USART2_RX_STA=0; if((u32)cmd<=0XFF) { while(DMA1_Channel7->CNDTR!=0); //等待通道7传输完成 USART2->DR=(u32)cmd; }else u2_printf("%s\r\n",cmd);//发送命令 if(ack&&waittime) //需要等待应答 { while(--waittime) //等待倒计时 { delay_ms(10); if(USART2_RX_STA&0X8000)//接收到期待的应答结果 { if(sim800c_check_cmd(ack))break;//得到有效数据 USART2_RX_STA=0; } } if(waittime==0)res=1; } return res; } 4.2 底层数据接收接口//接收SIM800C数据 //request:期待接收命令字符串 //waittimg:等待时间(单位:10ms) //返回值:0,发送成功(得到了期待的应答结果) // 1,发送失败 u8 sim800c_wait_request(u8 *request ,u16 waittime) { u8 res = 1; u8 key; if(request && waittime) { while(--waittime) { key=KEY_Scan(0); if(key==WKUP_PRES) return 2; delay_ms(10); if(USART2_RX_STA &0x8000)//接收到期待的应答结果 { if(sim800c_check_cmd(request)) break;//得到有效数据 USART2_RX_STA=0; } } if(waittime==0)res=0; } return res; }4.3 检测模块是否存在while(sim800c_send_cmd("AT","OK",100))//检测是否应答AT指令 { Show_Str(40,55,200,16,"未检测到模块!!!",16,0); delay_ms(800); LCD_Fill(40,55,200,55+16,WHITE); Show_Str(40,55,200,16,"尝试连接模块...",16,0); delay_ms(400); } 4.4 网络时间同步//NTP网络同步时间 void ntp_update(void) { sim800c_send_cmd("AT+SAPBR=3,1,\"Contype\",\"GPRS\"","OK",200);//配置承载场景1 sim800c_send_cmd("AT+SAPBR=3,1,\"APN\",\"CMNET\"","OK",200); sim800c_send_cmd("AT+SAPBR=1,1",0,200); //激活一个GPRS上下文 delay_ms(5); sim800c_send_cmd("AT+CNTPCID=1","OK",200); //设置CNTP使用的CID sim800c_send_cmd("AT+CNTP=\"202.120.2.101\",32","OK",200); //设置NTP服务器和本地时区(32时区 时间最准确) sim800c_send_cmd("AT+CNTP","+CNTP: 1",600); //同步网络时间 }4.5 拨打电话与接听电话//SIM800C拨号测试 //用于拨打电话和接听电话 //返回值:0,正常 //其他,错误代码 u8 sim800c_call_test(void) { u8 key; u16 lenx; u8 callbuf[20]; u8 pohnenumlen=0; //号码长度,最大15个数 u8 *p,*p1,*p2; u8 oldmode=0; u8 cmode=0; //模式 //0:等待拨号 //1:拨号中 //2:通话中 //3:接收到来电 LCD_Clear(WHITE); if(sim800c_send_cmd("AT+CTTSRING=0","OK",200))return 1; //设置TTS来电设置 0:来电有铃声 1:没有 if(sim800c_send_cmd("AT+CTTSPARAM=20,0,50,70,0","OK",200))return 1; //设置TTS声音大小、语调配置 if(sim800c_send_cmd("AT+CLIP=1","OK",200))return 1; //设置来电显示 if(sim800c_send_cmd("AT+COLP=1","OK",200))return 2; //设置被叫号码显示 p1=mymalloc(20); //申请20直接用于存放号码 if(p1==NULL)return 2; POINT_COLOR=RED; Show_Str_Mid(0,30,"SIM800C 拨号测试",16,240); Show_Str(40,70,200,16,"请拨号:",16,0); kbd_fn_tbl[0]="拨号"; kbd_fn_tbl[1]="返回"; sim800c_load_keyboard(0,180,(u8**)kbd_tbl1); POINT_COLOR=BLUE; while(1) { delay_ms(10); if(USART2_RX_STA&0X8000) //接收到数据 { sim_at_response(0); if(cmode==1||cmode==2) { if(cmode==1)if(sim800c_check_cmd("+COLP:"))cmode=2; //拨号成功 if(sim800c_check_cmd("NO CARRIER"))cmode=0; //拨号失败 if(sim800c_check_cmd("NO ANSWER"))cmode=0; //拨号失败 if(sim800c_check_cmd("ERROR"))cmode=0; //拨号失败 } if(sim800c_check_cmd("+CLIP:"))//接收到来电 { cmode=3; p=sim800c_check_cmd("+CLIP:"); p+=8; p2=(u8*)strstr((const char *)p,"\""); p2[0]=0;//添加结束符 strcpy((char*)p1,(char*)p); } USART2_RX_STA=0; } key=sim800c_get_keynum(0,180); if(key) { if(key<13) { if(cmode==0&&pohnenumlen<15) { callbuf[pohnenumlen++]=kbd_tbl[key-1][0]; u2_printf("AT+CLDTMF=2,\"%c\"\r\n",kbd_tbl[key-1][0]); delay_ms(55);//延时 u2_printf("AT+CTTS=2,\"%c\"\r\n",kbd_tbl[key-1][0]); //TTS语音 }else if(cmode==2)//通话中 { u2_printf("AT+CLDTMF=2,\"%c\"\r\n",kbd_tbl[key-1][0]); delay_ms(100); u2_printf("AT+VTS=%c\r\n",kbd_tbl[key-1][0]); LCD_ShowChar(40+56,90,kbd_tbl[key-1][0],16,0); } }else { if(key==13)if(pohnenumlen&&cmode==0)pohnenumlen--;//删除 if(key==14)//执行拨号 { if(cmode==0)//拨号模式 { callbuf[pohnenumlen]=0; //最后加入结束符 printf("ATD:%s\r\n",callbuf); u2_printf("ATD%s;\r\n",callbuf);//拨号 sim_at_response(1); delay_ms(10); //等待10ms cmode=1; //拨号中模式 }else { u2_printf("AT\r\n");//必须加上这句 delay_ms(10); sim800c_send_cmd("ATH","OK",100);//挂机 sim800c_send_cmd("ATH","OK",100);//挂机 cmode=0; } } if(key==15) { if(cmode==3)//接收到来电 { sim800c_send_cmd("ATA","OK",200);//发送应答指令 Show_Str(40+56,70,200,16,callbuf,16,0); cmode=2; }else { sim800c_send_cmd("ATH",0,0);//不管有没有在通话,都结束通话 break;//退出循环 } } } if(cmode==0)//只有在等待拨号模式有效 { callbuf[pohnenumlen]=0; LCD_Fill(40+56,70,239,70+16,WHITE); Show_Str(40+56,70,200,16,callbuf,16,0); } } if(oldmode!=cmode)//模式变化了 { switch(cmode) { case 0: kbd_fn_tbl[0]="拨号"; kbd_fn_tbl[1]="返回"; POINT_COLOR=RED; Show_Str(40,70,200,16,"请拨号:",16,0); LCD_Fill(40+56,70,239,70+16,WHITE); if(pohnenumlen) { POINT_COLOR=BLUE; Show_Str(40+56,70,200,16,callbuf,16,0); } break; case 1: POINT_COLOR=RED; Show_Str(40,70,200,16,"拨号中:",16,0); pohnenumlen=0; case 2: POINT_COLOR=RED; if(cmode==2)Show_Str(40,70,200,16,"通话中:",16,0); kbd_fn_tbl[0]="挂断"; kbd_fn_tbl[1]="返回"; break; case 3: POINT_COLOR=RED; Show_Str(40,70,200,16,"有来电:",16,0); POINT_COLOR=BLUE; Show_Str(40+56,70,200,16,p1,16,0); kbd_fn_tbl[0]="挂断"; kbd_fn_tbl[1]="接听"; break; } if(cmode==2)Show_Str(40,90,200,16,"DTMF音:",16,0); //通话中,可以通过键盘输入DTMF音 else LCD_Fill(40,90,120,90+16,WHITE); sim800c_load_keyboard(0,180,(u8**)kbd_tbl1); //显示键盘 oldmode=cmode; } if((lenx%50)==0) { LED0=!LED0; u2_printf("AT\r\n");//必须加上这句,不然接收不到来电显示 } lenx++; } myfree(p1); return 0; }
-
一、前言随着深度学习技术和计算能力的进步,AI生成视频(AIGV)已经从一个研究概念演变成了一种实用工具,其应用场景也在不断拓展。从自动合成新闻报道到虚拟人物的互动视频,从电影特效生成到游戏场景的实时渲染,AI生成视频正逐步成为内容创作者不可或缺的利器。不仅被专业人士所采用,也逐渐走向大众市场。这些工具能够帮助用户快速创建出高质量的视频内容,无论是用于商业宣传、艺术创作还是个人娱乐,都能够满足不同场景的需求。随着AI生成视频技术的日益成熟目前市场上较为流行的AI视频生成平台有从快手可灵、字节即梦、生数Vidu、智谱清影、PixVerse、Luma以及Runway Gen-3等。下面是这些平台工具的访问地址:快手可灵:cid:link_2字节即梦:https://jimeng.jianying.com/ai-tool/home?activeTab=video&subTab=video生数Vidu: cid:link_3智谱清影:cid:link_4PixVerse:cid:link_6Luma:cid:link_1Runway Gen-3:cid:link_5其中智谱清影 目前是完全免费的(还在内侧阶段)。 Runway Gen-3 是完全收费。其他的工具可以免费体验。二、AI视频生成工具生成视频的提示词:宁静的湖畔,阳光洒在清澈的水面上,五彩斑斓的石头点缀着湖岸。镜头缓缓扫过各种形状和颜色的石头——红、蓝、黄、绿,每一块都闪耀着独特的光芒。一只好奇的小松鼠穿梭其间,偶尔停下来捡起一颗石头仔细打量。2.1 快手可灵快手可灵:cid:link_22.2 字节即梦字节即梦:https://jimeng.jianying.com/ai-tool/home?activeTab=video&subTab=video生成卖火柴的小女孩。《卖火柴的小女孩》(画面:雪夜中的小镇,街道空旷而寂静) 旁白:“在遥远的一个小镇上,有一个寒冷的除夕夜。”(画面:小女孩穿着破旧的衣服,手中拿着一篮子火柴,独自走在街上) 旁白:“一个小女孩,赤着脚,穿着单薄的衣裳,手里拿着一把火柴,试图卖掉它们。”(画面:小女孩颤抖着点燃了一根火柴,画面转为暖色调,出现幻象中的温暖壁炉) 旁白:“她冷得无法忍受,于是她点燃了一根火柴。在火光中,她看到了一个温暖的壁炉。”(画面:火柴再次点燃,出现幻象中的美味烤鹅) 旁白:“饥饿驱使她点燃了第二根火柴,这次她看见了一只美味的烤鹅。”(画面:火柴燃烧,出现幻象中的圣诞树) 旁白:“接着是第三根火柴,一棵装饰精美的圣诞树出现在她面前。”(画面:火柴照亮了周围,出现幻象中的慈爱祖母) 旁白:“当她点燃第四根火柴时,她看到了她亲爱的祖母,那位唯一疼爱她的人。” 柴 (画面:小女孩点燃了所有的火柴,祖母的幻影变得更加清晰) 旁白:“她害怕失去祖母,于是她点燃了所有的火柴。祖母变得比以往任何时候都要明亮和温暖。”2.3 智谱清影智谱清影:cid:link_42.4 生数Vidu生数Vidu: cid:link_32.5 PixVersePixVerse:cid:link_62.6 LumaLuma:cid:link_12.7 Runway Gen-3Runway Gen-3:cid:link_5
-
一、Eclipse Paho介绍Eclipse Paho 是一个开源项目,由 Eclipse Foundation 主持,提供可靠的开源实现来处理 MQTT(Message Queuing Telemetry Transport)协议以及其他与物联网 (IoT) 相关的协议。MQTT 是一种轻量级的发布/订阅消息传输协议,专为具有低带宽和不可靠网络连接的设备设计。Paho 提供了多种语言的客户端库,使得开发者可以在各种平台上开发基于 MQTT 协议的应用程序。1.1 主要特点跨平台支持:Paho 支持多种操作系统和硬件平台,包括Windows、Linux、macOS 以及嵌入式系统。多语言实现:Paho 客户端库提供了多种编程语言的选择,如 C、C++、Java 和 Python 等。可靠性:Paho 能够在各种网络条件下可靠地工作,包括高延迟、丢包和间歇性连接等。安全性:Paho 支持 TLS/SSL 加密通信,以保证数据的安全传输。灵活性:除了基本的 MQTT 协议实现之外,Paho 还允许扩展和定制以适应特定的需求。1.2 Eclipse Paho MQTT C客户端库特点Eclipse Paho MQTT支持多种语言,其中的C客户端库是一个用于实现MQTT协议客户端的开源C语言库。跨平台支持:该库设计为可移植的,支持多种操作系统和硬件平台,包括Linux、Windows、MacOS以及嵌入式系统。易于集成:库的设计使得它易于集成到现有的C或C++项目中,为开发者提供了简单而强大的API来构建MQTT客户端。灵活的连接选项:支持TLS/SSL加密的MQTT连接,提供安全的通信通道。同时,支持QoS(服务质量)级别0(最多一次)、1(至少一次)和2(仅一次)的消息传递,确保消息传递的可靠性。异步操作:大多数库操作都是异步的,允许应用程序在等待网络响应时继续执行其他任务,提高了应用程序的响应性和效率。客户端和服务器消息处理:库支持客户端到服务器的消息发布(PUBLISH)以及从服务器到客户端的消息订阅(SUBSCRIBE)和接收(RECEIVE)。持久会话和遗嘱消息:支持持久会话,即使客户端断开连接也能保持订阅和QoS状态。同时,可以设置遗嘱消息,在客户端异常断开时发送特定消息。回调函数:通过提供回调函数来处理连接、断开连接、消息接收等事件,使得事件处理逻辑更加灵活。二、Eclipse Paho源码下载官网地址:cid:link_3库的下载地址:cid:link_4在页面上可以看到源码下载和编译好的库文件下载。提供了Linux下、Windows下编译好的库文件,可以直接使用。 如果你现在嵌入式平台上、其他平台上使用,那需要自己下载源码进行编译,使用。三、编译Eclipse Paho库文件3.1 下载编译openSSL如果要支持ssl加密的支持,那就需要先编译安装 openSSL。下载地址:cid:link_0编译步骤:1、解压缩tar zxf openssl-OpenSSL_1_1_1g.tar.gz2、进入目录,并配置输出目录和编译器cd openssl-OpenSSL_1_1_1g/./config no-asm shared no-async --prefix=`pwd`/ssl_result 如果是交叉编译器,可以指定 --cross-compile-prefix=arm-linux-。3、执行下面命令,删除Makefile文件的 -m64(如果指定了交叉编译器)sed -i 's/-m64//' Makefile执行后,可以避免出现这个编译错误:arm-linux-: error: unrecognized command line option '-m64'4、编译、安装make && make install成功编译后,在openssl-OpenSSL_1_1_1g/目录会生成一个ssl_result目录,可以看到里面生成的库.3.2 paho.mqtt.c 编译当前下载的是 paho.mqtt.c-1.3.13.tar.gz下载地址:cid:link_2wget cid:link_21、解压缩,创建要安装目录paho.mqtt.c_resulttar zxf paho.mqtt.c-1.3.13.tar.gzmkdir paho.mqtt.c_result/bin -pmkdir paho.mqtt.c_result/include -pmkdir paho.mqtt.c_result/lib -pmkdir paho.mqtt.c_result/share/man/man1 -p2、进入目录,交叉编译cd paho.mqtt.c-1.3.13/make CC=gcc CFLAGS:="-I `pwd`/../ssl_result/include" LDFLAGS:="-L `pwd`/../ssl_result/lib"如果是交叉编译需要指定交叉编译器完整名字:cd paho.mqtt.c-1.3.13/make CC=arm-linux-gcc CFLAGS:="-I `pwd`/../ssl_result/include" LDFLAGS:="-L `pwd`/../ssl_result/lib"参数介绍:CFLAGS:=“-I `pwd`/…/ssl_result/include”:指定前面编译的 openssl 的头文件;LDFLAGS:=“-L `pwd`/…/ssl_result/lib”:指定前面编译的 openssl 的库文件路径;3、make install,安装编译结果make install prefix=`pwd`/../paho.mqtt.c_result prefix=`pwd`/…/paho.mqtt.c_result :指定安装目录路径;编译完成后,会生成目录。3.3 paho.mqtt.cpp 编译当前下载的是paho.mqtt.cpp-1.3.2.tar.gz下载地址:cid:link_1如果你下载的版本跟我的一样,可以使用下面的脚本进行编译。编译之前,先创建一个脚本文件,名字为paho.mqtt.cpp_install,将下面代码粘贴进去保存。#! /bin/shRESULT_DIR=$(pwd)/result_dirRESULT_SSL=${RESULT_DIR}/ssl_resultRESULT_MQTT_C=${RESULT_DIR}/paho.mqtt.c_resultRESULT_MQTT_CPP=${RESULT_DIR}/paho.mqtt.cpp_resultCROSSS_COMPILE_TOOL=arm-linux- #如果你是需要交叉编译编译步骤:1、解压缩tar zxf paho.mqtt.cpp-1.3.2.tar.gz 2、进入目录cd paho.mqtt.cpp-1.3.2/3、执行 cmakemkdir build_arm cd build_arm编译不需要 openssl 的库cmake .. -DCMAKE_CXX_COMPILER=${CROSSS_COMPILE_TOOL}g++ \-DCMAKE_INSTALL_PREFIX=${RESULT_MQTT_CPP} \-DPAHO_MQTT_C_LIBRARIES=${RESULT_MQTT_C}/lib/libpaho-mqtt3a.so \-DPAHO_MQTT_C_INCLUDE_DIRS=${RESULT_MQTT_C}/include \-DPAHO_WITH_SSL=OFF \-DCMAKE_CXX_FLAGS="-std=gnu++11 -mcpu=cortex-a53"说明:如果你在PC机编译目标是PC机本身使用,-mcpu=cortex-a53 就不用写。 如果交叉编译的目标是嵌入式芯片,就如实写构架。编译带有 openssl 的库cmake .. -DCMAKE_CXX_COMPILER=${CROSSS_COMPILE_TOOL}g++ \-DCMAKE_INSTALL_PREFIX=${RESULT_MQTT_CPP} \-DPAHO_MQTT_C_LIBRARIES=${RESULT_MQTT_C}/lib/libpaho-mqtt3a.so \-DPAHO_MQTT_C_INCLUDE_DIRS=${RESULT_MQTT_C}/include \-DOPENSSL_SSL_LIBRARY=${RESULT_SSL}/lib/libssl.so \-DOPENSSL_INCLUDE_DIR=${RESULT_SSL}/include \-DOPENSSL_CRYPTO_LIBRARY=${RESULT_SSL}/lib/libcrypto.so \-DCMAKE_CXX_FLAGS="-std=gnu++11 -mcpu=cortex-a53"说明:如果你在PC机编译目标是PC机本身使用,-mcpu=cortex-a53 就不用写。 如果交叉编译的目标是嵌入式芯片,就如实写构架。4、编译make && make install5、将下载的源码包 paho.mqtt.cpp-1.3.2.tar.gz 和 上面保存的脚本paho.mqtt.cpp_install 放到同一目录,并且将前面编译好的openssl库、paho.mqtt.c库放在脚本指定的结果目录,当前是放到 result_dir 目录的。6、执行./paho.mqtt.cpp_install.sh 编译,编译完成后,在result_dir目录下会生成一个名为paho.mqtt.cpp_result的目录。四、代码案例4.1 订阅—async_subscribe.cpp这是使用了 libpaho-mqttpp3.so 进行订阅消息的源码,源码路径在源码的这个路径:paho.mqtt.cpp-1.3.2/src/samples/async_subscribe.cpp,只更改了服务器地址。完整代码如下:#include <iostream>#include <cstdlib>#include <string>#include <cstring>#include <cctype>#include <thread>#include <chrono>#include "mqtt/async_client.h"const std::string SERVER_ADDRESS("117.78.5.125:1883");const std::string CLIENT_ID("paho_cpp_async_subcribe");const std::string TOPIC("hello");const int QOS = 1;const int N_RETRY_ATTEMPTS = 5;/// Callbacks for the success or failures of requested actions.// This could be used to initiate further action, but here we just log the// results to the console.class action_listener : public virtual mqtt::iaction_listener{ std::string name_; void on_failure(const mqtt::token& tok) override { std::cout << name_ << " failure"; if (tok.get_message_id() != 0) std::cout << " for token: [" << tok.get_message_id() << "]" << std::endl; std::cout << std::endl; } void on_success(const mqtt::token& tok) override { std::cout << name_ << " success"; if (tok.get_message_id() != 0) std::cout << " for token: [" << tok.get_message_id() << "]" << std::endl; auto top = tok.get_topics(); if (top && !top->empty()) std::cout << "\ttoken topic: '" << (*top)[0] << "', ..." << std::endl; std::cout << std::endl; }public: action_listener(const std::string& name) : name_(name) {}};//** * Local callback & listener class for use with the client connection. * This is primarily intended to receive messages, but it will also monitor * the connection to the broker. If the connection is lost, it will attempt * to restore the connection and re-subscribe to the topic. */class callback : public virtual mqtt::callback, public virtual mqtt::iaction_listener{ // Counter for the number of connection retries int nretry_; // The MQTT client mqtt::async_client& cli_; // Options to use if we need to reconnect mqtt::connect_options& connOpts_; // An action listener to display the result of actions. action_listener subListener_; // This deomonstrates manually reconnecting to the broker by calling // connect() again. This is a possibility for an application that keeps // a copy of it's original connect_options, or if the app wants to // reconnect with different options. // Another way this can be done manually, if using the same options, is // to just call the async_client::reconnect() method. void reconnect() { std::this_thread::sleep_for(std::chrono::milliseconds(2500)); try { cli_.connect(connOpts_, nullptr, *this); } catch (const mqtt::exception& exc) { std::cerr << "Error: " << exc.what() << std::endl; exit(1); } } // Re-connection failure void on_failure(const mqtt::token& tok) override { std::cout << "Connection attempt failed" << std::endl; if (++nretry_ > N_RETRY_ATTEMPTS) exit(1); reconnect(); } // (Re)connection success // Either this or connected() can be used for callbacks. void on_success(const mqtt::token& tok) override {} // (Re)connection success void connected(const std::string& cause) override { std::cout << "\nConnection success" << std::endl; std::cout << "\nSubscribing to topic '" << TOPIC << "'\n" << "\tfor client " << CLIENT_ID << " using QoS" << QOS << "\n" << "\nPress Q<Enter> to quit\n" << std::endl; cli_.subscribe(TOPIC, QOS, nullptr, subListener_); } // Callback for when the connection is lost. // This will initiate the attempt to manually reconnect. void connection_lost(const std::string& cause) override { std::cout << "\nConnection lost" << std::endl; if (!cause.empty()) std::cout << "\tcause: " << cause << std::endl; std::cout << "Reconnecting..." << std::endl; nretry_ = 0; reconnect(); } // Callback for when a message arrives. void message_arrived(mqtt::const_message_ptr msg) override { std::cout << "Message arrived" << std::endl; std::cout << "\ttopic: '" << msg->get_topic() << "'" << std::endl; std::cout << "\tpayload: '" << msg->to_string() << "'\n" << std::endl; } void delivery_complete(mqtt::delivery_token_ptr token) override {}public: callback(mqtt::async_client& cli, mqtt::connect_options& connOpts) : nretry_(0), cli_(cli), connOpts_(connOpts), subListener_("Subscription") {}};/int main(int argc, char* argv[]){ // A subscriber often wants the server to remember its messages when its // disconnected. In that case, it needs a unique ClientID and a // non-clean session. mqtt::async_client cli(SERVER_ADDRESS, CLIENT_ID); mqtt::connect_options connOpts; connOpts.set_clean_session(false); // Install the callback(s) before connecting. callback cb(cli, connOpts); cli.set_callback(cb); // Start the connection. // When completed, the callback will subscribe to topic. try { std::cout << "Connecting to the MQTT server..." << std::flush; cli.connect(connOpts, nullptr, cb); } catch (const mqtt::exception& exc) { std::cerr << "\nERROR: Unable to connect to MQTT server: '" << SERVER_ADDRESS << "'" << exc << std::endl; return 1; } // Just block till user tells us to quit. while (std::tolower(std::cin.get()) != 'q') ; // Disconnect try { std::cout << "\nDisconnecting from the MQTT server..." << std::flush; cli.disconnect()->wait(); std::cout << "OK" << std::endl; } catch (const mqtt::exception& exc) { std::cerr << exc << std::endl; return 1; } return 0;}编译指令:g++ async_subscribe.cpp -I result_dir/paho.mqtt.cpp_result/include/ -I result_dir/paho.mqtt.c_result/include/ -L result_dir/paho.mqtt.cpp_result/lib/ -L result_dir/paho.mqtt.c_result/lib/ -l paho-mqttpp3 -l paho-mqtt3a 4.2 发布——async_publish.cpp这是使用了 libpaho-mqttpp3.so 进行发布消息的源码,源码路径在源码的这个路径:paho.mqtt.cpp-1.3.2/src/samples/async_publish.cpp,只更改了服务器地址。完整代码如下:#include <iostream>#include <cstdlib>#include <string>#include <thread>#include <atomic>#include <chrono>#include <cstring>#include "mqtt/async_client.h"using namespace std;const string DFLT_SERVER_ADDRESS { "117.78.5.125:1883" };const string CLIENT_ID { "paho_cpp_async_publish" };const string PERSIST_DIR { "./persist" };const string TOPIC { "hello" };const char* PAYLOAD1 = "Hello World!";const char* PAYLOAD2 = "Hi there!";const char* PAYLOAD3 = "Is anyone listening?";const char* PAYLOAD4 = "Someone is always listening.";const char* LWT_PAYLOAD = "Last will and testament.";const int QOS = 1;const auto TIMEOUT = std::chrono::seconds(10);//** * A callback class for use with the main MQTT client. */class callback : public virtual mqtt::callback{public: void connection_lost(const string& cause) override { cout << "\nConnection lost" << endl; if (!cause.empty()) cout << "\tcause: " << cause << endl; } void delivery_complete(mqtt::delivery_token_ptr tok) override { cout << "\tDelivery complete for token: " << (tok ? tok->get_message_id() : -1) << endl; }};//** * A base action listener. */class action_listener : public virtual mqtt::iaction_listener{protected: void on_failure(const mqtt::token& tok) override { cout << "\tListener failure for token: " << tok.get_message_id() << endl; } void on_success(const mqtt::token& tok) override { cout << "\tListener success for token: " << tok.get_message_id() << endl; }};//** * A derived action listener for publish events. */class delivery_action_listener : public action_listener{ atomic<bool> done_; void on_failure(const mqtt::token& tok) override { action_listener::on_failure(tok); done_ = true; } void on_success(const mqtt::token& tok) override { action_listener::on_success(tok); done_ = true; }public: delivery_action_listener() : done_(false) {} bool is_done() const { return done_; }};/int main(int argc, char* argv[]){ // A client that just publishes normally doesn't need a persistent // session or Client ID unless it's using persistence, then the local // library requires an ID to identify the persistence files. string address = (argc > 1) ? string(argv[1]) : DFLT_SERVER_ADDRESS, clientID = (argc > 2) ? string(argv[2]) : CLIENT_ID; cout << "Initializing for server '" << address << "'..." << endl; mqtt::async_client client(address, clientID, PERSIST_DIR); callback cb; client.set_callback(cb); auto connOpts = mqtt::connect_options_builder() .clean_session() .will(mqtt::message(TOPIC, LWT_PAYLOAD, strlen(LWT_PAYLOAD), QOS, false)) .finalize(); cout << " ...OK" << endl; try { cout << "\nConnecting..." << endl; mqtt::token_ptr conntok = client.connect(connOpts); cout << "Waiting for the connection..." << endl; conntok->wait(); cout << " ...OK" << endl; // First use a message pointer. cout << "\nSending message..." << endl; mqtt::message_ptr pubmsg = mqtt::make_message(TOPIC, PAYLOAD1); pubmsg->set_qos(QOS); client.publish(pubmsg)->wait_for(TIMEOUT); cout << " ...OK" << endl; // Now try with itemized publish. cout << "\nSending next message..." << endl; mqtt::delivery_token_ptr pubtok; pubtok = client.publish(TOPIC, PAYLOAD2, strlen(PAYLOAD2), QOS, false); cout << " ...with token: " << pubtok->get_message_id() << endl; cout << " ...for message with " << pubtok->get_message()->get_payload().size() << " bytes" << endl; pubtok->wait_for(TIMEOUT); cout << " ...OK" << endl; // Now try with a listener cout << "\nSending next message..." << endl; action_listener listener; pubmsg = mqtt::make_message(TOPIC, PAYLOAD3); pubtok = client.publish(pubmsg, nullptr, listener); pubtok->wait(); cout << " ...OK" << endl; // Finally try with a listener, but no token cout << "\nSending final message..." << endl; delivery_action_listener deliveryListener; pubmsg = mqtt::make_message(TOPIC, PAYLOAD4); client.publish(pubmsg, nullptr, deliveryListener); while (!deliveryListener.is_done()) { this_thread::sleep_for(std::chrono::milliseconds(100)); } cout << "OK" << endl; // Double check that there are no pending tokens auto toks = client.get_pending_delivery_tokens(); if (!toks.empty()) cout << "Error: There are pending delivery tokens!" << endl; // Disconnect cout << "\nDisconnecting..." << endl; client.disconnect()->wait(); cout << " ...OK" << endl; } catch (const mqtt::exception& exc) { cerr << exc.what() << endl; return 1; } return 0;}编译指令:g++ async_publish.cpp -I result_dir/paho.mqtt.cpp_result/include/ -I result_dir/paho.mqtt.c_result/include/ -L re
-
一、前言随着局域网(LAN)应用的广泛使用,网络通信已经成为软件设计中不可或缺的一部分。局域网聊天软件作为一种常见的网络应用,可以实现多个用户之间的实时通信,广泛应用于企业内部沟通和小型网络环境中。本项目设计并实现一个基于C语言的局域网群聊程序,通过UDP广播搜索在线用户,并在发现其他在线应用程序后,自动建立TCP连接,实现消息的收发。本程序展示了如何在Windows环境下使用Winsock API进行网络编程,提供了对UDP和TCP协议的实际应用,体现了网络通信中的多线程处理、广播通信和实时消息传递的关键技术点。二、好友探测功能在局域网内探测并发现其他在线用户是局域网聊天软件最主要的核心功能。该过程涉及到局域网广播(UDP广播)和TCP连接两个关键步骤。下面将详细介绍实现这一功能的方法和设计思路。2.1 使用UDP广播探测在线用户1.1 UDP广播的概念UDP(用户数据报协议)是一种无连接的、轻量级的传输协议,适用于发送小数据包。UDP广播允许将数据包发送到局域网内的所有设备,所有在监听特定端口的设备都能够接收到广播消息。这种特性使得UDP广播非常适合用于探测和发现局域网内的在线设备。1.2 探测思路在程序启动时,客户端会通过UDP广播发送一个上线通知消息,表示自己已在线。其他监听同一端口的客户端接收到这一消息后,可以获知该客户端的IP地址,并识别出它在线。具体的实现步骤如下:创建UDP套接字:为UDP通信创建一个套接字,并配置为允许广播。发送广播消息:程序向局域网内的广播地址(通常为255.255.255.255)发送一个消息,例如"HELLO, I'M ONLINE"。这个消息会被局域网内所有监听相同端口的设备接收。监听UDP消息:每个客户端都持续监听来自局域网内的UDP消息。一旦接收到广播消息,客户端会记录发送方的IP地址和端口,以确认该客户端在线。2.2 建立TCP连接实现通信2.1 TCP连接的必要性UDP广播虽然可以有效地发现在线用户,但由于其无连接的特点,不适合用于长时间的可靠通信。因此,在发现其他在线用户后,程序需要通过TCP(传输控制协议)建立可靠的点对点连接。TCP是一种面向连接的协议,能够确保数据的完整性和顺序传输,非常适合用于聊天消息的传递。2.2 连接建立的流程接收广播后尝试连接:当客户端接收到来自其他用户的UDP广播后,会通过TCP连接到该用户。客户端会使用从UDP消息中获取的IP地址和预定义的TCP端口号,发起连接请求。接受连接请求:已经在线的客户端会开启一个TCP监听套接字,等待来自其他客户端的连接请求。一旦有请求到达,程序将接受连接,并启动一个独立的线程处理与该客户端之间的消息通信。消息收发:通过建立的TCP连接,用户可以实时发送和接收聊天消息,确保消息在网络不稳定的情况下仍能可靠传输。2.3 多线程处理的必要性由于UDP广播接收、TCP连接监听和消息收发等操作需要同时进行,程序采用了多线程的设计。每个功能模块都运行在独立的线程中,确保它们可以并行处理,互不干扰。这样不仅提高了程序的响应速度,还增强了用户体验,确保通信的实时性。2.4 总结通过UDP广播发现局域网内的在线用户,然后利用TCP协议建立可靠的通信连接,这是局域网聊天软件的核心设计思路。UDP广播的轻量和广泛性使得在线用户的探测变得高效,而TCP连接则保证了后续通信的可靠性。多线程的引入进一步优化了程序的性能,使得该局域网聊天软件在实际应用中表现出色。三、代码实现下面是完整的代码。在VS2022里运行测试。#define _WINSOCK_DEPRECATED_NO_WARNINGS#include <stdio.h>#include <stdlib.h>#include <string.h>#include <winsock2.h>#include <ws2tcpip.h>#include <process.h>#pragma comment(lib, "ws2_32.lib")#define UDP_PORT 8888#define TCP_PORT 8889#define BROADCAST_ADDR "255.255.255.255"#define BUFFER_SIZE 1024typedef struct ClientInfo { SOCKET socket; struct sockaddr_in address;} ClientInfo;void udp_broadcast_listener(void* param);void tcp_connection_listener(void* param);void tcp_message_listener(void* param);int main() { WSADATA wsaData; SOCKET udp_socket, tcp_socket; struct sockaddr_in udp_addr, tcp_addr; char buffer[BUFFER_SIZE]; struct sockaddr_in client_addr; int addr_len = sizeof(client_addr); // 初始化 Winsock if (WSAStartup(MAKEWORD(2, 2), &wsaData) != 0) { printf("WSAStartup failed\n"); return 1; } // 创建 UDP 套接字 udp_socket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); if (udp_socket == INVALID_SOCKET) { printf("UDP socket creation failed\n"); WSACleanup(); return 1; } // 配置 UDP 广播地址 memset(&udp_addr, 0, sizeof(udp_addr)); udp_addr.sin_family = AF_INET; udp_addr.sin_port = htons(UDP_PORT); udp_addr.sin_addr.s_addr = inet_addr(BROADCAST_ADDR); // 启动 UDP 广播监听线程 _beginthread(udp_broadcast_listener, 0, NULL); // 启动 TCP 连接监听线程 _beginthread(tcp_connection_listener, 0, NULL); // 向局域网内广播自己上线 strcpy(buffer, "HELLO, I'M ONLINE"); sendto(udp_socket, buffer, strlen(buffer), 0, (struct sockaddr*)&udp_addr, sizeof(udp_addr)); while (1) { fgets(buffer, BUFFER_SIZE, stdin); buffer[strcspn(buffer, "\n")] = 0; // 移除换行符 sendto(udp_socket, buffer, strlen(buffer), 0, (struct sockaddr*)&udp_addr, sizeof(udp_addr)); } closesocket(udp_socket); WSACleanup(); return 0;}void udp_broadcast_listener(void* param) { SOCKET udp_socket; struct sockaddr_in udp_addr, sender_addr; char buffer[BUFFER_SIZE]; int addr_len = sizeof(sender_addr); // 创建 UDP 套接字 udp_socket = socket(AF_INET, SOCK_DGRAM, IPPROTO_UDP); if (udp_socket == INVALID_SOCKET) { printf("UDP socket creation failed in listener\n"); return; } // 配置 UDP 地址 memset(&udp_addr, 0, sizeof(udp_addr)); udp_addr.sin_family = AF_INET; udp_addr.sin_port = htons(UDP_PORT); udp_addr.sin_addr.s_addr = INADDR_ANY; // 绑定套接字 if (bind(udp_socket, (struct sockaddr*)&udp_addr, sizeof(udp_addr)) == SOCKET_ERROR) { printf("UDP socket binding failed\n"); closesocket(udp_socket); return; } while (1) { int recv_len = recvfrom(udp_socket, buffer, BUFFER_SIZE, 0, (struct sockaddr*)&sender_addr, &addr_len); if (recv_len > 0) { buffer[recv_len] = '\0'; printf("Received UDP broadcast from %s: %s\n", inet_ntoa(sender_addr.sin_addr), buffer); // 如果接收到"HELLO, I'M ONLINE",尝试建立TCP连接 if (strcmp(buffer, "HELLO, I'M ONLINE") == 0) { SOCKET tcp_socket; struct sockaddr_in tcp_addr; tcp_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (tcp_socket == INVALID_SOCKET) { printf("TCP socket creation failed\n"); continue; } // 配置 TCP 地址 memset(&tcp_addr, 0, sizeof(tcp_addr)); tcp_addr.sin_family = AF_INET; tcp_addr.sin_port = htons(TCP_PORT); tcp_addr.sin_addr.s_addr = sender_addr.sin_addr.s_addr; if (connect(tcp_socket, (struct sockaddr*)&tcp_addr, sizeof(tcp_addr)) == SOCKET_ERROR) { printf("TCP connection failed to %s\n", inet_ntoa(tcp_addr.sin_addr)); closesocket(tcp_socket); } else { printf("Connected to %s\n", inet_ntoa(tcp_addr.sin_addr)); // 启动 TCP 消息监听线程 ClientInfo* client = (ClientInfo*)malloc(sizeof(ClientInfo)); client->socket = tcp_socket; client->address = tcp_addr; _beginthread(tcp_message_listener, 0, client); } } } } closesocket(udp_socket);}void tcp_connection_listener(void* param) { SOCKET tcp_socket, client_socket; struct sockaddr_in tcp_addr, client_addr; int addr_len = sizeof(client_addr); // 创建 TCP 套接字 tcp_socket = socket(AF_INET, SOCK_STREAM, IPPROTO_TCP); if (tcp_socket == INVALID_SOCKET) { printf("TCP socket creation failed\n"); return; } // 配置 TCP 地址 memset(&tcp_addr, 0, sizeof(tcp_addr)); tcp_addr.sin_family = AF_INET; tcp_addr.sin_port = htons(TCP_PORT); tcp_addr.sin_addr.s_addr = INADDR_ANY; // 绑定套接字 if (bind(tcp_socket, (struct sockaddr*)&tcp_addr, sizeof(tcp_addr)) == SOCKET_ERROR) { printf("TCP socket binding failed\n"); closesocket(tcp_socket); return; } // 开始监听 if (listen(tcp_socket, 5) == SOCKET_ERROR) { printf("TCP socket listen failed\n"); closesocket(tcp_socket); return; } printf("TCP connection listener started...\n"); while (1) { client_socket = accept(tcp_socket, (struct sockaddr*)&client_addr, &addr_len); if (client_socket == INVALID_SOCKET) { printf("TCP accept failed\n"); continue; } printf("Accepted connection from %s\n", inet_ntoa(client_addr.sin_addr)); // 启动 TCP 消息监听线程 ClientInfo* client = (ClientInfo*)malloc(sizeof(ClientInfo)); client->socket = client_socket; client->address = client_addr; _beginthread(tcp_message_listener, 0, client); } closesocket(tcp_socket);}void tcp_message_listener(void* param) { ClientInfo* client = (ClientInfo*)param; char buffer[BUFFER_SIZE]; int recv_len; while ((recv_len = recv(client->socket, buffer, BUFFER_SIZE, 0)) > 0) { buffer[recv_len] = '\0'; printf("Message from %s: %s\n", inet_ntoa(client->address.sin_addr), buffer); } printf("Connection closed by %s\n", inet_ntoa(client->address.sin_addr)); closesocket(client->socket); free(client);}程序在主函数里通过 WSAStartup 函数初始化Winsock库,这是一种Windows平台上的网络编程库,提供了网络通信所需的API。初始化成功后,程序可以使用Winsock提供的各种网络功能。创建了两个主要的套接字:UDP套接字:用于广播消息和接收其他设备的广播。TCP套接字:用于建立点对点的通信连接。在程序启动时,通过UDP广播向局域网内所有设备发送一个“HELLO, I'M ONLINE”的消息。这一消息用来告知局域网内的其他用户自己的存在,从而实现在线用户的探测。为了接收其他用户的广播消息,程序创建了一个UDP套接字并绑定到特定的端口上(UDP_PORT)。程序通过这个套接字监听局域网内的所有广播消息,提取发送者的IP地址,并处理接收到的消息。一旦接收到来自其他在线用户的UDP广播消息,程序会尝试通过TCP建立连接。步骤包括:从UDP消息中提取发送者的IP地址。使用提取的IP地址和预定义的TCP端口号发起TCP连接请求。如果连接成功,程序将建立一个可靠的点对点通信通道,用于后续的聊天消息传递。程序创建一个TCP套接字,并在特定端口上进行监听,等待其他用户的连接请求。当有新的连接请求到达时,程序接受该连接,并为每个连接创建一个新的线程,以处理与该连接相关的消息通信。用户在键盘上输入消息后,程序通过UDP套接字广播该消息到局域网内的所有在线用户。此功能确保所有在线的用户都能看到发送的消息。程序通过TCP连接接收来自其他用户的消息。接收到的消息将被显示在终端上,提供实时的聊天功能。每个TCP连接使用一个独立的线程进行处理,确保消息的及时传递和处理。程序采用了多线程技术来并行处理不同的任务,确保系统的响应性和效率。主要线程包括:UDP广播监听线程:处理UDP广播消息的接收和处理。TCP连接监听线程:接受来自其他用户的TCP连接请求。TCP消息处理线程:处理与每个已连接用户之间的消息交换。在程序退出时,所有打开的套接字都会被关闭,资源得到释放。程序通过调用 closesocket 函数关闭套接字,并调用 WSACleanup 进行Winsock库的清理,确保程序在退出时不会泄漏资源。
-
一、前言LibJPEG库是一个广泛使用的开源C库,用于处理JPEG图像的压缩和解压缩。该库由独立JPEG小组(Independent JPEG Group, IJG)开发,提供了功能强大的API,用于创建和读取JPEG文件。LibJPEG库支持JPEG的所有常见功能,包括高质量的压缩、解压缩、图像处理、颜色空间转换等。采用DCT(离散余弦变换)算法实现了高效的图像压缩,同时支持各种图像质量的调整。LibJPEG的灵活性和性能使其成为图像处理应用中的标准工具,被广泛应用于图像编辑软件、图像查看器、图像传输等多个领域。该库跨平台兼容,支持Windows、Linux、macOS等操作系统,开发者可以轻松将其集成到不同的平台和应用中。LibJPEG库还具有良好的文档支持,提供详细的编程指南和示例代码,帮助开发者快速上手和实现复杂的图像处理功能。由于其开源性质和广泛的应用,LibJPEG在业界享有很高的声誉,成为许多图像处理项目的首选库。LibJPEG库的源码下载地址:cid:link_1下载后是一个ZIP的压缩包,解压看到的数据:如果要下载编译好的二进制文件,可以去这里下载:cid:link_0二、函数接口解释LibJPEG库提供了一组函数接口,用于实现JPEG图像的压缩和解压缩操作。以下是一些常用的LibJPEG库函数接口及其详细讲解,包括函数功能、参数作用等。2.1 JPEG压缩相关函数jpeg_create_compress功能:初始化JPEG压缩对象。参数:struct jpeg_compress_struct *cinfo:指向JPEG压缩对象的指针。说明:必须在进行任何其他JPEG压缩操作之前调用该函数。jpeg_stdio_dest功能:设置压缩数据的目标为标准I/O流。参数:struct jpeg_compress_struct *cinfo:指向JPEG压缩对象的指针。FILE *outfile:目标文件的文件指针。说明:指定JPEG数据输出的目标文件。jpeg_set_defaults功能:设置JPEG压缩参数的默认值。参数:struct jpeg_compress_struct *cinfo:指向JPEG压缩对象的指针。说明:设置一组默认的JPEG压缩参数。jpeg_set_quality功能:设置JPEG压缩质量。参数:struct jpeg_compress_struct *cinfo:指向JPEG压缩对象的指针。int quality:压缩质量,范围为0(最差)到100(最佳)。boolean force_baseline:如果为TRUE,强制生成符合JPEG标准的文件。说明:调整JPEG压缩的质量参数。jpeg_start_compress功能:启动压缩过程。参数:struct jpeg_compress_struct *cinfo:指向JPEG压缩对象的指针。boolean write_all_tables:如果为TRUE,写入所有JPEG标记。说明:在压缩数据之前调用该函数。jpeg_write_scanlines功能:写入压缩数据行。参数:struct jpeg_compress_struct *cinfo:指向JPEG压缩对象的指针。JSAMPARRAY scanlines:指向存储图像数据的缓冲区。JDIMENSION num_lines:要写入的行数。说明:写入图像数据到JPEG压缩对象中。jpeg_finish_compress功能:完成压缩过程。参数:struct jpeg_compress_struct *cinfo:指向JPEG压缩对象的指针。说明:在完成所有数据写入后调用,关闭JPEG压缩对象。jpeg_destroy_compress功能:销毁JPEG压缩对象。参数:struct jpeg_compress_struct *cinfo:指向JPEG压缩对象的指针。说明:释放JPEG压缩对象分配的所有资源。2.2 JPEG解压缩相关函数jpeg_create_decompress功能:初始化JPEG解压缩对象。参数:struct jpeg_decompress_struct *cinfo:指向JPEG解压缩对象的指针。说明:必须在进行任何其他JPEG解压缩操作之前调用该函数。jpeg_stdio_src功能:设置解压数据的源为标准I/O流。参数:struct jpeg_decompress_struct *cinfo:指向JPEG解压缩对象的指针。FILE *infile:源文件的文件指针。说明:指定JPEG数据输入的源文件。jpeg_read_header功能:读取JPEG文件的头部信息。参数:struct jpeg_decompress_struct *cinfo:指向JPEG解压缩对象的指针。boolean require_image:如果为TRUE,要求图像数据存在。说明:读取JPEG文件的头部信息,解析JPEG图像的元数据。jpeg_start_decompress功能:启动解压缩过程。参数:struct jpeg_decompress_struct *cinfo:指向JPEG解压缩对象的指针。说明:在解压缩数据之前调用该函数。jpeg_read_scanlines功能:读取解压缩的数据行。参数:struct jpeg_decompress_struct *cinfo:指向JPEG解压缩对象的指针。JSAMPARRAY scanlines:指向存储解压缩数据的缓冲区。JDIMENSION max_lines:要读取的最大行数。说明:从JPEG解压缩对象中读取图像数据。jpeg_finish_decompress功能:完成解压缩过程。参数:struct jpeg_decompress_struct *cinfo:指向JPEG解压缩对象的指针。说明:在完成所有数据读取后调用,关闭JPEG解压缩对象。jpeg_destroy_decompress功能:销毁JPEG解压缩对象。参数:struct jpeg_decompress_struct *cinfo:指向JPEG解压缩对象的指针。说明:释放JPEG解压缩对象分配的所有资源。2.3 辅助函数jpeg_std_error功能:初始化JPEG错误处理对象。参数:struct jpeg_error_mgr *err:指向JPEG错误处理对象的指针。说明:设置默认的JPEG错误处理例程。这些函数共同构成了LibJPEG库的核心接口,通过它们可以实现JPEG图像的高效压缩和解压缩。在使用这些函数时,需要按照特定的调用顺序来确保正确的操作和资源管理。三、代码实现以下是将RGB565图像转换为RGB888并压缩为JPEG格式的完整示例代码:#include <stdio.h>#include <stdlib.h>#include <stdint.h>#include <jpeglib.h>// 将RGB565转换为RGB888void RGB565_to_RGB888(uint16_t rgb565, uint8_t *r, uint8_t *g, uint8_t *b) { *r = ((rgb565 >> 11) & 0x1F) << 3; *g = ((rgb565 >> 5) & 0x3F) << 2; *b = (rgb565 & 0x1F) << 3;}// 将RGB565格式图像转换为RGB888格式void convert_RGB565_to_RGB888(const uint8_t *rgb565_image, uint8_t *rgb888_image, int width, int height) { for (int i = 0; i < width * height; i++) { uint16_t rgb565 = ((uint16_t)rgb565_image[2 * i + 1] << 8) | rgb565_image[2 * i]; RGB565_to_RGB888(rgb565, &rgb888_image[3 * i], &rgb888_image[3 * i + 1], &rgb888_image[3 * i + 2]); }}// 将RGB888格式图像压缩为JPEG格式void compress_to_JPEG(const uint8_t *rgb888_image, int width, int height, const char *filename) { struct jpeg_compress_struct cinfo; struct jpeg_error_mgr jerr; FILE *outfile; JSAMPROW row_pointer[1]; int row_stride; cinfo.err = jpeg_std_error(&jerr); jpeg_create_compress(&cinfo); if ((outfile = fopen(filename, "wb")) == NULL) { fprintf(stderr, "can't open %s\n", filename); exit(1); } jpeg_stdio_dest(&cinfo, outfile); cinfo.image_width = width; cinfo.image_height = height; cinfo.input_components = 3; cinfo.in_color_space = JCS_RGB; jpeg_set_defaults(&cinfo); jpeg_set_quality(&cinfo, 75, TRUE); jpeg_start_compress(&cinfo, TRUE); row_stride = width * 3; while (cinfo.next_scanline < cinfo.image_height) { row_pointer[0] = (JSAMPROW) &rgb888_image[cinfo.next_scanline * row_stride]; jpeg_write_scanlines(&cinfo, row_pointer, 1); } jpeg_finish_compress(&cinfo); fclose(outfile); jpeg_destroy_compress(&cinfo);}int main() { // 假设图像尺寸为100x100 int width = 100; int height = 100; // 示例RGB565数据 uint8_t rgb565_image[20000]; for (int i = 0; i < 20000; i++) { rgb565_image[i] = rand() % 256; // 用随机数据填充 } // 分配RGB888图像数据的内存 uint8_t *rgb888_image = (uint8_t *)malloc(width * height * 3); if (rgb888_image == NULL) { fprintf(stderr, "Unable to allocate memory for RGB888 image\n"); return 1; } // 转换RGB565到RGB888 convert_RGB565_to_RGB888(rgb565_image, rgb888_image, width, height); // 压缩并保存为JPEG文件 compress_to_JPEG(rgb888_image, width, height, "output.jpg"); // 释放内存 free(rgb888_image); printf("Compression to JPEG completed.\n"); return 0;}
-
一、前言需求: Qt开发Android程序过程中,点击按钮就打开一个PPT文件。Qt在Windows上要打开PPT文件或者其他文件很容易。可以使用QDesktopServices打开文件,非常方便。QDesktopServices提供了静态接口调用系统级别的功能。这里用的QDesktopServices 是 Qt 框架中的一个类,用于在跨平台应用程序中方便地访问和使用主机操作系统的桌面服务和功能。该类提供了一些静态方法,用于打开网址、文件和邮件客户端等系统默认应用程序。它的主要目的是让开发者能够轻松调用系统级别的功能,而不需要直接编写与操作系统相关的代码。代码如下:QUrl url = QUrl::fromLocalFile("storage/emulated/0/Download/UseHelp.ppt"); bool IsOK = QDesktopServices::openUrl(url); 下面是QDesktopServices 的帮助文档介绍。QDesktopServices 只适用于桌面应用程序。 如果在Linux系统下想要打开PPT文件,也可以采用QDesktopServices 来实现。前提也是需要先安装LibreOffice或OpenOffice才可以。在Qt的文档里也提供了openUrl静态方法的使用说明。使用 QDesktopServices::openUrl在Linux系统上打开PPT文件。#include <QDesktopServices>#include <QUrl>#include <QDebug>int main(int argc, char *argv[]){ QApplication app(argc, argv); QString pptFilePath = "/path/to/your/presentation.ppt"; QUrl pptUrl = QUrl::fromLocalFile(pptFilePath); if (!QDesktopServices::openUrl(pptUrl)) { qWarning() << "Failed to open PPT file."; } return app.exec();}但是在Android移动平台上,这个办法就行不通了。 需要通过JNI接口与Android系统交互,才可以完成系统级别的操作。二、通过JNI与Android系统交互在Qt for Android中,可以通过Qt提供的Java Native Interface (JNI)与Android系统交互,可以调用系统功能,打开PPT文件。下面演示如何在Qt中使用C++代码,通过JNI调用Android的Intent来打开PPT文件。2.1 操作步骤在Qt项目中添加Android相关的权限和活动声明:在AndroidManifest.xml文件中添加必要的权限和活动声明。使用Qt的JNI接口调用Android Intent:在C++代码中使用Qt的JNI接口来调用Android的Intent。2.2 代码(1)AndroidManifest.xml在AndroidManifest.xml文件中添加以下权限声明,确保应用有权读取文件:<manifest xmlns:android="http://schemas.android.com/apk/res/android" package="org.qtproject.example"> <application android:label="@string/app_name"> <activity android:name="org.qtproject.qt5.android.bindings.QtActivity" android:configChanges="orientation|screenSize" android:label="@string/app_name"> <intent-filter> <action android:name="android.intent.action.MAIN"/> <category android:name="android.intent.category.LAUNCHER"/> </intent-filter> </activity> </application> <uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/></manifest>(2)Qt C++代码在Qt C++代码中使用JNI接口调用Android的Intent来打开PPT文件。#include <QCoreApplication>#include <QAndroidJniObject>#include <QtAndroid>#include <QDebug>void openPPTFile(const QString &filePath) { QAndroidJniObject activity = QAndroidJniObject::callStaticObjectMethod("org/qtproject/qt5/android/QtNative", "activity", "()Landroid/app/Activity;"); if (activity.isValid()) { QAndroidJniObject intent("android/content/Intent", "()V"); intent.callObjectMethod("setAction", "(Ljava/lang/String;)Landroid/content/Intent;", QAndroidJniObject::fromString("android.intent.action.VIEW").object<jstring>()); QAndroidJniObject uri = QAndroidJniObject::callStaticObjectMethod( "android/net/Uri", "parse", "(Ljava/lang/String;)Landroid/net/Uri;", QAndroidJniObject::fromString(filePath).object<jstring>()); intent.callObjectMethod("setDataAndType", "(Landroid/net/Uri;Ljava/lang/String;)Landroid/content/Intent;", uri.object<jobject>(), QAndroidJniObject::fromString("application/vnd.ms-powerpoint").object<jstring>()); intent.callObjectMethod("addFlags", "(I)Landroid/content/Intent;", 0x10000000); // Intent.FLAG_ACTIVITY_NEW_TASK activity.callMethod<void>("startActivity", "(Landroid/content/Intent;)V", intent.object<jobject>()); } else { qDebug() << "Failed to get the activity."; }}int main(int argc, char *argv[]){ QCoreApplication app(argc, argv); QString pptFilePath = "/sdcard/Download/sample.pptx"; // 指定要打开的PPT文件路径 openPPTFile(pptFilePath); return app.exec();}2.3 代码说明openPPTFile函数:该函数接受PPT文件的路径作为参数,并使用JNI接口调用Android的Intent来打开该文件。QAndroidJniObject:用于与Java对象和方法交互。setAction和setDataAndType方法:设置Intent的操作和数据类型。startActivity方法:启动Intent以打开PPT文件。2.4 编译和运行需要先在Android设备上安装合适的PPT查看应用(比如:WPS),并且PPT文件路径正确。编译和运行Qt项目时,应用将通过系统默认的应用打开指定的PPT文件。
-
一、前言BMP (Bitmap) 图像格式是一种无损压缩的位图文件格式,最初由微软公司在Windows操作系统中引入,用于存储图像数据。BMP格式的主要优点是它简单易用,且支持多种颜色深度。这种格式不包含任何压缩算法,这意味着图像的质量不会因为保存而损失,但这也导致了文件大小相对较大。当前做的项目是采用STM32F103ZET6单片机接上OV7725(带FIFO)摄像头实现图像采集,拍照功能。 OV7725摄像头输出的格式是RGB565像素格式,为了方便将OV7725摄像头返回的图像数据放在SD卡里存储,并且能够在电脑上打开,通过图片查看软件查看。就需要将RGB565像素数据封装成一张图片格式,也就是相当于加一个壳子。这样电脑上的图片查看器就可以正常查看图片了。目前的图片格式有很多,平时最常见的有JPG、PNG、BMP这些格式。 这里面的JPG是压缩格式,保存的图片可以很小,JPEG使用离散余弦变换(DCT)压缩,这个算法在单片机上实现的要求毕竟高,毕竟单片机的性能摆在这里。 而BMP是不包含任何压缩算法,存储的是原始的像素格式,作为单片机里拍照存储这是首选的图片封装格式了。整个项目设计完的核心功能是:通过OV7725摄像头采集一帧RGB565格式的图像,并将其封装成BMP格式后,利用FATFS文件系统存储到SD卡上。项目中,STM32单片机通过SPI协议与SD卡进行通信。由于OV7725摄像头输出的是RGB565格式的数据,而标准BMP文件使用RGB888格式存储像素数据,因此还涉及到了图像格式的转换问题。要完成这个项目涉及的技术其实也有几个的:(1)SD卡的驱动编写,SD卡支持SDIO和SPI两种协议。 要说简单那自然首选SPI协议,不过就是速度稍微慢一点。(2)OV7725摄像头的驱动编写,毕竟要从摄像头里读取数据。分为控制协议和数据总线。(3)FATFS文件系统的移植,如果在单片机上要以文件的形式管理SD卡,那肯定是需要文件系统了。(4)BMP图片的格式理解,要将图片保存为BMP图片格式。需要完全理解BMP图片格式的是如何的封装的。这篇文章最重要的是内容是讲解“ BMP图片如何封装,学习BMP图像格式封装,RGB565与RGB888像素点转换。二、BMP文件结构2.1 BMP图片的格式BMP 文件的内部格式组成:(1)文件头 (File Header)类型标识 (bfType): 两个字节,通常为 BM (0x424D),表明文件类型为BMP。文件大小 (bfSize): 四个字节,表示整个文件的大小(包括文件头、信息头和像素数据)。保留字段 (bfReserved1, bfReserved2): 通常是0。数据偏移量 (bfOffBits): 四个字节,指明像素数据相对于文件起始位置的偏移量。(2)信息头 (Info Header)头大小 (biSize): 四个字节,信息头的大小。宽度 (biWidth): 四个字节,图像的宽度(以像素为单位)。高度 (biHeight): 四个字节,图像的高度(以像素为单位)。高度值可以是正数也可以是负数;正数表示从左下角开始绘制,负数则表示从左上角开始绘制。平面数 (biPlanes): 通常是1。位数 (biBitCount): 每个像素的位数,常见的值有1、4、8、16、24或32。压缩方法 (biCompression): 指定使用的压缩方法,如果是0,则表示没有压缩。图像大小 (biSizeImage): 压缩后的图像大小,如果未压缩,则该值可能为0。水平分辨率 (biXPelsPerMeter): 水平方向上的分辨率(每米像素数)。垂直分辨率 (biYPelsPerMeter): 垂直方向上的分辨率(每米像素数)。调色板数目 (biClrUsed): 调色板中的颜色数目,如果为0,则表示所有可能的颜色都被使用。重要颜色数目 (biClrImportant): 重要的颜色数目,如果为0,则表示所有颜色都同样重要。(3)颜色表 (Color Table)如果位数小于24,则存在一个颜色表,其中定义了每个像素值所对应的RGB颜色。(4)像素数据 (Pixel Data)图像的实际像素数据按照从左到右、从下到上的顺序排列。为了保证每一行的字节数为4的倍数,通常会在每行末尾添加填充字节。下面是BMP文件格式的一个详细描述,包括每个字段的名称、长度、含义以及它们在文件中的位置。字段名称类型长度 (字节)描述bfType字符串2文件类型的标识,通常为 BM (0x424D)bfSizeDWORD4整个文件的大小,包括文件头、信息头和像素数据bfReserved1WORD2保留字段,应设为0bfReserved2WORD2保留字段,应设为0bfOffBitsDWORD4像素数据相对于文件起始位置的偏移量biSizeDWORD4信息头的大小,通常为40 (0x28)biWidthLONG4图像的宽度(以像素为单位),可以是正数或负数biHeightLONG4图像的高度(以像素为单位),可以是正数或负数biPlanesWORD2平面数,通常为1biBitCountWORD2每个像素的位数,常见的值有1、4、8、16、24或32biCompressionDWORD4压缩方法,如果是0,则表示没有压缩biSizeImageDWORD4压缩后的图像大小,如果未压缩,则该值可能为0biXPelsPerMeterLONG4水平方向上的分辨率(每米像素数),通常为0biYPelsPerMeterLONG4垂直方向上的分辨率(每米像素数),通常为0biClrUsedDWORD4调色板中的颜色数目,如果为0,则表示所有可能的颜色都被使用biClrImportantDWORD4重要的颜色数目,如果为0,则表示所有颜色都同样重要Color TableRGBQUAD0 or more调色板(仅当位数小于24时存在),每个颜色占用4字节Pixel DataBYTE[]变长像素数据,按从左到右、从下到上的顺序排列,每行可能有填充字节说明文件头 (File Header) : 从文件的开头到 bfOffBits 字段结束。信息头 (Info Header) : 从 biSize 字段开始,直到 biClrImportant 字段结束。颜色表 (Color Table) : 如果位数小于24,则存在一个颜色表,用于定义每个像素值所对应的RGB颜色。像素数据 (Pixel Data) : 图像的实际像素数据按照从左到右、从下到上的顺序排列。为了保证每一行的字节数为4的倍数,会在每行末尾添加填充字节。对于24位的BMP文件(即 biBitCount 的值为24),不会存在颜色表,每个像素直接由三个字节(RGB888格式)表示。2.2 RGB888与RGB565格式是什么?RGB565和RGB888都是色彩模型在计算机图形学中的具体实现方式,它们分别代表了不同位深的颜色编码方式。这两种格式主要用于存储图像数据,特别是在显示设备和图像处理软件中。(1)RGB565RGB565 是一种16位的彩色图像格式,其中红色和蓝色各占用5位,绿色占用6位。这是因为人眼对绿色更为敏感,因此给绿色分配更多的位数来提高颜色精度。这种格式通常用于节省存储空间或减少内存带宽的需求,尤其是在早期的移动设备和嵌入式系统中非常常见。位分配:11-15位: 5位红色 (R)5-10位: 6位绿色 (G)0-4位: 5位蓝色 (B)这种格式的总位数为16位,可以表示 (2^{16}) 或者 65,536 种不同的颜色。(2)RGB888RGB888 是一种24位的彩色图像格式,每种颜色(红、绿、蓝)都使用8位来表示。这意味着每种颜色都有256级灰度等级,总共可以表示 (2^{24}) 或者 16,777,216 种不同的颜色。位分配:16-23位: 8位红色 (R)8-15位: 8位绿色 (G)0-7位: 8位蓝色 (B)由于RGB888格式使用更多的位数来表示颜色,所以它能够提供更丰富的色彩细节,这对于高保真度的图像来说是非常重要的。(3)区别位深:RGB565: 使用16位,每像素5:6:5的位分配。RGB888: 使用24位,每像素8:8:8的位分配。颜色范围:RGB565: 可以表示大约65,536种颜色。RGB888: 可以表示大约16,777,216种颜色。用途:RGB565: 更适合于需要节省存储空间的应用,如旧式的显示器、手机屏幕等。RGB888: 适用于需要高色彩保真的应用,如专业摄影、图形设计等领域。性能:RGB565: 在存储和传输方面更加高效,但是颜色精度较低。RGB888: 颜色精度更高,但需要更多的存储空间和传输带宽。(4)如何构成RGB565:每个像素由两个字节组成。例如,一个红色像素可能表示为 0xF800(红色部分接近最大值,绿色和蓝色部分接近最小值)。RGB888:每个像素由三个字节组成。例如,一个红色像素可能表示为 0xFF0000(红色部分为最大值255,绿色和蓝色部分为0)。(5)示例可以创建一个简单的例子来说明这些格式是如何工作的。假设有一个像素,它的红色、绿色和蓝色分量分别为128(十六进制为0x80)。RGB565: 对于每个颜色通道,需要将8位的值转换为相应的位数。红色: 0x80 -> 0x1F (5位)绿色: 0x80 -> 0x20 (6位)蓝色: 0x80 -> 0x1F (5位)所以,一个灰色像素在RGB565格式下的值可能是 0x1F201F。RGB888: 我们直接使用8位值。红色: 0x80绿色: 0x80蓝色: 0x80这样,一个灰色像素在RGB888格式下的值将是 0x808080。三、实现代码3.1 RGB565转RGB888的代码下面是一个将 RGB565 数组转换为 RGB888 数组的 C 语言函数:#include <stdint.h>#include <stdlib.h>#include <stdio.h>// 将 RGB565 转换为 RGB888 的函数void RGB565_to_RGB888_array(const uint16_t *rgb565_array, size_t length, uint8_t *rgb888_array) { for (size_t i = 0; i < length; i++) { uint16_t rgb565 = rgb565_array[i]; // 提取 RGB565 中的颜色分量 uint8_t red = (rgb565 >> 11) & 0x1F; // 5 bits uint8_t green = (rgb565 >> 5) & 0x3F; // 6 bits uint8_t blue = rgb565 & 0x1F; // 5 bits // 将颜色分量扩展到 8 位 uint8_t r = (red << 3) | (red >> 2); // 5 bits to 8 bits uint8_t g = (green << 2) | (green >> 4); // 6 bits to 8 bits uint8_t b = (blue << 3) | (blue >> 2); // 5 bits to 8 bits // 将结果存储到 RGB888 数组 rgb888_array[i * 3] = r; rgb888_array[i * 3 + 1] = g; rgb888_array[i * 3 + 2] = b; }}int main() { // 示例 RGB565 数组 uint16_t rgb565_array[] = {0x1F3F, 0x07E0, 0xF800}; size_t length = sizeof(rgb565_array) / sizeof(rgb565_array[0]); // 分配 RGB888 数组内存 uint8_t *rgb888_array = (uint8_t *)malloc(length * 3 * sizeof(uint8_t)); if (rgb888_array == NULL) { perror("Unable to allocate memory for RGB888 array"); return 1; } // 转换 RGB565 数组到 RGB888 数组 RGB565_to_RGB888_array(rgb565_array, length, rgb888_array); // 打印 RGB888 结果 for (size_t i = 0; i < length; i++) { printf("RGB888[%zu]: R=%d, G=%d, B=%d\n", i, rgb888_array[i * 3], rgb888_array[i * 3 + 1], rgb888_array[i * 3 + 2]); } // 释放分配的内存 free(rgb888_array); return 0;}这个函数 RGB565_to_RGB888_array 接收一个 RGB565 数组和数组的长度,并返回一个 RGB888 数组。每个 RGB565 值被转换为三个 8 位的 RGB 分量,并存储在提供的 RGB888 数组中。示例中的 main 函数展示了如何调用这个转换函数并打印结果。3.2 BMP图片封装: 头文件#ifndef BMP_H#define BMP_H#include "ff.h"#include "string.h"#include "sys.h"#pragma pack(1) /* 必须在结构体定义之前使用,这是为了让结构体中各成员按1字节对齐 *//*需要文件信息头:14个字节 */typedef struct tagBITMAPFILEHEADER{ unsigned short bfType; //保存图片类似。 'BM' unsigned long bfSize; //图片的大小 unsigned short bfReserved1; unsigned short bfReserved2; unsigned long bfOffBits; //RGB数据偏移地址}BITMAPFILEHEADER;/* 位图信息头 */typedef struct tagBITMAPINFOHEADER { /* bmih */ unsigned long biSize; //结构体大小 unsigned long biWidth; //宽度 unsigned long biHeight; //高度 unsigned short biPlanes; unsigned short biBitCount; //颜色位数 unsigned long biCompression; unsigned long biSizeImage; unsigned long biXPelsPerMeter; unsigned long biYPelsPerMeter; unsigned long biClrUsed; unsigned long biClrImportant;}BITMAPINFOHEADER;#define RGB888_RED 0x00ff0000 #define RGB888_GREEN 0x0000ff00 #define RGB888_BLUE 0x000000ff #define RGB565_RED 0xf800 #define RGB565_GREEN 0x07e0 #define RGB565_BLUE 0x001f u8 photograph_BMP(u8 *filename,int Width,int Height);void photograph_open(u8 *filename,int Width,int Height);void photograph_write(u16 *buff);void photograph_close(void);#endif3.3 BMP图片封装: 源文件#include "bmp.h" unsigned short RGB888ToRGB565(unsigned int n888Color) { unsigned short n565Color = 0; // 获取RGB单色,并截取高位 unsigned char cRed = (n888Color & RGB888_RED) >> 19; unsigned char cGreen = (n888Color & RGB888_GREEN) >> 10; unsigned char cBlue = (n888Color & RGB888_BLUE) >> 3; // 连接 n565Color = (cRed << 11) + (cGreen << 5) + (cBlue << 0); return n565Color; } unsigned int RGB565ToRGB888(unsigned short n565Color) { unsigned int n888Color = 0; // 获取RGB单色,并填充低位 unsigned char cRed = (n565Color & RGB565_RED) >> 8; unsigned char cGreen = (n565Color & RGB565_GREEN) >> 3; unsigned char cBlue = (n565Color & RGB565_BLUE) << 3; // 连接 n888Color = (cRed << 16) + (cGreen << 8) + (cBlue << 0); return n888Color; } //拍摄BMP的图片u8 photograph_BMP(u8 *filename,int Width,int Height){ u32 cnt; int x,y; u8 res; char *p; u16 c16; //16位颜色值 u32 c32; //24位颜色值 BITMAPFILEHEADER BmpHead; //保存图片文件头的信息 BITMAPINFOHEADER BmpInfo; //图片参数信息 /*1. 创建BMP文件*/ FIL file; res = f_open(&file,(char*)filename, FA_OPEN_ALWAYS | FA_READ | FA_WRITE); //读写加创建 if(res!=0)return 1; /*2. 填充图片数据头*/ memset(&BmpHead,0,sizeof(BITMAPFILEHEADER)); p=(char*)&BmpHead.bfType; //填充BMP图片的类型 *p='B'; *(p+1)='M'; //BmpHead.bfType=0x4d42;//'B''M' //0x4d42 BmpHead.bfSize=Width*Height*3+54; //图片的总大小 BmpHead.bfOffBits=54; //图片数据的偏移量 res=f_write(&file,&BmpHead,sizeof(BITMAPFILEHEADER),&cnt);//写入图片文件头到文文件 if(res!=0)return 1; /*3. 填充图片参数*/ memset(&BmpInfo,0,sizeof(BITMAPINFOHEADER)); BmpInfo.biSize=sizeof(BITMAPINFOHEADER); //当前结构体大小 BmpInfo.biWidth=Width; BmpInfo.biHeight=Height; BmpInfo.biPlanes=1; BmpInfo.biBitCount=24; res=f_write(&file,&BmpInfo,sizeof(BITMAPINFOHEADER),&cnt);//写入图片文件头到文文件 if(res!=0)return 1; /*4. 读取图像参数进行填充*/ for(y=Height-1;y>=0;y--) //因为BMP图片特性,所有需要从LCD最后一行开始读 { for(x=0;x<Width;x++) { //c16=LcdReadPoint(x,y); //LCD读点函数 c32=RGB565ToRGB888(c16); //将16位的颜色转为32位 f_write(&file,&c32,3,&cnt); //写入图片数据 } } /*. 关闭文件*/ f_close(&file); return 0;}BITMAPFILEHEADER BmpHead; //保存图片文件头的信息BITMAPINFOHEADER BmpInfo; //图片参数信息#include <stdio.h>FIL BMP_file;//拍摄1: 创建文件void photograph_open(u8 *filename,int Width,int Height){ u32 cnt; u8 res; char *p; /*1. 创建BMP文件*/ res = f_open(&BMP_file,(char*)filename, FA_OPEN_ALWAYS | FA_READ | FA_WRITE); //读写加创建 if(res!=0) { printf("%s文件打开失败.!\r\n",filename); return; } /*2. 填充图片数据头*/ memset(&BmpHead,0,sizeof(BITMAPFILEHEADER)); p=(char*)&BmpHead.bfType; //填充BMP图片的类型 *p='B'; *(p+1)='M'; //BmpHead.bfType=0x4d42;//'B''M' //0x4d42 BmpHead.bfSize=Width*Height*3+54; //图片的总大小 BmpHead.bfOffBits=54; //图片数据的偏移量 res=f_write(&BMP_file,&BmpHead,sizeof(BITMAPFILEHEADER),&cnt);//写入图片文件头到文文件 if(res!=0) { printf("%s BMP文件头1写入失败.!\r\n",filename); return; } else { printf("%s BMP文件头1写入成功!.%d字节.\r\n",filename,cnt); } /*3. 填充图片参数*/ memset(&BmpInfo,0,sizeof(BITMAPINFOHEADER)); BmpInfo.biSize=sizeof(BITMAPINFOHEADER); //当前结构体大小 BmpInfo.biWidth=Width; BmpInfo.biHeight=Height; BmpInfo.biPlanes=1; BmpInfo.biBitCount=24; res=f_write(&BMP_file,&BmpInfo,sizeof(BITMAPINFOHEADER),&cnt);//写入图片文件头到文文件 if(res!=0) { printf("%s BMP文件头2数据写入失败.!\r\n",filename); return; } else { printf("%s BMP文件头2数据写入成功.!%d字节\r\n",filename,cnt); }}//拍摄2: 写文件void photograph_write(u16 *buff){ u32 c32; //24位颜色值 UINT cnt; u8 res; int x; /*4. 读取图像参数进行填充*/ for(x=0;x<320;x++) { c32=RGB565ToRGB888(buff[x]); //将16位的颜色转为32位 res=f_write(&BMP_file,&c32,3,&cnt); //写入图片数据 if(res!=0) { printf("图像数据写入失败.%d\r\n",x); break; } }}//拍摄3: 关闭文件void photograph_close(void){ /*. 关闭文件*/ f_close(&BMP_file);}
-
一、前言在当今物联网(IoT)技术快速发展的背景下,嵌入式系统在各种应用场景中扮演着越来越重要的角色。随着物联网设备数量的不断增长,数据传输成为了关键的一环,尤其是在资源受限的环境中,如何高效地传输数据变得尤为重要。本文将介绍一个基于STM32F103ZET6微控制器的图像采集系统,该系统利用OV7725摄像头采集RGB565格式的图像,并通过MQTT协议将图像数据上传至阿里云物联网平台。 但是原始RGB565图像数据量巨大,直接传输会带来较高的网络负载,需要设计有效的压缩算法来减小数据体积。考虑到单片机资源有限,无法使用第三方优秀的开源库,当前就基于Run-Length Encoding (RLE)无损压缩算法来减小图像数据的传输负担。RLE算法是一种简单有效的压缩方法,尤其适用于图像中存在大量连续重复像素值的情况。通过对重复像素的值和重复次数进行记录,可以显著减小图像数据的大小,从而降低网络传输的负载。下面会详细介绍RLE算法的设计与实现过程,以及如何在STM32F103ZET6微控制器上实现图像数据的压缩与解压,最终实现在阿里云物联网平台上进行高效的数据传输,并通过APP上位机进行图像数据的拉取与显示。当前文章最重要的知识点是介绍: 数据的压缩和解压。二、算法选型在单片机上进行图像数据压缩,需要选择是使用轻量级、简单但有效的压缩算法。以下2个是可以用的压缩算法,适合在单片机上实现并能够有效减小数据量。2.1 Run-Length Encoding (RLE)RLE 是一种非常简单的无损压缩算法,特别适用于图像中有大量连续重复像素值的情况。通过记录重复像素的值和重复次数来实现压缩。下面是算法的核心实现: 解压和压缩#include <stdint.h>#include <stdlib.h>#include <stdio.h>// RLE 压缩函数size_t RLE_compress(const uint16_t *input, size_t length, uint8_t *output) { size_t out_index = 0; for (size_t i = 0; i < length; ) { uint16_t value = input[i]; size_t run_length = 1; while (i + run_length < length && input[i + run_length] == value && run_length < 255) { run_length++; } output[out_index++] = (uint8_t)(run_length); output[out_index++] = (uint8_t)(value >> 8); output[out_index++] = (uint8_t)(value & 0xFF); i += run_length; } return out_index;}// RLE 解压函数size_t RLE_decompress(const uint8_t *input, size_t length, uint16_t *output) { size_t out_index = 0; for (size_t i = 0; i < length; i += 3) { uint8_t run_length = input[i]; uint16_t value = (input[i + 1] << 8) | input[i + 2]; for (size_t j = 0; j < run_length; j++) { output[out_index++] = value; } } return out_index;}2.2 Differential Pulse-Code Modulation (DPCM)DPCM 适用于图像中连续像素值变化较小的情况。通过记录相邻像素值的差值来实现压缩。下面是算法的核心实现: 解压和压缩#include <stdint.h>#include <stdlib.h>#include <stdio.h>// DPCM 压缩函数size_t DPCM_compress(const uint16_t *input, size_t length, int16_t *output) { output[0] = input[0]; for (size_t i = 1; i < length; i++) { output[i] = input[i] - input[i - 1]; } return length;}// DPCM 解压函数size_t DPCM_decompress(const int16_t *input, size_t length, uint16_t *output) { output[0] = input[0]; for (size_t i = 1; i < length; i++) { output[i] = output[i - 1] + input[i]; } return length;}三、采用RLE算法实现图像压缩下面这段代码采用Run-Length Encoding (RLE) 算法对 RGB565 格式图像数据进行压缩和解压。代码里定义了一个常量数组 gImage_rgb565_100x100,存储 100x100 图像的 RGB565 像素数据(通过实际摄像头采集得到的)。RLE 压缩函数 RLE_compress 遍历输入的 RGB565 数据数组,查找连续相同的像素值,并记录它们的长度和值,将这些信息存储到输出的压缩数组中。每次遇到一段连续的相同值时,它会记录该段的长度(最大为 255)和该像素值的高位和低位字节。压缩后的数据长度返回给调用者。解压函数 RLE_decompress 读取压缩后的数组,根据记录的长度和值还原原始数据,将这些值存储到输出的解压数组中。main 函数中,计算图像数据的长度,然后分配内存用于存储压缩和解压后的数据,分别调用压缩和解压函数进行处理。最后,打印出原始数据大小、压缩后的数据大小以及压缩率,以验证压缩效果。所有动态分配的内存最终被释放,以避免内存泄漏。通过这样的实现,可以在不使用第三方库的情况下,有效地减少图像数据的传输负荷,提高传输效率。实现代码:#include <stdint.h>#include <stdio.h>#include <stdlib.h>// RLE 压缩函数size_t RLE_compress(const uint16_t* input, size_t length, uint8_t* output) { size_t out_index = 0; for (size_t i = 0; i < length; ) { uint16_t value = input[i]; size_t run_length = 1; while (i + run_length < length && input[i + run_length] == value && run_length < 255) { run_length++; } output[out_index++] = (uint8_t)(run_length); output[out_index++] = (uint8_t)(value >> 8); output[out_index++] = (uint8_t)(value & 0xFF); i += run_length; } return out_index;}// RLE 解压函数size_t RLE_decompress(const uint8_t* input, size_t length, uint16_t* output) { size_t out_index = 0; for (size_t i = 0; i < length; i += 3) { uint8_t run_length = input[i]; uint16_t value = (input[i + 1] << 8) | input[i + 2]; for (size_t j = 0; j < run_length; j++) { output[out_index++] = value; } } return out_index;}int main() { size_t length = sizeof(gImage_rgb565_100x100) / 2; uint16_t* rgb565_array = (uint16_t*)gImage_rgb565_100x100; // 分配 RLE 压缩后的数组 uint8_t* rle_compressed = (uint8_t*)malloc(length * 3 * sizeof(uint8_t)); if (rle_compressed == NULL) { perror("Unable to allocate memory for RLE compressed array"); return 1; } // RLE 压缩 size_t rle_compressed_length = RLE_compress(rgb565_array, length, rle_compressed); // 分配 RLE 解压后的数组 uint16_t* rle_decompressed = (uint16_t*)malloc(length * sizeof(uint16_t)); if (rle_decompressed == NULL) { perror("Unable to allocate memory for RLE decompressed array"); return 1; } // RLE 解压 size_t rle_decompressed_length = RLE_decompress(rle_compressed, rle_compressed_length, rle_decompressed); // 打印 RLE 压缩率 printf("RLE 压缩算法:\n"); printf("原始大小: %zu bytes\n", length * 2); printf("压缩后大小: %zu bytes\n", rle_compressed_length); printf("压缩比: %.2f%%\n", (double)rle_compressed_length / (length * 2) * 100); // 释放内存 free(rle_compressed); free(rle_decompressed); return 0;}运行效果如下:四、哈夫曼编码实现压缩和解压哈夫曼编码是一种常用的无损压缩算法,通过构建哈夫曼树,根据字符频率生成最优编码,从而实现数据压缩。4.1 哈夫曼编码压缩自定义数据与还原下面代码使用哈夫曼编码对一段自定义数据进行压缩和解压,并打印压缩前后的大小和压缩率,验证压缩效果。#include <stdio.h>#include <stdlib.h>#include <string.h>// 定义树节点结构typedef struct Node { unsigned char ch; unsigned int freq; struct Node* left, * right;} Node;// 优先队列typedef struct { Node** nodes; int size; int capacity;} PriorityQueue;PriorityQueue* createPriorityQueue(int capacity) { PriorityQueue* pq = (PriorityQueue*)malloc(sizeof(PriorityQueue)); pq->nodes = (Node**)malloc(capacity * sizeof(Node*)); pq->size = 0; pq->capacity = capacity; return pq;}void swapNodes(Node** a, Node** b) { Node* t = *a; *a = *b; *b = t;}void heapify(PriorityQueue* pq, int idx) { int smallest = idx; int left = 2 * idx + 1; int right = 2 * idx + 2; if (left < pq->size && pq->nodes[left]->freq < pq->nodes[smallest]->freq) { smallest = left; } if (right < pq->size && pq->nodes[right]->freq < pq->nodes[smallest]->freq) { smallest = right; } if (smallest != idx) { swapNodes(&pq->nodes[smallest], &pq->nodes[idx]); heapify(pq, smallest); }}Node* extractMin(PriorityQueue* pq) { Node* temp = pq->nodes[0]; pq->nodes[0] = pq->nodes[pq->size - 1]; pq->size--; heapify(pq, 0); return temp;}void insertPriorityQueue(PriorityQueue* pq, Node* node) { pq->size++; int i = pq->size - 1; while (i && node->freq < pq->nodes[(i - 1) / 2]->freq) { pq->nodes[i] = pq->nodes[(i - 1) / 2]; i = (i - 1) / 2; } pq->nodes[i] = node;}int isLeaf(Node* node) { return !(node->left) && !(node->right);}PriorityQueue* buildPriorityQueue(unsigned char data[], int freq[], int size) { PriorityQueue* pq = createPriorityQueue(size); for (int i = 0; i < size; ++i) { if (freq[data[i]] > 0) { Node* node = (Node*)malloc(sizeof(Node)); node->ch = data[i]; node->freq = freq[data[i]]; node->left = node->right = NULL; pq->nodes[pq->size++] = node; } } for (int i = (pq->size - 1) / 2; i >= 0; --i) { heapify(pq, i); } return pq;}Node* buildHuffmanTree(unsigned char data[], int freq[], int size) { Node* left, * right, * top; PriorityQueue* pq = buildPriorityQueue(data, freq, size); while (pq->size != 1) { left = extractMin(pq); right = extractMin(pq); top = (Node*)malloc(sizeof(Node)); top->ch = '\0'; top->freq = left->freq + right->freq; top->left = left; top->right = right; insertPriorityQueue(pq, top); } return extractMin(pq);}void printCodes(Node* root, int arr[], int top, char** huffmanCodes) { if (root->left) { arr[top] = 0; printCodes(root->left, arr, top + 1, huffmanCodes); } if (root->right) { arr[top] = 1; printCodes(root->right, arr, top + 1, huffmanCodes); } if (isLeaf(root)) { huffmanCodes[root->ch] = (char*)malloc(top + 1); for (int i = 0; i < top; ++i) { huffmanCodes[root->ch][i] = '0' + arr[i]; } huffmanCodes[root->ch][top] = '\0'; }}char** buildHuffmanCodes(unsigned char data[], int freq[], int size) { Node* root = buildHuffmanTree(data, freq, size); int arr[100], top = 0; char** huffmanCodes = (char**)malloc(256 * sizeof(char*)); for (int i = 0; i < 256; ++i) { huffmanCodes[i] = NULL; } printCodes(root, arr, top, huffmanCodes); return huffmanCodes;}void compress(unsigned char data[], int dataSize, char** huffmanCodes, unsigned char** compressedData, int* compressedSize) { int bitCount = 0; for (int i = 0; i < dataSize; ++i) { bitCount += strlen(huffmanCodes[data[i]]); } *compressedSize = (bitCount + 7) / 8; *compressedData = (unsigned char*)malloc(*compressedSize); memset(*compressedData, 0, *compressedSize); int byteIndex = 0, bitIndex = 0; for (int i = 0; i < dataSize; ++i) { char* code = huffmanCodes[data[i]]; for (int j = 0; code[j] != '\0'; ++j) { if (code[j] == '1') { (*compressedData)[byteIndex] |= (1 << (7 - bitIndex)); } bitIndex++; if (bitIndex == 8) { bitIndex = 0; byteIndex++; } } }}void decompress(unsigned char* compressedData, int compressedSize, Node* root, unsigned char** decompressedData, int dataSize) { *decompressedData = (unsigned char*)malloc(dataSize + 1); Node* current = root; int byteIndex = 0, bitIndex = 0; for (int i = 0; i < dataSize; ++i) { while (!isLeaf(current)) { if (compressedData[byteIndex] & (1 << (7 - bitIndex))) { current = current->right; } else { current = current->left; } bitIndex++; if (bitIndex == 8) { bitIndex = 0; byteIndex++; } } (*decompressedData)[i] = current->ch; current = root; } (*decompressedData)[dataSize] = '\0';}int main() { // 自定义数据 unsigned char data[] = "我是DS小龙哥 我正在测试这一段数据,看看能不能压缩成功"; int dataSize = strlen((char*)data); // 计算频率 int freq[256] = { 0 }; for (int i = 0; i < dataSize; ++i) { freq[data[i]]++; } // 构建哈夫曼编码 char** huffmanCodes = buildHuffmanCodes(data, freq, 256); // 压缩 unsigned char* compressedData; int compressedSize; compress(data, dataSize, huffmanCodes, &compressedData, &compressedSize); // 解压 unsigned char* decompressedData; Node* root = buildHuffmanTree(data, freq, 256); decompress(compressedData, compressedSize, root, &decompressedData, dataSize); // 打印压缩前后的大小和压缩率 printf("Original size: %d bytes\n", dataSize); printf("Compressed size: %d bytes\n", compressedSize); printf("Compression ratio: %.2f%%\n", (double)compressedSize / dataSize * 100); // 验证解压结果 printf("Decompressed data: %s\n", decompressedData); // 释放内存 free(compressedData); free(decompressedData); for (int i = 0; i < 256; ++i) { if (huffmanCodes[i]) { free(huffmanCodes[i]); } } free(huffmanCodes); return 0;}测试效果:4.2 哈夫曼编码压缩完成图像的压缩和还原#include <stdio.h>#include <stdlib.h>#include <string.h>// 定义树节点结构typedef struct Node { unsigned short ch; // 使用 unsigned short 表示 RGB565 数据 unsigned int freq; struct Node* left, * right;} Node;// 优先队列typedef struct { Node** nodes; int size; int capacity;} PriorityQueue;PriorityQueue* createPriorityQueue(int capacity) { PriorityQueue* pq = (PriorityQueue*)malloc(sizeof(PriorityQueue)); pq->nodes = (Node**)malloc(capacity * sizeof(Node*)); pq->size = 0; pq->capacity = capacity; return pq;}void swapNodes(Node** a, Node** b) { Node* t = *a; *a = *b; *b = t;}void heapify(PriorityQueue* pq, int idx) { int smallest = idx; int left = 2 * idx + 1; int right = 2 * idx + 2; if (left < pq->size && pq->nodes[left]->freq < pq->nodes[smallest]->freq) { smallest = left; } if (right < pq->size && pq->nodes[right]->freq < pq->nodes[smallest]->freq) { smallest = right; } if (smallest != idx) { swapNodes(&pq->nodes[smallest], &pq->nodes[idx]); heapify(pq, smallest); }}Node* extractMin(PriorityQueue* pq) { Node* temp = pq->nodes[0]; pq->nodes[0] = pq->nodes[pq->size - 1]; pq->size--; heapify(pq, 0); return temp;}void insertPriorityQueue(PriorityQueue* pq, Node* node) { pq->size++; int i = pq->size - 1; while (i && node->freq < pq->nodes[(i - 1) / 2]->freq) { pq->nodes[i] = pq->nodes[(i - 1) / 2]; i = (i - 1) / 2; } pq->nodes[i] = node;}int isLeaf(Node* node) { return !(node->left) && !(node->right);}PriorityQueue* buildPriorityQueue(unsigned short data[], int freq[], int size) { PriorityQueue* pq = createPriorityQueue(size); for (int i = 0; i < size; ++i) { if (freq[data[i]] > 0) { Node* node = (Node*)malloc(sizeof(Node)); node->ch = data[i]; node->freq = freq[data[i]]; node->left = node->right = NULL; pq->nodes[pq->size++] = node; } } for (int i = (pq->size - 1) / 2; i >= 0; --i) { heapify(pq, i); } return pq;}Node* buildHuffmanTree(unsigned short data[], int freq[], int size) { Node* left, * right, * top; PriorityQueue* pq = buildPriorityQueue(data, freq, size); while (pq->size != 1) { left = extractMin(pq); right = extractMin(pq); top = (Node*)malloc(sizeof(Node)); top->ch = 0; top->freq = left->freq + right->freq; top->left = left; top->right = right; insertPriorityQueue(pq, top); } return extractMin(pq);}void printCodes(Node* root, int arr[], int top, char** huffmanCodes) { if (root->left) { arr[top] = 0; printCodes(root->left, arr, top + 1, huffmanCodes); } if (root->right) { arr[top] = 1; printCodes(root->right, arr, top + 1, huffmanCodes); } if (isLeaf(root)) { huffmanCodes[root->ch] = (char*)malloc(top + 1); for (int i = 0; i < top; ++i) { huffmanCodes[root->ch][i] = '0' + arr[i]; } huffmanCodes[root->ch][top] = '\0'; }}char** buildHuffmanCodes(unsigned short data[], int freq[], int size) { Node* root = buildHuffmanTree(data, freq, size); int arr[100], top = 0; char** huffmanCodes = (char**)malloc(65536 * sizeof(char*)); // 65536 表示所有可能的 RGB565 值 for (int i = 0; i < 65536; ++i) { huffmanCodes[i] = NULL; } printCodes(root, arr, top, huffmanCodes); return huffmanCodes;}void compress(unsigned short data[], int dataSize, char** huffmanCodes, unsigned char** compressedData, int* compressedSize) { int bitCount = 0; for (int i = 0; i < dataSize; ++i) { bitCount += strlen(huffmanCodes[data[i]]); } *compressedSize = (bitCount + 7) / 8; *compressedData = (unsigned char*)malloc(*compressedSize); memset(*compressedData, 0, *compressedSize); int byteIndex = 0, bitIndex = 0; for (int i = 0; i < dataSize; ++i) { char* code = huffmanCodes[data[i]]; for (int j = 0; code[j] != '\0'; ++j) { if (code[j] == '1') { (*compressedData)[byteIndex] |= (1 << (7 - bitIndex)); } bitIndex++; if (bitIndex == 8) { bitIndex = 0; byteIndex++; } } }}void decompress(unsigned char* compressedData, int compressedSize, Node* root, unsigned short** decompressedData, int dataSize) { *decompressedData = (unsigned short*)malloc(dataSize * sizeof(unsigned short)); Node* current = root; int byteIndex = 0, bitIndex = 0; for (int i = 0; i < dataSize; ++i) { while (!isLeaf(current)) { if (compressedData[byteIndex] & (1 << (7 - bitIndex))) { current = current->right; } else { current = current->left; } bitIndex++; if (bitIndex == 8) { bitIndex = 0; byteIndex++; } } (*decompressedData)[i] = current->ch; current = root; }}int main() { int dataSize = sizeof(gImage_rgb565_100x100) / 2; unsigned short* data = (unsigned short*)gImage_rgb565_100x100; // 计算频率 int freq[65536] = { 0 }; for (int i = 0; i < dataSize; ++i) { freq[data[i]]++; } // 构建哈夫曼编码 char** huffmanCodes = buildHuffmanCodes(data, freq, 65536); // 压缩 unsigned char* compressedData; int compressedSize; compress(data, dataSize, huffmanCodes, &compressedData, &compressedSize); // 解压 unsigned short* decompressedData; Node* root = buildHuffmanTree(data, freq, 65536); decompress(compressedData, compressedSize, root, &decompressedData, dataSize); // 打印压缩前后的大小和压缩率 printf("Original size: %d bytes\n", dataSize * 2); printf("Compressed size: %d bytes\n", compressedSize); printf("Compression ratio: %.2f%%\n", ((double)compressedSize / (dataSize * 2)) * 100); // 验证解压结果(仅在开发时使用) for (int i = 0; i < dataSize; ++i) { if (data[i] != decompressedData[i]) { printf("Error: Decompressed data does not match original data at index %d.\n", i); break; } } // 释放内存 free(compressedData); free(decompressedData); for (int i = 0; i < 65536; ++i) { if (huffmanCodes[i]) { free(huffmanCodes[i]); } } free(huffmanCodes); return 0;}
-
一、前言霍夫曼编码(Huffman Coding)是一种广泛使用的数据压缩算法,特别适用于无损数据压缩。它是由David A. Huffman在1952年提出的,并且通常用于文件压缩和传输中减少数据量。霍夫曼编码的核心思想是使用变长编码表对源数据进行编码,其中较频繁出现的数据项会被赋予较短的编码,而较少出现的数据项则会被赋予较长的编码。该编码方法通过构建一种特殊的二叉树——霍夫曼树,为数据中的各个符号分配长度可变的前缀码,使得高频出现的符号具有较短的编码,而低频出现的符号则具有较长的编码。这种特性使得霍夫曼编码非常适合于压缩具有不均匀符号频率分布的数据集,能够有效地减小数据的存储空间或传输所需的带宽。霍夫曼编码的工作原理如下:首先统计输入数据中每个符号出现的频率;然后基于这些频率值构造一个最小堆,其中堆中的每个元素都是一个只包含一个符号及其频率的树节点;接着反复从堆中取出频率最小的两个节点,合并成一个新的内部节点,该节点的频率为两个子节点频率之和,并将这个新节点放回堆中;重复这一过程直到堆中只剩下一个节点,这个节点即为霍夫曼树的根节点;最后,从根节点出发遍历整棵树,定义从根到任一叶节点的路径上,向左走标记为0,向右走标记为1,这样每个叶节点就对应了一个唯一的二进制编码,这就是霍夫曼编码。霍夫曼编码的一个关键特征是其编码具有前缀性质,即没有任何一个符号的编码是另一个符号编码的前缀,这保证了在解码过程中可以唯一确定每一个编码所代表的符号,从而确保了压缩和解压过程的一致性。此外,霍夫曼编码是熵编码的一种形式,它利用了数据的统计特性来进行压缩,理论上可以达到接近于信息熵的压缩效率,即在理想情况下,压缩后的数据量等于数据的信息熵。霍夫曼编码在文件压缩软件中,如WinZip、7-Zip,使用霍夫曼编码来压缩文件;在网络通信中,为了提高传输效率,会采用霍夫曼编码来压缩数据流;在图像处理和视频编码中,霍夫曼编码经常与其他编码技术结合使用,比如JPEG图像压缩标准就使用了霍夫曼编码来进一步压缩量化后的离散余弦变换系数。下面是霍夫曼编码的基本步骤:统计字符频率:首先需要统计输入数据中每个字符出现的次数。创建霍夫曼树(也称为最优二叉树) :创建一个叶子节点集合,每个叶子节点包含一个字符和它的频率。反复执行以下操作,直到所有节点合并成一棵树:从集合中选出两个频率最低的节点作为新节点的左右子节点。新节点的频率是其两个子节点频率之和。将这个新节点加入集合中。最终剩下的那棵树就是霍夫曼树。生成编码规则:从霍夫曼树的根节点出发,向左走标记为0,向右走标记为1。每个叶子节点对应的路径上的数字序列即为其霍夫曼编码。编码数据:使用生成的霍夫曼编码表对原始数据进行编码。解码数据:解码时只需按照霍夫曼树从根节点开始,根据0或1沿着树向下移动即可还原出原始数据。霍夫曼编码的一个重要特点是它是前缀编码,这意味着没有一个编码是另一个编码的前缀。这样可以确保编码后的数据可以唯一地解码回原始数据。举个简单的例子来说明霍夫曼编码的过程:假设我们有这样一个字符串:“ABACDAACAC”,其中字符A出现了5次,B出现了1次,C出现了4次,D出现了1次。统计字符频率:A: 5B: 1C: 4D: 1创建霍夫曼树:构建初始节点集合:{A(5), B(1), C(4), D(1)}合并B(1)和D(1),得到一个新节点BD(2)合并C(4)和BD(2),得到一个新节点CBDB(6)最后合并A(5)和CBDB(6),得到霍夫曼树的根节点。生成编码规则:A: 0B: 110C: 10D: 111编码数据:“ABACDAACAC”编码后变为:“0110100011101000100”二、算法设计(C语言)2.1 霍夫曼编码算法实现下面代码里实现了创建霍夫曼树、生成霍夫曼编码、对输入文本进行编码和解码的功能。编译运行这段代码可以得到每个字符的霍夫曼编码。#include <stdio.h> #include <stdlib.h> #include <string.h> #define MAX_TREE_HT 100 // 霍夫曼树节点 struct MinHeapNode { char data; unsigned freq; struct MinHeapNode* left, * right; }; // 最小堆 struct MinHeap { unsigned size; unsigned capacity; struct MinHeapNode** array; }; // 创建新节点 struct MinHeapNode* newNode(char data, unsigned freq) { struct MinHeapNode* temp = (struct MinHeapNode*)malloc(sizeof(struct MinHeapNode)); temp->left = temp->right = NULL; temp->data = data; temp->freq = freq; return temp; } // 创建最小堆 struct MinHeap* createMinHeap(unsigned capacity) { struct MinHeap* minHeap = (struct MinHeap*)malloc(sizeof(struct MinHeap)); minHeap->size = 0; minHeap->capacity = capacity; minHeap->array = (struct MinHeapNode**)malloc(minHeap->capacity * sizeof(struct MinHeapNode*)); return minHeap; } // 交换两个最小堆节点 void swapMinHeapNode(struct MinHeapNode** a, struct MinHeapNode** b) { struct MinHeapNode* t = *a; *a = *b; *b = t; } // 堆化 void minHeapify(struct MinHeap* minHeap, int idx) { int smallest = idx; int left = 2 * idx + 1; int right = 2 * idx + 2; if (left < minHeap->size && minHeap->array[left]->freq < minHeap->array[smallest]->freq) smallest = left; if (right < minHeap->size && minHeap->array[right]->freq < minHeap->array[smallest]->freq) smallest = right; if (smallest != idx) { swapMinHeapNode(&minHeap->array[smallest], &minHeap->array[idx]); minHeapify(minHeap, smallest); } } // 检查大小是否为1 int isSizeOne(struct MinHeap* minHeap) { return (minHeap->size == 1); } // 提取最小值节点 struct MinHeapNode* extractMin(struct MinHeap* minHeap) { struct MinHeapNode* temp = minHeap->array[0]; minHeap->array[0] = minHeap->array[minHeap->size - 1]; --minHeap->size; minHeapify(minHeap, 0); return temp; } // 插入最小堆 void insertMinHeap(struct MinHeap* minHeap, struct MinHeapNode* minHeapNode) { ++minHeap->size; int i = minHeap->size - 1; while (i && minHeapNode->freq < minHeap->array[(i - 1) / 2]->freq) { minHeap->array[i] = minHeap->array[(i - 1) / 2]; i = (i - 1) / 2; } minHeap->array[i] = minHeapNode; } // 构建最小堆 void buildMinHeap(struct MinHeap* minHeap) { int n = minHeap->size - 1; int i; for (i = (n - 1) / 2; i >= 0; --i) minHeapify(minHeap, i); } // 检查是否是叶子节点 int isLeaf(struct MinHeapNode* root) { return !(root->left) && !(root->right); } // 创建和构建最小堆 struct MinHeap* createAndBuildMinHeap(char data[], int freq[], int size) { struct MinHeap* minHeap = createMinHeap(size); for (int i = 0; i < size; ++i) minHeap->array[i] = newNode(data[i], freq[i]); minHeap->size = size; buildMinHeap(minHeap); return minHeap; } // 构建霍夫曼树 struct MinHeapNode* buildHuffmanTree(char data[], int freq[], int size) { struct MinHeapNode* left, * right, * top; struct MinHeap* minHeap = createAndBuildMinHeap(data, freq, size); while (!isSizeOne(minHeap)) { left = extractMin(minHeap); right = extractMin(minHeap); top = newNode('$', left->freq + right->freq); top->left = left; top->right = right; insertMinHeap(minHeap, top); } return extractMin(minHeap); } // 打印编码 void printCodes(struct MinHeapNode* root, int arr[], int top) { if (root->left) { arr[top] = 0; printCodes(root->left, arr, top + 1); } if (root->right) { arr[top] = 1; printCodes(root->right, arr, top + 1); } if (isLeaf(root)) { printf("%c: ", root->data); for (int i = 0; i < top; ++i) printf("%d", arr[i]); printf("\n"); } } // 霍夫曼编码主函数 void HuffmanCodes(char data[], int freq[], int size) { struct MinHeapNode* root = buildHuffmanTree(data, freq, size); int arr[MAX_TREE_HT], top = 0; printCodes(root, arr, top); } // 主函数 int main() { char arr[] = { 'a', 'b', 'c', 'd', 'e', 'f' }; int freq[] = { 5, 9, 12, 13, 16, 45 }; int size = sizeof(arr) / sizeof(arr[0]); HuffmanCodes(arr, freq, size); return 0; }复制2.2 数据的编码与还原以下是使用霍夫曼编码算法对一段数据进行压缩和还原。代码包括生成霍夫曼树、生成编码表、对数据进行压缩和还原的功能。#include <stdio.h> #include <stdlib.h> #include <string.h> #define MAX_TREE_HT 100 #define MAX_CHAR 256 // 霍夫曼树节点 struct MinHeapNode { unsigned char data; unsigned freq; struct MinHeapNode* left, * right; }; // 最小堆 struct MinHeap { unsigned size; unsigned capacity; struct MinHeapNode** array; }; // 创建新节点 struct MinHeapNode* newNode(unsigned char data, unsigned freq) { struct MinHeapNode* temp = (struct MinHeapNode*)malloc(sizeof(struct MinHeapNode)); temp->left = temp->right = NULL; temp->data = data; temp->freq = freq; return temp; } // 创建最小堆 struct MinHeap* createMinHeap(unsigned capacity) { struct MinHeap* minHeap = (struct MinHeap*)malloc(sizeof(struct MinHeap)); minHeap->size = 0; minHeap->capacity = capacity; minHeap->array = (struct MinHeapNode**)malloc(minHeap->capacity * sizeof(struct MinHeapNode*)); return minHeap; } // 交换两个最小堆节点 void swapMinHeapNode(struct MinHeapNode** a, struct MinHeapNode** b) { struct MinHeapNode* t = *a; *a = *b; *b = t; } // 堆化 void minHeapify(struct MinHeap* minHeap, int idx) { int smallest = idx; int left = 2 * idx + 1; int right = 2 * idx + 2; if (left < minHeap->size && minHeap->array[left]->freq < minHeap->array[smallest]->freq) smallest = left; if (right < minHeap->size && minHeap->array[right]->freq < minHeap->array[smallest]->freq) smallest = right; if (smallest != idx) { swapMinHeapNode(&minHeap->array[smallest], &minHeap->array[idx]); minHeapify(minHeap, smallest); } } // 检查大小是否为1 int isSizeOne(struct MinHeap* minHeap) { return (minHeap->size == 1); } // 提取最小值节点 struct MinHeapNode* extractMin(struct MinHeap* minHeap) { struct MinHeapNode* temp = minHeap->array[0]; minHeap->array[0] = minHeap->array[minHeap->size - 1]; --minHeap->size; minHeapify(minHeap, 0); return temp; } // 插入最小堆 void insertMinHeap(struct MinHeap* minHeap, struct MinHeapNode* minHeapNode) { ++minHeap->size; int i = minHeap->size - 1; while (i && minHeapNode->freq < minHeap->array[(i - 1) / 2]->freq) { minHeap->array[i] = minHeap->array[(i - 1) / 2]; i = (i - 1) / 2; } minHeap->array[i] = minHeapNode; } // 构建最小堆 void buildMinHeap(struct MinHeap* minHeap) { int n = minHeap->size - 1; int i; for (i = (n - 1) / 2; i >= 0; --i) minHeapify(minHeap, i); } // 检查是否是叶子节点 int isLeaf(struct MinHeapNode* root) { return !(root->left) && !(root->right); } // 创建和构建最小堆 struct MinHeap* createAndBuildMinHeap(unsigned char data[], int freq[], int size) { struct MinHeap* minHeap = createMinHeap(size); for (int i = 0; i < size; ++i) minHeap->array[i] = newNode(data[i], freq[i]); minHeap->size = size; buildMinHeap(minHeap); return minHeap; } // 构建霍夫曼树 struct MinHeapNode* buildHuffmanTree(unsigned char data[], int freq[], int size) { struct MinHeapNode* left, * right, * top; struct MinHeap* minHeap = createAndBuildMinHeap(data, freq, size); while (!isSizeOne(minHeap)) { left = extractMin(minHeap); right = extractMin(minHeap); top = newNode('$', left->freq + right->freq); top->left = left; top->right = right; insertMinHeap(minHeap, top); } return extractMin(minHeap); } // 生成编码表 void generateCodes(struct MinHeapNode* root, int arr[], int top, char* codes[]) { if (root->left) { arr[top] = 0; generateCodes(root->left, arr, top + 1, codes); } if (root->right) { arr[top] = 1; generateCodes(root->right, arr, top + 1, codes); } if (isLeaf(root)) { codes[root->data] = (char*)malloc(top + 1); for (int i = 0; i < top; ++i) { codes[root->data][i] = arr[i] + '0'; } codes[root->data][top] = '\0'; } } // 压缩数据 void compressData(const char* data, char* codes[], char* compressed) { while (*data) { strcat(compressed, codes[(unsigned char)*data]); data++; } } // 解码霍夫曼树 void decodeHuffmanTree(struct MinHeapNode* root, char* encoded, char* decoded) { struct MinHeapNode* current = root; while (*encoded) { if (*encoded == '0') { current = current->left; } else { current = current->right; } if (isLeaf(current)) { *decoded++ = current->data; current = root; } encoded++; } *decoded = '\0'; } // 打印编码表 void printCodes(char* codes[]) { for (int i = 0; i < MAX_CHAR; i++) { if (codes[i]) { printf("%c: %s\n", i, codes[i]); } } } // 主函数 int main() { const char* data = "我是DS小龙哥-这是一段测试的数据,如果你可以正确的看到我,说明解码已经成功了"; int freq[MAX_CHAR] = { 0 }; for (int i = 0; data[i]; i++) { freq[(unsigned char)data[i]]++; } unsigned char uniqueChars[MAX_CHAR]; int uniqueFreqs[MAX_CHAR]; int size = 0; for (int i = 0; i < MAX_CHAR; i++) { if (freq[i]) { uniqueChars[size] = i; uniqueFreqs[size] = freq[i]; size++; } } struct MinHeapNode* root = buildHuffmanTree(uniqueChars, uniqueFreqs, size); char* codes[MAX_CHAR] = { 0 }; int arr[MAX_TREE_HT], top = 0; generateCodes(root, arr, top, codes); printf("Huffman Codes:\n"); printCodes(codes); char compressed[1024] = { 0 }; compressData(data, codes, compressed); printf("\nCompressed Data: %s\n", compressed); char decoded[1024] = { 0 }; decodeHuffmanTree(root, compressed, decoded); printf("\nDecoded Data: %s\n", decoded); for (int i = 0; i < MAX_CHAR; i++) { if (codes[i]) { free(codes[i]); } } return 0; }复制这段代码实现了霍夫曼编码算法的压缩和解码功能。霍夫曼编码是一种无损数据压缩算法,利用字符在数据中出现的频率构建霍夫曼树,从而生成字符的二进制编码。代码先定义了霍夫曼树节点和最小堆的结构,包含创建新节点、交换节点、堆化节点、提取最小值节点、插入最小堆和构建最小堆的功能。然后,通过辅助函数isLeaf检查节点是否为叶子节点。通过buildHuffmanTree函数构建霍夫曼树,将字符和其对应的频率插入最小堆,反复提取两个最小频率节点,并将它们合并成一个新节点,再插回最小堆,直到堆中只剩一个节点,即为霍夫曼树的根节点。通过generateCodes函数递归遍历霍夫曼树,生成每个字符的霍夫曼编码,并存储在编码表中。compressData函数使用生成的编码表将输入数据压缩为霍夫曼编码。decodeHuffmanTree函数通过遍历霍夫曼树解码压缩后的数据,恢复原始数据。printCodes辅助函数用于打印生成的霍夫曼编码表。在主函数中,统计输入数据中每个字符的频率,然后构建霍夫曼树,生成编码表,对输入数据进行压缩,最后再对压缩后的数据进行解码,并打印结果。代码在实现霍夫曼编码和解码过程中,展示了从频率统计、树的构建、编码生成到数据压缩和解码的完整流程。
-
前言当前文章介绍如何在Linux下使用FFmpeg转码其他视频格式到AVS格式的指南,包括编译FFmpeg以支持XAVS编码和如何使用FFmpeg进行转码。AVS (Audio Video Coding Standard) 格式是一种由中国主导制定的视频编码标准,全称为“中国数字音视频编解码技术标准”(China Digital Audio Video Coding Standard),主要应用于高清电视广播、数字电视、网络视频传输等领域。AVS 标准提高视频压缩效率,降低计算复杂度,并减少专利费用,为中国及其他国家提供一种自主可控的视频编码技术。AVS 视频编码标准由多个子标准组成,其中 AVS1-P2(也称为 AVS+)是最广泛使用的版本之一,被设计用于高清晰度电视广播服务,并被中国数字地面电视广播标准 DTMB 采纳为推荐的视频编码格式。AVS1-P2 提供了与 H.264/AVC 相当的压缩效率,同时减少了计算复杂度,使得其在硬件实现上更为经济高效。技术特点高效压缩:AVS 标准采用了多种先进的压缩技术,如帧内预测、帧间预测、运动补偿、熵编码等,以实现高效的视频数据压缩。低计算复杂度:与 H.264/AVC 相比,AVS 设计上更加注重计算效率,减少了复杂的运算过程,从而降低了硬件实现的成本。自主知识产权:AVS 作为一种中国主导的标准,避免了因专利费而产生的高额成本,对于国内厂商来说具有一定的成本优势。支持多种应用场景:AVS 支持多种视频分辨率和帧率,适用于从标清到高清甚至超高清的各种视频应用场合。一个avs2编码的视频信息如下:[davs2 info]: Manager 26c5cacfc00: Sequence Resolution: 3840x2160.[davs2 info]: Dec[ 0] 26c5cad0780: COI of the first frame is 25.[davs2 info]: davs2: 1.6.205 5313a0a9f7e63110.10, 2022-02-20 13:39:38[davs2 info]: CPU Capabilities: MMX2 SSE2Fast SSSE3 SSE4.2 AVX AVX2 FMA3 LZCNT BMI2[davs2 info]: Manager 26c5cacfc00: using 1 thread(s): 1(frame/AEC)+0(pool/REC), 2 tasks "streams": [ { "index": 0, "codec_name": "avs2", "codec_long_name": "AVS2-P2/IEEE1857.4", "codec_type": "video", "codec_tag_string": "[0][0][0][0]", "codec_tag": "0x0000", "width": 3840, "height": 2160, "coded_width": 3840, "coded_height": 2160, "closed_captions": 0, "film_grain": 0, "has_b_frames": 0, "pix_fmt": "yuv420p10le", "level": -99, "refs": 1, "r_frame_rate": "50/1", "avg_frame_rate": "50/1", "time_base": "1/1000", "start_pts": 23, "start_time": "0.023000", "extradata_size": 45, "disposition": { "default": 0, "dub": 0, "original": 0, "comment": 0, "lyrics": 0, "karaoke": 0, "forced": 0, "hearing_impaired": 0, "visual_impaired": 0, "clean_effects": 0, "attached_pic": 0, "timed_thumbnails": 0, "captions": 0, "descriptions": 0, "metadata": 0, "dependent": 0, "still_image": 0 }, "tags": { "DURATION": "00:00:06.163000000" } }, { "index": 1, "codec_name": "ac3", "codec_long_name": "ATSC A/52A (AC-3)", "codec_type": "audio", "codec_tag_string": "[0][0][0][0]", "codec_tag": "0x0000", "sample_fmt": "fltp", "sample_rate": "48000", "channels": 6, "channel_layout": "5.1(side)", "bits_per_sample": 0, "r_frame_rate": "0/0", "avg_frame_rate": "0/0", "time_base": "1/1000", "start_pts": 24, "start_time": "0.024000", "bit_rate": "448000", "disposition": { "default": 0, "dub": 0, "original": 0, "comment": 0, "lyrics": 0, "karaoke": 0, "forced": 0, "hearing_impaired": 0, "visual_impaired": 0, "clean_effects": 0, "attached_pic": 0, "timed_thumbnails": 0, "captions": 0, "descriptions": 0, "metadata": 0, "dependent": 0, "still_image": 0 }, "tags": { "DURATION": "00:00:06.008000000" } } ]}一、如何编译FFmpeg以支持XAVS编码1. 下载FFmpeg源代码使用Subversion(SVN)从FFmpeg官方仓库下载FFmpeg源代码:svn checkout svn://svn.ffmpeg.org/ffmpeg/trunk ffmpeg2. 下载XAVS源代码从SourceForge下载XAVS源代码:svn co https://xavs.svn.sourceforge.net/svnroot/xavs/trunk xavs3. 编译XAVS代码安装XAVS并将其头文件(.h)和静态库文件(.a)安装到系统目录,例如 /usr/local:cd xavs./configure --enable-sharedmakemake install4. 配置FFmpeg在配置FFmpeg时添加以下选项以启用GPL许可证和XAVS支持:cd ../ffmpeg./configure --enable-gpl --enable-libxavs确保FFmpeg项目也安装到与XAVS库相同的目录,本例中为 /usr/local:makemake install5. 检查XAVS支持运行以下命令来验证FFmpeg是否成功构建了AVS视频编码器:./ffmpeg -formats | less如果看到类似 cavsvideo raw Chinese AVS video 的输出,则表示AVS视频编码器已成功集成。二、如何使用FFmpeg转码视频到AVS2.1 转码命令示例下面的命令展示了如何使用FFmpeg将视频转码成AVS格式:./ffmpeg -vframes 300 -cqp 29 -refs 2 -bf 2 -g 16 -s widthxheight -i "INPUT_FILE_NAME" -vcodec libxavs "OUTPUT_FILE_NAME"其中:-vframes: 设置要编码的帧数。-cqp: 设置常量量化参数。-refs: 设置参考帧的数量。-bf: 设置启用的B帧数量。-g: 设置GOP(图像组)大小,即I帧之间的距离。-s: 设置分辨率,格式为宽度x高度。-i: 指定输入文件名。-vcodec libxavs: 指定使用XAVS编码器。2.2 常用参数一些常用的FFmpeg参数及其含义:-b 或 -vb: 设置比特率或视频比特率。-s: 分辨率,格式为宽度x高度。-r: 帧速率。-refs: 参考帧的数量。-bf: 启用的B帧数量。-g: GOP大小,即I帧之间的距离。-cqp: 常量量化参数,应为大于等于0的整数。-me_method: 运动估计方法。-qmin/-qmax: 最小/最大量化步长。-vframes: 要编码的帧数。2.3 示例H.264转AVS(不含B帧):./ffmpeg -i test0.avi -g 60 -b 600k -vcodec libxavs output1.cavsH.264转AVS(含B帧):./ffmpeg -i test0.avi -b 600k -bf 2 -vcodec libxavs output2.cavs以上步骤和命令可以用来在Linux环境下使用FFmpeg将视频文件转码为AVS格式。
-
一、前言随着大数据时代的到来,交通数据量急剧增加,由此带来的交通安全问题日益凸显。传统的驾驶人信用管理系统在数据存储和管理上存在着诸多不足之处,例如中心化存储方案无法有效地进行信用存证及数据溯源。区块链技术以其去中心化和不可篡改的特性,在数据存储和管理方面展现出了巨大的潜力。区块链的固有特性也带来了另一个挑战——一旦数据被写入区块链,几乎不可能对其进行修改,这在某些情况下是不利的。为了解决这一问题,当前文章重点研究了如何构建具有高随机性的哈希算法——高度随机哈希函数(HRHF)。HRHF算法通过结合纠错码与SM3算法的Merkle-Damgård迭代结构,不仅增强了哈希值的随机性,还保证了算法的安全性和执行效率。实验结果显示,与经典的SHA-256算法相比,HRHF算法在多个关键指标上均有显著提升。HRHF算法结合了纠错码与SM3算法的Merkle-Damgård迭代结构,通过这种方式增强了哈希值的随机性。选用了纠错能力更强的线性分组码与SM3算法相结合,并构造生成哈希值具有更强随机性的哈希函数。实验结果显示该算法不仅具有理想的雪崩效应特性,而且攻击者更难以逆推出原始消息,从而具备了更高的算法安全性。二、算法设计原理2.1 算法的创新点(1)通过调整循环左移位数来进一步提升哈希值的随机性;(2)通过优化迭代结构来提高算法的执行效率。算法的具体使用流程:初始化状态向量,为生成256位哈希值准备。计算纠错码的生成矩阵。对生成矩阵进行循环左移操作以增加随机性。对输入数据进行迭代压缩操作。输出最终的256位哈希值。2.2 实现代码(C++)#include <iostream>#include <vector>#include <cstdint>#include <cassert>// 哈希值长度constexpr size_t HASH_LENGTH = 32; // 256 bits// 线性分组码参数constexpr size_t CODE_LENGTH = 32; // 码长constexpr size_t CODE_DIMENSION = 6; // 维度// 生成矩阵初始化std::vector<std::vector<uint32_t>> generateMatrix() { // 实际应用中需要通过算法计算得到 std::vector<std::vector<uint32_t>> matrix(CODE_DIMENSION, std::vector<uint32_t>(CODE_LENGTH)); // 示例矩阵 for (size_t i = 0; i < CODE_DIMENSION; ++i) { matrix[i][i] = 1; } return matrix;}// 生成码字std::vector<uint32_t> generateCodeWord(const std::vector<std::vector<uint32_t>>& matrix) { std::vector<uint32_t> codeWord(CODE_LENGTH); // 假设这里使用生成矩阵生成码字 for (size_t i = 0; i < CODE_LENGTH; ++i) { codeWord[i] = 0; // 初始化码字 for (size_t j = 0; j < CODE_DIMENSION; ++j) { // 模2加法 codeWord[i] ^= matrix[j][i]; } } return codeWord;}// 循环左移uint32_t rotateLeft(uint32_t value, int shift) { return (value << shift) | (value >> (32 - shift));}// 消息预处理std::vector<uint32_t> preprocessMessage(const std::vector<uint8_t>& message) { // 添加消息长度,这里简化处理 std::vector<uint8_t> extendedMessage = message; extendedMessage.push_back(0x80); // 添加结束标志 extendedMessage.insert(extendedMessage.end(), 8, 0x00); // 添加长度占位符 // 将消息转换为32位整数数组 std::vector<uint32_t> words; for (size_t i = 0; i < extendedMessage.size(); i += 4) { uint32_t word = 0; for (size_t j = 0; j < 4 && i + j < extendedMessage.size(); ++j) { word |= static_cast<uint32_t>(extendedMessage[i + j]) << (24 - 8 * j); } words.push_back(word); } return words;}// 消息扩展void extendMessage(std::vector<uint32_t>& words) { const size_t WORD_COUNT = 64; // 扩展后的字数量 while (words.size() < WORD_COUNT) { uint32_t w = words.back(); uint32_t w16 = words[words.size() - 16]; words.push_back(w16 ^ rotateLeft(w16, 9) ^ rotateLeft(w16, 17) ^ rotateLeft(w, 15) ^ rotateLeft(w, 23)); }}// 布尔函数uint32_t ff(uint32_t x, uint32_t y, uint32_t z, size_t j) { if (j >= 16 && j <= 63) { return x ^ y ^ z; } else { return (x & y) | ((~x) & z); }}// 布尔函数uint32_t gg(uint32_t x, uint32_t y, uint32_t z, size_t j) { if (j >= 16 && j <= 63) { return x ^ y ^ z; } else { return (x & y) | (x & z) | (y & z); }}// 压缩函数void compress(std::vector<uint32_t>& state, const std::vector<uint32_t>& words) { const size_t ROUND_COUNT = 64; // 迭代次数 const size_t STATE_SIZE = 8; // 状态寄存器大小 const uint32_t T[ROUND_COUNT] = { /* 常量表 */ }; std::vector<uint32_t> ss1(STATE_SIZE), ss2(STATE_SIZE), tt1(STATE_SIZE), tt2(STATE_SIZE); for (size_t i = 0; i < ROUND_COUNT; ++i) { // 中间变量更新 ss1[i] = rotateLeft(state[0], 7) + rotateLeft(state[4], 12); ss2[i] = ss1[i] ^ rotateLeft(state[0], 12); tt1[i] = ff(state[0], state[1], state[2], i) + state[3] + ss2[i] + words[i]; tt2[i] = gg(state[4], state[5], state[6], i) + state[7] + ss1[i] + words[i + 64]; // 状态寄存器更新 state[0] = state[1]; state[1] = state[2]; state[2] = state[3]; state[3] = tt1[i]; state[4] = state[5]; state[5] = state[6]; state[6] = state[7]; state[7] = tt2[i]; }}// 主算法std::vector<uint8_t> hrhf(const std::vector<uint8_t>& message) { std::vector<uint32_t> state(HASH_LENGTH / 4, 0); // 初始化寄存器 std::vector<std::vector<uint32_t>> matrix = generateMatrix(); std::vector<uint32_t> codeWord = generateCodeWord(matrix); // 循环左移操作以增加随机性 for (size_t i = 0; i < CODE_DIMENSION; ++i) { codeWord[i] = rotateLeft(codeWord[i], 6); // 循环左移6位 } // 将码字分配给初始寄存器 for (size_t i = 0; i < HASH_LENGTH / 4; ++i) { state[i] = codeWord[i % CODE_DIMENSION]; } // 消息预处理 std::vector<uint32_t> words = preprocessMessage(message); // 打印预处理后的消息 std::cout << "Preprocessed Message: "; for (const auto& word : words) { std::cout << word << " "; } std::cout << std::endl; // 消息扩展 extendMessage(words); // 打印扩展后的消息 std::cout << "Extended Message: "; for (const auto& word : words) { std::cout << word << " "; } std::cout << std::endl; // 迭代压缩 compress(state, words); // 将32位整数转换为256位哈希值 std::vector<uint8_t> hash(HASH_LENGTH); for (size_t i = 0; i < HASH_LENGTH / 4; ++i) { uint32_t word = state[i]; for (size_t j = 0; j < 4; ++j) { hash[i * 4 + j] = word >> (24 - 8 * j); } } return hash;}int main() { std::vector<uint8_t> message = { 'q', 'y', 'x' }; std::vector<uint8_t> hash = hrhf(message); std::cout << "Hash Value: "; for (auto b : hash) { printf("%02x", b); } std::cout << std::endl; return 0;}运行结果:2.3 创新部分(1)循环左移位数的优化: 通过调整循环左移位数可以进一步提升哈希值的随机性。实验结果显示,在循环左移6位时,信息熵数值最高,这表明构造的初始常量值随机性最高,符合HRHF算法的设计目标。(2)迭代结构的优化: 通过优化迭代结构,提升了算法的执行效率。实验结果表明,在输入消息长度为401080字节的条件下,HRHF算法可以在1秒内完成4502000次运算,与SM3算法的运算效率基本一致,这表明HRHF算法可以支持快速运算。2.4 对比实验结果在相同的迭代结构下,HRHF算法的输出哈希值熵值相对于SM3算法有所增加,同时在哈希值长度都为256位的情况下,HRHF算法的轮函数复杂性更高,所产生的哈希值信息熵也高于SHA-256算法,这表明HRHF算法基于线性分组码在哈希值长度和迭代结构之间达到了有效的平衡,使得哈希值具有更高的随机性,同时也更好地隐藏了输入输出之间的关联性。 HRHF算法在运算效率和内存损耗方面也表现出了优势。2.5 Python代码实现import struct# 哈希值长度HASH_LENGTH = 32 # 256 bits# 线性分组码参数CODE_LENGTH = 32 # 码长CODE_DIMENSION = 6 # 维度# 生成矩阵初始化def generate_matrix(): matrix = [[0] * CODE_LENGTH for _ in range(CODE_DIMENSION)] # 示例矩阵 for i in range(CODE_DIMENSION): matrix[i][i] = 1 return matrix# 生成码字def generate_code_word(matrix): code_word = [0] * CODE_LENGTH # 使用生成矩阵生成码字 for i in range(CODE_LENGTH): for j in range(CODE_DIMENSION): # 模2加法 code_word[i] ^= matrix[j][i] return code_word# 循环左移def rotate_left(value, shift): return ((value << shift) & 0xFFFFFFFF) | (value >> (32 - shift))# 消息预处理def preprocess_message(message): extended_message = list(message) + [0x80] + [0x00] * 8 # 添加长度占位符 words = [] for i in range(0, len(extended_message), 4): word = 0 for j in range(min(4, len(extended_message) - i)): word |= extended_message[i + j] << (24 - 8 * j) words.append(word) return words# 消息扩展def extend_message(words): # 确保words至少有16个元素 while len(words) < 16: words.append(0) # 添加零填充 while len(words) < 128: w = words[-1] w16 = words[-16] words.append((w16 ^ rotate_left(w16, 9) ^ rotate_left(w16, 17)) ^ (rotate_left(w, 15) ^ rotate_left(w, 23)))# 布尔函数def ff(x, y, z, j): if 16 <= j <= 63: return x ^ y ^ z else: return (x & y) | ((~x) & z)# 布尔函数def gg(x, y, z, j): if 16 <= j <= 63: return x ^ y ^ z else: return (x & y) | (x & z) | (y & z)# 压缩函数def compress(state, words): for i in range(64): # 中间变量更新 ss1 = rotate_left(state[0], 7) + rotate_left(state[4], 12) ss1 &= 0xFFFFFFFF # Ensure 32-bit ss2 = ss1 ^ rotate_left(state[0], 12) tt1 = ff(state[0], state[1], state[2], i) + state[3] + ss2 + words[i] tt1 &= 0xFFFFFFFF # Ensure 32-bit tt2 = gg(state[4], state[5], state[6], i) + state[7] + ss1 + words[i + 64] tt2 &= 0xFFFFFFFF # Ensure 32-bit # 状态寄存器更新 state = [ state[1], state[2], state[3], tt1, state[5], state[6], state[7], tt2 ] return state# 主算法def hrhf(message): state = [0] * (HASH_LENGTH // 4) # 初始化寄存器 matrix = generate_matrix() code_word = generate_code_word(matrix) # 循环左移操作以增加随机性 for i in range(CODE_DIMENSION): code_word[i] = rotate_left(code_word[i], 6) # 循环左移6位 # 将码字分配给初始寄存器 for i in range(HASH_LENGTH // 4): state[i] = code_word[i % CODE_DIMENSION] # 消息预处理 words = preprocess_message(message) # 消息扩展 extend_message(words) # 迭代压缩 state = compress(state, words) # 将32位整数转换为256位哈希值 hash_value = bytearray(HASH_LENGTH) for i in range(HASH_LENGTH // 4): word = state[i] for j in range(4): hash_value[i * 4 + j] = (word >> (24 - 8 * j)) & 0xFF return hash_valueif __name__ == "__main__": message = b'qyx222' hash_value = hrhf(message) print(hash_value.hex())
-
一、前言Base64编码是一种广泛使用的编码方案,将任意二进制数据转换为可打印的ASCII字符字符串。这种编码方式之所以重要,是因为许多通信协议和存储介质对数据的可传输性和可存储性有特定的要求,它们可能无法直接处理或有效传输二进制数据。Base64编码通过使用64个字符的标准字符集——包括大写字母A-Z、小写字母a-z、数字0-9以及符号“+”和“/”,来表示二进制数据中的每一个6位组。为了标识编码的结束,Base64还使用了=作为填充字符。在实际应用中,Base64编码常见于电子邮件附件、在URLs中嵌入二进制数据、在网页中内联图像和字体文件、以及在配置文件和数据库中存储非文本数据等多种场景。例如,在HTML或CSS文件中,可以使用Base64编码的图像数据直接作为背景图像,而无需额外的HTTP请求,这在某些情况下可以提高页面加载速度,尽管这样做可能会增加文件大小,因为Base64编码通常会使原始数据膨胀约33%左右。在C语言中,Base64编码的实现主要涉及几个关键步骤:首先,输入的二进制数据被分成6位的区块;然后,每个6位区块被映射到Base64字符集中相应的字符;接下来,如果最后一个区块不足6位,使用0进行填充,并添加等于号作为填充字符以保持输出的长度一致。编码过程可以分解为以下步骤:将输入的二进制数据读入内存缓冲区。遍历缓冲区,每次取出24位数据(即3个字节),这足以生成4个Base64字符。将这24位分为4个6位组。使用6位组索引Base64字符集,找到对应的字符并输出。如果到达缓冲区末尾时剩余不足24位,使用0填充剩余位数,并输出相应的Base64字符,同时在输出字符串末尾添加等于号作为填充。在C语言中实现Base64编码时,可以定义一个包含64个字符的数组,存储Base64字符集,通过循环和位操作来处理数据。由于C语言提供了对内存和位操作的直接访问,因此在性能敏感的应用中,使用C语言实现的Base64编码可以非常高效。二、代码实操2.1 将二进制数据转为Base64编码下面是C语言程序示例,将给定的一串二进制数据转换成Base64编码并打印出来。程序使用了标准库函数,并且没有依赖任何外部库。程序中包含了创建Base64编码所需的所有步骤,如初始化字符集、读取输入数据、编码数据并打印结果。#include <stdio.h>#include <stdlib.h>#define BASE64_CHARS "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"#define CHUNK_SIZE 3#define BASE64_CHUNK_SIZE 4void base64_encode(unsigned char const *bytes_to_encode, unsigned int in_len, char *out_text){ unsigned char const *in = bytes_to_encode; unsigned char *out = (unsigned char*)out_text; unsigned int i; unsigned int j; unsigned int val; for (i = 0, j = 0; i < in_len - 2; i += CHUNK_SIZE, j += BASE64_CHUNK_SIZE) { val = ((in[i] & 0xFC) >> 2); out[j] = BASE64_CHARS[val]; val = ((in[i] & 0x03) << 4) | ((in[i + 1] & 0xF0) >> 4); out[j + 1] = BASE64_CHARS[val]; val = ((in[i + 1] & 0x0F) << 2) | ((in[i + 2] & 0xC0) >> 6); out[j + 2] = BASE64_CHARS[val]; val = (in[i + 2] & 0x3F); out[j + 3] = BASE64_CHARS[val]; } // Handle the last chunk gracefully. switch (in_len % CHUNK_SIZE) { case 1: out[j] = BASE64_CHARS[((in[i] & 0xFC) >> 2)]; out[j + 1] = BASE64_CHARS[((in[i] & 0x03) << 4)]; out[j + 2] = '='; out[j + 3] = '='; break; case 2: val = ((in[i] & 0xFC) >> 2); out[j] = BASE64_CHARS[val]; val = ((in[i] & 0x03) << 4) | ((in[i + 1] & 0xF0) >> 4); out[j + 1] = BASE64_CHARS[val]; out[j + 2] = BASE64_CHARS[((in[i + 1] & 0x0F) << 2)]; out[j + 3] = '='; break; }}int main(){ unsigned char data[] = {0x50, 0x4b, 0x03, 0x04, 0x14, 0x00, 0x00, 0x00, 0x08, 0x00, 0x27, 0x0b, 0xcf, 0xa3, 0x57, 0x67}; unsigned int data_len = sizeof(data); char encoded_data[100]; // Assuming enough space for the encoded string. base64_encode(data, data_len, encoded_data); encoded_data[data_len * 4 / 3] = '\0'; // Null terminate the string. printf("Original data: "); for (int i = 0; i < data_len; i++) { printf("%02x ", data[i]); } printf("\n"); printf("Encoded data: %s\n", encoded_data); return 0;}在例子中,data 数组包含了要被编码的数据。base64_encode 函数接受这些数据,并将其转换为Base64编码。编码后的字符串被存储在 encoded_data 数组中。注意,encoded_data 数组的大小应该足够容纳编码后的字符串,因为Base64编码后的字符串长度通常是原始数据长度的4/3倍。这个程序将打印出原始数据和编码后的Base64字符串。可以根据需要修改 data 数组的内容,以便测试不同的输入。2.2 实现图片的base64编码和解码下面是一个完整的C语言程序,实现了将图片文件编码为Base64字符串,并且可以将Base64字符串解码为图片并保存到本地磁盘。这个示例程序使用标准C库,不依赖于任何第三方库。#include <stdio.h>#include <stdlib.h>#include <string.h>// 函数:将二进制数据编码为Base64字符串char* base64_encode(const unsigned char* src, size_t len) { static const char base64_table[] = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; char* out, * pos; const unsigned char* end, * in; size_t olen; int line_len; olen = len * 4 / 3 + 4; // 输出长度 olen += olen / 72; // 换行符 olen++; // 结尾的NULL字符 out = (char*)malloc(olen); if (out == NULL) return NULL; end = src + len; in = src; pos = out; line_len = 0; while (end - in >= 3) { *pos++ = base64_table[in[0] >> 2]; *pos++ = base64_table[((in[0] & 0x03) << 4) | (in[1] >> 4)]; *pos++ = base64_table[((in[1] & 0x0f) << 2) | (in[2] >> 6)]; *pos++ = base64_table[in[2] & 0x3f]; in += 3; if (line_len += 4, line_len == 72) { *pos++ = '\n'; line_len = 0; } } if (end - in) { *pos++ = base64_table[in[0] >> 2]; if (end - in == 1) { *pos++ = base64_table[(in[0] & 0x03) << 4]; *pos++ = '='; } else { *pos++ = base64_table[((in[0] & 0x03) << 4) | (in[1] >> 4)]; *pos++ = base64_table[(in[1] & 0x0f) << 2]; } *pos++ = '='; } *pos = '\0'; return out;}// 函数:将Base64字符串解码为二进制数据unsigned char* base64_decode(const char* src, size_t* out_len) { static const unsigned char base64_table[] = { 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // +10 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x3e, 0xff, 0xff, // +20 0xff, 0x3f, 0x34, 0x35, 0x36, 0x37, 0x38, 0x39, 0x3a, 0x3b, // +30 0x3c, 0x3d, 0xff, 0xff, 0xff, 0x00, 0xff, 0xff, 0xff, 0x3f, // +40 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0x00, 0x01, 0x02, 0x03, // +50 0x04, 0x05, 0x06, 0x07, 0x08, 0x09, 0x0a, 0x0b, 0x0c, 0x0d, // +60 0x0e, 0x0f, 0x10, 0x11, 0x12, 0x13, 0x14, 0x15, 0x16, 0x17, // +70 0x18, 0x19, 0x3f, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, 0xff, // +80 0xff, 0xff, 0xff, 0x1a, 0x1b, 0x1c, 0x1d, 0x1e, 0x1f, 0x20, // +90 0x21, 0x22, 0x23, 0x24, 0x25, 0x26, 0x27, 0x28, 0x29, 0x2a, // +100 0x2b, 0x2c, 0x2d, 0x2e, 0x2f, 0x30, 0x31, 0x32, 0x33 // +110 }; unsigned char dtable[256], * out, * pos, block[4], tmp; size_t i, count, olen; int pad = 0; memset(dtable, 0x80, 256); for (i = 0; i < sizeof(base64_table); i++) dtable[base64_table[i]] = (unsigned char)i; dtable['='] = 0; count = 0; for (i = 0; i < strlen(src); i++) { if (dtable[src[i]] != 0x80) count++; } if (count == 0 || count % 4) return NULL; olen = count / 4 * 3; pos = out = (unsigned char*)malloc(olen); if (out == NULL) return NULL; for (i = 0; i < strlen(src); i++) { tmp = dtable[src[i]]; if (tmp == 0x80) continue; if (src[i] == '=') pad++; block[count++] = tmp; if (count == 4) { *pos++ = (block[0] << 2) | (block[1] >> 4); *pos++ = (block[1] << 4) | (block[2] >> 2); *pos++ = (block[2] << 6) | block[3]; count = 0; if (pad) { if (pad == 1) pos--; else if (pad == 2) pos -= 2; else { free(out); return NULL; } break; } } } *out_len = pos - out; return out;}int main() { FILE* fp; char* base64_data; unsigned char* decoded_data; size_t decoded_len, base64_len; char* filename = "test.png"; // 替换为你的图片文件名 char* output_filename = "decoded_image.png"; // 解码后保存的文件名 // 读取图片文件 fp = fopen(filename, "rb"); if (!fp) { fprintf(stderr, "无法打开文件 %s\n", filename); return 1; } fseek(fp, 0, SEEK_END); base64_len = ftell(fp); fseek(fp, 0, SEEK_SET); unsigned char* image_data = (unsigned char*)malloc(base64_len); fread(image_data, 1, base64_len, fp); fclose(fp); // 将图片数据编码为Base64字符串 base64_data = base64_encode(image_data, base64_len); free(image_data); if (!base64_data) { fprintf(stderr, "Base64 编码失败\n"); return 1; } // 输出Base64编码后的数据 printf("Base64 编码结果:\n%s\n", base64_data); // 解码Base64字符串为图片数据 decoded_data = base64_decode(base64_data, &decoded_len); free(base64_data); if (!decoded_data) { fprintf(stderr, "Base64 解码失败\n"); return 1; } // 将解码后的图片数据保存为文件 fp = fopen(output_filename, "wb"); if (!fp) { fprintf(stderr, "无法打开文件 %s 进行写入\n", output_filename); free(decoded_data); return 1; } fwrite(decoded_data, 1, decoded_len, fp); fclose(fp); free(decoded_data); printf("图片已成功解码并保存到 %s\n", output_filename); return 0;}
-
一、前言在数字化时代,信息安全与数据完整性成为了不可忽视的关键议题。在众多保障数据完整性的方法中,散列函数扮演着至关重要的角色。SHA-256(Secure Hash Algorithm 256)作为一种先进的散列算法,以其高度的安全性和广泛的应用性脱颖而出,尤其在文件校验、密码存储、数字签名等领域展现出了卓越的表现力。SHA-256算法是SHA-2家族的一员,由美国国家安全局(NSA)设计,经由美国国家标准与技术研究院(NIST)发布。不同于MD5算法,SHA-256不仅提供了更长的散列值长度——256位(32字节),增强了抗碰撞能力,还引入了一系列复杂的数学运算,包括模运算、位移、异或、加法等,以确保即使输入的微小变化也能引起散列值的显著差异,这种特性被称为雪崩效应。SHA-256的输出通常以64位十六进制字符串表示,每一个字符代表4位,总共需要32个字符来完整表示256位的散列值。在实现文件的SHA-256校验时,C语言结合Win32 API提供了强大的工具集。通过调用CryptoAPI(Cryptographic Application Programming Interface)中的相关函数,可以高效地计算出文件的SHA-256值。CryptoAPI是Windows操作系统内置的加密库,它封装了包括SHA-256在内的多种加密和散列算法,简化了复杂加密操作的实现难度。在C语言环境中,开发者可以通过一系列步骤,如获取加密服务提供者(CSP)句柄、创建散列对象、分块读取文件数据并进行散列计算、获取散列结果等,来完成SHA-256值的计算。这一过程不仅能够确保文件的完整性,还能有效抵御数据篡改和非法访问,尤其是在网络传输和存储环节,SHA-256的校验机制能够为数据的来源和状态提供强有力的验证。相较于MD5算法,SHA-256在安全性上有着显著的优势。MD5算法由于存在碰撞攻击的风险,即不同的输入可能导致相同的散列值,已被证实不够安全,尤其是对于高安全需求的场景。而SHA-256通过其更长的散列值和更复杂的运算逻辑,极大地降低了碰撞的概率,提升了算法的鲁棒性和安全性。因此,在涉及敏感信息处理、身份验证、电子交易等高风险领域,SHA-256成为了首选的散列算法。SHA-256算法凭借其强大的数据完整性保护能力和广泛的适用性,在现代信息安全体系中占据了举足轻重的地位。借助C语言与Win32 API的结合,开发者能够轻松地集成SHA-256校验功能,为数据的传输、存储和处理提供了一层坚实的保护罩。无论是对于个人用户还是企业机构,掌握并应用SHA-256算法,都是维护数据安全、防范潜在威胁的重要手段。二、代码实操2.1 计算数组的SHA-256值(win32-API)要在Windows上使用C语言并且不依赖于任何第三方库(如OpenSSL)来计算SHA-256哈希,可以使用Windows Crypto API (Cryptographic Application Programming Interface, CAPI)。下面是使用Windows Crypto API来计算一个字符串的SHA-256哈希值的示例代码:#include <windows.h>#include <wincrypt.h>#include <stdio.h>void PrintHex(const BYTE *data, DWORD len) { for (DWORD i = 0; i < len; i++) { printf("%02X", data[i]); }}int main() { HCRYPTPROV hProv = NULL; HCRYPTHASH hHash = NULL; // 定义要计算SHA-256的数组 BYTE testData[] = "Hello, this is a test message for SHA-256."; DWORD testDataLen = sizeof(testData) - 1; // 减1是因为数组末尾有'\0' // 初始化Crypto API if (!CryptAcquireContext(&hProv, NULL, NULL, PROV_RSA_FULL, CRYPT_VERIFYCONTEXT)) { printf("Error: %d\n", GetLastError()); return 1; } // 创建一个哈希对象 if (!CryptCreateHash(hProv, CALG_SHA_256, 0, 0, &hHash)) { printf("Error: %d\n", GetLastError()); CryptReleaseContext(hProv, 0); return 1; } // 添加数据到哈希对象 if (!CryptHashData(hHash, testData, testDataLen, 0)) { printf("Error: %d\n", GetLastError()); CryptDestroyHash(hHash); CryptReleaseContext(hProv, 0); return 1; } // 获取哈希摘要的大小 DWORD dwHashSize = 0; if (!CryptGetHashParam(hHash, HP_HASHVAL, NULL, &dwHashSize, 0)) { printf("Error: %d\n", GetLastError()); CryptDestroyHash(hHash); CryptReleaseContext(hProv, 0); return 1; } // 分配内存以保存哈希摘要 BYTE *pbHash = (BYTE *)malloc(dwHashSize); if (pbHash == NULL) { printf("Error allocating memory.\n"); CryptDestroyHash(hHash); CryptReleaseContext(hProv, 0); return 1; } // 获取哈希摘要 if (!CryptGetHashParam(hHash, HP_HASHVAL, pbHash, &dwHashSize, 0)) { printf("Error: %d\n", GetLastError()); free(pbHash); CryptDestroyHash(hHash); CryptReleaseContext(hProv, 0); return 1; } // 输出哈希摘要 printf("SHA-256 Digest: "); PrintHex(pbHash, dwHashSize); printf("\n"); // 清理 free(pbHash); CryptDestroyHash(hHash); CryptReleaseContext(hProv, 0); return 0;}这个程序初始化Crypto API环境,然后创建一个SHA-256哈希对象。接着,将要计算哈希的数据传递给CryptHashData函数,之后通过CryptGetHashParam获取哈希摘要。最后,使用PrintHex函数打印出十六进制格式的摘要。2.2 计算数组的SHA-256值(OpenSSL库)在C语言中使用SHA-256算法计算一段数组数据的散列值,可以选择使用OpenSSL库,下面是一个使用OpenSSL库的C语言代码示例,用于计算一个字节数组的SHA-256散列值,并将其以十六进制格式打印出来。确保开发环境中已经安装了OpenSSL库。在Visual Studio中,需要在项目属性的“链接器”->“输入”->“附加依赖项”中添加libeay32.lib和ssleay32.lib。如果是使用GCC编译器,编译时需要添加-lcrypto选项。接下来是C语言代码示例:#include <stdio.h>#include <string.h>#include <openssl/sha.h>// 函数声明,用于打印字节数据为十六进制void print_hex(const unsigned char *data, size_t len);int main() { // 定义一个测试数组 unsigned char testData[] = "Hello, this is a test message for SHA-256."; // SHA-256摘要长度 unsigned int digest_len = SHA256_DIGEST_LENGTH; // 创建一个字节数组存储摘要 unsigned char digest[digest_len]; // 计算SHA-256摘要 SHA256(testData, strlen((const char *)testData), digest); // 打印摘要的十六进制形式 printf("SHA-256 Digest: "); print_hex(digest, digest_len); printf("\n"); return 0;}// 函数实现,用于打印字节数据为十六进制void print_hex(const unsigned char *data, size_t len) { for (size_t i = 0; i < len; i++) { printf("%02x", data[i]); }}在这个代码中,定义了一个测试数组testData,使用SHA256函数计算其SHA-256摘要。SHA256_DIGEST_LENGTH是一个宏,定义了SHA-256摘要的长度,即32字节。print_hex函数用于将摘要数据转换为十六进制字符串并打印出来。2.3 计算文件的SHA-256值(win32-API)为了使用Win32 API计算一个文件的SHA-256哈希值,需要对文件进行分块读取并逐块添加到哈希上下文中。下面是一个使用CryptHashData函数和文件I/O函数来计算文件SHA-256哈希值的C语言示例代码:#include <windows.h>#include <wincrypt.h>#include <stdio.h>#include <stdlib.h>#define BUFFER_SIZE 4096void PrintHex(const BYTE *data, DWORD len) { for (DWORD i = 0; i < len; ++i) { printf("%02X", data[i]); }}int main(int argc, char **argv) { if (argc != 2) { printf("Usage: %s <filename>\n", argv[0]); return 1; } HCRYPTPROV hCryptProv = NULL; HCRYPTHASH hCryptHash = NULL; HANDLE hFile = CreateFile(argv[1], GENERIC_READ, FILE_SHARE_READ, NULL, OPEN_EXISTING, FILE_ATTRIBUTE_NORMAL, NULL); if (hFile == INVALID_HANDLE_VALUE) { printf("Failed to open file: %d\n", GetLastError()); return 1; } if (!CryptAcquireContext(&hCryptProv, NULL, NULL, PROV_RSA_FULL, CRYPT_VERIFYCONTEXT)) { printf("Error acquiring cryptographic context: %d\n", GetLastError()); CloseHandle(hFile); return 1; } if (!CryptCreateHash(hCryptProv, CALG_SHA_256, 0, 0, &hCryptHash)) { printf("Error creating hash object: %d\n", GetLastError()); CryptReleaseContext(hCryptProv, 0); CloseHandle(hFile); return 1; } BYTE buffer[BUFFER_SIZE]; DWORD bytesRead = 0; while (ReadFile(hFile, buffer, BUFFER_SIZE, &bytesRead, NULL)) { if (bytesRead == 0) break; if (!CryptHashData(hCryptHash, buffer, bytesRead, 0)) { printf("Error hashing data: %d\n", GetLastError()); CryptDestroyHash(hCryptHash); CryptReleaseContext(hCryptProv, 0); CloseHandle(hFile); return 1; } } if (GetLastError() != ERROR_SUCCESS) { printf("Error reading file: %d\n", GetLastError()); CryptDestroyHash(hCryptHash); CryptReleaseContext(hCryptProv, 0); CloseHandle(hFile); return 1; } DWORD hashSize = 0; if (!CryptGetHashParam(hCryptHash, HP_HASHVAL, NULL, &hashSize, 0)) { printf("Error getting hash size: %d\n", GetLastError()); CryptDestroyHash(hCryptHash); CryptReleaseContext(hCryptProv, 0); CloseHandle(hFile); return 1; } BYTE *hash = (BYTE *)malloc(hashSize); if (hash == NULL) { printf("Memory allocation failed.\n"); CryptDestroyHash(hCryptHash); CryptReleaseContext(hCryptProv, 0); CloseHandle(hFile); return 1; } if (!CryptGetHashParam(hCryptHash, HP_HASHVAL, hash, &hashSize, 0)) { printf("Error getting hash value: %d\n", GetLastError()); free(hash); CryptDestroyHash(hCryptHash); CryptReleaseContext(hCryptProv, 0); CloseHandle(hFile); return 1; } printf("SHA-256 Digest: "); PrintHex(hash, hashSize); printf("\n"); free(hash); CryptDestroyHash(hCryptHash); CryptReleaseContext(hCryptProv, 0); CloseHandle(hFile); return 0;}这段代码首先检查命令行参数,然后打开指定的文件,并使用CryptAcquireContext和CryptCreateHash初始化加密上下文和哈希对象。接着,从文件中读取数据,每次读取BUFFER_SIZE字节,直到文件结束。对于每个读取的数据块,都调用CryptHashData来更新哈希。最后,它调用CryptGetHashParam来获取最终的哈希值并打印出来。请注意,BUFFER_SIZE定义了每次读取文件的字节数,可以根据需要调整这个值。如果文件非常大,较大的缓冲区可以提高性能,但也会消耗更多内存。反之亦然,较小的缓冲区会降低内存使用量,但可能需要更多的磁盘I/O操作。
-
一、前言在当今这个数字化和智能化快速发展的时代,技术的边界正在不断被拓宽,创新的脚步从未停歇。从嵌入式系统的底层驱动到上层应用软件的开发,从物联网(IoT)的智慧医疗解决方案到计算机视觉的应用实践,技术领域的每一个角落都充满了无限可能。本文综合介绍一系列精选的技术主题,涵盖Qt图形界面编程、位操作与数据结构优化、函数指针的高级运用、CRC校验算法的实现、STM32微控制器的外设驱动、串行与网络通信协议的编程技巧、多线程并发处理机制、以及利用OpenCV进行图像处理的实用案例。将深入浅出地探讨如何在Qt中构建高效且用户友好的交互界面,解析C语言中位运算符的巧妙应用,演示双向链表与队列的灵活设计,以及函数指针在程序设计中的重要角色。揭示CRC校验算法的数学原理及其在C语言中的具体实现,分享基于STM32的舵机控制方法,剖析Windows平台下的串口编程与单片机通信细节,以及网络编程与ESP8266 WiFi模块的交互过程。讨论多线程环境下资源竞争的解决策略,介绍如何利用Win32 API进行线程管理,并通过一个实际案例展示如何计算文件的MD5值以确保数据的完整性。二、技术文章汇总【1】Qt窗口交互场景、子窗口数据获取cid:link_10在图形用户界面(GUI)开发中,窗口间的数据交互是常见且重要的功能。Qt作为一款强大的跨平台C++图形用户界面应用程序开发框架,提供了丰富的工具和机制来实现窗口间的交互。本文将深入探讨Qt中主窗口与子窗口之间的数据传递方式,特别是如何通过信号与槽机制实现子窗口数据的获取和更新,从而提升应用程序的灵活性和用户体验。【2】C语言实例_位操作运算符详解cid:link_0位操作是C语言编程中一项基础且强大的功能,它允许程序员直接对整数在内存中的二进制表示进行操作。掌握位操作不仅能够优化程序的性能,还能解决一些特定的编程问题。本文将详细解析C语言中的位操作运算符,包括左移、右移、按位与、按位或、按位异或等,并通过实例展示这些运算符在实际编程中的应用,帮助读者深入理解位操作的精髓。【3】 C语言实例_双向链表设计(队列)cid:link_1链表是数据结构中的基本组成部分,而双向链表作为链表的一种扩展形式,不仅包含了每个节点的数据,还包含了指向前一个节点和后一个节点的指针。这种结构使得双向链表在插入、删除和遍历等操作上更加灵活。本文将详细介绍如何使用C语言设计并实现一个双向链表,并基于这个双向链表实现一个简单的队列,展示双向链表在实际编程中的应用价值。【4】C语言实例_函数指针与指针函数cid:link_11函数指针和指针函数是C语言中两个容易混淆但又极其重要的概念。函数指针是指向函数的指针,而指针函数则是返回指针的函数。理解这两个概念对于深入理解C语言的指针机制和函数机制至关重要。本文将通过实例详细讲解函数指针和指针函数的定义、声明、使用以及它们之间的区别,帮助读者在编程中更加灵活地运用这些高级特性。【5】CRC校验算法详解、C语言实现cid:link_2CRC(循环冗余校验)是一种常用的数据校验算法,用于检测数据传输或存储过程中可能出现的错误。通过特定的多项式对数据进行模2除法运算,CRC能够生成一个校验码,该校验码与原始数据一同传输或存储。接收方再次使用相同的多项式对接收到的数据进行CRC运算,如果结果与原校验码一致,则认为数据无误。本文将详细介绍CRC校验算法的原理,并提供C语言实现的示例代码,帮助读者掌握这一重要的数据校验技术。【6】【华为云IOT】基于香橙派 AIpro 设计的医院人脸红外测温系统(从0开始开发)cid:link_3随着物联网技术的快速发展,智能设备在医疗领域的应用越来越广泛。本文将以华为云IOT平台为基础,介绍如何利用香橙派AIpro开发板设计并实现一个医院人脸红外测温系统。该系统集成了人脸识别和红外测温技术,能够自动识别进入医院的人员并进行体温检测,为疫情防控提供有力支持。本文将从零开始,逐步介绍系统的硬件选型、软件开发、云平台接入等关键环节,帮助读者了解智能医疗设备的开发流程和技术要点。【7】STM32驱动SG90舵机完成控制cid:link_4舵机作为一种常用的伺服电机,在机器人、无人机等领域有着广泛的应用。STM32作为一款高性能、低功耗的微控制器,能够轻松实现对舵机的控制。本文将详细介绍如何使用STM32微控制器驱动SG90舵机,包括硬件连接、PWM信号生成、舵机控制算法等关键内容。通过实例演示,读者将能够掌握STM32控制舵机的基本原理和方法,为后续的机器人或无人机项目打下坚实的基础。【8】Windows下串口编程与单片机串口设备通信(win32-API)cid:link_5串口通信是计算机与外部设备之间常用的一种通信方式,尤其在工业自动化、嵌入式系统等领域具有广泛的应用。Windows操作系统提供了丰富的API函数来支持串口编程。本文将详细介绍如何在Windows环境下使用win32-API进行串口编程,包括串口的打开、配置、读写操作以及关闭等关键步骤。同时,还将通过实例演示如何与单片机等串口设备进行通信,帮助读者掌握串口编程的基本技能。【9】Windows下网络编与ESP8266-WiFi通信(win32-API)cid:link_12随着物联网技术的普及,越来越多的设备需要通过网络进行互联互通。ESP8266作为一款低成本的WiFi模块,广泛应用于各种物联网项目中。本文将介绍如何在Windows环境下使用win32-API进行网络编程,并实现与ESP8266-WiFi模块的通信。内容将涵盖网络编程的基础知识、TCP/IP协议栈的应用、Socket编程的基本流程以及ESP8266的WiFi配置和通信协议等关键内容。通过实例演示,读者将能够掌握Windows下网络编程的基本技能,并实现与ESP8266等WiFi模块的通信。【10】Windows下线程的创建与使用(win32-API)cid:link_6在现代软件开发中,多线程编程是提高程序执行效率和响应速度的重要手段。Windows操作系统通过win32-API提供了一套丰富的线程管理函数,使得开发者能够在Windows平台上轻松创建、管理和销毁线程。本文将详细介绍如何在Windows环境下使用win32-API进行线程的创建和使用,包括线程的创建函数、线程同步机制(如互斥锁、事件等)、线程之间的通信方式以及线程的终止和清理等关键内容。通过实例演示,读者将能够掌握Windows下多线程编程的基本技巧,为开发高效、稳定的应用程序奠定基础。【11】Windows下线程的竞争与资源保护(win32-API)cid:link_7多线程编程虽然能够提升程序的性能,但也带来了线程间资源竞争和数据不一致的问题。为了解决这些问题,Windows提供了多种同步机制来保护共享资源,确保线程间的正确交互。本文将深入探讨Windows下线程间的竞争现象及其危害,并介绍如何使用win32-API提供的同步机制(如临界区、互斥锁、信号量等)来保护共享资源,避免数据竞争和死锁等问题。通过实例分析和代码演示,读者将能够深入理解线程同步的原理和方法,提升多线程程序的稳定性和可靠性。【12】华为云开发者云主机体验cid:link_8华为云作为领先的云计算服务提供商,为开发者提供了高性能、可靠、安全的云主机服务。本文将带领读者体验华为云开发者云主机的使用过程,包括云主机的创建、配置、登录、部署应用等关键步骤。通过实际操作,读者将能够感受到华为云云主机的便捷性和高效性,了解云计算在软件开发和部署中的重要作用。同时,本文还将介绍华为云提供的丰富开发工具和资源,帮助开发者更好地利用云平台进行项目开发和管理。【13】计算文件MD5值,判断唯一性(win32-API)cid:link_13在文件管理和数据传输中,经常需要验证文件的完整性和唯一性。MD5作为一种广泛使用的哈希算法,可以通过对文件内容进行哈希计算生成一个唯一的MD5值,用于判断文件是否发生变化或是否与其他文件重复。本文将介绍如何在Windows环境下使用win32-API计算文件的MD5值,并通过比较MD5值来判断文件的唯一性。通过实例演示,读者将能够掌握计算文件MD5值的基本方法,并将其应用于实际的项目开发中。【14】 利用OpenCV根据图片识别环境的亮度cid:link_9OpenCV是一款强大的计算机视觉库,提供了丰富的图像处理和分析功能。在环境亮度检测中,可以通过分析图片中的像素值来判断环境的亮度水平。本文将介绍如何利用OpenCV库对图片进行处理,提取图片的亮度信息,并根据亮度值判断环境的亮度等级。通过实例演示和代码分析,读者将能够掌握利用OpenCV进行环境亮度检测的基本方法,为后续的图像处理和计算机视觉项目提供有力支持。
-
一、前言在当代数字化转型的浪潮中,计算机视觉技术无疑占据了举足轻重的地位,其应用范围之广,影响力之深,已成为推动社会进步的关键力量之一。而OpenCV,作为计算机视觉领域的佼佼者,凭借其卓越的性能与广泛的兼容性,为开发者提供了强大的工具集,助力其实现复杂视觉任务的高效处理。环境亮度识别,实质上是一项要求计算机系统能够精准分析并量化图像中光照强度的技术。要求计算机能够理解并量化人类视觉系统对光线变化的感知,进而做出相应的判断和反应。无论是智能照明系统中的自动调光,还是自动驾驶汽车在复杂光照条件下的安全导航,亦或是安防监控中对异常光源的即时警报,这一技术都扮演着至关重要的角色。它不仅提高了自动化系统的效率和可靠性,还极大地丰富了人机交互的方式,让机器能够更加“善解人意”。例如,在智能家居系统中,自动调节室内光线以适应不同的活动需求或节能目的;在智能交通系统中,动态调整道路照明以应对天气变化和车流量波动;在增强现实领域,实时调整虚拟内容的显示效果以匹配真实世界的光照条件,这些都是环境亮度识别技术需要用到的地方。。OpenCV,作为一款开源的计算机视觉库,因其强大的功能和广泛的适用性,成为了实现环境亮度识别的理想工具。OpenCV不仅提供了丰富的图像处理函数,还支持多种图像分析算法,这使得开发者能够轻松地从图像中提取亮度信息,并将其转化为可操作的数据。通过加载一张图片,利用OpenCV的图像处理能力,可以计算出图片中亮度的分布情况,进而得到一个反映环境亮度水平的百分比值。本文章介绍如何利用OpenCV加载一张图片,运用OpenCV库内置的图像处理技术,识别并计算图片中的亮度百分比。二、OpenCV开发环境安装【1】OpenCV库下载(官网)OpenCV是开源的计算机视觉、机器学习软件库,其图片处理的功能非常强大,并且速度很快。 作为目标检测功能,OpenCV里本身就自带了很多的模型,比如: 人眼检测、鼻子检测、嘴巴检测、人脸检测、人体检测、猫脸检测等等,下载完OpenCV,就能直接进行图像识别测试体验,并且OpenCV也可以直接调用YOLO的模型,精确识别各种物体,yolo v3 里自带的模型文件可以精确识别常见的很多物体: 比如: 狗、汽车、自行车、人体、书本、手机等等。OpenCV下载地址:cid:link_1目前最新的版本是4.3,那么就下载最新的版本。下载下来是一个exe文件,双击就可以安装,实际就是解压,可以选择解压的路径,解压出来的文件包含源文件、库文件一大堆,比较大,可以直接放在一个固定的目录,后面程序里直接填路径来调用即可。 这个下载下来的库文件里只包含了X64的库,适用于MSVS 64位编译器。解压完成。解压后在build目录下看到有VC14和VC15的目录。这表示什么含义呢?OpenCV VC14和VC15的区别在于它们所使用的编译器版本不同。VC14使用的是Visual Studio 2015的编译器,而VC15使用的是Visual Studio 2017的编译器。这意味着VC15可以利用更先进的编译器技术,从而提高代码的性能和效率。此外,VC15还支持更多的C++11和C++14特性,使得开发更加方便和灵活。解释说明:VC11,Visual Studio 2012编译器VC14,Visual Studio 2015编译器VC15,Visual Studio 2017编译器- VC11构建需要安装Visual Studio 2012 x86或x64的Visual C ++ Redistributable- VC14构建需要安装Visual Studio 2015 x86或x64的Visual C ++ Redistributable- VC15构建需要安装Visual Studio 2017 x64或x86的Visual C ++ Redistributable在bin目录下的运行库需要拷贝到生成的应用程序目录下。【2】MinGw编译器如果想要使用MinGw编译器编译,可以从这里 cid:link_0 下载对应的OpenCV库进行使用。 GitHub的地址在CodeChina有镜像,可以从这里去下载,速度比较快:gitcode.net/mirrors/hui… 打开链接后,自己去选择适合自己编译器的版本,我的MinGW是730刚好就使用下面这个版本。下面分别介绍VS2017 64位编译器和MinGW 32位编译器如何引用OpenCV的库。(1)MSVC 64位编译器--QT的xx.pro工程文件里的写法INCLUDEPATH += C:/opencv/build/include\INCLUDEPATH += C:/opencv/build/include/opencv\INCLUDEPATH += C:/opencv/build/include/opencv2LIBS += -LC:/opencv/build/x64/vc14/lib\ -lopencv_world347dLIBS += -LC:/opencv/build/x64/vc14/lib\ -lopencv_world347(2)MinGW 32位编译器--QT的xx.pro工程文件里的写法INCLUDEPATH+=C:/OpenCV-MinGW-Build-OpenCV-3.4.7/include \ C:/OpenCV-MinGW-Build-OpenCV-3.4.7/include/opencv \ C:/OpenCV-MinGW-Build-OpenCV-3.4.7/include/opencv2LIBS+=C:/OpenCV-MinGW-Build-OpenCV-3.4.7/x86/mingw/bin/libopencv_*.dll工程编程成功之后,需要将OpenCV对应的dll文件拷贝到exe同级目录,否则运行时找不到dll会导致程序异常结束。 这些dll文件就是在OpenCV的bin目录下。OpenCV自带的模型文件在 C:\opencv\sources\data\haarcascades_cuda 这个目录下。这个就是人脸检测模型文件:三、实现代码3.1 识别亮度(C++)开发环境:在Windows下安装一个VS即可。我当前采用的版本是VS2020。#include <opencv2/opencv.hpp>#include <iostream>using namespace cv;using namespace std;// 计算亮度百分比的函数double calculateBrightnessPercentage(Mat image) { // 将图像转换为灰度图 Mat grayImage; cvtColor(image, grayImage, COLOR_BGR2GRAY); // 计算总像素数 int totalPixels = grayImage.rows * grayImage.cols; // 计算亮度较高的像素数(假设亮度阈值为200) int brightPixels = 0; for (int i = 0; i < grayImage.rows; ++i) { for (int j = 0; j < grayImage.cols; ++j) { if (grayImage.at<uchar>(i, j) > 200) { // 可根据需要调整阈值 brightPixels++; } } } // 计算亮度百分比 double percentage = (static_cast<double>(brightPixels) / totalPixels) * 100.0; return percentage;}int main() { // 加载图像 Mat image = imread("path_to_your_image.jpg"); if (image.empty()) { cout << "无法打开或找到图像" << endl; return -1; } // 计算亮度百分比 double brightnessPercentage = calculateBrightnessPercentage(image); // 输出亮度百分比 cout << "亮度百分比: " << brightnessPercentage << "%" << endl; return 0;}(1)头文件和命名空间:包括必要的OpenCV头文件 (opencv2/opencv.hpp),使用 cv 和 std 命名空间以便于调用。(2)calculateBrightnessPercentage 函数:使用 cvtColor 将输入图像转换为灰度图。计算图像中总像素数。统计像素灰度值高于设定阈值(本例中为200)的像素数。计算亮度百分比,即高亮像素数占总像素数的百分比。(3)main 函数:使用 imread 从指定路径加载图像。检查图像是否成功加载。调用 calculateBrightnessPercentage 函数计算图像的亮度百分比。输出计算得到的亮度百分比。3.2 识别亮度(Python)以下是使用Python和OpenCV计算图像亮度百分比的代码示例:import cv2import numpy as np# 计算亮度百分比的函数def calculate_brightness_percentage(image): # 转换为灰度图像 gray_image = cv2.cvtColor(image, cv2.COLOR_BGR2GRAY) # 计算总像素数 total_pixels = gray_image.shape[0] * gray_image.shape[1] # 计算亮度较高的像素数(假设亮度阈值为200) bright_pixels = np.sum(gray_image > 200) # 可根据需要调整阈值 # 计算亮度百分比 percentage = (bright_pixels / total_pixels) * 100.0 return percentagedef main(): # 加载图像 image = cv2.imread('path_to_your_image.jpg') if image is None: print("无法打开或找到图像") return # 计算亮度百分比 brightness_percentage = calculate_brightness_percentage(image) # 输出亮度百分比 print(f"亮度百分比: {brightness_percentage}%")if __name__ == "__main__": main()(1)函数 calculate_brightness_percentage:使用 cv2.cvtColor 将输入图像转换为灰度图。计算图像中总像素数。使用 NumPy 条件判断 gray_image > 200 来统计亮度较高的像素数(可以根据需要调整阈值 200)。计算亮度百分比,即高亮像素数占总像素数的百分比。(2)main 函数:使用 cv2.imread 从指定路径加载图像。检查图像是否成功加载。调用 calculate_brightness_percentage 函数计算图像的亮度百分比。输出计算得到的亮度百分比。3.3 颜色分类识别(C++)开发环境:在Windows下安装一个VS即可。我当前采用的版本是VS2020。下面是使用OpenCV(C++)加载一张图片,并识别出黑、白、红、橙、黄、绿、青、蓝、紫等颜色的占比的完整代码示例:#include <opencv2/opencv.hpp>#include <iostream>using namespace cv;using namespace std;// 定义颜色的阈值范围(HSV颜色空间)const int MIN_H_RED = 0;const int MAX_H_RED = 10;const int MIN_H_ORANGE = 11;const int MAX_H_ORANGE = 25;const int MIN_H_YELLOW = 26;const int MAX_H_YELLOW = 34;const int MIN_H_GREEN = 35;const int MAX_H_GREEN = 85;const int MIN_H_CYAN = 86;const int MAX_H_CYAN = 105;const int MIN_H_BLUE = 106;const int MAX_H_BLUE = 130;const int MIN_H_PURPLE = 131;const int MAX_H_PURPLE = 160;// 计算颜色比例的函数void calculate_color_percentages(Mat image) { Mat hsvImage; cvtColor(image, hsvImage, COLOR_BGR2HSV); // 初始化颜色像素统计变量 int totalPixels = hsvImage.rows * hsvImage.cols; int blackPixels = 0, whitePixels = 0; int redPixels = 0, orangePixels = 0, yellowPixels = 0; int greenPixels = 0, cyanPixels = 0, bluePixels = 0, purplePixels = 0; // 遍历图像像素 for (int i = 0; i < hsvImage.rows; ++i) { for (int j = 0; j < hsvImage.cols; ++j) { Vec3b pixel = hsvImage.at<Vec3b>(i, j); int hue = pixel[0]; // 色调 // 根据色调范围判断颜色 if (hue >= MIN_H_RED && hue <= MAX_H_RED) { redPixels++; } else if (hue >= MIN_H_ORANGE && hue <= MAX_H_ORANGE) { orangePixels++; } else if (hue >= MIN_H_YELLOW && hue <= MAX_H_YELLOW) { yellowPixels++; } else if (hue >= MIN_H_GREEN && hue <= MAX_H_GREEN) { greenPixels++; } else if (hue >= MIN_H_CYAN && hue <= MAX_H_CYAN) { cyanPixels++; } else if (hue >= MIN_H_BLUE && hue <= MAX_H_BLUE) { bluePixels++; } else if ((hue >= 0 && hue < MIN_H_RED) || (hue > MAX_H_PURPLE && hue <= 179)) { purplePixels++; } } } // 计算颜色百分比 double percentageBlack = (static_cast<double>(blackPixels) / totalPixels) * 100.0; double percentageWhite = (static_cast<double>(whitePixels) / totalPixels) * 100.0; double percentageRed = (static_cast<double>(redPixels) / totalPixels) * 100.0; double percentageOrange = (static_cast<double>(orangePixels) / totalPixels) * 100.0; double percentageYellow = (static_cast<double>(yellowPixels) / totalPixels) * 100.0; double percentageGreen = (static_cast<double>(greenPixels) / totalPixels) * 100.0; double percentageCyan = (static_cast<double>(cyanPixels) / totalPixels) * 100.0; double percentageBlue = (static_cast<double>(bluePixels) / totalPixels) * 100.0; double percentagePurple = (static_cast<double>(purplePixels) / totalPixels) * 100.0; // 输出颜色百分比 cout << "黑色百分比: " << percentageBlack << "%" << endl; cout << "白色百分比: " << percentageWhite << "%" << endl; cout << "红色百分比: " << percentageRed << "%" << endl; cout << "橙色百分比: " << percentageOrange << "%" << endl; cout << "黄色百分比: " << percentageYellow << "%" << endl; cout << "绿色百分比: " << percentageGreen << "%" << endl; cout << "青色百分比: " << percentageCyan << "%" << endl; cout << "蓝色百分比: " << percentageBlue << "%" << endl; cout << "紫色百分比: " << percentagePurple << "%" << endl;}int main() { // 加载图像 Mat image = imread("path_to_your_image.jpg"); if (image.empty()) { cout << "无法打开或找到图像" << endl; return -1; } // 计算颜色比例 calculate_color_percentages(image); return 0;}3.4 颜色分类识别(python)以下是相应的Python版本代码,用于加载图像并计算黑、白、红、橙、黄、绿、青、蓝、紫颜色的占比:import cv2import numpy as np# 定义颜色的阈值范围(HSV颜色空间)COLOR_THRESHOLDS = { 'black': ([0, 0, 0], [180, 255, 30]), # 黑色 'white': ([0, 0, 231], [180, 18, 255]), # 白色 'red': ([0, 43, 46], [10, 255, 255]), # 红色 'orange': ([11, 43, 46], [25, 255, 255]), # 橙色 'yellow': ([26, 43, 46], [34, 255, 255]), # 黄色 'green': ([35, 43, 46], [85, 255, 255]), # 绿色 'cyan': ([86, 43, 46], [105, 255, 255]), # 青色 'blue': ([106, 43, 46], [130, 255, 255]), # 蓝色 'purple': ([131, 43, 46], [160, 255, 255]) # 紫色}# 计算颜色比例的函数def calculate_color_percentages(image): hsv_image = cv2.cvtColor(image, cv2.COLOR_BGR2HSV) # 初始化颜色像素统计变量 total_pixels = hsv_image.shape[0] * hsv_image.shape[1] color_percentages = {} # 遍历颜色阈值范围 for color_name, (lower, upper) in COLOR_THRESHOLDS.items(): lower_np = np.array(lower, dtype=np.uint8) upper_np = np.array(upper, dtype=np.uint8) # 根据颜色阈值创建掩码 mask = cv2.inRange(hsv_image, lower_np, upper_np) # 计算掩码中白色像素的数量 num_pixels = cv2.countNonZero(mask) # 计算百分比 percentage = (num_pixels / total_pixels) * 100.0 color_percentages[color_name] = percentage return color_percentagesif __name__ == "__main__": # 加载图像 image_path = 'path_to_your_image.jpg' image = cv2.imread(image_path) if image is None: print(f"无法打开或找到图像:{image_path}") else: # 计算颜色比例 percentages = calculate_color_percentages(image) # 输出颜色百分比 for color, percentage in percentages.items(): print(f"{color}百分比: {percentage:.2f}%")(1)颜色定义和阈值范围:使用HSV颜色空间来识别颜色,定义了各种颜色的HSV阈值范围。每个颜色都有一个对应的最小和最大HSV值。(2)calculate_color_percentages 函数:使用 cv2.cvtColor 将图像从BGR色彩空间转换为HSV色彩空间。初始化颜色百分比的字典。针对每种颜色的HSV阈值范围,使用 cv2.inRange 创建颜色掩码。使用 cv2.countNonZero 计算掩码中非零像素的数量,即符合颜色条件的像素数量。计算每种颜色在图像中的百分比。(3)main 函数:加载指定路径的图像。检查图像是否成功加载。调用 calculate_color_percentages 函数计算并输出图像中各种颜色的百分比。
推荐直播
-
全面解析华为云EI-API服务:理论基础与实践应用指南
2024/11/29 周五 18:20-20:20
Alex 华为云学堂技术讲师
本期直播给大家带来的是理论与实践结合的华为云EI-API的服务介绍。从“主要功能,应用场景,实践案例,调用流程”四个维度来深入解析“语音交互API,文字识别API,自然语言处理API,图像识别API及图像搜索API”五大场景下API服务,同时结合实验,来加深开发者对API服务理解。
回顾中 -
企业员工、应届毕业生、在读研究生共探项目实践
2024/12/02 周一 19:00-21:00
姚圣伟 在职软件工程师 昇腾社区优秀开发者 华为云云享专家 HCDG天津地区发起人
大神带你一键了解和掌握LeakyReLU自定义算子在ONNX网络中应用和优化技巧,在线分享如何入门,以及在工作中如何结合实际项目进行学习
即将直播 -
昇腾云服务ModelArts深度解析:理论基础与实践应用指南
2024/12/03 周二 14:30-16:30
Alex 华为云学堂技术讲师
如何快速创建和部署模型,管理全周期AI工作流呢?本期直播聚焦华为昇腾云服务ModelArts一站式AI开发平台功能介绍,同时结合基于ModelArts 的实践性实验,帮助开发者从理论到实验更好地理解和使用ModelArts。
去报名
热门标签