当前位置: 首页 > news >正文

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, ImageFilter
from moviepy import VideoFileClip, CompositeAudioClip
import imageio
from PyQt5.QtWidgets import (QApplication, QWidget, QVBoxLayout, QHBoxLayout,QLabel, QLineEdit, QPushButton, QFileDialog,QProgressBar, QMessageBox, QCheckBox, QColorDialog,QComboBox, QSpinBox, QListWidget, QMenu, QAction)
from PyQt5.QtCore import QThread, pyqtSignal, QSettings, Qt
from PyQt5.QtGui import QPixmap, QMovie
import math
import random
from PIL.ImageFilter import EMBOSS, CONTOUR, FIND_EDGES, SHARPEN #为新增滤镜效果方法

第二步:创建功能类

两个:

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

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

与上次不同的是,本次添加了很多扩展的实用性功能:

1,给FPS(帧数)和GIF的"高","宽"值的设置新增了智能推荐功能(即在用户选定视频/图片文件后,程序会根据用户所给的文件大小和尺存自动填充FPS(帧数)和GIF的"高","宽"值),同时也保留了用户的个性化设置功能。

2,新增文件预览功能(即用户在选定视频/图片文件后,窗口会加载出文件的预览图像,方便用户查看选中的文件是否为需要处理的文件)。

3,新增水印相关参数设置(包括水印颜色,水印大小,水印动画,水印位置),用户可自行对任意参数进行个性化设置。

4,添加了图片滤镜功能,让用户体验不一样的GIF呈现效果。 

5,新增历史纪录功能,用户可以看到以往文件的存放位置纪录,点击纪录,用户可以查看该纪录下文件的相关参数。

#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", config['text_size'])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)text_position = self._get_animated_text_position(index)draw.text(text_position, self.config['text'], font=self.font, fill=self.config['text_color'])if self.config['filter']:img = self._apply_filter(img)self.progress_updated.emit(int((index + 1) / self.total_frames * 100))return imgdef _get_animated_text_position(self, index):x, y = self.config['text_position']base_x, base_y = self.config['text_position']max_width, max_height = self.config['size']if self.config['text_animation'] == '滚动':x = (x + index * 5) % max_widthelif self.config['text_animation'] == '渐入':alpha = min(255, index * 10)self.config['text_color'] = self.config['text_color'][:3] + (alpha,)elif self.config['text_animation'] == '跳动':y = base_y + int(10 * math.sin(index * 0.5))elif self.config['text_animation'] == '闪烁':alpha = 128 + int(127 * math.sin(index))self.config['text_color'] = self.config['text_color'][:3] + (alpha,)elif self.config['text_animation'] == '随机移动':if index % 10 == 0:self.rand_x = random.randint(0, max_width - 100)self.rand_y = random.randint(0, max_height - 30)x, y = self.rand_x, self.rand_yreturn (x, y)def _apply_filter(self, img):filter_map = {'黑白': lambda x: x.convert('L'),'复古': lambda x: x.convert('RGB').filter(ImageFilter.SMOOTH),'模糊': lambda x: x.filter(ImageFilter.BLUR),'边缘检测': lambda x: x.filter(FIND_EDGES),'锐化': lambda x: x.filter(SHARPEN),'浮雕': lambda x: x.filter(EMBOSS),'轮廓': lambda x: x.filter(CONTOUR),'油画': lambda x: x.filter(ImageFilter.SMOOTH_MORE).filter(ImageFilter.EDGE_ENHANCE)}return filter_map.get(self.config['filter'], lambda x: x)(img)#新79区def _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)text_position = self._get_animated_text_position(i)draw.text(text_position, self.config['text'], font=self.font, fill=self.config['text_color'])if self.config['filter']:img = self._apply_filter(img)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')#Gif窗口生成类
class GifMakerGUI(QWidget):def __init__(self):super().__init__()self.initUI()self.worker = Noneself.settings = QSettings("MyCompany", "GifMaker")self.history = []self.load_history()# 添加上下文菜单self.history_list.setContextMenuPolicy(Qt.CustomContextMenu)self.history_list.customContextMenuRequested.connect(self.show_history_menu)def show_history_menu(self, pos):menu = QMenu()delete_action = QAction("删除选中项", self)clear_action = QAction("清空历史记录", self)delete_action.triggered.connect(self.delete_selected_history)clear_action.triggered.connect(self.clear_history)menu.addAction(delete_action)menu.addAction(clear_action)menu.exec_(self.history_list.mapToGlobal(pos))def delete_selected_history(self):selected = self.history_list.currentRow()if selected >= 0:del self.history[selected]self.settings.setValue("history", self.history)self.update_history_list()def clear_history(self):self.history = []self.settings.setValue("history", self.history)self.update_history_list()def initUI(self):self.setWindowTitle('高级GIF制作工具')self.setGeometry(300, 300, 800, 600)layout = QVBoxLayout()# 源文件选择self.source_btn = QPushButton('选择源文件(视频/图片)')self.source_btn.clicked.connect(self.select_source)self.source_label = QLabel('未选择文件')self.preview_label = QLabel()layout.addWidget(self.source_btn)layout.addWidget(self.source_label)layout.addWidget(self.preview_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()self.size_h_entry = QLineEdit()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)# 水印颜色、大小、位置和动画设置watermark_layout = QHBoxLayout()self.color_btn = QPushButton('选择水印颜色')self.color_btn.clicked.connect(self.select_color)self.text_size_spin = QSpinBox()self.text_size_spin.setRange(10, 100)self.text_size_spin.setValue(24)self.position_combo = QComboBox()self.position_combo.addItems(["左上", "右上", "左下", "右下", "居中"])self.animation_combo = QComboBox()#self.animation_combo.addItems(["无", "滚动", "渐入"])self.animation_combo.addItems(["无", "滚动", "渐入", "跳动", "闪烁", "随机移动"])watermark_layout.addWidget(self.color_btn)watermark_layout.addWidget(QLabel('水印大小:'))watermark_layout.addWidget(self.text_size_spin)watermark_layout.addWidget(QLabel('水印位置:'))watermark_layout.addWidget(self.position_combo)watermark_layout.addWidget(QLabel('水印动画:'))watermark_layout.addWidget(self.animation_combo)layout.addLayout(watermark_layout)# 图片滤镜设置filter_layout = QHBoxLayout()self.filter_combo = QComboBox()#self.filter_combo.addItems(["无", "黑白", "复古", "模糊"])self.filter_combo.addItems(["无", "黑白", "复古", "模糊", "边缘检测","锐化", "浮雕", "轮廓", "油画"])filter_layout.addWidget(QLabel('图片滤镜:'))filter_layout.addWidget(self.filter_combo)layout.addLayout(filter_layout)# 历史记录管理self.history_list = QListWidget()self.history_list.itemDoubleClicked.connect(self.load_from_history)layout.addWidget(QLabel('历史记录:'))layout.addWidget(self.history_list)# 进度条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]self.show_preview(self.source_files[0])self.recommend_size(self.source_files[0])def show_preview(self, file_path):if file_path.suffix.lower() in ['.mp4', '.mov', '.avi']:with VideoFileClip(str(file_path)) as clip:frame = clip.get_frame(0)img = Image.fromarray(frame)img.thumbnail((200, 200))img.save("preview.png")self.preview_label.setPixmap(QPixmap("preview.png"))else:img = Image.open(file_path)img.thumbnail((200, 200))img.save("preview.png")self.preview_label.setPixmap(QPixmap("preview.png"))def recommend_size(self, file_path):if file_path.suffix.lower() in ['.mp4', '.mov', '.avi']:with VideoFileClip(str(file_path)) as clip:width, height = clip.sizeelse:with Image.open(file_path) as img:width, height = img.size# 推荐尺寸recommended_width = min(640, width)recommended_height = int((recommended_width / width) * height)self.size_w_entry.setText(str(recommended_width))self.size_h_entry.setText(str(recommended_height))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 select_color(self):color = QColorDialog.getColor()if color.isValid():self.text_color = color.getRgb()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','text_color': getattr(self, 'text_color', (255, 0, 0)),'text_position': self.get_text_position(),'text_size': self.text_size_spin.value(),'text_animation': self.animation_combo.currentText(),'filter': self.filter_combo.currentText()}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 get_text_position(self):position = self.position_combo.currentText()width, height = int(self.size_w_entry.text()), int(self.size_h_entry.text())position_map = {"左上": (10, 10),"右上": (width - 100, 10),"左下": (10, height - 30),"右下": (width - 100, height - 30),"居中": (width // 2, height // 2)}return position_map.get(position, (10, 10))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}')self.save_history()self.update_history_list()def show_error(self, message):self.start_btn.setEnabled(True)QMessageBox.critical(self, '错误', message)def save_history(self):history_item = {'source_files': [str(f) for f in self.source_files],'output_path': self.output_entry.text(),'fps': self.fps_entry.text(),'size': f"{self.size_w_entry.text()},{self.size_h_entry.text()}",'text': self.text_entry.text(),'text_color': getattr(self, 'text_color', (255, 0, 0)),'text_size': self.text_size_spin.value(),'text_position': self.position_combo.currentText(),'text_animation': self.animation_combo.currentText(),'filter': self.filter_combo.currentText()}self.history.append(history_item)self.settings.setValue("history", self.history)def load_history(self):self.history = self.settings.value("history", [])self.update_history_list()def update_history_list(self):self.history_list.clear()for item in self.history:self.history_list.addItem(item['output_path'])def load_from_history(self, item):index = self.history_list.row(item)history_item = self.history[index]self.source_files = [Path(f) for f in history_item['source_files']]self.source_label.setText(f'已选择 {len(self.source_files)} 个文件')self.output_entry.setText(history_item['output_path'])self.fps_entry.setText(history_item['fps'])size = history_item['size'].split(",")self.size_w_entry.setText(size[0])self.size_h_entry.setText(size[1])self.text_entry.setText(history_item['text'])self.text_color = history_item['text_color']self.text_size_spin.setValue(int(history_item['text_size']))self.position_combo.setCurrentText(history_item['text_position'])self.animation_combo.setCurrentText(history_item['text_animation'])self.filter_combo.setCurrentText(history_item['filter'])

第三步:创建驱动程序单元

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

#驱动程序单元
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, ImageFilter
from moviepy import VideoFileClip, CompositeAudioClip
import imageio
from PyQt5.QtWidgets import (QApplication, QWidget, QVBoxLayout, QHBoxLayout,QLabel, QLineEdit, QPushButton, QFileDialog,QProgressBar, QMessageBox, QCheckBox, QColorDialog,QComboBox, QSpinBox, QListWidget, QMenu, QAction)
from PyQt5.QtCore import QThread, pyqtSignal, QSettings, Qt
from PyQt5.QtGui import QPixmap, QMovie
import math
import random
from PIL.ImageFilter import EMBOSS, CONTOUR, FIND_EDGES, SHARPEN #为新增滤镜效果方法#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", config['text_size'])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)text_position = self._get_animated_text_position(index)draw.text(text_position, self.config['text'], font=self.font, fill=self.config['text_color'])if self.config['filter']:img = self._apply_filter(img)self.progress_updated.emit(int((index + 1) / self.total_frames * 100))return imgdef _get_animated_text_position(self, index):x, y = self.config['text_position']base_x, base_y = self.config['text_position']max_width, max_height = self.config['size']if self.config['text_animation'] == '滚动':x = (x + index * 5) % max_widthelif self.config['text_animation'] == '渐入':alpha = min(255, index * 10)self.config['text_color'] = self.config['text_color'][:3] + (alpha,)elif self.config['text_animation'] == '跳动':y = base_y + int(10 * math.sin(index * 0.5))elif self.config['text_animation'] == '闪烁':alpha = 128 + int(127 * math.sin(index))self.config['text_color'] = self.config['text_color'][:3] + (alpha,)elif self.config['text_animation'] == '随机移动':if index % 10 == 0:self.rand_x = random.randint(0, max_width - 100)self.rand_y = random.randint(0, max_height - 30)x, y = self.rand_x, self.rand_yreturn (x, y)def _apply_filter(self, img):filter_map = {'黑白': lambda x: x.convert('L'),'复古': lambda x: x.convert('RGB').filter(ImageFilter.SMOOTH),'模糊': lambda x: x.filter(ImageFilter.BLUR),'边缘检测': lambda x: x.filter(FIND_EDGES),'锐化': lambda x: x.filter(SHARPEN),'浮雕': lambda x: x.filter(EMBOSS),'轮廓': lambda x: x.filter(CONTOUR),'油画': lambda x: x.filter(ImageFilter.SMOOTH_MORE).filter(ImageFilter.EDGE_ENHANCE)}return filter_map.get(self.config['filter'], lambda x: x)(img)#新79区def _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)text_position = self._get_animated_text_position(i)draw.text(text_position, self.config['text'], font=self.font, fill=self.config['text_color'])if self.config['filter']:img = self._apply_filter(img)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')#Gif窗口生成类
class GifMakerGUI(QWidget):def __init__(self):super().__init__()self.initUI()self.worker = Noneself.settings = QSettings("MyCompany", "GifMaker")self.history = []self.load_history()# 添加上下文菜单self.history_list.setContextMenuPolicy(Qt.CustomContextMenu)self.history_list.customContextMenuRequested.connect(self.show_history_menu)def show_history_menu(self, pos):menu = QMenu()delete_action = QAction("删除选中项", self)clear_action = QAction("清空历史记录", self)delete_action.triggered.connect(self.delete_selected_history)clear_action.triggered.connect(self.clear_history)menu.addAction(delete_action)menu.addAction(clear_action)menu.exec_(self.history_list.mapToGlobal(pos))def delete_selected_history(self):selected = self.history_list.currentRow()if selected >= 0:del self.history[selected]self.settings.setValue("history", self.history)self.update_history_list()def clear_history(self):self.history = []self.settings.setValue("history", self.history)self.update_history_list()def initUI(self):self.setWindowTitle('高级GIF制作工具')self.setGeometry(300, 300, 800, 600)layout = QVBoxLayout()# 源文件选择self.source_btn = QPushButton('选择源文件(视频/图片)')self.source_btn.clicked.connect(self.select_source)self.source_label = QLabel('未选择文件')self.preview_label = QLabel()layout.addWidget(self.source_btn)layout.addWidget(self.source_label)layout.addWidget(self.preview_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()self.size_h_entry = QLineEdit()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)# 水印颜色、大小、位置和动画设置watermark_layout = QHBoxLayout()self.color_btn = QPushButton('选择水印颜色')self.color_btn.clicked.connect(self.select_color)self.text_size_spin = QSpinBox()self.text_size_spin.setRange(10, 100)self.text_size_spin.setValue(24)self.position_combo = QComboBox()self.position_combo.addItems(["左上", "右上", "左下", "右下", "居中"])self.animation_combo = QComboBox()#self.animation_combo.addItems(["无", "滚动", "渐入"])self.animation_combo.addItems(["无", "滚动", "渐入", "跳动", "闪烁", "随机移动"])watermark_layout.addWidget(self.color_btn)watermark_layout.addWidget(QLabel('水印大小:'))watermark_layout.addWidget(self.text_size_spin)watermark_layout.addWidget(QLabel('水印位置:'))watermark_layout.addWidget(self.position_combo)watermark_layout.addWidget(QLabel('水印动画:'))watermark_layout.addWidget(self.animation_combo)layout.addLayout(watermark_layout)# 图片滤镜设置filter_layout = QHBoxLayout()self.filter_combo = QComboBox()#self.filter_combo.addItems(["无", "黑白", "复古", "模糊"])self.filter_combo.addItems(["无", "黑白", "复古", "模糊", "边缘检测","锐化", "浮雕", "轮廓", "油画"])filter_layout.addWidget(QLabel('图片滤镜:'))filter_layout.addWidget(self.filter_combo)layout.addLayout(filter_layout)# 历史记录管理self.history_list = QListWidget()self.history_list.itemDoubleClicked.connect(self.load_from_history)layout.addWidget(QLabel('历史记录:'))layout.addWidget(self.history_list)# 进度条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]self.show_preview(self.source_files[0])self.recommend_size(self.source_files[0])def show_preview(self, file_path):if file_path.suffix.lower() in ['.mp4', '.mov', '.avi']:with VideoFileClip(str(file_path)) as clip:frame = clip.get_frame(0)img = Image.fromarray(frame)img.thumbnail((200, 200))img.save("preview.png")self.preview_label.setPixmap(QPixmap("preview.png"))else:img = Image.open(file_path)img.thumbnail((200, 200))img.save("preview.png")self.preview_label.setPixmap(QPixmap("preview.png"))def recommend_size(self, file_path):if file_path.suffix.lower() in ['.mp4', '.mov', '.avi']:with VideoFileClip(str(file_path)) as clip:width, height = clip.sizeelse:with Image.open(file_path) as img:width, height = img.size# 推荐尺寸recommended_width = min(640, width)recommended_height = int((recommended_width / width) * height)self.size_w_entry.setText(str(recommended_width))self.size_h_entry.setText(str(recommended_height))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 select_color(self):color = QColorDialog.getColor()if color.isValid():self.text_color = color.getRgb()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','text_color': getattr(self, 'text_color', (255, 0, 0)),'text_position': self.get_text_position(),'text_size': self.text_size_spin.value(),'text_animation': self.animation_combo.currentText(),'filter': self.filter_combo.currentText()}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 get_text_position(self):position = self.position_combo.currentText()width, height = int(self.size_w_entry.text()), int(self.size_h_entry.text())position_map = {"左上": (10, 10),"右上": (width - 100, 10),"左下": (10, height - 30),"右下": (width - 100, height - 30),"居中": (width // 2, height // 2)}return position_map.get(position, (10, 10))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}')self.save_history()self.update_history_list()def show_error(self, message):self.start_btn.setEnabled(True)QMessageBox.critical(self, '错误', message)def save_history(self):history_item = {'source_files': [str(f) for f in self.source_files],'output_path': self.output_entry.text(),'fps': self.fps_entry.text(),'size': f"{self.size_w_entry.text()},{self.size_h_entry.text()}",'text': self.text_entry.text(),'text_color': getattr(self, 'text_color', (255, 0, 0)),'text_size': self.text_size_spin.value(),'text_position': self.position_combo.currentText(),'text_animation': self.animation_combo.currentText(),'filter': self.filter_combo.currentText()}self.history.append(history_item)self.settings.setValue("history", self.history)def load_history(self):self.history = self.settings.value("history", [])self.update_history_list()def update_history_list(self):self.history_list.clear()for item in self.history:self.history_list.addItem(item['output_path'])def load_from_history(self, item):index = self.history_list.row(item)history_item = self.history[index]self.source_files = [Path(f) for f in history_item['source_files']]self.source_label.setText(f'已选择 {len(self.source_files)} 个文件')self.output_entry.setText(history_item['output_path'])self.fps_entry.setText(history_item['fps'])size = history_item['size'].split(",")self.size_w_entry.setText(size[0])self.size_h_entry.setText(size[1])self.text_entry.setText(history_item['text'])self.text_color = history_item['text_color']self.text_size_spin.setValue(int(history_item['text_size']))self.position_combo.setCurrentText(history_item['text_position'])self.animation_combo.setCurrentText(history_item['text_animation'])self.filter_combo.setCurrentText(history_item['filter'])#驱动程序单元
if __name__ == '__main__':app = QApplication(sys.argv)ex = GifMakerGUI()ex.show()sys.exit(app.exec_())

第五步:运行效果展示

 第六步:操作指南

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

 第七步:注意事项

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

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

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

- 水印动画及图片滤镜并不多,还需继续优化。

- 历史纪录删除问题。

后面我会对以上问题进行优化/处理,敬请期待!

相关文章:

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

前言:在抖音,快手等社交平台上,我们常常见到各种各样的GIF动画。在各大评论区里面,GIF图片以其短小精悍、生动有趣的特点,被广泛用于分享各种有趣的场景、搞笑的瞬间、精彩的动作等,能够快速吸引我们的注意…...

PhpStorm下载、安装、配置教程

前面的文章中,都是把.php文件放在WampServer的www目录下,通过浏览器访问运行。这篇文章就简单介绍一下PhpStorm这个php集成开发工具的使用。 目录 下载PhpStorm 安装PhpStorm 配置PhpStorm 修改个性化设置 修改字符编码 配置php的安装路径 使用Ph…...

春节假期旅游热潮下,景区医疗安全如何全面升级?

春节假期旅游热潮下,景区医疗安全如何全面升级? 随着旅游业的不断繁荣,春节假期期间,各大景区再次迎来了游客的高峰期。面对如此庞大的客流量,景区不仅要在服务接待上下功夫,更要将医疗安全保障工作提升到…...

惠普HP工作站如何关闭关闭RAID?

惠普HP工作站如何关闭关闭RAID? 前言进入BIOS进入“先进”选项卡,点击“系统选项”。取消勾选“sSATA控制器RAID模式”,按下F10保存重启。 前言 惠普工作站默认启用了RAID模式,导致许多PE工具无法识别RAID模式下的硬盘。可以通过…...

ESP-Skainet智能语音助手,ESP32-S3物联网方案,设备高效语音交互

在科技飞速发展的今天,智能语音助手正逐渐渗透到我们生活的方方面面,而智能语音助手凭借其卓越的技术优势,成为了智能生活领域的一颗璀璨明星。 ESP-Skainet智能语音助手的强大之处在于其支持唤醒词引擎(WakeNet)、离…...

mac下生成.icns图标

笔记原因: 今日需要在mac下开发涉及图标文件的使用及icons文件的生成,所以记录一下。 网络上都是一堆命令行需要打印太麻烦了,写一个一键脚本。 步骤一 将需要生成的png格式文件重命名为“pic.png” mv xxxx.png pic.png 步骤二 下载我…...

【MySQL】centos 7 忘记数据库密码

vim /etc/my.cnf文件; 在[mysqld]后添加skip-grant-tables(登录时跳过权限检查) 重启MySQL服务:sudo systemctl restart mysqld 登录mysql,输入mysql –uroot –p;直接回车(Enter) 输…...

【kafka的零拷贝原理】

kafka的零拷贝原理 一、零拷贝技术概述二、Kafka中的零拷贝原理三、零拷贝技术的优势四、零拷贝技术的实现细节五、注意事项一、零拷贝技术概述 零拷贝(Zero-Copy)是一种减少数据拷贝次数,提高数据传输效率的技术。 在传统的数据传输过程中,数据需要在用户态和内核态之间…...

【JavaEE】Spring Web MVC

目录 一、Spring Web MVC简介 1.1 MVC简介1.2 Spring MVC1.3 RequestMapping注解1.3.1 使用1.3.2 RequestMapping的请求设置 1.3.2.1 方法11.3.2.2 方法2 二、Postman介绍 2.1 创建请求2.2 界面如下:2.3 传参介绍 一、Spring Web MVC简介 官方文档介绍&#xff…...

《解锁GANs黑科技:打造影视游戏的逼真3D模型》

在游戏与影视制作领域,逼真的3D模型是构建沉浸式虚拟世界的关键要素。从游戏中栩栩如生的角色形象,到影视里震撼人心的宏大场景,高品质3D模型的重要性不言而喻。随着人工智能技术的飞速发展,生成对抗网络(GANs&#xf…...

【大数据技术】词频统计样例(hadoop+mapreduce+yarn)

词频统计(hadoop+mapreduce+yarn) 搭建完全分布式高可用大数据集群(VMware+CentOS+FinalShell) 搭建完全分布式高可用大数据集群(Hadoop+MapReduce+Yarn) 在阅读本文前,请确保已经阅读过以上两篇文章,成功搭建了Hadoop+MapReduce+Yarn的大数据集群环境。 写在前面 Wo…...

deepseek与openai关系

‌DeepSeek与OpenAI之间的关系主要体现在技术竞争和合作的可能性上。‌ 首先,DeepSeek是由中国的深度求索公司开发的,成立于2023年,专注于人工智能技术研发。其大模型DeepSeek-R1在数学、代码、自然语言推理等任务上的性能能够比肩OpenAI的G…...

51页精品PPT | 数据中台与数据治理服务及案例

案例的核心内容围绕数据中台与数据治理服务展开,详细介绍了数据治理的整体方法论、数据中台的建设路径以及如何通过数据治理和数据中台提升业务效率和数据质量。本案例强调了数据治理的重要性,包括数据标准、数据质量、数据安全等方面的管理,…...

使用 cipher /w 清除磁盘删除残留数据(Windows) - 随笔

cipher命令是Windows 系统自带的一个用于管理文件加密和磁盘数据清除的工具。通过 cipher /w 命令,可以清除磁盘上已删除文件的残留数据,确保这些数据无法被恢复。以下是一个简易的批处理脚本,用于清除指定磁盘上的加密数据。 echo off :: 清…...

RuntimeWarning: invalid value encountered in sqrt

代码出处: GitHub - wangsen1312/joints2smpl: fit smpl parameters model using 3D joints RuntimeWarning: invalid value encountered in sqrt 你可以通过以下几种方式解决这个问题: 1. 检查负值或零行列式 确保协方差矩阵是正半定的,这…...

3步打造C# API安全密盾

引言:API 安全的重要性 在数字化浪潮中,应用程序编程接口(API)已成为不同软件系统之间通信和数据交互的关键桥梁。无论是企业内部的微服务架构,还是面向外部用户的在线服务,API 都承担着数据传输和业务逻辑…...

项目实操:windows批处理拉取git库和处理目录、文件

初级代码游戏的专栏介绍与文章目录-CSDN博客 我的github:codetoys,所有代码都将会位于ctfc库中。已经放入库中我会指出在库中的位置。 这些代码大部分以Linux为目标但部分代码是纯C的,可以在任何平台上使用。 源码指引:github源…...

接入 deepseek 实现AI智能问诊

1. 准备工作 注册 DeepSeek 账号 前往 DeepSeek 官网 注册账号并获取 API Key。 创建 UniApp 项目 使用 HBuilderX 创建一个新的 UniApp 项目(选择 Vue3 或 Vue2 模板)。 安装依赖 如果需要在 UniApp 中使用 HTTP 请求,推荐使用 uni.requ…...

ubuntu22.04源码编译mysql8.0.X详细流程【由deepseek提供】

以下是在 Ubuntu 22.04 上从源代码编译安装 MySQL 8.0.X(以 MySQL 8.0.37 为例)的详细操作流程: 一、准备工作 1. 更新系统 sudo apt update && sudo apt upgrade -y sudo apt install -y build-essential cmake pkg-config libssl…...

富唯智能复合机器人拓展工业新维度

富唯智能复合机器人是富唯智能倾力打造的一款集高度自动化、智能化和多功能性于一体的机器人。它融合了机械、电子、计算机、传感器等多个领域的前沿技术,通过精密的算法和控制系统,实现了对复杂生产环境的快速适应和高效作业。 富唯智能复合机器人的特点…...

【2】高并发导出场景下,服务器性能瓶颈优化方案-异步导出

Java 异步导出是一种在处理大量数据或复杂任务时优化性能和用户体验的重要技术。 1. 异步导出的优势 异步导出是指将导出操作从主线程中分离出来,通过后台线程或异步任务完成数据处理和文件生成。这种方式可以显著减少用户等待时间,避免系统阻塞&#x…...

verdi 查看覆盖率

点击Tools -> Coverage,会出现一个Verdi:vdCoverage:1页面点击File->Open/Add Database, 会出现一个 Open/Add Coverage Database页面, 在Design Hierarchy/Testbench Location 中输入 vdb路径点击… , 会出现当前路径下的文件&#xf…...

【React】路由处理的常见坑与解决方法,React Router 的动态路由与懒加载问题

在使用 React Router 时,动态路由和懒加载是非常常见的需求,但也可能会遇到一些坑。以下是常见问题以及对应的解决方法。 一、React Router 动态路由常见问题 1. 动态路由匹配问题 动态路由通常通过 :param 定义路径参数,但如果路径参数与静态路由有重叠,可能会导致匹配问…...

OKHttp拦截器解析

OKHttp涉及到拦截器大概的执行步骤为: 1.通过newCall生成RealCall对象 具体代码如下: Override public Call newCall(Request request) {return new RealCall(this, request, false /* for web socket */);}2.调用Call的execute方法 当然这也可以是执…...

顺序表和链表

线性表 线性表(linear list)是n 个具有相同特性的数据元素的有限序列。线性表是一种在实际中广泛使用的数据结构。 常见的线性表:顺序表、链表、栈、队列、字符串… 线性表在逻辑上是线性结构,也就说是连续的一条直线。但是在物…...

若依框架使用(低级)

克隆源码 浏览器搜索若依,选择 RuoYi-Vue RuoYi-Vue RuoYi-Vue 重要的事情说三遍,进入gitee 下面这个页面(注意红色框起来的部分) 进入Gitee进行下载 我下载的是最新的springboot3 下载好后我们可以选择一个文件夹&#xff0…...

Spring JDBC模块解析 -深入SqlParameterSource

在前面的博客中,我们探讨了Spring Data Access Module中的主要组件: JdbcTemplate和SimpleJdbcInsert。在这两部分的基础上,我们将继续探讨更详细 的用法,包括如何使用RowMapper和SqlParameterSource等高级主题。 JdbcTemplate …...

SQL中Limit的用法详解

SQL中的LIMIT关键字是一个非常有用的工具,它可以用来限制查询结果返回的记录数量。文章将详细解析LIMIT关键字的使用方法,包括它的基本用法,以及在查询数据时如何配合使用LIMIT与OFFSET。我会通过示例代码演示LIMIT在单行结果集和多行结果集情…...

mac 安装 dotnet 环境

目录 一、安装准备 二、安装方法(两种任选) 方法 1:使用官方安装包(推荐新手) 方法 2:使用 Homebrew(适合开发者) 1. 安装 Homebrew(如未安装) 2. 通过 …...

DeepSeek辅助学术写作【句子重写】效果如何?

句子重写(功能指数:★★★★★) 当我们想引用一篇文章中的一-些我们认为写得很好的句子时,如果直接将原文加人自己的文章,那么即使我们标注上了引用,也依旧会被查重软件计算在重复比例中。查重比例过高的话,会影响投稿或毕业答辩送…...

SpringUI Web高端动态交互元件库

Axure Web高端动态交互元件库是一个专为Web设计与开发领域设计的高质量资源集合,旨在加速原型设计和开发流程。以下是关于这个元件库的详细介绍: 一、概述 Axure Web高端动态交互元件库是一个集成了多种预制、高质量交互组件的工具集合。这些组件经过精…...

QT +FFMPEG4.3 拉取 RTMP/http-flv 流播放 AVFrame转Qimage

QT FFMPEG4.3 拉取 RTMP/http-flv 流播放 Cc_Video_thread.h #ifndef CC_VIDEO_THREAD_H #define CC_VIDEO_THREAD_H#include <QThread> #include <QAtomicInt> #include <QImage>#ifdef __cplusplus extern "C" { #endif #include <libavfor…...

Docker最佳实践:安装Nacos

文章目录 Docker最佳实践&#xff1a;安装Nacos一、引言二、安装 Nacos1、拉取 Nacos Docker 镜像2、启动 Nacos 容器 三、配置 Nacos&#xff08;可选&#xff09;四、使用示例1、服务注册2、服务发现 五、总结 Docker最佳实践&#xff1a;安装Nacos 一、引言 Nacos 是阿里巴…...

106,【6】 buuctf web [SUCTF 2019]CheckIn

进入靶场 文件上传 老规矩&#xff0c;桌面有啥传啥 过滤了<? 寻找不含<?的一句话木马 文件名 123(2).php.jpg 文件内容 GIF89a? <script language"php">eval($_GET[123]);</script> 123即密码&#xff0c;可凭借个人喜好更换 再上传一个文…...

【Linux】27.Linux 多线程(1)

文章目录 1. Linux线程概念1.1 线程和进程1.2 虚拟地址是如何转换到物理地址的1.3 线程的优点1.4 线程的缺点1.5 线程异常1.6 线程用途 2. Linux进程VS线程2.1 进程和线程2.2 关于进程线程的问题 3. Linux线程控制3.1 POSIX线程库3.2 创建线程3.3 线程终止3.4 线程等待3.5 分离…...

旋转变压器工作及解调原理

旋转变压器 旋转变压器是一种精密的位置、速度检测装置&#xff0c;广泛应用在伺服控制、机器人、机械工具、汽车、电力等领域。但是&#xff0c;旋转变压器在使用时并不能直接提供角度或位置信息&#xff0c;需要特殊的激励信号和解调、计算措施&#xff0c;才能将旋转变压器…...

字符串转浮点数函数atof、strtod、strtof和strtold使用场景

字符串转浮点数函数 atof、strtod、strtof 和 strtold 在 C 语言标准库中都有各自的使用场景&#xff0c;它们的主要功能是将字符串转换为浮点数&#xff0c;但在处理的浮点数类型、错误处理机制和精度方面有所不同。 一、atof 函数使用场景 atof&#xff08;ASCII to Float&…...

GD32F4xx系列微控制器中,定时器的主模式(Master Mode)和从模式(Slave Mode)

在GD32F4xx系列微控制器中&#xff0c;定时器的主模式&#xff08;Master Mode&#xff09;和从模式&#xff08;Slave Mode&#xff09;是两种不同的工作模式&#xff0c;它们的主要区别在于定时器的操作是否依赖于外部信号或另一个定时器的输出信号。以下是对这两种模式的详细…...

深度学习系列--03.激活函数

一.定义 激活函数是一种添加到人工神经网络中的函数&#xff0c;它为神经网络中神经元的输出添加了非线性特性 在神经网络中&#xff0c;神经元接收来自其他神经元的输入&#xff0c;并通过加权求和等方式计算出一个净输入值。激活函数则根据这个净输入值来决定神经元是否应该…...

在linux 中搭建deepseek 做微调,硬件配置要求说明

搭建 可参考 使用deepseek-CSDN博客 官方网站&#xff1a;DeepSeek DeepSeek 是一个基于深度学习的开源项目&#xff0c;旨在通过深度学习技术来提升搜索引擎的准确性和效率。如果你想在 Linux 系统上搭建 DeepSeek&#xff0c;你可以遵循以下步骤。这里我将提供一个基本的指…...

机器学习之数学基础:线性代数、微积分、概率论 | PyTorch 深度学习实战

前一篇文章&#xff0c;使用线性回归模型逼近目标模型 | PyTorch 深度学习实战 本系列文章 GitHub Repo: https://github.com/hailiang-wang/pytorch-get-started 本篇文章内容来自于 强化学习必修课&#xff1a;引领人工智能新时代【梗直哥瞿炜】 线性代数、微积分、概率论 …...

MySQL - Navicat自动备份MySQL数据

对于从事IT开发的工程师&#xff0c;数据备份我想大家并不陌生&#xff0c;这件工程太重要了&#xff01;对于比较重要的数据&#xff0c;我们希望能定期备份&#xff0c;每天备份1次或多次&#xff0c;或者是每周备份1次或多次。 如果大家在平时使用Navicat操作数据库&#x…...

javaEE-9.HTML入门

目录 一.什么是html 二.认识html标签 1.标签的特点: 2.html文件基本结构 3.标签的层次结构 三、html工具 四、创建第一个文件 五.html常见标签 1标题标签h1-h6 2.段落标签:p 3.换行标签:br 4.图片标签:img 图片路径有1三种表示形式: 5.超链接:a 链接的几种形式: …...

springcloud微服务使用不同端口启动同一服务

若想同时启动两个服务&#xff0c;则会产生端口冲突&#xff0c;在启动类设置界面&#xff0c;添加虚拟机选项&#xff0c;随后设置 -Dserver.portxxxx即可...

JavaScript系列(61)--边缘计算应用开发详解

JavaScript边缘计算应用开发详解 &#x1f310; 今天&#xff0c;让我们深入探讨JavaScript的边缘计算应用开发。边缘计算是一种将计算和数据存储分布到更靠近数据源的位置的架构模式&#xff0c;它能够提供更低的延迟和更好的实时性能。 边缘计算基础架构 &#x1f31f; &am…...

【容器技术01】使用 busybox 构建 Mini Linux FS

使用 busybox 构建 Mini Linux FS 构建目标 在 Linux 文件系统下构建一个 Mini 的文件系统&#xff0c;构建目标如下&#xff1a; minilinux ├── bin │ ├── ls │ ├── top │ ├── ps │ ├── sh │ └── … ├── dev ├── etc │ ├── g…...

Effective Python系列(1.3):使用zip函数同时遍历两个迭代器

zip函数是 Python 中的一个内置函数&#xff0c;用于将多个可迭代对象&#xff08;如列表、元组等&#xff09;的元素配对&#xff0c;生成一个迭代器。 使用 zip 函数的好处之一就是能够节省内存空间&#xff0c;因为该函数会创建惰性生成器&#xff0c;每次遍历时只生成一个元…...

gitlab个别服务无法启动可能原因

目录 一、gitlab的puma服务一直重启 1. 查看日志 2. 检查配置文件 3. 重新配置和重启 GitLab 4. 检查系统资源 5. 检查依赖和服务状态 6. 清理和优化 7. 升级 GitLab 8. 查看社区和文档 二、 gitlab个别服务无法启动可能原因 1.服务器内存或磁盘已满 2.puma端口冲突…...

基于Springboot+vue的租车网站系统

基于SpringbootVue的租车网站系统是一个现代化的在线租车平台&#xff0c;它结合了Springboot的后端开发能力和Vue的前端交互优势&#xff0c;为用户和汽车租赁公司提供了一个高效、便捷、易用的租车体验和管理工具。以下是对该系统的详细介绍&#xff1a; 一、系统架构 后…...

Github - 记录一次对“不小心包含了密码的PR”的修复

Github - 记录一次对“不小心包含了密码的PR”的修复 前言 和好朋友一起开发一个字节跳动青训营抖音电商后端(now private)的项目&#xff0c;某大佬不小心把本地一密码commit上去并提了PR。 PR一旦发出则无法被删除&#xff0c;且其包含的commit也能被所有能看到这个仓库的…...