Python 实现图片剪切

Python对图片处理的库是非常丰富的,所以有很多库能实现这一目标的方法如Pillow,OpenCV,moviepy等等,而这里给的gif处理方法主要使用的是Pillow,moviepy库的

安装Pillow,moviepy

终端上
1
2
pip install Pillow
pip install moviepy

对于图片的剪切主要是通过坐标进行判断,需要提供一个矩形坐标,即四个坐标点进行定位,然后对原图进行剪切处理。

那么其他废话不多说,直接上代码。(学别人讲的话,直接上代码确实很爽)

除GIF的图片文件处理

1
2
3
4
5
6
7
8
9
U_x = 144.73429951690818		    # x轴坐标
U_y = 76.13526570048306 # y轴坐标
U_width = 640.0000000000001 # 剪切大小
U_height = 640.0000000000001
u_rotate = 90 # 旋转角度

img = Image.open('input.img')
crop_im = img.crop((U_x, U_y, U_x + U_width, U_y + U_height)).resize((400, 400),Image.LANCZOS).rotate(u_rotate)
# 注意这里的Image.LANCZOS,如果你的Pillow是10.0直接CV那么啥事情,但是其他的版本可能是Image.ANTIALIAS如9.5

备注:LANCZOS实际上和ANTIALIAS指向的是同一种图像插值模式算法

GIF图片文件处理

#用Pillow处理

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
# -*- coding: utf-8 -*-
# @Time : 2023/8/3 11:25
# @Author : BXZDYG
# @Software: PyCharm
# @File : Pillow_crop_gif.py
import time

from PIL import Image, ImageSequence

if __name__ == '__main__':
first_time = time.time() # 时间戳检验剪切速度
start=time.perf_counter() # 计算机时间检验剪切速度
U_x = 144.73429951690818 # x轴坐标
U_y = 76.13526570048306 # y轴坐标
U_width = 640.0000000000001 # 剪切大小
U_height = 640.0000000000001
u_rotate = 90 # 旋转角度
frames = []

with Image.open('input.gif') as im:
idx = 0
for frame in ImageSequence.Iterator(im):
frame = frame.crop((U_x, U_y, U_x + U_width, U_y + U_height)).resize((400, 400)).rotate(u_rotate)
frame.info['duration'] = im.info['duration']
frames.append(frame)
idx += 1
frames[0].save('out.gif',
save_all=True, append_images=frames[1:], loop=0, duration=im.info['duration'], quality=80)
print('时间戳说——总共用时:',time.time()-first_time,'秒') # 总共用时: 4-5 秒
print('计算机说——总共用时:',time.perf_counter()-start,'秒') # 总共用时: 4-5 秒

#用moviepy进行图片剪切

备注:如果你要求对分辨率的在处理的话,那么需要Pillow的版本为9.5,10.0和其底层的resize中设置的图像插值模式冲突,是Image.ANTIALIAS,手动打上去还改不了,而且速度极慢,是用Pillow和结合Pillow库的两倍。而且在movie官方文档也不建议在大型框架和web上使用moviepy.editor子模块

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
# @Time    : 2023/8/3 11:26
# @Author : BXZDYG
# @Software: PyCharm
from moviepy.editor import *
import time

if __name__ == '__main__':
first_time = time.time() # 时间戳检验剪切速度
start = time.perf_counter() # CPU时间检验剪切速度
x = 144.73429951690818
y = 76.13526570048306
width = 640.0000000000001
height = 640.0000000000001
t_rotate = 90
gif = VideoFileClip('head_cap.gif')
# 获取帧数
nframes = gif.reader.nframes

# 设置每帧duration
durations = [0.1] * nframes

images = [gif.get_frame(t) for t in range(nframes)]

clip = ImageSequenceClip(images, durations=durations)

rotated = clip.crop(x1=x, y1=y, x2=x + width, y2=y + height).resize((400, 400)).rotate(t_rotate)
# resized = rotated.resize((400, 400)) # 调整分辨率
# resized.write_gif('23.gif', fps=20)
rotated.write_gif('23.gif', fps=20)

print('时间戳说——总共用时:', time.time() - first_time, '秒') # 总共用时: 10+ 秒
print('CPU说——总共用时:', time.perf_counter() - start, '秒') # 总共用时: 10+ 秒

#用Pillow对每一帧图片进行剪切,moviepy生成gif文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# -*- coding: utf-8 -*-
# @Time : 2023/8/3 11:26
# @Author : BXZDYG
# @Software: PyCharm
# @File : Movie_Pillow_crop_gif.py
import time

import numpy as np
from PIL import Image
from moviepy.editor import VideoFileClip


def pillow_movie_crop(img):
'''
:param img:
:return: 存在缺点帧数丢失,图片变大
'''
im = Image.fromarray(img)
cropped = im.crop((x, y, x+width, y+height)).resize((400,400)).rotate(t_rotate)
return np.array(cropped)


if __name__ == '__main__':
first_time=time.time()
x = 144.73429951690818
y = 76.13526570048306
width = 640.0000000000001
height = 640.0000000000001
t_rotate = 90
clip = VideoFileClip("head_cap.gif", has_mask=False) # 在加载GIF时指定保留所有帧
cropped_clip = clip.fl_image(pillow_movie_crop)
cropped_clip.write_gif("21.gif", fuzz=10, opt='nq') # 在写出GIF时指定优化参数,减少质量损
print('总共用时:',time.time()-first_time,'秒') # 4.687000274658203 秒 基本上4-5s左右

所以其实使用Pillow库是一个优选

Django 文件上传

启用 media

settings.py
1
2
3
# 在末尾加入
MEDIA_ROOT = os.path.join(BASE_DIR, 'media')
MEDIA_URL = '/media/'
utils.py 自主封装的自定义文件字段,对上传文件进行限制
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
class RestrictedFileField(FileField):
""" max_upload_size:
2.5MB - 2621440
5MB - 5242880
10MB - 10485760
20MB - 20971520
50MB - 5242880
100MB 104857600
250MB - 214958080
500MB - 429916160
"""

def __init__(self, *args, **kwargs):
self.content_types = kwargs.pop("content_types", [])
self.max_upload_size = kwargs.pop("max_upload_size", [])
super().__init__(*args, **kwargs)
def clean(self, *args, **kwargs):
data = super().clean(*args, **kwargs) # clean()方法来自于FileField的父类Field, 用于验证

file = data.file
try:
content_type = file.content_type
# 自定义验证
if content_type in self.content_types:
if file.size > self.max_upload_size:
raise forms.ValidationError('文件大小要求为{}. 当前文件大小为 {}'.format(filesizeformat(self.max_upload_size), filesizeformat(file.size)))
else:
raise forms.ValidationError('当前文件格式不被运行,仅支持{}.'.format(self.content_types))
except AttributeError:
pass
return data
models.py
1
2
3
4
5
6
7
8
from utils import RestrictedFileField
class UserInfo(models.Model)
avatar = RestrictedFileField(verbose_name=u'头像', max_length=50, default='avatar/用户.png',
upload_to='avatar/',
content_types=['image/jpeg', 'image/png', 'image/gif', 'image/bmp',
'image/tiff'],max_upload_size=5242880)
id = models.BigAutoField(verbose_name='UID', primary_key=True)
……
urls.py
1
2
3
4
5
6
7
8
9
from django.urls import path, include, re_path
from django.views.static import serve
from django.conf import settings
from . import views

urlpatterns = [
# 上传文件位置
re_path(r'^media/(?P<path>.*)$', serve, {'document_root': settings.MEDIA_ROOT}, name='media'),
……
views.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
from django import forms
from app.error import errorResponse
class UserInfoForm(forms.ModelForm):
# avatar = forms.FileField(label='头像')
class Meta:
model = UserInfo
fields = '__all__'
def edit_info(request):
uid = request.session['uid'] # 用户登录存储在session中的信息
userinfo = UserInfo.objects.filter(id=uid).get()
if request.method == 'GET':
form = UserInfoForm(instance=userinfo)
return render(request, 'space_edit_info.html', {
'userinfo': userinfo,
'form': form,
'title': '个人中心-编辑个人信息'
})
form = UserInfoForm(instance=userinfo, data=request.POST)
if form.is_valid():
form.save()
return redirect('/space/edit_info/')
return errorResponse(request, errMsg=form['avatar'].errors[0] )

此处的errorResponse是自己封装的处理错误页函数,这样可以在前端显示自定义头像组件的错误

1
2
3
4
5
6
7
def errorResponse(request, errMsg):
'''
:param request:
:param errMsg:返回错误信息到前端页面
:return:
'''
return render(request, 'error.html', {'errMsg': errMsg})

Django 图片上传剪切综合案例实现

前端获取所需的坐标数据

使用基于JQuery的cropper和Bootstrap,可以更方便的实现获取用户对图片剪切的操作(缩放,旋转等)后的数据和坐标。

jQuery cropper是一款使用简单且功能强大的图片剪裁jquery插件。该插件支持图片放大,缩小,旋转,裁剪和预览等功能。

素材来源17素材,不用登录另存为网页就行。

注意演示里的最右上角x最好点一下,因为我们要的里面的<iframe>标签的html文档,然后保存下来的就是我们需要的素材,然后自己处理一下。文件主要如下

sitelogo.js是利用cropper获取坐标等数据,然后携带数据向处理的url放送请求

目标的url又写在了签单的html文件里 from的action,修改这个即可。

这里我使用的是Django的url反向解析,更便利一些,有着不少好处。

而在sitelogo.js中,主要要去关注两部分

这部分是序列化坐标和旋转角度数据,这里的x,y是左上的坐标,根据height和width可以计算出选中的矩形整个坐标。

发送ajax请求

前端的文件主要知道这些就可以了,自己不喜欢这些样式,想再多添加些功能可以自己改。

而前端重中之重就是记得Django有一个csrf安全验证机制,如果没有在中间件关闭它,就需要在前端添提交表单添加一个{% csrf_token%}或者在对应函数前面标记注释。再或者直接根据上面这张ajax提交代码的图片,在你自己写的js代码中添加在headers中.

@csrf_exempt

1
2
3
from django.views.decorators.csrf import csrf_exempt
#在对应函数前面标记注释
@csrf_exempt

后端根据坐标对图片进行剪切

#models.py 继承上面

urls.py继承上面且扩充
1
2
path('upload_avatar/', views.upload_avatar, name='upload_avatar'),
path('ajax_avatar_upload/', views.ajax_avatar_upload, name='ajax_avatar_upload'),
views.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
import re
import os
import json
import uuid
from django.shortcuts import render, redirect, get_object_or_404
from django.urls import reverse
from django.views.decorators.csrf import csrf_exempt
from django import forms
from PIL import Image, ImageSequence
from .models import UserInfo
# 这里使用的是Form而不是ModelForm,你可以直接引用上面写的UserInfo,用其自定义的头像字段,当然这里只是为了讲解和展示,你自己可以试试我说的
class AvatarUploadForm(forms.Form):
avatar_file = forms.ImageField()

def upload_avatar(request):
uid = request.session['uid']
userinfo = UserInfo.objects.filter(id=uid).get()
return render(request, 'Document.html', {
'userinfo': userinfo
})


def ajax_avatar_upload(request):
uid = request.session['uid']
userinfo = get_object_or_404(UserInfo, userName=username)

if request.method == "POST":
form = AvatarUploadForm(request.POST, request.FILES)
if form.is_valid():
img = request.FILES['avatar_file'] # 获取上传图片
data = request.POST['avatar_data'] # 获取ajax返回图片坐标
# print(img.size) # 图片大小

if img.size > 5242880:
# 相当于if img.size / 1024 / 1024 > 5:
return JsonResponse({"message": "上传图片大小应小于5MB, 请重新上传。", })

current_avatar = userinfo.avatar
cropped_avatar = crop_image(current_avatar, img, data, userinfo.id)
userinfo.avatar = cropped_avatar # 将图片路径修改到当前会员数据库
userinfo.save()
# 向前台返回一个json,result值是图片路径
data = {"result": userinfo.avatar.url, }
return JsonResponse(data)

else:
return JsonResponse({"message": "请重新上传。只能上传图片"})

return HttpResponseRedirect(reverse('upload_avatar'))


def crop_image(current_avatar, file, data, uid):
'''

:param current_avatar: 当前头像
:param file: 上传文件
:param data: 坐标
:param uid: 用户id
:return: 保存的图片路径名
'''
# 随机生成新的图片名,自定义路径。
ext = file.name.split('.')[-1]
file_name = '{}.{}'.format(uuid.uuid4().hex[:10], ext)
cropped_avatar = os.path.join(str(uid), "avatar", file_name)
# 相对根目录路径
file_path = os.path.join("media", str(uid), "avatar", file_name)

# 获取Ajax发送的裁剪参数data,先用json解析。
coords = json.loads(data)
t_x = int(coords['x'])
t_y = int(coords['y'])
t_width = t_x + int(coords['width'])
t_height = t_y + int(coords['height'])
t_rotate = coords['rotate']
if abs(t_rotate)<=90: #这里旋转小于等于90度居然是相反方向
t_rotate=-t_rotate
# 通过文件名判断是否为gif
if file_name.endswith('.gif'):
'''成功了就是保存的文件会变大,还有保存速度太慢了4~5s,前台应该做个动画'''
frames = []

with Image.open(file) as im:
idx = 0
for frame in ImageSequence.Iterator(im):
frame = frame.crop((t_x, t_y, t_width, t_height)).resize((400, 400)).rotate(t_rotate)
frame.info['duration'] = im.info['duration']
frames.append(frame)
idx += 1
frames[0].save(file_path,
save_all=True, append_images=frames[1:], loop=0, duration=im.info['duration'], quality=80)
else:

# 裁剪图片,压缩尺寸为400*400。
img = Image.open(file)
# 由于我的Pillow版本是 10.0 Image.ANTIALIAS 被弃用
crop_im = img.crop((t_x, t_y, t_width, t_height)).resize((400, 400), Image.LANCZOS).rotate(t_rotate)

directory = os.path.dirname(file_path)
if not os.path.exists(directory):
os.makedirs(directory)
crop_im.save(file_path)

# 如果头像不是默认头像,删除老头像图片, 节省空间
# 这部分可以删去,或者限制用户可以查看最近10张头像历史
if not current_avatar == os.path.join("media", "avatar", "用户.png"):
current_avatar_path = os.path.join("media", str(uid), "avatar",os.path.basename(current_avatar.url))
os.remove(current_avatar_path)

return cropped_avatar

效果展示

#静图剪切

静图剪切动画
静图剪切动画

#动图剪切(这一步其实挺慢的,中间等待的部分剪去了)

gif动图剪切
gif动图剪切

总结:

当初自己整这个可是整了挺久的,很难找到直接拿来用的,很多都是前端VUE处理的,没有直接是Html或是Html+JQuery,而且那些头像处理没有对GiF处理的步骤,就观在那些大型网站来看,现在好像确实没有多少支持用gif动图为头像的。

而且这个前端处理的js文件坐标点好像有点偏移……

虽然历经不少,也找到了解决办法,但是还是那么难找,整了挺久的。找到了还是要自己理解和运用,就这样吧,希望我的文章能给你提供帮助。

希望能够我一个赞同/赞/收藏辣(‾◡◝)

大家有什么问题可以在评论区大胆留盐(不用加密(bushi

与CSDN同步发布,我应该在博客园也发一份~


本破站由 @BXZDYG 使用 Stellar 主题创建。
本博客部分素材来源于网络,如有侵权请联系1476341845@qq.com删除
本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议,转载请注明出处。

本"页面"访问 次 | 👀总访问 次 | 总访客
全部都是博主用心学编写的啊!不是ai啊 只要保留原作者姓名并在基于原作创作的新作品适用同类型的许可协议,即可基于非商业目的对原作重新编排、改编或者再创作。