如何在保存之前使用PIL调整新上传的图像大小?

20

我想将新图像调整为高度和宽度为800px并保存它们。应用程序不能存储实际图像。有什么建议吗?

这是我的代码,它保存原始图像,而不是缩放后的照片:

models.py:

class Photo(models.Model):        
    photo = models.ImageField(upload_to='photos/default/')


    def save(self):

        if not self.id and not self.photo:
            return            

        super(Photo, self).save()

        image = Image.open(self.photo)
        (width, height) = image.size

        "Max width and height 800"        
        if (800 / width < 800 / height):
            factor = 800 / height
        else:
            factor = 800 / width

        size = ( width / factor, height / factor)
        image.resize(size, Image.ANTIALIAS)
        image.save(self.photo.path)

你的代码有什么问题?你没有说明它的错误在哪里。你展示的代码将把图像保存到数据库中。如果你不想保存未经调整大小的图像,你必须在保存之前调整它的大小 - 即在表单级别。 - Timmy O'Mahony
@pastylegs 在表单级别上,我应该使用哪种方法以及如何使用? - beni
你是通过Django管理界面上传还是使用自定义表单? - Timmy O'Mahony
通过自定义表单。第一个答案解决了我的问题。谢谢。 - beni
3
如果有人因为某些原因使用那个代码,因子部分有几个错误:因子应该是浮点数(即800.0 / 宽度),在设置大小时应该乘以因子(而不是除以因子!),设置大小时记得强制转换回整数类型,并且< 应该是 >。当然,也要修复已接受答案的错误。 - pertz
8个回答

14
image = image.resize(size, Image.ANTIALIAS)

resize是非破坏性的,它返回一个新图像。


我知道,但是我如何在不保存原始照片的情况下保存调整大小后的照片? - beni
7
请将“image.resize(size, Image.ANTIALIAS)”替换成“image = image.resize(size, Image.ANTIALIAS)”。当前你正在调整大小,但忽略了结果并保存原始图像。 - wmil

14

1
+1 因为这个项目可以与 django-storages+boto 无缝配合,完美运行。太棒了,省去了我很多麻烦! - kungphu
这个项目在使用django storages + boto时无法直接运行,导致我从API上传文件失败。 - max4ever
我也喜欢它;因为这个项目非常小,实际上我只是复制了表单类并使用它... - Paul Bormans

4
我搜索了如何在保存之前调整上传照片大小的解决方案。在StackOverflow上有很多信息,但没有完整的解决方案。这是我认为适用于需要此功能的人的最终解决方案。
开发亮点:
  • 使用Pillow进行图像处理(需要两个软件包:libjpeg-dev、zlib1g-dev)
  • 使用Model和ImageField作为存储方式
  • 使用multipart/form的HTTP POST或PUT
  • 无需手动将文件保存到磁盘
  • 创建多个分辨率并存储其尺寸
  • 未修改模型本身
安装Pillow:
$ sudo apt-get install libjpeg-dev
$ sudo apt-get install zlib1g-dev
$ pip install -I Pillow

myapp/models.py

from django.db import models

class Post(models.Model):
    caption = models.CharField(max_length=100, default=None, blank=True)
    image_w = models.PositiveIntegerField(default=0)
    image_h = models.PositiveIntegerField(default=0)
    image = models.ImageField(upload_to='images/%Y/%m/%d/', default=None, 
                blank=True, width_field='image_w', height_field='image_h')
    thumbnail = models.ImageField(upload_to='images/%Y/%m/%d/', default=None,
                blank=True)

这个模型可以保存带有缩略图和可选标题的照片。这应该类似于真实世界的使用情况。
我们的目标是将照片调整为640x640,并生成一个150x150的缩略图。我们还需要在我们的Web API中返回照片的尺寸。image_w和image_h是表中缓存的尺寸。请注意,声明ImageField时需要添加引号。但是,缩略图不需要宽度和高度字段(因此您可以看到不同之处)。
这就是模型。我们不需要覆盖或添加任何函数到模型中。

myapp/serializers.py

我们将使用一个 ModelSerializer 来处理来自HTTP POST的传入数据。
from rest_framework import serializers
from myapp.models import Post

class PostSerializer(serializers.ModelSerializer):
    class Meta:
        model = Post
        fields = ('caption',)

我们不希望序列化器处理上传的照片,我们将自己处理。因此,在字段中不需要包含'image'。

myapp/views.py

@api_view(['POST'])
@parser_classes((MultiPartParser,))
def handle_uploaded_image(request):
    # process images first.  if error, quit.
    if not 'uploaded_media' in request.FILES:
        return Response({'msg': 'Photo missing.'}, status.HTTP_400_BAD_REQUEST)
    try:
        im = Image.open(StringIO(request.FILES['uploaded_media'].read()))
    except IOError:
        return Response({'msg': 'Bad image.'}, status.HTTP_400_BAD_REQUEST)

    serializer = PostSerializer(data=request.DATA, files=request.FILES)
    if not serializer.is_valid():
        return Response({'msg': serializer.errors}, status.HTTP_400_BAD_REQUEST)

    post = Post.create()
    if serializer.data['caption'] is not None:
        post.caption = serializer.data['caption']

    filename = uuid.uuid4()
    name = '%s_0.jpg' % (filename)
    post.image.save(name=name, content=resize_image(im, 640))
    name = '%s_1.jpg' % (filename)
    post.thumbnail.save(name=name, content=resize_image(im, 150))

    post.save()
    return Response({'msg': 'success',
        'caption': post.caption,
        'image': {
            'url': request.build_absolute_uri(post.image.url),
            'width': post.image_w,
            'height': post.image_h,
        }
        'thumbnail': request.build_absolute_uri(post.thumbnail.url),
    }, status.HTTP_201_CREATED)

共享py文件中的辅助函数

def resize_image(im, edge):
    (width, height) = im.size
    (width, height) = scale_dimension(w, h, long_edge=edge)
    content = StringIO()
    im.resize((width, height), Image.ANTIALIAS).save(fp=content, format='JPEG', dpi=[72, 72])
    return ContentFile(content.getvalue())

def scale_dimension(width, height, long_edge):
    if width > height:
        ratio = long_edge * 1. / width
    else:
        ratio = long_edge * 1. / height
    return int(width * ratio), int(height * ratio)

这里我们不使用Image.thumbnail,因为它会改变原始图像。如果我们想要存储多个分辨率(例如低分辨率和高分辨率用于不同的目的),则此代码是合适的。我发现将传入消息调整大小两次会降低质量。
我们也不直接将图像保存到磁盘。我们通过ContentFile(内存中内容的文件对象)将图像推送到ImageField,并让ImageField完成其工作。我们希望多个图像副本具有相同的文件名和不同的后缀。
鸣谢
以下是我参考构建此解决方案的代码链接:

如果您还想验证图像,请参见此内容:https://dev59.com/kmIi5IYBdhLWcg3w_AfV#20762344


1

以下是对我有用的方法,从django-resized中获得了一些启示。

@receiver(pre_save, sender=MyUser)
@receiver(pre_save, sender=Gruppo)
def ridimensiona_immagine(sender, instance=None, created=False, **kwargs):
    foto = instance.foto

    foto.file.seek(0)
    thumb = PIL.Image.open(foto.file)
    thumb.thumbnail((
        200, 
        200
        ), PIL.Image.ANTIALIAS)


    buffer = StringIO.StringIO()
    thumb.save(buffer, "PNG")
    image_file = InMemoryUploadedFile(buffer, None, 'test.png', 'image/png', buffer.len, None)

    instance.foto.file = image_file

0
一个简单的解决方案是在保存图像之前,在表单中使用此方法调整图像大小:(需要 pip install pillow
import os
from io import BytesIO
from PIL import Image as PilImage
from django.core.files.base import ContentFile
from django.core.files.uploadedfile import InMemoryUploadedFile, TemporaryUploadedFile

def resize_uploaded_image(image, max_width, max_height):
    size = (max_width, max_height)

    # Uploaded file is in memory
    if isinstance(image, InMemoryUploadedFile):
        memory_image = BytesIO(image.read())
        pil_image = PilImage.open(memory_image)
        img_format = os.path.splitext(image.name)[1][1:].upper()
        img_format = 'JPEG' if img_format == 'JPG' else img_format

        if pil_image.width > max_width or pil_image.height > max_height:
            pil_image.thumbnail(size)

        new_image = BytesIO()
        pil_image.save(new_image, format=img_format)

        new_image = ContentFile(new_image.getvalue())
        return InMemoryUploadedFile(new_image, None, image.name, image.content_type, None, None)

    # Uploaded file is in disk
    elif isinstance(image, TemporaryUploadedFile):
        path = image.temporary_file_path()
        pil_image = PilImage.open(path)

        if pil_image.width > max_width or pil_image.height > max_height:
            pil_image.thumbnail(size)
            pil_image.save(path)
            image.size = os.stat(path).st_size

    return image

然后在表单中图像字段的清理方法中使用它:

class ImageForm(forms.Form):
    IMAGE_WIDTH = 450
    IMAGE_HEIGHT = 450
    
    image = forms.ImageField()

    def clean_image(self):
        image = self.cleaned_data.get('image')
        image = resize_uploaded_image(image, self.IMAGE_WIDTH, self.IMAGE_HEIGHT)
        return image

0

我还没有测试过这个解决方案,但它可能会有帮助!

#subidas.py
import PIL
from PIL import Image
def achichar_tamanho(path):
    img = Image.open(path)
    img = img.resize((230,230), PIL.Image.ANTIALIAS)
    img.save(path)

在你的models.py文件中,进行以下更改:
from subidas import achicar_tamanho

class Productos(models.Model):
    imagen = models.ImageField(default="emg_bol/productos/interrogacion.png", upload_to='emg_bol/productos/', blank=True, null=True, help_text="Image of the product")
    tiempo_ultima_actualizacion = models.DateTimeField( help_text="Cuando fue la ultima vez, que se modificaron los datos de este producto", auto_now=True)
    prioridad = models.IntegerField(max_length=4, default=99, help_text="Frecuencia (medida en unidad), con que el producto es despachado para la venta")
    proveedor = models.ForeignKey(Proveedores, help_text="El que provello del producto'")  

    def save(self, *args, **kwargs):
        super(Productos,self).save(*args, **kwargs)
        pcod = "%s_%s" % ( re.sub('\s+', '', self.descripcion)[:3], self.id)
        self.producto_cod = pcod
        achichar_tamanho(self.imagen.path)
        super(Productos,self).save(*args, **kwargs)

我真的不知道,在实施这些更改之前和之后有什么区别。


0

如果你正在使用 Python < 3,你应该考虑使用:

from __future__ import division

在这种情况下,你的除法运算结果将会是浮点数。

1
使用//可能是更好的选择,因为代码的其余部分可能会假定除法结果将是浮点数。 - Ali Rasim Kocal
1
这似乎完全离题了 :-/ - Simon Steinberger

0

对我来说,Django Resized很好用,而且非常易懂,只需pip安装它并查看文档即可。


你的答案可以通过添加更多支持性信息来改进。请[编辑]以添加更多细节,例如引用或文档,以便其他人可以确认你的答案是否正确。您可以在帮助中心找到有关如何编写好答案的更多信息。 - Community

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接