【项目】pyqt5基于python的照片整蛊项目
🎨 FacePrank - 超好玩的照片整蛊工具!用Python打造专属捏脸神器

🎭 想恶搞朋友的照片吗?想给证件照做个"微整形"吗?这个基于PyQt5的图像变形工具让你轻松实现各种搞怪效果!【本文章已经绑定项目资源 可以直接下载,或者文章复制代码跟着流程做】
文章目录
- 🎨 FacePrank - 超好玩的照片整蛊工具!用Python打造专属捏脸神器
- 📸 效果展示
- ✨ 项目简介
- 🎯 核心特性
- 🛠️ 环境配置
- 📦 方式一:使用Conda创建虚拟环境(推荐)
- 📦 方式二:使用pip直接安装
- 📋 依赖说明
- 🚀 快速启动
- 🔧 克隆或下载项目
- ▶️ 运行程序
- 📖 使用指南
- 🎬 基本操作流程
- 💡 使用技巧
- 🎨 创意玩法
- 🔥 五大核心工具详解
- 🔍 工具一:放大工具
- 🔎 工具二:缩小工具
- ✋ 工具三:拖拽扭曲工具
- 🔄 工具四:旋转工具
- 🧹 工具五:橡皮擦工具
- 💻 核心技术实现
- 🏗️ 技术架构
- 🔬 变形算法原理
- 1. 放大/缩小算法
- 2. 旋转算法
- 3. 拖拽扭曲算法
- 4. 橡皮擦算法(向量化优化)
- 🎯 双线性插值
- 🖱️ 交互优化
- Ctrl+滚轮缩放实现
- 右键拖拽视图
- 持续效果实现
- 📂 中文路径支持
- 📁 项目结构
- 🎨 完整源代码
- 主程序:main.py
- 🎯 系统要求
- ❓ 常见问题 FAQ
- Q1: 运行时提示"No module named 'PyQt5'"
- Q2: 图片变形后画质下降怎么办?
- Q3: 支持哪些图片格式?
- Q4: 如何撤销操作?
- Q5: Windows下运行报错"DLL load failed"
- Q6: 中文路径下无法加载图片?
- Q7: 能处理多大的图片?
- 🚀 未来改进方向
- 📝 许可证
- 🎉 总结
- 📚 附录:requirements.txt
- 🔗 相关链接
📸 效果展示


在此处插入:原图 vs 整蛊后的对比照片
用FacePrank可以轻松实现:放大眼睛、缩小鼻子、扭曲表情、旋转漩涡等各种搞笑效果!
✨ 项目简介
FacePrank 是一个功能强大的图像变形工具,使用Python和PyQt5开发。无需复杂的PS技能,只需鼠标点击拖拽,就能对照片进行各种有趣的变形处理。无论是恶搞朋友、制作表情包,还是进行创意设计,这个工具都能满足你的需求!
🎯 核心特性
- 🖱️ 简单易用:纯鼠标操作,无需专业技能
- 🎨 五大工具:放大、缩小、拖拽扭曲、旋转、橡皮擦
- 📂 拖拽上传:支持直接拖拽图片到窗口
- 🔍 Ctrl+滚轮缩放:精确查看和编辑细节
- 🖐️ 右键拖拽视图:自由移动查看区域
- ⚙️ 参数可调:画笔半径、变化强度随心调节
- 💾 中文路径支持:完美支持中文文件名和路径
- 🔄 实时预览:所见即所得的编辑体验
🛠️ 环境配置
📦 方式一:使用Conda创建虚拟环境(推荐)
第一步:创建虚拟环境
conda create -n faceprank python=3.8
第二步:激活环境
conda activate faceprank
第三步:配置清华镜像源(提速)
pip config set global.index-url https://pypi.tuna.tsinghua.edu.cn/simple
第四步:安装依赖
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple PyQt5>=5.15.0 opencv-python>=4.5.0 numpy>=1.19.0
或者使用requirements.txt安装:
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txt
📦 方式二:使用pip直接安装
如果你不使用Conda,也可以直接用pip安装:
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple PyQt5>=5.15.0 opencv-python>=4.5.0 numpy>=1.19.0
📋 依赖说明
| 库名称 | 版本要求 | 用途 |
|---|---|---|
| PyQt5 | ≥5.15.0 | 图形界面框架 |
| opencv-python | ≥4.5.0 | 图像处理核心 |
| numpy | ≥1.19.0 | 数值计算和数组操作 |
🚀 快速启动
🔧 克隆或下载项目
# 如果你有git
git clone <项目地址>
cd FacePrank# 或者直接下载ZIP解压
▶️ 运行程序
python main.py
启动成功后,会看到一个现代化的深色主题界面,中央有提示文字:“📁 点击上传图片按钮 或 拖拽图片到此处”
📖 使用指南
🎬 基本操作流程
1️⃣ 上传图片
- 方法一:点击工具栏的
📁 上传图片按钮,选择图片文件 - 方法二:直接拖拽图片文件到窗口中(推荐!更方便)
支持的格式:PNG、JPG、JPEG、BMP、GIF
2️⃣ 选择工具
在顶部工具栏选择你需要的变形工具:
| 工具图标 | 工具名称 | 快捷说明 |
|---|---|---|
| 🔍 | 放大工具 | 点击位置向外扩展,产生凸起效果 |
| 🔎 | 缩小工具 | 点击位置向内收缩,产生凹陷效果 |
| ✋ | 拖拽扭曲 | 拖拽鼠标实现推拉扭曲效果 |
| 🔄 | 旋转工具 | 点击产生螺旋漩涡效果 |
| 🧹 | 橡皮擦 | 擦除变形,恢复原图 |
3️⃣ 调整参数
-
画笔半径:控制影响范围(10-800像素,默认170)
- 滑块拖动或直接输入数值
- 较小半径适合精细调整
- 较大半径适合大范围变形
-
变化强度:控制变形程度(0.01-1.0,默认0.20)
- 数值越大,变形越明显
- 建议从小值开始尝试
-
旋转方向:仅旋转工具有效
- ⟳ 顺时针
- ⟲ 逆时针
4️⃣ 查看与导航
-
Ctrl + 滚轮:放大缩小图像(0.1x - 10x)
- 鼠标位置为缩放中心
- 适合查看和编辑细节
-
右键拖拽:移动视图位置
- 按住鼠标右键拖动
- 配合缩放功能精确编辑
5️⃣ 开始编辑
-
放大/缩小/旋转:左键点击图像位置
- 按住不放会持续累积效果
- 移动鼠标位置会跟随
-
拖拽扭曲:按住鼠标左键拖动
- 拖拽路径上的像素会随鼠标移动
-
橡皮擦:按住鼠标左键拖动
- 经过区域恢复为原始图像
6️⃣ 保存结果
点击 💾 保存 按钮,选择保存路径和格式(PNG或JPG)
💡 使用技巧
- 📂 拖拽上传最方便:直接把图片拖到窗口,有绿色高亮提示
- 🔍 放大查看细节:Ctrl+滚轮放大图像,可以看清每个像素
- 🎯 小画笔精细调整:处理眼睛、鼻子等小部位时,减小画笔半径
- 💪 强度从小到大:先用小强度测试效果,再逐步加大
- ⏱️ 按住鼠标累积:需要明显效果时,按住鼠标不放
- 🔄 多次点击加强:在同一位置多次点击可以加强效果
- 🧹 局部恢复原图:用橡皮擦可以只修正某些过度变形的区域
- ↺ 一键重置:点击重置按钮恢复原图(包括缩放和偏移)
🎨 创意玩法
- 放大眼睛:使用放大工具点击眼睛位置,制作大眼萌照
- 缩小鼻子:使用缩小工具点击鼻子,打造精致小脸
- 扭曲表情:使用拖拽工具拉扯嘴角,制作搞笑表情
- 漩涡背景:使用旋转工具点击背景,营造魔幻效果
- 组合变形:多种工具配合使用,创造独特效果
🔥 五大核心工具详解
🔍 工具一:放大工具
作用原理:点击的区域像素会向外扩展挤压,产生凸起效果
适用场景:
- 放大眼睛,制作大眼萌照
- 放大嘴巴,制作夸张表情
- 突出某个特定部位
使用方法:
- 选择
🔍 放大工具 - 调整画笔半径(建议50-150)
- 左键点击要放大的位置
- 按住不放可以持续放大
效果示例:眼睛从正常大小变成圆圆的大眼睛 👁️
🔎 工具二:缩小工具
作用原理:半径内的像素向中心靠拢收缩,产生凹陷效果
适用场景:
- 缩小鼻子,打造小巧鼻梁
- 缩小脸颊,制作瓜子脸
- 减小某些突出部位
使用方法:
- 选择
🔎 缩小工具 - 调整画笔半径(建议40-120)
- 左键点击要缩小的位置
- 按住不放可以持续缩小
提示:缩小工具与放大工具效果相反,可以互相配合使用
✋ 工具三:拖拽扭曲工具
作用原理:路径上的像素会随着鼠标移动而拖拽变形
适用场景:
- 推、拉、扭曲面部特征
- 制作各种搞怪表情
- 自由变形任何区域
使用方法:
- 选择
✋ 拖拽扭曲工具 - 调整画笔半径和强度
- 按住鼠标左键拖动
- 拖拽路径会产生变形效果
技巧:这是最灵活的工具,可以实现各种创意变形
🔄 工具四:旋转工具
作用原理:周围像素会以点击点为轴进行螺旋旋转
适用场景:
- 制作漩涡特效
- 营造魔幻氛围
- 创造艺术效果
使用方法:
- 选择
🔄 旋转工具 - 选择旋转方向(⟳顺时针 或 ⟲逆时针)
- 调整画笔半径(建议100-300)
- 左键点击要旋转的位置
- 按住不放产生更强烈的旋转效果
创意玩法:在眼睛位置使用旋转工具,制作催眠效果
🧹 工具五:橡皮擦工具
作用原理:擦除变形效果,将修改的区域还原为原始图像
适用场景:
- 修正过度变形的区域
- 局部恢复原图
- 精细调整效果
使用方法:
- 选择
🧹 橡皮擦工具 - 调整画笔半径(擦除范围)
- 按住鼠标左键拖动
- 经过的区域会恢复原样
特点:
- 支持边缘羽化,过渡自然
- 只恢复经过路径和画笔范围内的像素
- 可以配合其他工具实现精细控制
💻 核心技术实现
🏗️ 技术架构
FacePrank
├── 界面层 (PyQt5)
│ ├── 主窗口 (QMainWindow)
│ ├── 工具栏 (QToolBar)
│ ├── 画布组件 (QLabel)
│ └── 控制面板 (QSlider + QLineEdit)
│
├── 图像处理层 (OpenCV)
│ ├── 图像加载与保存
│ ├── 格式转换 (BGR ↔ RGB)
│ └── 重映射与插值
│
└── 算法层 (NumPy)├── 网格映射变形├── 距离场计算├── 双线性插值└── 向量化运算
🔬 变形算法原理
1. 放大/缩小算法
核心思想:基于距离的像素位移
# 计算到中心点的距离
distances = np.sqrt(dx**2 + dy**2)# 计算影响因子(越近影响越大)
factor = 1.0 - (distances / brush_radius)
factor = factor ** 2 # 平滑过渡# 放大:像素向内收缩
scale = 1.0 - strength * factor * mask
new_x = cx + dx * scale
new_y = cy + dy * scale# 缩小:像素向外扩展
scale = 1.0 + strength * factor * mask
new_x = cx + dx * scale
new_y = cy + dy * scale
关键点:
- 使用平方函数实现平滑过渡
- mask确保只影响半径内像素
- strength控制变形强度
2. 旋转算法
核心思想:极坐标旋转变换
# 计算旋转角度(距离越近旋转越多)
angle = (1.0 - distances / brush_radius) * π * strength * direction# 旋转矩阵变换
cos_angle = np.cos(angle)
sin_angle = np.sin(angle)new_x = cx + dx * cos_angle - dy * sin_angle
new_y = cy + dx * sin_angle + dy * cos_angle
关键点:
- 使用旋转矩阵进行坐标变换
- direction参数控制顺时针/逆时针
- 角度随距离衰减,产生漩涡效果
3. 拖拽扭曲算法
核心思想:路径方向的力场扭曲
# 计算拖拽向量
drag_x = end_x - start_x
drag_y = end_y - start_y# 计算影响因子
factor = (1.0 - distances / brush_radius) ** 2# 应用拖拽位移
new_x = x_indices - drag_x * factor * mask * strength
new_y = y_indices - drag_y * factor * mask * strength
关键点:
- 基于鼠标移动方向计算位移向量
- 使用力场衰减实现自然过渡
- 支持连续拖拽的流畅效果
4. 橡皮擦算法(向量化优化)
核心思想:权重混合原图与变形图
# 计算距离和混合因子
distances = np.sqrt((x_coords - cx)**2 + (y_coords - cy)**2)
factor = (1.0 - distances / brush_radius) ** 0.5# 向量化混合
blended = current_image * (1 - factor) + original_image * factor
关键点:
- 使用NumPy向量化代替双重循环,性能提升10倍以上
- 边缘羽化实现自然过渡
- 只混合mask为True的区域
🎯 双线性插值
所有变形算法最后都使用OpenCV的remap函数进行双线性插值:
new_image = cv2.remap(image, new_x.astype(np.float32),new_y.astype(np.float32), cv2.INTER_LINEAR)
这确保了变形后图像的平滑性和视觉质量。
🖱️ 交互优化
Ctrl+滚轮缩放实现
def wheelEvent(self, event):if event.modifiers() == Qt.ControlModifier:# 获取鼠标位置对应的图像坐标old_image_pos = self.get_image_pos(event.pos())# 计算缩放增量zoom_factor = 1.1 if delta > 0 else 0.9self.zoom_scale *= zoom_factor# 调整偏移以保持鼠标位置下的图像点不变# ... 坐标变换计算 ...
关键点:
- 检测Ctrl键修饰符
- 以鼠标位置为中心进行缩放
- 自动调整偏移量,保持缩放中心不变
右键拖拽视图
def mouseMoveEvent(self, event):if self.is_panning:delta = event.pos() - self.pan_start_posself.offset_x += delta.x()self.offset_y += delta.y()# 限制偏移范围...
持续效果实现
使用QTimer定时器实现按住鼠标持续变形:
def start_continuous_effect(self):self.continuous_timer = QTimer(self)self.continuous_timer.timeout.connect(self.apply_continuous_effect)self.continuous_timer.start(50) # 每50ms应用一次
📂 中文路径支持
使用NumPy的文件IO函数支持中文路径:
# 加载图像
image_data = np.fromfile(file_path, dtype=np.uint8)
image = cv2.imdecode(image_data, cv2.IMREAD_COLOR)# 保存图像
_, encoded_img = cv2.imencode('.png', image)
encoded_img.tofile(file_path)
📁 项目结构
FacePrank/
├── main.py # 主程序文件(1098行)
│ ├── ImageCanvas类 # 图像画布和交互处理
│ │ ├── 图像加载与显示
│ │ ├── 鼠标事件处理
│ │ ├── 五大变形算法
│ │ ├── 缩放和平移
│ │ └── 拖拽上传支持
│ │
│ └── FaceWarpApp类 # 主窗口和UI
│ ├── 工具栏创建
│ ├── 控制面板
│ ├── 参数调节
│ └── 文件操作
│
├── requirements.txt # Python依赖
├── README.md # 项目说明
└── face.jpg # 示例图片
🎨 完整源代码
主程序:main.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
捏脸APP - 简单的图像变形工具
支持放大、缩小、拖拽扭曲、旋转等变形效果
"""import sys
import numpy as np
from PyQt5.QtWidgets import (QApplication, QMainWindow, QWidget, QVBoxLayout, QHBoxLayout, QPushButton, QLabel, QSlider, QFileDialog, QToolBar, QAction, QSizePolicy)
from PyQt5.QtCore import Qt, QPoint, QRect
from PyQt5.QtGui import QImage, QPixmap, QPainter, QPen, QColor
import cv2class ImageCanvas(QLabel):"""图像画布类,处理图像显示和鼠标交互"""def __init__(self, parent=None):super().__init__(parent)self.parent_window = parentself.image = Noneself.display_image = Noneself.original_image = Noneself.current_tool = None # 当前工具: 'enlarge', 'shrink', 'drag', 'rotate', 'eraser'self.brush_radius = 170 # 默认画笔半径self.effect_strength = 0.20 # 变化强度,范围0.01-1.0,默认0.20self.rotate_direction = 1 # 旋转方向:1为顺时针,-1为逆时针self.is_drawing = Falseself.last_point = None# 用于持续效果的定时器self.continuous_timer = Noneself.continuous_pos = None# 图像缩放和平移self.zoom_scale = 1.0 # 缩放比例self.offset_x = 0 # X轴偏移self.offset_y = 0 # Y轴偏移self.is_panning = False # 是否正在拖拽视图self.pan_start_pos = None # 拖拽起始位置self.setMinimumSize(800, 600)self.setAlignment(Qt.AlignCenter)self.setStyleSheet("""QLabel { background-color: #2b2b2b; color: #9E9E9E;font-size: 24px;font-weight: bold;border: 3px dashed #555;border-radius: 10px;}""")self.setText('📁 点击"上传图片"按钮\n或\n拖拽图片到此处')# 启用拖放功能self.setAcceptDrops(True)def load_image(self, file_path):"""加载图像(支持中文路径)"""try:# 使用np.fromfile()读取文件,支持中文路径image_data = np.fromfile(file_path, dtype=np.uint8)self.original_image = cv2.imdecode(image_data, cv2.IMREAD_COLOR)if self.original_image is None:return False# BGR转RGBself.original_image = cv2.cvtColor(self.original_image, cv2.COLOR_BGR2RGB)self.image = self.original_image.copy()self.display_image = self.image.copy()self.update_display()return Trueexcept Exception as e:print(f"加载图片失败: {e}")return Falsedef update_display(self):"""更新显示的图像(支持缩放和平移)"""if self.image is None:return# 图片加载后移除虚线边框self.setStyleSheet("""QLabel { background-color: #2b2b2b; }""")h, w, ch = self.image.shapebytes_per_line = ch * wq_image = QImage(self.image.data, w, h, bytes_per_line, QImage.Format_RGB888)# 创建pixmappixmap = QPixmap.fromImage(q_image)# 应用缩放if self.zoom_scale != 1.0:# 先按原始比例缩放到窗口base_scaled = pixmap.scaled(self.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation)# 再应用用户缩放new_width = int(base_scaled.width() * self.zoom_scale)new_height = int(base_scaled.height() * self.zoom_scale)scaled_pixmap = pixmap.scaled(new_width, new_height, Qt.KeepAspectRatio, Qt.SmoothTransformation)else:scaled_pixmap = pixmap.scaled(self.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation)# 如果图像大于窗口,需要裁剪并应用偏移if scaled_pixmap.width() > self.width() or scaled_pixmap.height() > self.height():# 计算裁剪区域x = max(0, min(-self.offset_x, scaled_pixmap.width() - self.width()))y = max(0, min(-self.offset_y, scaled_pixmap.height() - self.height()))w = min(self.width(), scaled_pixmap.width())h = min(self.height(), scaled_pixmap.height())# 裁剪scaled_pixmap = scaled_pixmap.copy(x, y, w, h)self.setPixmap(scaled_pixmap)def get_image_pos(self, widget_pos):"""将窗口坐标转换为图像坐标(考虑缩放和偏移)"""if self.image is None:return Noneh, w = self.image.shape[:2]# 计算基础缩放(适应窗口)widget_aspect = self.width() / self.height()image_aspect = w / hif widget_aspect > image_aspect:base_height = self.height()base_width = int(base_height * image_aspect)else:base_width = self.width()base_height = int(base_width / image_aspect)# 应用用户缩放display_width = int(base_width * self.zoom_scale)display_height = int(base_height * self.zoom_scale)# 计算图像在widget中的位置(考虑偏移)if display_width <= self.width():x_offset = (self.width() - display_width) // 2else:x_offset = self.offset_xif display_height <= self.height():y_offset = (self.height() - display_height) // 2else:y_offset = self.offset_y# 转换为显示图像坐标img_x = widget_pos.x() - x_offsetimg_y = widget_pos.y() - y_offsetif img_x < 0 or img_y < 0 or img_x >= display_width or img_y >= display_height:return None# 缩放到原始图像尺寸scale_x = w / display_widthscale_y = h / display_heightorig_x = int(img_x * scale_x)orig_y = int(img_y * scale_y)# 确保在图像范围内orig_x = max(0, min(orig_x, w - 1))orig_y = max(0, min(orig_y, h - 1))return QPoint(orig_x, orig_y)def mousePressEvent(self, event):"""鼠标按下事件"""if event.button() == Qt.LeftButton and self.image is not None and self.current_tool:# 左键:使用工具self.is_drawing = Truepos = self.get_image_pos(event.pos())if pos:self.last_point = posself.continuous_pos = posif self.current_tool in ['enlarge', 'shrink', 'rotate']:self.apply_effect(pos)# 启动定时器实现持续效果self.start_continuous_effect()elif self.current_tool == 'eraser':# 橡皮擦:开始擦除self.apply_eraser(pos)elif event.button() == Qt.RightButton and self.image is not None:# 右键:拖拽视图self.is_panning = Trueself.pan_start_pos = event.pos()self.setCursor(Qt.ClosedHandCursor)def mouseMoveEvent(self, event):"""鼠标移动事件"""if self.is_panning and self.pan_start_pos:# 右键拖拽视图delta = event.pos() - self.pan_start_posself.offset_x += delta.x()self.offset_y += delta.y()# 限制偏移范围if self.image is not None:h, w = self.image.shape[:2]widget_aspect = self.width() / self.height()image_aspect = w / hif widget_aspect > image_aspect:base_height = self.height()base_width = int(base_height * image_aspect)else:base_width = self.width()base_height = int(base_width / image_aspect)display_width = int(base_width * self.zoom_scale)display_height = int(base_height * self.zoom_scale)# 限制偏移max_offset_x = max(0, display_width - self.width())max_offset_y = max(0, display_height - self.height())self.offset_x = max(-max_offset_x, min(0, self.offset_x))self.offset_y = max(-max_offset_y, min(0, self.offset_y))self.pan_start_pos = event.pos()self.update_display()elif self.is_drawing and self.image is not None and self.current_tool:# 左键使用工具pos = self.get_image_pos(event.pos())if pos:if self.current_tool == 'drag':self.apply_drag_effect(self.last_point, pos)self.last_point = poselif self.current_tool == 'eraser':# 橡皮擦:沿路径擦除if self.last_point:self.apply_eraser_path(self.last_point, pos)self.last_point = poselif self.current_tool in ['enlarge', 'shrink', 'rotate']:# 移动时更新持续效果的位置self.continuous_pos = posself.last_point = posdef mouseReleaseEvent(self, event):"""鼠标释放事件"""if event.button() == Qt.LeftButton:self.is_drawing = Falseself.last_point = Noneself.continuous_pos = None# 停止持续效果self.stop_continuous_effect()elif event.button() == Qt.RightButton:self.is_panning = Falseself.pan_start_pos = Noneself.setCursor(Qt.ArrowCursor)def start_continuous_effect(self):"""启动持续效果定时器"""from PyQt5.QtCore import QTimerif self.continuous_timer is None:self.continuous_timer = QTimer(self)self.continuous_timer.timeout.connect(self.apply_continuous_effect)self.continuous_timer.start(50) # 每50ms应用一次效果def stop_continuous_effect(self):"""停止持续效果定时器"""if self.continuous_timer is not None:self.continuous_timer.stop()def apply_continuous_effect(self):"""持续应用效果"""if self.continuous_pos and self.current_tool in ['enlarge', 'shrink', 'rotate']:self.apply_effect(self.continuous_pos)def apply_effect(self, center):"""应用效果(放大、缩小、旋转)"""if self.image is None:returnh, w = self.image.shape[:2]cx, cy = center.x(), center.y()# 确保中心点在图像范围内if cx < 0 or cy < 0 or cx >= w or cy >= h:return# 创建网格y_indices, x_indices = np.mgrid[0:h, 0:w]# 计算到中心点的距离dx = x_indices - cxdy = y_indices - cydistances = np.sqrt(dx**2 + dy**2)# 创建影响掩码(在半径内的像素)mask = distances <= self.brush_radiusif self.current_tool == 'enlarge':# 放大效果:像素向内收缩(修正:原来的shrink逻辑)factor = 1.0 - (distances / self.brush_radius)factor = np.clip(factor, 0, 1)factor = factor ** 2 # 平滑过渡# 使用effect_strength控制强度scale = 1.0 - self.effect_strength * factor * masknew_x = cx + dx * scalenew_y = cy + dy * scaleelif self.current_tool == 'shrink':# 缩小效果:像素向外扩展(修正:原来的enlarge逻辑)factor = 1.0 - (distances / self.brush_radius)factor = np.clip(factor, 0, 1)factor = factor ** 2# 使用effect_strength控制强度scale = 1.0 + self.effect_strength * factor * masknew_x = cx + dx * scalenew_y = cy + dy * scaleelif self.current_tool == 'rotate':# 旋转效果:像素螺旋旋转# rotate_direction: 1为顺时针,-1为逆时针angle = np.where(mask, (1.0 - distances / self.brush_radius) * np.pi * self.effect_strength * self.rotate_direction, 0)cos_angle = np.cos(angle)sin_angle = np.sin(angle)new_x = cx + dx * cos_angle - dy * sin_anglenew_y = cy + dx * sin_angle + dy * cos_angleelse:return# 限制坐标范围new_x = np.clip(new_x, 0, w - 1)new_y = np.clip(new_y, 0, h - 1)# 使用双线性插值进行重映射new_image = cv2.remap(self.image, new_x.astype(np.float32), new_y.astype(np.float32), cv2.INTER_LINEAR)self.image = new_imageself.update_display()def apply_eraser(self, center, update_display=True):"""应用橡皮擦效果(恢复原始图像)- 使用NumPy向量化优化"""if self.image is None or self.original_image is None:returnh, w = self.image.shape[:2]cx, cy = center.x(), center.y()# 确保中心点在图像范围内if cx < 0 or cy < 0 or cx >= w or cy >= h:return# 计算画笔范围x1 = max(0, cx - self.brush_radius)y1 = max(0, cy - self.brush_radius)x2 = min(w, cx + self.brush_radius)y2 = min(h, cy + self.brush_radius)# 使用NumPy向量化操作代替双重循环y_coords, x_coords = np.ogrid[y1:y2, x1:x2]# 计算距离矩阵distances = np.sqrt((x_coords - cx)**2 + (y_coords - cy)**2)# 创建圆形遮罩mask = distances <= self.brush_radiusif not mask.any():return# 计算混合因子(边缘羽化)factor = np.zeros_like(distances)factor[mask] = 1.0 - (distances[mask] / self.brush_radius)factor = factor ** 0.5 # 平滑过渡曲线# 扩展factor到3通道factor_3d = factor[:, :, np.newaxis]# 向量化混合操作region = self.image[y1:y2, x1:x2]original_region = self.original_image[y1:y2, x1:x2]# 只混合mask为True的区域mask_3d = mask[:, :, np.newaxis]blended = region * (1 - factor_3d) + original_region * factor_3dself.image[y1:y2, x1:x2] = np.where(mask_3d, blended, region).astype(np.uint8)if update_display:self.update_display()def apply_eraser_path(self, start_pos, end_pos):"""沿路径应用橡皮擦效果(优化版)"""if self.image is None or self.original_image is None or start_pos is None:return# 计算路径上的点sx, sy = start_pos.x(), start_pos.y()ex, ey = end_pos.x(), end_pos.y()# 计算距离和步数dist = np.sqrt((ex - sx)**2 + (ey - sy)**2)if dist < 1:return# 根据画笔半径调整步数,大画笔可以用更少的步数step_size = max(self.brush_radius // 4, 3)steps = max(int(dist / step_size), 1)# 沿路径插值多个点,批量处理,不每次都刷新for i in range(steps + 1):t = i / steps if steps > 0 else 0px = int(sx + (ex - sx) * t)py = int(sy + (ey - sy) * t)# 只在最后一个点才刷新显示self.apply_eraser(QPoint(px, py), update_display=(i == steps))def apply_drag_effect(self, start_pos, end_pos):"""应用拖拽扭曲效果"""if self.image is None or start_pos is None:returnh, w = self.image.shape[:2]sx, sy = start_pos.x(), start_pos.y()ex, ey = end_pos.x(), end_pos.y()# 计算拖拽向量drag_x = ex - sxdrag_y = ey - syif drag_x == 0 and drag_y == 0:return# 创建网格y_indices, x_indices = np.mgrid[0:h, 0:w]# 计算到起始点的距离dx = x_indices - sxdy = y_indices - sydistances = np.sqrt(dx**2 + dy**2)# 创建影响掩码mask = distances <= self.brush_radius# 计算影响因子(距离越近影响越大)factor = np.where(distances < self.brush_radius,1.0 - (distances / self.brush_radius),0)factor = factor ** 2 # 平滑过渡# 应用拖拽位移,使用effect_strength控制强度new_x = x_indices - drag_x * factor * mask * self.effect_strengthnew_y = y_indices - drag_y * factor * mask * self.effect_strength# 限制坐标范围new_x = np.clip(new_x, 0, w - 1)new_y = np.clip(new_y, 0, h - 1)# 重映射new_image = cv2.remap(self.image, new_x.astype(np.float32),new_y.astype(np.float32), cv2.INTER_LINEAR)self.image = new_imageself.update_display()def reset_image(self):"""重置图像到原始状态"""if self.original_image is not None:self.image = self.original_image.copy()# 重置缩放和偏移self.zoom_scale = 1.0self.offset_x = 0self.offset_y = 0self.update_display()def wheelEvent(self, event):"""鼠标滚轮事件(Ctrl+滚轮缩放)"""if self.image is None:return# 检测Ctrl键modifiers = QApplication.keyboardModifiers()if modifiers == Qt.ControlModifier:# 获取鼠标位置对应的图像坐标mouse_pos = event.pos()old_image_pos = self.get_image_pos(mouse_pos)# 计算缩放增量delta = event.angleDelta().y()zoom_factor = 1.1 if delta > 0 else 0.9# 更新缩放比例old_zoom = self.zoom_scaleself.zoom_scale *= zoom_factor# 限制缩放范围self.zoom_scale = max(0.1, min(10.0, self.zoom_scale))# 如果缩放真的改变了,调整偏移以保持鼠标位置下的图像点不变if old_zoom != self.zoom_scale and old_image_pos:# 计算新的显示尺寸h, w = self.image.shape[:2]widget_aspect = self.width() / self.height()image_aspect = w / hif widget_aspect > image_aspect:base_height = self.height()base_width = int(base_height * image_aspect)else:base_width = self.width()base_height = int(base_width / image_aspect)old_display_width = int(base_width * old_zoom)old_display_height = int(base_height * old_zoom)new_display_width = int(base_width * self.zoom_scale)new_display_height = int(base_height * self.zoom_scale)# 计算鼠标在显示图像中的相对位置if old_display_width <= self.width():old_x_offset = (self.width() - old_display_width) // 2else:old_x_offset = self.offset_xif old_display_height <= self.height():old_y_offset = (self.height() - old_display_height) // 2else:old_y_offset = self.offset_yimg_x_in_display = mouse_pos.x() - old_x_offsetimg_y_in_display = mouse_pos.y() - old_y_offset# 计算新的偏移以保持鼠标下的点不变ratio_x = img_x_in_display / old_display_width if old_display_width > 0 else 0.5ratio_y = img_y_in_display / old_display_height if old_display_height > 0 else 0.5new_x_in_display = ratio_x * new_display_widthnew_y_in_display = ratio_y * new_display_heightif new_display_width > self.width():self.offset_x = mouse_pos.x() - new_x_in_displayelse:self.offset_x = 0if new_display_height > self.height():self.offset_y = mouse_pos.y() - new_y_in_displayelse:self.offset_y = 0# 限制偏移范围max_offset_x = max(0, new_display_width - self.width())max_offset_y = max(0, new_display_height - self.height())self.offset_x = max(-max_offset_x, min(0, self.offset_x))self.offset_y = max(-max_offset_y, min(0, self.offset_y))self.update_display()# 更新状态栏显示缩放比例if self.parent_window:self.parent_window.statusBar().showMessage(f"缩放: {self.zoom_scale:.1f}x")event.accept()else:event.ignore()def dragEnterEvent(self, event):"""拖拽进入事件"""if event.mimeData().hasUrls():# 检查是否是图片文件urls = event.mimeData().urls()if urls:file_path = urls[0].toLocalFile()if file_path.lower().endswith(('.png', '.jpg', '.jpeg', '.bmp', '.gif')):event.acceptProposedAction()# 显示拖拽提示self.setStyleSheet("""QLabel { background-color: #1a4d2e; color: #4CAF50;font-size: 24px;font-weight: bold;border: 3px dashed #4CAF50;border-radius: 10px;}""")if self.image is None:self.setText('✓ 松开鼠标即可上传图片')def dragLeaveEvent(self, event):"""拖拽离开事件"""if self.image is None:# 恢复原始样式self.setStyleSheet("""QLabel { background-color: #2b2b2b; color: #9E9E9E;font-size: 24px;font-weight: bold;border: 3px dashed #555;border-radius: 10px;}""")self.setText('📁 点击"上传图片"按钮\n或\n拖拽图片到此处')def dropEvent(self, event):"""拖拽放下事件"""if event.mimeData().hasUrls():urls = event.mimeData().urls()if urls:file_path = urls[0].toLocalFile()if file_path.lower().endswith(('.png', '.jpg', '.jpeg', '.bmp', '.gif')):# 加载图片if self.load_image(file_path):if self.parent_window:self.parent_window.statusBar().showMessage(f"已加载: {file_path}")event.acceptProposedAction()else:if self.parent_window:self.parent_window.statusBar().showMessage("加载图片失败!")# 恢复提示self.setStyleSheet("""QLabel { background-color: #2b2b2b; color: #9E9E9E;font-size: 24px;font-weight: bold;border: 3px dashed #555;border-radius: 10px;}""")self.setText('📁 点击"上传图片"按钮\n或\n拖拽图片到此处')def resizeEvent(self, event):"""窗口大小改变时重新显示图像"""super().resizeEvent(event)self.update_display()class FaceWarpApp(QMainWindow):"""捏脸APP主窗口"""def __init__(self):super().__init__()self.init_ui()def init_ui(self):"""初始化用户界面"""self.setWindowTitle("捏脸APP - 图像变形工具")self.setGeometry(100, 100, 1600, 900)self.setMinimumSize(1600, 800) # 设置最小窗口尺寸,确保所有按钮可见# 创建中心部件central_widget = QWidget()self.setCentralWidget(central_widget)# 主布局main_layout = QVBoxLayout(central_widget)main_layout.setSpacing(15)main_layout.setContentsMargins(15, 15, 15, 15)# 创建工具栏self.create_toolbar()# 创建图像画布self.canvas = ImageCanvas(self)main_layout.addWidget(self.canvas)# 创建控制面板control_panel = self.create_control_panel()main_layout.addLayout(control_panel)# 设置样式self.setStyleSheet("""QMainWindow {background-color: #1a1a1a;}QWidget {font-family: "Microsoft YaHei UI", "Segoe UI", Arial;}QPushButton {background-color: #2196F3;color: white;border: none;padding: 10px 20px;border-radius: 6px;font-size: 15px;font-weight: bold;min-width: 100px;min-height: 42px;}QPushButton:hover {background-color: #42A5F5;}QPushButton:pressed {background-color: #1976D2;}QPushButton:checked {background-color: #4CAF50;}QLabel {color: #E0E0E0;font-size: 16px;font-weight: bold;}QSlider::groove:horizontal {height: 10px;background: #424242;border-radius: 5px;}QSlider::handle:horizontal {background: #2196F3;width: 22px;height: 22px;margin: -6px 0;border-radius: 11px;}QSlider::handle:horizontal:hover {background: #42A5F5;}QToolBar {background-color: #2d2d2d;border: none;spacing: 10px;padding: 10px;}QToolButton {background-color: #2196F3;color: white;border: none;padding: 10px 18px;border-radius: 6px;font-size: 15px;font-weight: bold;min-width: 95px;min-height: 42px;}QToolButton:hover {background-color: #42A5F5;}QToolButton:pressed {background-color: #1976D2;}QToolButton:checked {background-color: #4CAF50;}QStatusBar {background-color: #2d2d2d;color: #E0E0E0;font-size: 14px;}""")def create_toolbar(self):"""创建工具栏"""from PyQt5.QtCore import QSizetoolbar = QToolBar()toolbar.setMovable(False)toolbar.setIconSize(QSize(28, 28))toolbar.setToolButtonStyle(Qt.ToolButtonTextBesideIcon)toolbar.setStyleSheet("""QToolBar {spacing: 8px;}""")self.addToolBar(toolbar)# 上传图片按钮upload_action = QAction("📁 上传图片", self)upload_action.triggered.connect(self.load_image)toolbar.addAction(upload_action)toolbar.addSeparator()# 工具按钮self.enlarge_btn = QAction("🔍 放大", self)self.enlarge_btn.setCheckable(True)self.enlarge_btn.triggered.connect(lambda: self.set_tool('enlarge'))toolbar.addAction(self.enlarge_btn)self.shrink_btn = QAction("🔎 缩小", self)self.shrink_btn.setCheckable(True)self.shrink_btn.triggered.connect(lambda: self.set_tool('shrink'))toolbar.addAction(self.shrink_btn)self.drag_btn = QAction("✋ 拖拽扭曲", self)self.drag_btn.setCheckable(True)self.drag_btn.triggered.connect(lambda: self.set_tool('drag'))toolbar.addAction(self.drag_btn)self.rotate_btn = QAction("🔄 旋转", self)self.rotate_btn.setCheckable(True)self.rotate_btn.triggered.connect(lambda: self.set_tool('rotate'))toolbar.addAction(self.rotate_btn)self.eraser_btn = QAction("🧹 橡皮擦", self)self.eraser_btn.setCheckable(True)self.eraser_btn.triggered.connect(lambda: self.set_tool('eraser'))toolbar.addAction(self.eraser_btn)toolbar.addSeparator()# 重置按钮reset_action = QAction("↺ 重置", self)reset_action.triggered.connect(self.reset_image)toolbar.addAction(reset_action)# 保存按钮save_action = QAction("💾 保存", self)save_action.triggered.connect(self.save_image)toolbar.addAction(save_action)self.tool_buttons = [self.enlarge_btn, self.shrink_btn, self.drag_btn, self.rotate_btn, self.eraser_btn]def create_control_panel(self):"""创建控制面板"""from PyQt5.QtWidgets import QButtonGroup, QRadioButton, QGroupBox, QLineEditfrom PyQt5.QtGui import QIntValidator, QDoubleValidatorlayout = QHBoxLayout()layout.setSpacing(20)# 画笔半径控制radius_label = QLabel("画笔半径:")layout.addWidget(radius_label)self.radius_slider = QSlider(Qt.Horizontal)self.radius_slider.setMinimum(10)self.radius_slider.setMaximum(800) # 提高到800self.radius_slider.setValue(170) # 默认170self.radius_slider.setMinimumWidth(250)self.radius_slider.valueChanged.connect(self.update_brush_radius_from_slider)layout.addWidget(self.radius_slider)self.radius_value_label = QLabel("170 px")self.radius_value_label.setMinimumWidth(70)layout.addWidget(self.radius_value_label)# 画笔半径输入框self.radius_input = QLineEdit()self.radius_input.setText("170")self.radius_input.setMaximumWidth(60)self.radius_input.setValidator(QIntValidator(10, 800))self.radius_input.setStyleSheet("""QLineEdit {background-color: #3d3d3d;color: #E0E0E0;border: 2px solid #555;border-radius: 4px;padding: 5px;font-size: 15px;}QLineEdit:focus {border: 2px solid #2196F3;}""")self.radius_input.returnPressed.connect(self.update_brush_radius_from_input)self.radius_input.editingFinished.connect(self.update_brush_radius_from_input)layout.addWidget(self.radius_input)layout.addSpacing(30)# 变化强度控制strength_label = QLabel("变化强度:")layout.addWidget(strength_label)self.strength_slider = QSlider(Qt.Horizontal)self.strength_slider.setMinimum(1) # 0.01self.strength_slider.setMaximum(100) # 1.0self.strength_slider.setValue(20) # 默认0.20self.strength_slider.setMinimumWidth(250)self.strength_slider.valueChanged.connect(self.update_effect_strength_from_slider)layout.addWidget(self.strength_slider)self.strength_value_label = QLabel("0.20")self.strength_value_label.setMinimumWidth(50)layout.addWidget(self.strength_value_label)# 变化强度输入框self.strength_input = QLineEdit()self.strength_input.setText("0.20")self.strength_input.setMaximumWidth(60)# 允许输入0.01-1.0,最多3位小数(如0.001)strength_validator = QDoubleValidator(0.01, 1.0, 3)strength_validator.setNotation(QDoubleValidator.StandardNotation)self.strength_input.setValidator(strength_validator)self.strength_input.setStyleSheet("""QLineEdit {background-color: #3d3d3d;color: #E0E0E0;border: 2px solid #555;border-radius: 4px;padding: 5px;font-size: 15px;}QLineEdit:focus {border: 2px solid #2196F3;}""")self.strength_input.returnPressed.connect(self.update_effect_strength_from_input)self.strength_input.editingFinished.connect(self.update_effect_strength_from_input)layout.addWidget(self.strength_input)layout.addSpacing(30)# 旋转方向控制rotate_group_box = QGroupBox("旋转方向")rotate_group_box.setStyleSheet("""QGroupBox {color: #E0E0E0;font-size: 16px;font-weight: bold;border: 2px solid #424242;border-radius: 8px;margin-top: 10px;padding-top: 10px;}QGroupBox::title {subcontrol-origin: margin;left: 15px;padding: 0 5px;}QRadioButton {color: #E0E0E0;font-size: 15px;spacing: 8px;}QRadioButton::indicator {width: 20px;height: 20px;}QRadioButton::indicator:unchecked {border: 2px solid #666;border-radius: 10px;background: #2d2d2d;}QRadioButton::indicator:checked {border: 2px solid #2196F3;border-radius: 10px;background: #2196F3;}""")rotate_layout = QHBoxLayout()rotate_layout.setSpacing(15)self.clockwise_radio = QRadioButton("⟳ 顺时针")self.clockwise_radio.setChecked(True)self.clockwise_radio.toggled.connect(lambda: self.set_rotate_direction(1))self.counterclockwise_radio = QRadioButton("⟲ 逆时针")self.counterclockwise_radio.toggled.connect(lambda: self.set_rotate_direction(-1))rotate_layout.addWidget(self.clockwise_radio)rotate_layout.addWidget(self.counterclockwise_radio)rotate_group_box.setLayout(rotate_layout)layout.addWidget(rotate_group_box)layout.addStretch()return layoutdef set_tool(self, tool_name):"""设置当前工具"""# 取消其他工具的选中状态for btn in self.tool_buttons:btn.setChecked(False)# 设置当前工具if tool_name == 'enlarge':self.enlarge_btn.setChecked(True)elif tool_name == 'shrink':self.shrink_btn.setChecked(True)elif tool_name == 'drag':self.drag_btn.setChecked(True)elif tool_name == 'rotate':self.rotate_btn.setChecked(True)elif tool_name == 'eraser':self.eraser_btn.setChecked(True)self.canvas.current_tool = tool_namedef update_brush_radius_from_slider(self, value):"""从滑块更新画笔半径"""self.canvas.brush_radius = valueself.radius_value_label.setText(f"{value} px")self.radius_input.setText(str(value))def update_brush_radius_from_input(self):"""从输入框更新画笔半径"""try:value = int(self.radius_input.text())value = max(10, min(800, value)) # 限制范围self.canvas.brush_radius = valueself.radius_slider.setValue(value)self.radius_value_label.setText(f"{value} px")self.radius_input.setText(str(value))except ValueError:# 输入无效,恢复当前值self.radius_input.setText(str(self.canvas.brush_radius))def update_effect_strength_from_slider(self, value):"""从滑块更新变化强度"""strength = value / 100.0 # 转换为0.01-1.0self.canvas.effect_strength = strengthself.strength_value_label.setText(f"{strength:.2f}")self.strength_input.setText(f"{strength:.2f}")def update_effect_strength_from_input(self):"""从输入框更新变化强度"""try:value = float(self.strength_input.text())value = max(0.01, min(1.0, value)) # 限制范围self.canvas.effect_strength = valueself.strength_slider.setValue(int(value * 100))self.strength_value_label.setText(f"{value:.2f}")self.strength_input.setText(f"{value:.2f}")except ValueError:# 输入无效,恢复当前值self.strength_input.setText(f"{self.canvas.effect_strength:.2f}")def set_rotate_direction(self, direction):"""设置旋转方向"""self.canvas.rotate_direction = directiondef load_image(self):"""加载图像"""file_path, _ = QFileDialog.getOpenFileName(self, "选择图片", "", "图片文件 (*.png *.jpg *.jpeg *.bmp);;所有文件 (*.*)")if file_path:if self.canvas.load_image(file_path):self.statusBar().showMessage(f"已加载: {file_path}")else:self.statusBar().showMessage("加载图片失败!")def reset_image(self):"""重置图像"""self.canvas.reset_image()self.statusBar().showMessage("已重置图像")def save_image(self):"""保存图像(支持中文路径)"""if self.canvas.image is None:self.statusBar().showMessage("没有可保存的图像!")returnfile_path, _ = QFileDialog.getSaveFileName(self, "保存图片", "", "PNG文件 (*.png);;JPEG文件 (*.jpg);;所有文件 (*.*)")if file_path:try:# RGB转BGRimage_bgr = cv2.cvtColor(self.canvas.image, cv2.COLOR_RGB2BGR)# 使用cv2.imencode()和tofile()保存,支持中文路径# 根据文件扩展名确定编码格式ext = file_path.lower().split('.')[-1]if ext in ['jpg', 'jpeg']:encode_param = [int(cv2.IMWRITE_JPEG_QUALITY), 95]_, encoded_img = cv2.imencode('.jpg', image_bgr, encode_param)else:_, encoded_img = cv2.imencode('.png', image_bgr)encoded_img.tofile(file_path)self.statusBar().showMessage(f"已保存: {file_path}")except Exception as e:self.statusBar().showMessage(f"保存失败: {str(e)}")def main():app = QApplication(sys.argv)window = FaceWarpApp()window.show()sys.exit(app.exec_())if __name__ == '__main__':main()
代码亮点:
- ✅ 1098行完整代码
- ✅ 详细中文注释
- ✅ 面向对象设计
- ✅ 模块化结构
- ✅ 支持中文路径
🎯 系统要求
| 项目 | 要求 |
|---|---|
| 操作系统 | Windows / macOS / Linux |
| Python版本 | ≥ 3.6 |
| 内存 | 建议 2GB 以上 |
| 磁盘空间 | 约 500MB(含依赖库) |
| 支持格式 | PNG, JPG, JPEG, BMP, GIF |
❓ 常见问题 FAQ
Q1: 运行时提示"No module named ‘PyQt5’"
解决方案:
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple PyQt5
Q2: 图片变形后画质下降怎么办?
原因:使用了过大的画笔半径和强度
建议:
- 使用较小的画笔半径(50-150)
- 降低变化强度(0.1-0.3)
- 多次小幅度变形,避免一次性大幅度变形
Q3: 支持哪些图片格式?
支持常见的图片格式:PNG、JPG、JPEG、BMP、GIF
Q4: 如何撤销操作?
目前没有逐步撤销功能,但可以:
- 使用
🧹 橡皮擦工具局部恢复原图 - 点击
↺ 重置按钮恢复到原始状态
Q5: Windows下运行报错"DLL load failed"
解决方案:
# 重新安装opencv-python
pip uninstall opencv-python
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple opencv-python
Q6: 中文路径下无法加载图片?
本工具已经完美支持中文路径!使用了 np.fromfile() 和 tofile() 方法。
Q7: 能处理多大的图片?
理论上没有限制,但建议:
- 普通照片:≤ 4000×3000 像素
- 高分辨率图片:可能会变慢,建议先缩小
🚀 未来改进方向
- 撤销/重做功能:支持多步撤销
- 图层系统:支持多图层编辑
- 更多工具:添加模糊、锐化等滤镜
- 预设效果:一键应用常见变形
- 批量处理:支持批量处理多张图片
- 动画导出:导出变形过程为GIF或视频
- AI辅助:自动识别人脸关键点
- 性能优化:GPU加速处理
📝 许可证
本项目采用 MIT License 开源协议。
MIT LicenseCopyright (c) 2024 FacePrankPermission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
🎉 总结
FacePrank 是一个功能丰富、使用简单的照片整蛊工具。通过五大核心变形工具,你可以轻松制作各种搞笑照片。无论是恶搞朋友、制作表情包,还是进行创意设计,这个工具都能满足你的需求!
核心优势:
- ✅ 纯Python实现,代码简洁易懂
- ✅ 基于NumPy向量化运算,性能优秀
- ✅ 现代化UI设计,操作流畅
- ✅ 完美支持中文路径
- ✅ 开箱即用,无需复杂配置
快来试试吧! 🎭
📚 附录:requirements.txt
PyQt5>=5.15.0
opencv-python>=4.5.0
numpy>=1.19.0
安装命令(使用清华镜像源):
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple -r requirements.txt
单独安装各个包:
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple PyQt5>=5.15.0
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple opencv-python>=4.5.0
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple numpy>=1.19.0
🔗 相关链接
- PyQt5 官方文档:https://www.riverbankcomputing.com/static/Docs/PyQt5/
- OpenCV Python 教程:https://docs.opencv.org/master/d6/d00/tutorial_py_root.html
- NumPy 官方文档:https://numpy.org/doc/stable/
- 清华大学开源软件镜像站:https://mirrors.tuna.tsinghua.edu.cn/help/pypi/
💡 提示:如果觉得这个项目有帮助,欢迎点赞⭐、收藏📝、分享🔗!有问题欢迎在评论区留言交流!
关键词:Python PyQt5 OpenCV 图像处理 照片整蛊 捏脸工具 图像变形 NumPy GUI开发
最后更新:2025年
🎉 祝你玩得开心! 🎉
