Python-基于PyQt5,Pillow,pathilb,imageio,moviepy,sys的GIF(动图)制作工具

前言:在抖音,快手等社交平台上,我们常常见到各种各样的GIF动画。在各大评论区里面,GIF图片以其短小精悍、生动有趣的特点,被广泛用于分享各种有趣的场景、搞笑的瞬间、精彩的动作等,能够快速吸引我们的注意力,增强内容的传播性和互动性。生活中,我们将各种有趣的人物表情、动作、台词等制作成GIF表情包,既可以更生动地表达我们此时的情感和态度,也让聊天的过程拥有了更多的趣味性和幽默感。当然,GIF动画不止在娱乐领域里应用广泛,在计算机的网页设计中很多时候也会使用GIF动画可以为页面增添动态效果,使页面更加生动活泼,吸引用户的注意力。例如,可以在网页的标题、导航栏、按钮等元素中添加GIF动画,提升页面的视觉效果和用户体验等。总而言之,GIF动画在我们的日常生活中扮演着重要的角色,我们有必要了解GIF动画的制作方法及相关制作工具。话不多说,我们今天就来学习一下如何利用Python来制作一款GIF动画工具。

编程思路:本次编程我们将会调用到Python中的众多库:包括诸如PyQt5,pillow,moviepy等的第三方库和sys,pathlib等的标准库。PyQt5被用于创建一个图形用户界面 (GUI) 应用程序(具体为代码中的GifMakerGUI类)。我们将创建窗口和布局(这里包括GUI窗口的大小,位置等),创建GUI中的相关组件(如按钮,标签,菜单等),处理事件和信号(主要负责将用户触发的事件与GUI控件联系起来),应用程序的启动和运行等。Pillow是Python中很重要的一个图片处理库,利用它我们可以对图片进行图像操作(包括图片的加载,操作,保存等),图像转换(包括图像颜色表示模式的转换(如RGB转HSV),以及图像尺寸大小的设置),图像序列处理(保存图像序列为GIF或其他格式),图像合成等操作。与pillow不同,moviepy是一个视频文件处理库(具体来说,它可以进行视频剪辑(打开,截取视频文件,也能进行音频处理(合成音频剪辑,视频音频合并,音频文件保存等))。imageio库比较简单,它主要被用于处理图像序列(简单来说就是将一系列图像保存为动画文件,如本次的GIF)。

第一步:导入库

标准库:sys,pathlib。

第三方库:PyQt5,pillow,imageio,moviepy。

#导入库
import sys
from pathlib import Path
from PIL import Image, ImageDraw, ImageFont
from moviepy import VideoFileClip, CompositeAudioClip
import imageio
from PyQt5.QtWidgets import (QApplication, QWidget, QVBoxLayout, QHBoxLayout,QLabel, QLineEdit, QPushButton, QFileDialog,QProgressBar, QMessageBox)
from PyQt5.QtCore import QThread, pyqtSignal

第二步:创建功能类

两个:

GifCreator类,在后台线程中创建GIF或MP4文件(主要取决于用户是否选择音频文件(不选则生成gif动图,选则生成MP4文件))。

GifMakerGUI类,用于创建一个图形用户界面(GUI)应用程序,允许我们选择源文件(视频或图片)、添加音频文件、设置输出路径和参数,并启动GIF或MP4文件(同上)的生成过程。

#创建gif生成类
class GifCreator(QThread):progress_updated = pyqtSignal(int)finished = pyqtSignal(str)error_occurred = pyqtSignal(str)#初始化函数def __init__(self, config):super().__init__()self.config = configself.font = Noneif config['text']:try:self.font = ImageFont.truetype("arial.ttf", 24)except:self.font = ImageFont.load_default()def run(self):try:if self.config['audio_path'] and not self.config['output'].endswith('.mp4'):self.error_occurred.emit("音频仅支持MP4输出格式")returnif self.config['source_type'] == 'video':self._create_from_video()else:self._create_from_images()if self.config['audio_path']:self._add_audio()self.finished.emit(self.config['output'])except Exception as e:self.error_occurred.emit(str(e))def _process_frame(self, frame, index):img = Image.fromarray(frame).resize(self.config['size'])if self.config['text']:draw = ImageDraw.Draw(img)draw.text((10, 10), self.config['text'], font=self.font, fill=(255, 0, 0))self.progress_updated.emit(int((index + 1) / self.total_frames * 100))return imgdef _create_from_video(self):with VideoFileClip(str(self.config['sources'][0])) as clip:if self.config['duration']:clip = clip.subclip(0, self.config['duration'])self.total_frames = int(clip.duration * self.config['fps'])frames = []for i, frame in enumerate(clip.iter_frames(fps=self.config['fps'])):frames.append(self._process_frame(frame, i))frames[0].save(self.config['output'],save_all=True,append_images=frames[1:],optimize=True,duration=1000 // self.config['fps'],loop=0)def _create_from_images(self):images = []self.total_frames = len(self.config['sources'])for i, img_path in enumerate(self.config['sources']):with Image.open(img_path) as img:img = img.convert('RGBA').resize(self.config['size'])if self.config['text']:draw = ImageDraw.Draw(img)draw.text((10, 10), self.config['text'], font=self.font, fill=(255, 0, 0))images.append(img)self.progress_updated.emit(int((i + 1) / self.total_frames * 100))imageio.mimsave(self.config['output'],[img.convert('P', palette=Image.ADAPTIVE) for img in images],fps=self.config['fps'],palettesize=256)def _add_audio(self):video_clip = VideoFileClip(self.config['output'])audio_clip = CompositeAudioClip([VideoFileClip(self.config['audio_path']).audio])final_clip = video_clip.set_audio(audio_clip)final_clip.write_videofile(self.config['output'], codec='libx264')#创建主程窗口类
class GifMakerGUI(QWidget):#初始化函数def __init__(self):super().__init__()self.initUI()self.worker = None#初始化用户界面def initUI(self):self.setWindowTitle('GIF制作工具(初级)')self.setGeometry(300, 300, 600, 400)layout = QVBoxLayout()# 源文件选择self.source_btn = QPushButton('选择源文件(视频/图片)')self.source_btn.clicked.connect(self.select_source)self.source_label = QLabel('未选择文件')layout.addWidget(self.source_btn)layout.addWidget(self.source_label)# 音频文件选择self.audio_btn = QPushButton('添加音频文件')self.audio_btn.clicked.connect(self.select_audio)self.audio_label = QLabel('未选择音频文件')layout.addWidget(self.audio_btn)layout.addWidget(self.audio_label)# 输出设置output_layout = QHBoxLayout()self.output_btn = QPushButton('选择输出路径')self.output_btn.clicked.connect(self.select_output)self.output_entry = QLineEdit()output_layout.addWidget(self.output_btn)output_layout.addWidget(self.output_entry)layout.addLayout(output_layout)# 参数设置params_layout = QHBoxLayout()self.fps_entry = QLineEdit('10')self.size_w_entry = QLineEdit('640')self.size_h_entry = QLineEdit('480')self.duration_entry = QLineEdit()self.text_entry = QLineEdit()params_layout.addWidget(QLabel('FPS:'))params_layout.addWidget(self.fps_entry)params_layout.addWidget(QLabel('宽:'))params_layout.addWidget(self.size_w_entry)params_layout.addWidget(QLabel('高:'))params_layout.addWidget(self.size_h_entry)layout.addLayout(params_layout)text_layout = QHBoxLayout()text_layout.addWidget(QLabel('文字水印:'))text_layout.addWidget(self.text_entry)layout.addLayout(text_layout)# 进度条self.progress = QProgressBar()layout.addWidget(self.progress)# 操作按钮self.start_btn = QPushButton('开始生成')self.start_btn.clicked.connect(self.start_process)layout.addWidget(self.start_btn)self.setLayout(layout)def select_source(self):files, _ = QFileDialog.getOpenFileNames(self, '选择需处理的文件', '','视频文件 (*.mp4 *.mov *.avi);;图片文件 (*.png *.jpg *.jpeg)')if files:self.source_label.setText(f'已选择 {len(files)} 个文件')self.source_files = [Path(f) for f in files]def select_audio(self):file, _ = QFileDialog.getOpenFileName(self, '选择音频文件', '', '音频文件 (*.mp3 *.wav)')if file:self.audio_label.setText(Path(file).name)self.audio_file = filedef select_output(self):file, _ = QFileDialog.getSaveFileName(self, '保存输出文件', '','GIF文件 (*.gif);;MP4文件 (*.mp4)')if file:self.output_entry.setText(file)def validate_input(self):required = [(self.source_files, '请选择源文件'),(self.output_entry.text(), '请设置输出路径'),(self.fps_entry.text().isdigit(), 'FPS必须为数字'),(self.size_w_entry.text().isdigit(), '宽度必须为数字'),(self.size_h_entry.text().isdigit(), '高度必须为数字')]for condition, message in required:if not condition:QMessageBox.warning(self, '输入错误', message)return Falsereturn Truedef start_process(self):if not self.validate_input():returnconfig = {'sources': self.source_files,'output': self.output_entry.text(),'fps': int(self.fps_entry.text()),'size': (int(self.size_w_entry.text()), int(self.size_h_entry.text())),'duration': None,  # 可添加持续时间设置'text': self.text_entry.text(),'audio_path': getattr(self, 'audio_file', None),'source_type': 'video' if self.source_files[0].suffix.lower() in ['.mp4', '.mov', '.avi'] else 'image'}self.worker = GifCreator(config)self.worker.progress_updated.connect(self.update_progress)self.worker.finished.connect(self.process_finished)self.worker.error_occurred.connect(self.show_error)self.start_btn.setEnabled(False)self.worker.start()def update_progress(self, value):self.progress.setValue(value)def process_finished(self, output_path):self.start_btn.setEnabled(True)QMessageBox.information(self, '完成', f'文件已生成:{output_path}')def show_error(self, message):self.start_btn.setEnabled(True)QMessageBox.critical(self, '错误', message)

第三步:创建驱动单元

我们需要创建一个单独的单元来驱动整个程序的正常运行,这就是驱动单元。

#驱动程序单元
if __name__ == '__main__':app = QApplication(sys.argv)ex = GifMakerGUI()ex.show()sys.exit(app.exec_())

第四步:完整代码展示

#导入库
import sys
from pathlib import Path
from PIL import Image, ImageDraw, ImageFont
from moviepy import VideoFileClip, CompositeAudioClip
import imageio
from PyQt5.QtWidgets import (QApplication, QWidget, QVBoxLayout, QHBoxLayout,QLabel, QLineEdit, QPushButton, QFileDialog,QProgressBar, QMessageBox)
from PyQt5.QtCore import QThread, pyqtSignal#创建gif生成类
class GifCreator(QThread):progress_updated = pyqtSignal(int)finished = pyqtSignal(str)error_occurred = pyqtSignal(str)#初始化函数def __init__(self, config):super().__init__()self.config = configself.font = Noneif config['text']:try:self.font = ImageFont.truetype("arial.ttf", 24)except:self.font = ImageFont.load_default()def run(self):try:if self.config['audio_path'] and not self.config['output'].endswith('.mp4'):self.error_occurred.emit("音频仅支持MP4输出格式")returnif self.config['source_type'] == 'video':self._create_from_video()else:self._create_from_images()if self.config['audio_path']:self._add_audio()self.finished.emit(self.config['output'])except Exception as e:self.error_occurred.emit(str(e))def _process_frame(self, frame, index):img = Image.fromarray(frame).resize(self.config['size'])if self.config['text']:draw = ImageDraw.Draw(img)draw.text((10, 10), self.config['text'], font=self.font, fill=(255, 0, 0))self.progress_updated.emit(int((index + 1) / self.total_frames * 100))return imgdef _create_from_video(self):with VideoFileClip(str(self.config['sources'][0])) as clip:if self.config['duration']:clip = clip.subclip(0, self.config['duration'])self.total_frames = int(clip.duration * self.config['fps'])frames = []for i, frame in enumerate(clip.iter_frames(fps=self.config['fps'])):frames.append(self._process_frame(frame, i))frames[0].save(self.config['output'],save_all=True,append_images=frames[1:],optimize=True,duration=1000 // self.config['fps'],loop=0)def _create_from_images(self):images = []self.total_frames = len(self.config['sources'])for i, img_path in enumerate(self.config['sources']):with Image.open(img_path) as img:img = img.convert('RGBA').resize(self.config['size'])if self.config['text']:draw = ImageDraw.Draw(img)draw.text((10, 10), self.config['text'], font=self.font, fill=(255, 0, 0))images.append(img)self.progress_updated.emit(int((i + 1) / self.total_frames * 100))imageio.mimsave(self.config['output'],[img.convert('P', palette=Image.ADAPTIVE) for img in images],fps=self.config['fps'],palettesize=256)def _add_audio(self):video_clip = VideoFileClip(self.config['output'])audio_clip = CompositeAudioClip([VideoFileClip(self.config['audio_path']).audio])final_clip = video_clip.set_audio(audio_clip)final_clip.write_videofile(self.config['output'], codec='libx264')#创建主程窗口类
class GifMakerGUI(QWidget):#初始化函数def __init__(self):super().__init__()self.initUI()self.worker = None#初始化用户界面def initUI(self):self.setWindowTitle('GIF制作工具(初级)')self.setGeometry(300, 300, 600, 400)layout = QVBoxLayout()# 源文件选择self.source_btn = QPushButton('选择源文件(视频/图片)')self.source_btn.clicked.connect(self.select_source)self.source_label = QLabel('未选择文件')layout.addWidget(self.source_btn)layout.addWidget(self.source_label)# 音频文件选择self.audio_btn = QPushButton('添加音频文件')self.audio_btn.clicked.connect(self.select_audio)self.audio_label = QLabel('未选择音频文件')layout.addWidget(self.audio_btn)layout.addWidget(self.audio_label)# 输出设置output_layout = QHBoxLayout()self.output_btn = QPushButton('选择输出路径')self.output_btn.clicked.connect(self.select_output)self.output_entry = QLineEdit()output_layout.addWidget(self.output_btn)output_layout.addWidget(self.output_entry)layout.addLayout(output_layout)# 参数设置params_layout = QHBoxLayout()self.fps_entry = QLineEdit('10')self.size_w_entry = QLineEdit('640')self.size_h_entry = QLineEdit('480')self.duration_entry = QLineEdit()self.text_entry = QLineEdit()params_layout.addWidget(QLabel('FPS:'))params_layout.addWidget(self.fps_entry)params_layout.addWidget(QLabel('宽:'))params_layout.addWidget(self.size_w_entry)params_layout.addWidget(QLabel('高:'))params_layout.addWidget(self.size_h_entry)layout.addLayout(params_layout)text_layout = QHBoxLayout()text_layout.addWidget(QLabel('文字水印:'))text_layout.addWidget(self.text_entry)layout.addLayout(text_layout)# 进度条self.progress = QProgressBar()layout.addWidget(self.progress)# 操作按钮self.start_btn = QPushButton('开始生成')self.start_btn.clicked.connect(self.start_process)layout.addWidget(self.start_btn)self.setLayout(layout)def select_source(self):files, _ = QFileDialog.getOpenFileNames(self, '选择需处理的文件', '','视频文件 (*.mp4 *.mov *.avi);;图片文件 (*.png *.jpg *.jpeg)')if files:self.source_label.setText(f'已选择 {len(files)} 个文件')self.source_files = [Path(f) for f in files]def select_audio(self):file, _ = QFileDialog.getOpenFileName(self, '选择音频文件', '', '音频文件 (*.mp3 *.wav)')if file:self.audio_label.setText(Path(file).name)self.audio_file = filedef select_output(self):file, _ = QFileDialog.getSaveFileName(self, '保存输出文件', '','GIF文件 (*.gif);;MP4文件 (*.mp4)')if file:self.output_entry.setText(file)def validate_input(self):required = [(self.source_files, '请选择源文件'),(self.output_entry.text(), '请设置输出路径'),(self.fps_entry.text().isdigit(), 'FPS必须为数字'),(self.size_w_entry.text().isdigit(), '宽度必须为数字'),(self.size_h_entry.text().isdigit(), '高度必须为数字')]for condition, message in required:if not condition:QMessageBox.warning(self, '输入错误', message)return Falsereturn Truedef start_process(self):if not self.validate_input():returnconfig = {'sources': self.source_files,'output': self.output_entry.text(),'fps': int(self.fps_entry.text()),'size': (int(self.size_w_entry.text()), int(self.size_h_entry.text())),'duration': None,  # 可添加持续时间设置'text': self.text_entry.text(),'audio_path': getattr(self, 'audio_file', None),'source_type': 'video' if self.source_files[0].suffix.lower() in ['.mp4', '.mov', '.avi'] else 'image'}self.worker = GifCreator(config)self.worker.progress_updated.connect(self.update_progress)self.worker.finished.connect(self.process_finished)self.worker.error_occurred.connect(self.show_error)self.start_btn.setEnabled(False)self.worker.start()def update_progress(self, value):self.progress.setValue(value)def process_finished(self, output_path):self.start_btn.setEnabled(True)QMessageBox.information(self, '完成', f'文件已生成:{output_path}')def show_error(self, message):self.start_btn.setEnabled(True)QMessageBox.critical(self, '错误', message)#驱动程序单元
if __name__ == '__main__':app = QApplication(sys.argv)ex = GifMakerGUI()ex.show()sys.exit(app.exec_())

第五步:运行效果展示

第六步:操作指南

运行程序,待程序初始化完成弹出窗口后,点击"选择源文件(视频/图片)",在系统中选择你想要处理的视频/图片("添加音频文件"这一步可以省略,因为本次我们学习的是GIF制作,不需要这一步)。接着我们点击"选择输出路径",选择最终生成的GIF存放的位置。接着自行设置FPS(帧数),GIF高,宽以及"文字水印"的内容等。最后点击"开始生成"按钮,窗口会出现进对条提示处理进度,当进度条满100%后,需再等待一段时间(此时程序正在将处理好的文件存放在指定位置)。当窗口弹出小窗口提示"文件已生成",点击小窗口中的"OK"按钮。返回你设置的"选择输出路径"的存放路径,就可以看到生成的GIF动画。

第七步:注意事项

- FPS值越大,文件处理时间越长,请谨慎设置;GIF的高,宽同理。(已设置默认值)

- 水印暂不支持中文字体,后面会改进。

- 水印颜色默认为红色,位置为视频/图片左上方。

- 文件的大小同样会影响程序的处理时间。

后面我会对以上问题进行优化/处理,并添加更多新奇功能,敬请期待!

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/13136.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

使用线性回归模型逼近目标模型 | PyTorch 深度学习实战

前一篇文章,计算图 Compute Graph 和自动求导 Autograd | PyTorch 深度学习实战 本系列文章 GitHub Repo: https://github.com/hailiang-wang/pytorch-get-started 使用线性回归模型逼近目标模型 什么是回归什么是线性回归使用 PyTorch 实现线性回归模型代码执行结…

【蓝桥杯嵌入式】2_LED

1、电路图 74HC573是八位锁存器,当控制端LE脚为高电平时,芯片“导通”,LE为低电平时芯片“截止”即将输出状态“锁存”,led此时不会改变状态,所以可通过led对应的八个引脚的电平来控制led的状态,原理图分析…

尝试在Office里调用免费大语言模型的阶段性进展

我个人觉得通过api而不是直接浏览器客户端聊天调用大语言模型是使用人工智能大模型的一个相对进阶的阶段。 于是就尝试了一下。我用的是老师木 袁进辉博士新创的硅基流动云上的免费的大模型。——虽然自己获赠了不少免费token,但测试阶段用不上。 具体步骤如下&am…

LabVIEW自定义测量参数怎么设置?

以下通过一个温度采集案例,说明在 LabVIEW 中设置自定义测量参数的具体方法: 案例背景 ​ 假设使用 NI USB-6009 数据采集卡 和 热电偶传感器 监测温度,需自定义以下参数: 采样率:1 kHz 输入量程:0~10 V&a…

理解 C 与 C++ 中的 const 常量与数组大小的关系

博客主页: [小ᶻ☡꙳ᵃⁱᵍᶜ꙳] 本文专栏: C语言 文章目录 💯前言💯数组大小的常量要求💯C 语言中的数组大小要求💯C 中的数组大小要求💯为什么 C 中 const 变量可以作为数组大小💯进一步的…

【Elasticsearch】文本分类聚合Categorize Text Aggregation

响应参数讲解: key (字符串)由 categorization_analyzer 提取的标记组成,这些标记是类别中所有输入字段值的共同部分。 doc_count (整数)与类别匹配的文档数量。 max_matching_length (整数)从…

基于SpringBoot的信息技术知识赛系统的设计与实现(源码+SQL脚本+LW+部署讲解等)

专注于大学生项目实战开发,讲解,毕业答疑辅导,欢迎高校老师/同行前辈交流合作✌。 技术范围:SpringBoot、Vue、SSM、HLMT、小程序、Jsp、PHP、Nodejs、Python、爬虫、数据可视化、安卓app、大数据、物联网、机器学习等设计与开发。 主要内容:…

Windows Docker笔记-安装docker

安装环境 操作系统:Windows 11 家庭中文版 docker版本:Docker Desktop version: 4.36.0 (175267) 注意: Docker Desktop 支持以下Windows操作系统: 支持的版本:Windows 10(家庭版、专业版、企业版、教育…

《Kettle保姆级教学-界面介绍》

目录 一、Kettle介绍二、界面介绍1.界面构成2、菜单栏详细介绍2.1 【文件F】2.2 【编辑】2.3 【视图】2.4 【执行】2.5 【工具】2.6 【帮助】 3、转换界面介绍4、作业界面介绍5、执行结果 一、Kettle介绍 Kettle 是一个开源的 ETL(Extract, Transform, Load&#x…

新型智慧城市建设方案-1

智慧城市建设的背景与需求 随着信息技术的飞速发展,新型智慧城市建设成为推动城市现代化、提升城市管理效率的重要途径。智慧城市通过整合信息资源,优化城市规划、建设和管理,旨在打造更高效、便捷、宜居的城市环境。 智慧城市建设的主要内容…

【Java计算机毕业设计】基于Springboot的物业信息管理系统【源代码+数据库+LW文档+开题报告+答辩稿+部署教程+代码讲解】

源代码数据库LW文档(1万字以上)开题报告答辩稿 部署教程代码讲解代码时间修改教程 一、开发工具、运行环境、开发技术 开发工具 1、操作系统:Window操作系统 2、开发工具:IntelliJ IDEA或者Eclipse 3、数据库存储&#xff1a…

ollama部署deepseek实操记录

1. 安装 ollama 1.1 下载并安装 官网 https://ollama.com/ Linux安装命令 https://ollama.com/download/linux curl -fsSL https://ollama.com/install.sh | sh安装成功截图 3. 开放外网访问 1、首先停止ollama服务:systemctl stop ollama 2、修改ollama的servic…

Agentic Automation:基于Agent的企业认知架构重构与数字化转型跃迁---我的AI经典战例

文章目录 Agent代理Agent组成 我在企业实战AI Agent企业痛点我构建的AI Agent App 项目开源 & 安装包下载 大家好,我是工程师令狐,今天想给大家讲解一下AI智能体,以及企业与AI智能体的结合,文章中我会列举自己在企业中Agent实…

图论常见算法

图论常见算法 算法prim算法Dijkstra算法 用途最小生成树(MST):最短路径:拓扑排序:关键路径: 算法用途适用条件时间复杂度Kruskal最小生成树无向图(稀疏图)O(E log E)Prim最小生成树无…

手机上运行AI大模型(Deepseek等)

最近deepseek的大火,让大家掀起新一波的本地部署运行大模型的热潮,特别是deepseek有蒸馏的小参数量版本,电脑上就相当方便了,直接ollamaopen-webui这种类似的组合就可以轻松地实现,只要硬件,如显存&#xf…

Java进阶学习之路

Java进阶之路 提示:这里可以添加系列文章的所有文章的目录,目录需要自己手动添加 提示:写完文章后,目录可以自动生成,如何生成可参考右边的帮助文档 目录 Java进阶之路前言一、Java入门 Java基础 1、Java概述 1.1 什…

SpringBoot使用 easy-captcha 实现验证码登录功能

文章目录 一、 环境准备1. 解决思路2. 接口文档3. redis下载 二、后端实现1. 引入依赖2. 添加配置3. 后端代码实现4. 前端代码实现 在前后端分离的项目中,登录功能是必不可少的。为了提高安全性,通常会加入验证码验证。 easy-captcha 是一个简单易用的验…

Android 常用命令和工具解析之Battery Historian

Batterystats是包含在 Android 框架中的一种工具,用于收集设备上的电池数据。您可以使用adb bugreport命令抓取日志,将收集的电池数据转储到开发机器,并生成可使用 Battery Historian 分析的报告。Battery Historian 会将报告从 Batterystats…

如何安装PHP依赖库 更新2025.2.3

要在PHP项目中安装依赖,首先需要确保你的系统已经安装了Composer。Composer是PHP的依赖管理工具,它允许你声明项目所需的库,并管理它们。以下是如何安装Composer和在PHP项目中安装依赖的步骤: 一. 安装Composer 对于Windows用户…

DeepSeek各版本说明与优缺点分析

DeepSeek各版本说明与优缺点分析 DeepSeek是最近人工智能领域备受瞩目的一个语言模型系列,其在不同版本的发布过程中,逐步加强了对多种任务的处理能力。本文将详细介绍DeepSeek的各版本,从版本的发布时间、特点、优势以及不足之处&#xff0…