Python+pyqt5實(shí)現(xiàn)一個圖像變形工具
效果展示


在此處插入: 整蠱后的照片
用FacePrank可以輕松實(shí)現(xiàn):放大眼睛、縮小鼻子、扭曲表情、旋轉(zhuǎn)漩渦等各種搞笑效果!
項(xiàng)目簡介
FacePrank 是一個功能強(qiáng)大的圖像變形工具,使用Python和PyQt5開發(fā)。無需復(fù)雜的PS技能,只需鼠標(biāo)點(diǎn)擊拖拽,就能對照片進(jìn)行各種有趣的變形處理。無論是惡搞朋友、制作表情包,還是進(jìn)行創(chuàng)意設(shè)計,這個工具都能滿足你的需求!
核心特性
- 簡單易用:純鼠標(biāo)操作,無需專業(yè)技能
- 五大工具:放大、縮小、拖拽扭曲、旋轉(zhuǎn)、橡皮擦
- 拖拽上傳:支持直接拖拽圖片到窗口
- Ctrl+滾輪縮放:精確查看和編輯細(xì)節(jié)
- 右鍵拖拽視圖:自由移動查看區(qū)域
- 參數(shù)可調(diào):畫筆半徑、變化強(qiáng)度隨心調(diào)節(jié)
- 中文路徑支持:完美支持中文文件名和路徑
- 實(shí)時預(yù)覽:所見即所得的編輯體驗(yàn)
環(huán)境配置
方式一:使用Conda創(chuàng)建虛擬環(huán)境(推薦)
第一步:創(chuàng)建虛擬環(huán)境
conda create -n faceprank python=3.8
第二步:激活環(huán)境
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 | 數(shù)值計算和數(shù)組操作 |
快速啟動
克隆或下載項(xiàng)目
# 如果你有g(shù)it git clone <項(xiàng)目地址> cd FacePrank # 或者直接下載ZIP解壓
運(yùn)行程序
python main.py
啟動成功后,會看到一個現(xiàn)代化的深色主題界面,中央有提示文字:“?? 點(diǎn)擊上傳圖片按鈕 或 拖拽圖片到此處”
使用指南
基本操作流程
上傳圖片
- 方法一:點(diǎn)擊工具欄的
上傳圖片按鈕,選擇圖片文件 - 方法二:直接拖拽圖片文件到窗口中(推薦!更方便)
支持的格式:PNG、JPG、JPEG、BMP、GIF
選擇工具
在頂部工具欄選擇你需要的變形工具:
| 工具圖標(biāo) | 工具名稱 | 快捷說明 |
|---|---|---|
| ?? | 放大工具 | 點(diǎn)擊位置向外擴(kuò)展,產(chǎn)生凸起效果 |
| ?? | 縮小工具 | 點(diǎn)擊位置向內(nèi)收縮,產(chǎn)生凹陷效果 |
| ? | 拖拽扭曲 | 拖拽鼠標(biāo)實(shí)現(xiàn)推拉扭曲效果 |
| ?? | 旋轉(zhuǎn)工具 | 點(diǎn)擊產(chǎn)生螺旋漩渦效果 |
| ?? | 橡皮擦 | 擦除變形,恢復(fù)原圖 |
調(diào)整參數(shù)
1.畫筆半徑:控制影響范圍(10-800像素,默認(rèn)170)
- 滑塊拖動或直接輸入數(shù)值
- 較小半徑適合精細(xì)調(diào)整
- 較大半徑適合大范圍變形
2.變化強(qiáng)度:控制變形程度(0.01-1.0,默認(rèn)0.20)
- 數(shù)值越大,變形越明顯
- 建議從小值開始嘗試
3.旋轉(zhuǎn)方向:僅旋轉(zhuǎn)工具有效
- ? 順時針
- ? 逆時針
查看與導(dǎo)航
1.Ctrl + 滾輪:放大縮小圖像(0.1x - 10x)
- 鼠標(biāo)位置為縮放中心
- 適合查看和編輯細(xì)節(jié)
2.右鍵拖拽:移動視圖位置
- 按住鼠標(biāo)右鍵拖動
- 配合縮放功能精確編輯
開始編輯
1.放大/縮小/旋轉(zhuǎn):左鍵點(diǎn)擊圖像位置
- 按住不放會持續(xù)累積效果
- 移動鼠標(biāo)位置會跟隨
2.拖拽扭曲:按住鼠標(biāo)左鍵拖動
拖拽路徑上的像素會隨鼠標(biāo)移動
3.橡皮擦:按住鼠標(biāo)左鍵拖動
經(jīng)過區(qū)域恢復(fù)為原始圖像
保存結(jié)果
點(diǎn)擊 保存 按鈕,選擇保存路徑和格式(PNG或JPG)
使用技巧
- 拖拽上傳最方便:直接把圖片拖到窗口,有綠色高亮提示
- 放大查看細(xì)節(jié):Ctrl+滾輪放大圖像,可以看清每個像素
- 小畫筆精細(xì)調(diào)整:處理眼睛、鼻子等小部位時,減小畫筆半徑
- 強(qiáng)度從小到大:先用小強(qiáng)度測試效果,再逐步加大
- 按住鼠標(biāo)累積:需要明顯效果時,按住鼠標(biāo)不放
- 多次點(diǎn)擊加強(qiáng):在同一位置多次點(diǎn)擊可以加強(qiáng)效果
- 局部恢復(fù)原圖:用橡皮擦可以只修正某些過度變形的區(qū)域
- 一鍵重置:點(diǎn)擊重置按鈕恢復(fù)原圖(包括縮放和偏移)
創(chuàng)意玩法
- 放大眼睛:使用放大工具點(diǎn)擊眼睛位置,制作大眼萌照
- 縮小鼻子:使用縮小工具點(diǎn)擊鼻子,打造精致小臉
- 扭曲表情:使用拖拽工具拉扯嘴角,制作搞笑表情
- 漩渦背景:使用旋轉(zhuǎn)工具點(diǎn)擊背景,營造魔幻效果
- 組合變形:多種工具配合使用,創(chuàng)造獨(dú)特效果
五大核心工具詳解
工具一:放大工具
作用原理:點(diǎn)擊的區(qū)域像素會向外擴(kuò)展擠壓,產(chǎn)生凸起效果
適用場景:
- 放大眼睛,制作大眼萌照
- 放大嘴巴,制作夸張表情
- 突出某個特定部位
使用方法:
- 選擇
放大工具 - 調(diào)整畫筆半徑(建議50-150)
- 左鍵點(diǎn)擊要放大的位置
- 按住不放可以持續(xù)放大
效果示例:眼睛從正常大小變成圓圓的大眼睛
工具二:縮小工具
作用原理:半徑內(nèi)的像素向中心靠攏收縮,產(chǎn)生凹陷效果
適用場景:
- 縮小鼻子,打造小巧鼻梁
- 縮小臉頰,制作瓜子臉
- 減小某些突出部位
使用方法:
- 選擇
縮小工具 - 調(diào)整畫筆半徑(建議40-120)
- 左鍵點(diǎn)擊要縮小的位置
- 按住不放可以持續(xù)縮小
提示:縮小工具與放大工具效果相反,可以互相配合使用
工具三:拖拽扭曲工具
作用原理:路徑上的像素會隨著鼠標(biāo)移動而拖拽變形
適用場景:
- 推、拉、扭曲面部特征
- 制作各種搞怪表情
- 自由變形任何區(qū)域
使用方法:
- 選擇
拖拽扭曲工具 - 調(diào)整畫筆半徑和強(qiáng)度
- 按住鼠標(biāo)左鍵拖動
- 拖拽路徑會產(chǎn)生變形效果
技巧:這是最靈活的工具,可以實(shí)現(xiàn)各種創(chuàng)意變形
工具四:旋轉(zhuǎn)工具
作用原理:周圍像素會以點(diǎn)擊點(diǎn)為軸進(jìn)行螺旋旋轉(zhuǎn)
適用場景:
- 制作漩渦特效
- 營造魔幻氛圍
- 創(chuàng)造藝術(shù)效果
使用方法:
- 選擇
旋轉(zhuǎn)工具 - 選擇旋轉(zhuǎn)方向(?順時針 或 ?逆時針)
- 調(diào)整畫筆半徑(建議100-300)
- 左鍵點(diǎn)擊要旋轉(zhuǎn)的位置
- 按住不放產(chǎn)生更強(qiáng)烈的旋轉(zhuǎn)效果
創(chuàng)意玩法:在眼睛位置使用旋轉(zhuǎn)工具,制作催眠效果
工具五:橡皮擦工具
作用原理:擦除變形效果,將修改的區(qū)域還原為原始圖像
適用場景:
- 修正過度變形的區(qū)域
- 局部恢復(fù)原圖
- 精細(xì)調(diào)整效果
使用方法:
- 選擇
橡皮擦工具 - 調(diào)整畫筆半徑(擦除范圍)
- 按住鼠標(biāo)左鍵拖動
- 經(jīng)過的區(qū)域會恢復(fù)原樣
特點(diǎn):
- 支持邊緣羽化,過渡自然
- 只恢復(fù)經(jīng)過路徑和畫筆范圍內(nèi)的像素
- 可以配合其他工具實(shí)現(xiàn)精細(xì)控制
核心技術(shù)實(shí)現(xiàn)
技術(shù)架構(gòu)
FacePrank
├── 界面層 (PyQt5)
│ ├── 主窗口 (QMainWindow)
│ ├── 工具欄 (QToolBar)
│ ├── 畫布組件 (QLabel)
│ └── 控制面板 (QSlider + QLineEdit)
│
├── 圖像處理層 (OpenCV)
│ ├── 圖像加載與保存
│ ├── 格式轉(zhuǎn)換 (BGR ↔ RGB)
│ └── 重映射與插值
│
└── 算法層 (NumPy)
├── 網(wǎng)格映射變形
├── 距離場計算
├── 雙線性插值
└── 向量化運(yùn)算
變形算法原理
1. 放大/縮小算法
核心思想:基于距離的像素位移
# 計算到中心點(diǎn)的距離 distances = np.sqrt(dx**2 + dy**2) # 計算影響因子(越近影響越大) factor = 1.0 - (distances / brush_radius) factor = factor ** 2 # 平滑過渡 # 放大:像素向內(nèi)收縮 scale = 1.0 - strength * factor * mask new_x = cx + dx * scale new_y = cy + dy * scale # 縮?。合袼叵蛲鈹U(kuò)展 scale = 1.0 + strength * factor * mask new_x = cx + dx * scale new_y = cy + dy * scale
關(guān)鍵點(diǎn):
- 使用平方函數(shù)實(shí)現(xiàn)平滑過渡
- mask確保只影響半徑內(nèi)像素
- strength控制變形強(qiáng)度
2. 旋轉(zhuǎn)算法
核心思想:極坐標(biāo)旋轉(zhuǎn)變換
# 計算旋轉(zhuǎn)角度(距離越近旋轉(zhuǎn)越多) angle = (1.0 - distances / brush_radius) * π * strength * direction # 旋轉(zhuǎn)矩陣變換 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
關(guān)鍵點(diǎn):
- 使用旋轉(zhuǎn)矩陣進(jìn)行坐標(biāo)變換
- direction參數(shù)控制順時針/逆時針
- 角度隨距離衰減,產(chǎn)生漩渦效果
3. 拖拽扭曲算法
核心思想:路徑方向的力場扭曲
# 計算拖拽向量 drag_x = end_x - start_x drag_y = end_y - start_y # 計算影響因子 factor = (1.0 - distances / brush_radius) ** 2 # 應(yīng)用拖拽位移 new_x = x_indices - drag_x * factor * mask * strength new_y = y_indices - drag_y * factor * mask * strength
關(guān)鍵點(diǎn):
- 基于鼠標(biāo)移動方向計算位移向量
- 使用力場衰減實(shí)現(xiàn)自然過渡
- 支持連續(xù)拖拽的流暢效果
4. 橡皮擦算法(向量化優(yōu)化)
核心思想:權(quán)重混合原圖與變形圖
# 計算距離和混合因子 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
關(guān)鍵點(diǎn):
- 使用NumPy向量化代替雙重循環(huán),性能提升10倍以上
- 邊緣羽化實(shí)現(xiàn)自然過渡
- 只混合mask為True的區(qū)域
雙線性插值
所有變形算法最后都使用OpenCV的remap函數(shù)進(jìn)行雙線性插值:
new_image = cv2.remap(image,
new_x.astype(np.float32),
new_y.astype(np.float32),
cv2.INTER_LINEAR)
這確保了變形后圖像的平滑性和視覺質(zhì)量。
交互優(yōu)化
Ctrl+滾輪縮放實(shí)現(xiàn)
def wheelEvent(self, event):
if event.modifiers() == Qt.ControlModifier:
# 獲取鼠標(biāo)位置對應(yīng)的圖像坐標(biāo)
old_image_pos = self.get_image_pos(event.pos())
# 計算縮放增量
zoom_factor = 1.1 if delta > 0 else 0.9
self.zoom_scale *= zoom_factor
# 調(diào)整偏移以保持鼠標(biāo)位置下的圖像點(diǎn)不變
# ... 坐標(biāo)變換計算 ...
關(guān)鍵點(diǎn):
- 檢測Ctrl鍵修飾符
- 以鼠標(biāo)位置為中心進(jìn)行縮放
- 自動調(diào)整偏移量,保持縮放中心不變
右鍵拖拽視圖
def mouseMoveEvent(self, event):
if self.is_panning:
delta = event.pos() - self.pan_start_pos
self.offset_x += delta.x()
self.offset_y += delta.y()
# 限制偏移范圍...
持續(xù)效果實(shí)現(xiàn)
使用QTimer定時器實(shí)現(xiàn)按住鼠標(biāo)持續(xù)變形:
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應(yīng)用一次
中文路徑支持
使用NumPy的文件IO函數(shù)支持中文路徑:
# 加載圖像
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)
項(xiàng)目結(jié)構(gòu)
FacePrank/
├── main.py # 主程序文件(1098行)
│ ├── ImageCanvas類 # 圖像畫布和交互處理
│ │ ├── 圖像加載與顯示
│ │ ├── 鼠標(biāo)事件處理
│ │ ├── 五大變形算法
│ │ ├── 縮放和平移
│ │ └── 拖拽上傳支持
│ │
│ └── FaceWarpApp類 # 主窗口和UI
│ ├── 工具欄創(chuàng)建
│ ├── 控制面板
│ ├── 參數(shù)調(diào)節(jié)
│ └── 文件操作
│
├── requirements.txt # Python依賴
├── README.md # 項(xiàng)目說明
└── face.jpg # 示例圖片
完整源代碼
主程序:main.py
#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
捏臉APP - 簡單的圖像變形工具
支持放大、縮小、拖拽扭曲、旋轉(zhuǎn)等變形效果
"""
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 cv2
class ImageCanvas(QLabel):
"""圖像畫布類,處理圖像顯示和鼠標(biāo)交互"""
def __init__(self, parent=None):
super().__init__(parent)
self.parent_window = parent
self.image = None
self.display_image = None
self.original_image = None
self.current_tool = None # 當(dāng)前工具: 'enlarge', 'shrink', 'drag', 'rotate', 'eraser'
self.brush_radius = 170 # 默認(rèn)畫筆半徑
self.effect_strength = 0.20 # 變化強(qiáng)度,范圍0.01-1.0,默認(rèn)0.20
self.rotate_direction = 1 # 旋轉(zhuǎn)方向:1為順時針,-1為逆時針
self.is_drawing = False
self.last_point = None
# 用于持續(xù)效果的定時器
self.continuous_timer = None
self.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('?? 點(diǎn)擊"上傳圖片"按鈕\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轉(zhuǎn)RGB
self.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 True
except Exception as e:
print(f"加載圖片失敗: {e}")
return False
def update_display(self):
"""更新顯示的圖像(支持縮放和平移)"""
if self.image is None:
return
# 圖片加載后移除虛線邊框
self.setStyleSheet("""
QLabel {
background-color: #2b2b2b;
}
""")
h, w, ch = self.image.shape
bytes_per_line = ch * w
q_image = QImage(self.image.data, w, h, bytes_per_line, QImage.Format_RGB888)
# 創(chuàng)建pixmap
pixmap = QPixmap.fromImage(q_image)
# 應(yīng)用縮放
if self.zoom_scale != 1.0:
# 先按原始比例縮放到窗口
base_scaled = pixmap.scaled(self.size(), Qt.KeepAspectRatio, Qt.SmoothTransformation)
# 再應(yīng)用用戶縮放
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)
# 如果圖像大于窗口,需要裁剪并應(yīng)用偏移
if scaled_pixmap.width() > self.width() or scaled_pixmap.height() > self.height():
# 計算裁剪區(qū)域
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):
"""將窗口坐標(biāo)轉(zhuǎn)換為圖像坐標(biāo)(考慮縮放和偏移)"""
if self.image is None:
return None
h, w = self.image.shape[:2]
# 計算基礎(chǔ)縮放(適應(yīng)窗口)
widget_aspect = self.width() / self.height()
image_aspect = w / h
if 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)
# 應(yīng)用用戶縮放
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) // 2
else:
x_offset = self.offset_x
if display_height <= self.height():
y_offset = (self.height() - display_height) // 2
else:
y_offset = self.offset_y
# 轉(zhuǎn)換為顯示圖像坐標(biāo)
img_x = widget_pos.x() - x_offset
img_y = widget_pos.y() - y_offset
if img_x < 0 or img_y < 0 or img_x >= display_width or img_y >= display_height:
return None
# 縮放到原始圖像尺寸
scale_x = w / display_width
scale_y = h / display_height
orig_x = int(img_x * scale_x)
orig_y = int(img_y * scale_y)
# 確保在圖像范圍內(nèi)
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):
"""鼠標(biāo)按下事件"""
if event.button() == Qt.LeftButton and self.image is not None and self.current_tool:
# 左鍵:使用工具
self.is_drawing = True
pos = self.get_image_pos(event.pos())
if pos:
self.last_point = pos
self.continuous_pos = pos
if self.current_tool in ['enlarge', 'shrink', 'rotate']:
self.apply_effect(pos)
# 啟動定時器實(shí)現(xiàn)持續(xù)效果
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 = True
self.pan_start_pos = event.pos()
self.setCursor(Qt.ClosedHandCursor)
def mouseMoveEvent(self, event):
"""鼠標(biāo)移動事件"""
if self.is_panning and self.pan_start_pos:
# 右鍵拖拽視圖
delta = event.pos() - self.pan_start_pos
self.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 / h
if 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 = pos
elif self.current_tool == 'eraser':
# 橡皮擦:沿路徑擦除
if self.last_point:
self.apply_eraser_path(self.last_point, pos)
self.last_point = pos
elif self.current_tool in ['enlarge', 'shrink', 'rotate']:
# 移動時更新持續(xù)效果的位置
self.continuous_pos = pos
self.last_point = pos
def mouseReleaseEvent(self, event):
"""鼠標(biāo)釋放事件"""
if event.button() == Qt.LeftButton:
self.is_drawing = False
self.last_point = None
self.continuous_pos = None
# 停止持續(xù)效果
self.stop_continuous_effect()
elif event.button() == Qt.RightButton:
self.is_panning = False
self.pan_start_pos = None
self.setCursor(Qt.ArrowCursor)
def start_continuous_effect(self):
"""啟動持續(xù)效果定時器"""
from PyQt5.QtCore import QTimer
if 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應(yīng)用一次效果
def stop_continuous_effect(self):
"""停止持續(xù)效果定時器"""
if self.continuous_timer is not None:
self.continuous_timer.stop()
def apply_continuous_effect(self):
"""持續(xù)應(yīng)用效果"""
if self.continuous_pos and self.current_tool in ['enlarge', 'shrink', 'rotate']:
self.apply_effect(self.continuous_pos)
def apply_effect(self, center):
"""應(yīng)用效果(放大、縮小、旋轉(zhuǎn))"""
if self.image is None:
return
h, w = self.image.shape[:2]
cx, cy = center.x(), center.y()
# 確保中心點(diǎn)在圖像范圍內(nèi)
if cx < 0 or cy < 0 or cx >= w or cy >= h:
return
# 創(chuàng)建網(wǎng)格
y_indices, x_indices = np.mgrid[0:h, 0:w]
# 計算到中心點(diǎn)的距離
dx = x_indices - cx
dy = y_indices - cy
distances = np.sqrt(dx**2 + dy**2)
# 創(chuàng)建影響掩碼(在半徑內(nèi)的像素)
mask = distances <= self.brush_radius
if self.current_tool == 'enlarge':
# 放大效果:像素向內(nèi)收縮(修正:原來的shrink邏輯)
factor = 1.0 - (distances / self.brush_radius)
factor = np.clip(factor, 0, 1)
factor = factor ** 2 # 平滑過渡
# 使用effect_strength控制強(qiáng)度
scale = 1.0 - self.effect_strength * factor * mask
new_x = cx + dx * scale
new_y = cy + dy * scale
elif self.current_tool == 'shrink':
# 縮小效果:像素向外擴(kuò)展(修正:原來的enlarge邏輯)
factor = 1.0 - (distances / self.brush_radius)
factor = np.clip(factor, 0, 1)
factor = factor ** 2
# 使用effect_strength控制強(qiáng)度
scale = 1.0 + self.effect_strength * factor * mask
new_x = cx + dx * scale
new_y = cy + dy * scale
elif self.current_tool == 'rotate':
# 旋轉(zhuǎn)效果:像素螺旋旋轉(zhuǎn)
# 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_angle
new_y = cy + dx * sin_angle + dy * cos_angle
else:
return
# 限制坐標(biāo)范圍
new_x = np.clip(new_x, 0, w - 1)
new_y = np.clip(new_y, 0, h - 1)
# 使用雙線性插值進(jìn)行重映射
new_image = cv2.remap(self.image, new_x.astype(np.float32),
new_y.astype(np.float32), cv2.INTER_LINEAR)
self.image = new_image
self.update_display()
def apply_eraser(self, center, update_display=True):
"""應(yīng)用橡皮擦效果(恢復(fù)原始圖像)- 使用NumPy向量化優(yōu)化"""
if self.image is None or self.original_image is None:
return
h, w = self.image.shape[:2]
cx, cy = center.x(), center.y()
# 確保中心點(diǎn)在圖像范圍內(nèi)
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向量化操作代替雙重循環(huán)
y_coords, x_coords = np.ogrid[y1:y2, x1:x2]
# 計算距離矩陣
distances = np.sqrt((x_coords - cx)**2 + (y_coords - cy)**2)
# 創(chuàng)建圓形遮罩
mask = distances <= self.brush_radius
if not mask.any():
return
# 計算混合因子(邊緣羽化)
factor = np.zeros_like(distances)
factor[mask] = 1.0 - (distances[mask] / self.brush_radius)
factor = factor ** 0.5 # 平滑過渡曲線
# 擴(kuò)展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的區(qū)域
mask_3d = mask[:, :, np.newaxis]
blended = region * (1 - factor_3d) + original_region * factor_3d
self.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):
"""沿路徑應(yīng)用橡皮擦效果(優(yōu)化版)"""
if self.image is None or self.original_image is None or start_pos is None:
return
# 計算路徑上的點(diǎn)
sx, sy = start_pos.x(), start_pos.y()
ex, ey = end_pos.x(), end_pos.y()
# 計算距離和步數(shù)
dist = np.sqrt((ex - sx)**2 + (ey - sy)**2)
if dist < 1:
return
# 根據(jù)畫筆半徑調(diào)整步數(shù),大畫筆可以用更少的步數(shù)
step_size = max(self.brush_radius // 4, 3)
steps = max(int(dist / step_size), 1)
# 沿路徑插值多個點(diǎn),批量處理,不每次都刷新
for i in range(steps + 1):
t = i / steps if steps > 0 else 0
px = int(sx + (ex - sx) * t)
py = int(sy + (ey - sy) * t)
# 只在最后一個點(diǎn)才刷新顯示
self.apply_eraser(QPoint(px, py), update_display=(i == steps))
def apply_drag_effect(self, start_pos, end_pos):
"""應(yīng)用拖拽扭曲效果"""
if self.image is None or start_pos is None:
return
h, w = self.image.shape[:2]
sx, sy = start_pos.x(), start_pos.y()
ex, ey = end_pos.x(), end_pos.y()
# 計算拖拽向量
drag_x = ex - sx
drag_y = ey - sy
if drag_x == 0 and drag_y == 0:
return
# 創(chuàng)建網(wǎng)格
y_indices, x_indices = np.mgrid[0:h, 0:w]
# 計算到起始點(diǎn)的距離
dx = x_indices - sx
dy = y_indices - sy
distances = np.sqrt(dx**2 + dy**2)
# 創(chuàng)建影響掩碼
mask = distances <= self.brush_radius
# 計算影響因子(距離越近影響越大)
factor = np.where(distances < self.brush_radius,
1.0 - (distances / self.brush_radius),
0)
factor = factor ** 2 # 平滑過渡
# 應(yīng)用拖拽位移,使用effect_strength控制強(qiáng)度
new_x = x_indices - drag_x * factor * mask * self.effect_strength
new_y = y_indices - drag_y * factor * mask * self.effect_strength
# 限制坐標(biāo)范圍
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_image
self.update_display()
def reset_image(self):
"""重置圖像到原始狀態(tài)"""
if self.original_image is not None:
self.image = self.original_image.copy()
# 重置縮放和偏移
self.zoom_scale = 1.0
self.offset_x = 0
self.offset_y = 0
self.update_display()
def wheelEvent(self, event):
"""鼠標(biāo)滾輪事件(Ctrl+滾輪縮放)"""
if self.image is None:
return
# 檢測Ctrl鍵
modifiers = QApplication.keyboardModifiers()
if modifiers == Qt.ControlModifier:
# 獲取鼠標(biāo)位置對應(yīng)的圖像坐標(biāo)
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_scale
self.zoom_scale *= zoom_factor
# 限制縮放范圍
self.zoom_scale = max(0.1, min(10.0, self.zoom_scale))
# 如果縮放真的改變了,調(diào)整偏移以保持鼠標(biāo)位置下的圖像點(diǎn)不變
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 / h
if 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)
# 計算鼠標(biāo)在顯示圖像中的相對位置
if old_display_width <= self.width():
old_x_offset = (self.width() - old_display_width) // 2
else:
old_x_offset = self.offset_x
if old_display_height <= self.height():
old_y_offset = (self.height() - old_display_height) // 2
else:
old_y_offset = self.offset_y
img_x_in_display = mouse_pos.x() - old_x_offset
img_y_in_display = mouse_pos.y() - old_y_offset
# 計算新的偏移以保持鼠標(biāo)下的點(diǎn)不變
ratio_x = img_x_in_display / old_display_width if old_display_width > 0 else 0.5
ratio_y = img_y_in_display / old_display_height if old_display_height > 0 else 0.5
new_x_in_display = ratio_x * new_display_width
new_y_in_display = ratio_y * new_display_height
if new_display_width > self.width():
self.offset_x = mouse_pos.x() - new_x_in_display
else:
self.offset_x = 0
if new_display_height > self.height():
self.offset_y = mouse_pos.y() - new_y_in_display
else:
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()
# 更新狀態(tài)欄顯示縮放比例
if self.parent_window:
self.parent_window.statusBar().showMessage(f"縮放: {self.zoom_scale:.1f}x")
event.accept()
else:
event.ignore()
def dragEnterEvent(self, event):
"""拖拽進(jìn)入事件"""
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('? 松開鼠標(biāo)即可上傳圖片')
def dragLeaveEvent(self, event):
"""拖拽離開事件"""
if self.image is None:
# 恢復(fù)原始樣式
self.setStyleSheet("""
QLabel {
background-color: #2b2b2b;
color: #9E9E9E;
font-size: 24px;
font-weight: bold;
border: 3px dashed #555;
border-radius: 10px;
}
""")
self.setText('?? 點(diǎn)擊"上傳圖片"按鈕\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("加載圖片失?。?)
# 恢復(fù)提示
self.setStyleSheet("""
QLabel {
background-color: #2b2b2b;
color: #9E9E9E;
font-size: 24px;
font-weight: bold;
border: 3px dashed #555;
border-radius: 10px;
}
""")
self.setText('?? 點(diǎn)擊"上傳圖片"按鈕\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) # 設(shè)置最小窗口尺寸,確保所有按鈕可見
# 創(chuàng)建中心部件
central_widget = QWidget()
self.setCentralWidget(central_widget)
# 主布局
main_layout = QVBoxLayout(central_widget)
main_layout.setSpacing(15)
main_layout.setContentsMargins(15, 15, 15, 15)
# 創(chuàng)建工具欄
self.create_toolbar()
# 創(chuàng)建圖像畫布
self.canvas = ImageCanvas(self)
main_layout.addWidget(self.canvas)
# 創(chuàng)建控制面板
control_panel = self.create_control_panel()
main_layout.addLayout(control_panel)
# 設(shè)置樣式
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):
"""創(chuàng)建工具欄"""
from PyQt5.QtCore import QSize
toolbar = 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("?? 旋轉(zhuǎn)", 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):
"""創(chuàng)建控制面板"""
from PyQt5.QtWidgets import QButtonGroup, QRadioButton, QGroupBox, QLineEdit
from PyQt5.QtGui import QIntValidator, QDoubleValidator
layout = 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) # 提高到800
self.radius_slider.setValue(170) # 默認(rèn)170
self.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)
# 變化強(qiáng)度控制
strength_label = QLabel("變化強(qiáng)度:")
layout.addWidget(strength_label)
self.strength_slider = QSlider(Qt.Horizontal)
self.strength_slider.setMinimum(1) # 0.01
self.strength_slider.setMaximum(100) # 1.0
self.strength_slider.setValue(20) # 默認(rèn)0.20
self.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)
# 變化強(qiáng)度輸入框
self.strength_input = QLineEdit()
self.strength_input.setText("0.20")
self.strength_input.setMaximumWidth(60)
# 允許輸入0.01-1.0,最多3位小數(shù)(如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)
# 旋轉(zhuǎn)方向控制
rotate_group_box = QGroupBox("旋轉(zhuǎn)方向")
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 layout
def set_tool(self, tool_name):
"""設(shè)置當(dāng)前工具"""
# 取消其他工具的選中狀態(tài)
for btn in self.tool_buttons:
btn.setChecked(False)
# 設(shè)置當(dāng)前工具
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_name
def update_brush_radius_from_slider(self, value):
"""從滑塊更新畫筆半徑"""
self.canvas.brush_radius = value
self.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 = value
self.radius_slider.setValue(value)
self.radius_value_label.setText(f"{value} px")
self.radius_input.setText(str(value))
except ValueError:
# 輸入無效,恢復(fù)當(dāng)前值
self.radius_input.setText(str(self.canvas.brush_radius))
def update_effect_strength_from_slider(self, value):
"""從滑塊更新變化強(qiáng)度"""
strength = value / 100.0 # 轉(zhuǎn)換為0.01-1.0
self.canvas.effect_strength = strength
self.strength_value_label.setText(f"{strength:.2f}")
self.strength_input.setText(f"{strength:.2f}")
def update_effect_strength_from_input(self):
"""從輸入框更新變化強(qiáng)度"""
try:
value = float(self.strength_input.text())
value = max(0.01, min(1.0, value)) # 限制范圍
self.canvas.effect_strength = value
self.strength_slider.setValue(int(value * 100))
self.strength_value_label.setText(f"{value:.2f}")
self.strength_input.setText(f"{value:.2f}")
except ValueError:
# 輸入無效,恢復(fù)當(dāng)前值
self.strength_input.setText(f"{self.canvas.effect_strength:.2f}")
def set_rotate_direction(self, direction):
"""設(shè)置旋轉(zhuǎn)方向"""
self.canvas.rotate_direction = direction
def 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("沒有可保存的圖像!")
return
file_path, _ = QFileDialog.getSaveFileName(
self, "保存圖片", "",
"PNG文件 (*.png);;JPEG文件 (*.jpg);;所有文件 (*.*)"
)
if file_path:
try:
# RGB轉(zhuǎn)BGR
image_bgr = cv2.cvtColor(self.canvas.image, cv2.COLOR_RGB2BGR)
# 使用cv2.imencode()和tofile()保存,支持中文路徑
# 根據(jù)文件擴(kuò)展名確定編碼格式
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()
代碼亮點(diǎn):
- 1098行完整代碼
- 詳細(xì)中文注釋
- 面向?qū)ο笤O(shè)計
- 模塊化結(jié)構(gòu)
- 支持中文路徑
系統(tǒng)要求
| 項(xiàng)目 | 要求 |
|---|---|
| 操作系統(tǒng) | Windows / macOS / Linux |
| Python版本 | ≥ 3.6 |
| 內(nèi)存 | 建議 2GB 以上 |
| 磁盤空間 | 約 500MB(含依賴庫) |
| 支持格式 | PNG, JPG, JPEG, BMP, GIF |
常見問題 FAQ
Q1: 運(yùn)行時提示"No module named ‘PyQt5’"
解決方案:
pip install -i https://pypi.tuna.tsinghua.edu.cn/simple PyQt5
Q2: 圖片變形后畫質(zhì)下降怎么辦?
原因:使用了過大的畫筆半徑和強(qiáng)度
建議:
- 使用較小的畫筆半徑(50-150)
- 降低變化強(qiáng)度(0.1-0.3)
- 多次小幅度變形,避免一次性大幅度變形
Q3: 支持哪些圖片格式?
支持常見的圖片格式:PNG、JPG、JPEG、BMP、GIF
Q4: 如何撤銷操作?
目前沒有逐步撤銷功能,但可以:
- 使用
橡皮擦工具局部恢復(fù)原圖 - 點(diǎn)擊
重置按鈕恢復(fù)到原始狀態(tài)
Q5: Windows下運(yùn)行報錯"DLL load failed"
解決方案:
# 重新安裝opencv-python pip uninstall opencv-python pip install -i https://pypi.tuna.tsinghua.edu.cn/simple opencv-python
Q6: 中文路徑下無法加載圖片?
本工具已經(jīng)完美支持中文路徑!使用了 np.fromfile() 和 tofile() 方法。
Q7: 能處理多大的圖片?
理論上沒有限制,但建議:
- 普通照片:≤ 4000×3000 像素
- 高分辨率圖片:可能會變慢,建議先縮小
未來改進(jìn)方向
- 撤銷/重做功能:支持多步撤銷
- 圖層系統(tǒng):支持多圖層編輯
- 更多工具:添加模糊、銳化等濾鏡
- 預(yù)設(shè)效果:一鍵應(yīng)用常見變形
- 批量處理:支持批量處理多張圖片
- 動畫導(dǎo)出:導(dǎo)出變形過程為GIF或視頻
- AI輔助:自動識別人臉關(guān)鍵點(diǎn)
- 性能優(yōu)化:GPU加速處理
總結(jié)
FacePrank 是一個功能豐富、使用簡單的照片整蠱工具。通過五大核心變形工具,你可以輕松制作各種搞笑照片。無論是惡搞朋友、制作表情包,還是進(jìn)行創(chuàng)意設(shè)計,這個工具都能滿足你的需求!
核心優(yōu)勢:
- ? 純Python實(shí)現(xiàn),代碼簡潔易懂
- ? 基于NumPy向量化運(yùn)算,性能優(yōu)秀
- ? 現(xiàn)代化UI設(shè)計,操作流暢
- ? 完美支持中文路徑
- ? 開箱即用,無需復(fù)雜配置
附錄: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
單獨(dú)安裝各個包:
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
以上就是Python+pyqt5實(shí)現(xiàn)一個圖像變形工具的詳細(xì)內(nèi)容,更多關(guān)于Python圖像變形的資料請關(guān)注腳本之家其它相關(guān)文章!
相關(guān)文章
Python?Pandas實(shí)現(xiàn)將嵌套JSON數(shù)據(jù)轉(zhuǎn)換DataFrame
對于復(fù)雜的JSON數(shù)據(jù)進(jìn)行分析時,通常的做法是將JSON數(shù)據(jù)結(jié)構(gòu)轉(zhuǎn)換為Pandas?DataFrame,所以本文就來看看將嵌套JSON數(shù)據(jù)轉(zhuǎn)換為Pandas?DataFrame的具體方法吧2024-01-01
Python Pytest裝飾器@pytest.mark.parametrize詳解
本文主要介紹了Python Pytest裝飾器@pytest.mark.parametrize詳解,文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2021-08-08
Python繪圖系統(tǒng)之自定義一個坐標(biāo)設(shè)置控件
這篇文章主要為大家詳細(xì)介紹了Python如何編寫一個繪圖系統(tǒng),可以實(shí)現(xiàn)自定義一個坐標(biāo)設(shè)置控件,文中的示例代碼講解詳細(xì),感興趣的可以了解一下2023-08-08
Python利用memory_profiler查看內(nèi)存占用情況
memory_profiler是第三方模塊,用于監(jiān)視進(jìn)程的內(nèi)存消耗以及python程序內(nèi)存消耗的逐行分析。本文將利用memory_profiler查看代碼運(yùn)行占用內(nèi)存情況,感興趣的可以了解一下2022-06-06
Python生成任意波形并存為txt的實(shí)現(xiàn)
本文主要介紹了Python生成任意波形并存為txt的實(shí)現(xiàn),文中通過示例代碼介紹的非常詳細(xì),具有一定的參考價值,感興趣的小伙伴們可以參考一下2021-11-11
python+selenium的web自動化上傳操作的實(shí)現(xiàn)
這篇文章主要介紹了python+selenium的web自動化上傳操作的實(shí)現(xiàn),文章圍繞主題展開詳細(xì)的內(nèi)容介紹,具有一定的參考價值,需要的朋友可以參考一下2022-08-08

