【Python】Labelme/PIL读取图片朝向错误解决

news2024/11/24 14:42:45

文章目录

  • 一、问题背景
  • 二、产生原因
  • 三、解决方案

一、问题背景

发现使用labelme直接读取含imageData(将图片bytes数据使用base64编码后的str数据)的json文件时,读上来的图片会发生自动旋转的问题。比如原先是横放的图,读进来后就成竖放的了,图片朝向(orientation)错误

我们可以用一个形象的例子来说明这种错误:
在这里插入图片描述

为什么图片会多此一举地弄一个朝向信息,而不是一步到位呢?

试想这样一个例子:你想给女朋友拍照,她很高,所以你把相机旋转了90度,最后成功拍了个全身照。

可能这个过程你已经重复过无数次,觉得非常自然。但仔细想想,相机的镜头是不会旋转的,也就是说如果人为地把相机旋转90度再拍照,拍下来的女朋友就会是横着站立,而不是正常的竖着站立。就像当你歪着头时,看到的世界也是歪着的。

为了防止这种诡异的现象出现,相机很机智地给图片加上了orientation标签,并“提醒”自己:这张图拍的时候我被旋转了90度,给主人看的时候要记得正过来。
在这里插入图片描述

所以这种记录方式的产生其实是两个因素导致的:①相机总是忠实地记录当时的场景,正的就是正的,反的就是反的;②相机希望记录当时的朝向,以方便做任何朝向上的调整。


二、产生原因

那么具体来说,在labelme的图片读取过程中哪里出问题会导致平时自动调整朝向的图片没有调整呢?

接着我们顺着labelme的流程走一遍

  1. 获取图片信息:labelme在读取json时会有一个if语句,如果有imageData,则直接获取imageData;如果没有imageData,则顺着imagePath找图片文件,然后读取图片文件:

    if data["imageData"] is not None:
        imageData = base64.b64decode(data["imageData"])
        if PY2 and QT4:
            imageData = utils.img_data_to_png_data(imageData)
    else:
        # relative path from label file to relative path from cwd
        imagePath = osp.join(osp.dirname(filename), data["imagePath"])
        imageData = self.load_image_file(imagePath)
    

    那么直接读imageData和读文件有什么区别呢?

  2. 缺少imagaData,使用imagePath:此时直接调用PIL库读取图片文件。注意PIL读取图片上来后,是不会应用exif属性的,也就是说不会应用图片创建之后的朝向改变(包括旋转和对称)操作。

    但是!!!

    labelme很机智地调用了PIL自带的旋转功能,将图片转成了exif里的朝向。

    matplotlib底层也是PIL,也是像这样读上来再调整朝向。

    @staticmethod
    def load_image_file(filename):
        try:
            image_pil = PIL.Image.open(filename)
        except IOError:
            logger.error("Failed opening image file: {}".format(filename))
            return
    
        # apply orientation to image according to exif
        image_pil = utils.apply_exif_orientation(image_pil)
    
        with io.BytesIO() as f:
            ext = osp.splitext(filename)[1].lower()
            if PY2 and QT4:
                format = "PNG"
            elif ext in [".jpg", ".jpeg"]:
                format = "JPEG"
            else:
                format = "PNG"
            image_pil.save(f, format=format)
            f.seek(0)
            return f.read()
    

    综上所述,从imagePath读取图片,是不会出问题的。

  3. 直接使用imageData:并没有对图片数据做什么处理,关键就是因为没做什么处理,导致该根据exif信息调整朝向的图没有调整。

  4. 加载图片:最后使用pyqt加载图片:

    image = QtGui.QImage.fromData(self.imageData)
    

    看不到pyqt的源码,但它肯定不会去旋转图片就是了,否则之前的代码根本没必要对PIL读上来的图旋转一次。

    综上所述,从imageData读取图片,不会应用图片创建之后的调整朝向操作。


三、解决方案

思路很简单:对图片批量处理,用PIL读上来后都应用一次旋转,再保存成新的图片就行了。

附上朝向表:

参数行起始(0行位置)列起始(0列位置)调整操作
1
2水平翻转
3180°
4垂直翻转
5顺时针90°+水平翻转
6顺时针90°
7顺时针90°+垂直翻转
8逆时针90°

在这里插入图片描述

细节来了:

从上图的朝向表中我们可以看出,朝向只能是1-8的整数。而实际情况是,有的时候我们获取不到exif属性,更不用说朝向了,或者能获取到朝向,但居然是0。

我们先继续沿用之前举的宝藏的例子:

在这里插入图片描述

也就是说如果图片原本就不带有朝向信息,当然不用担心朝向变化的问题了。

一般来说不是相机拍摄的图片自然是不会有exif属性了,比如表情包、画图软件里画的图、随手截的图等。而有exif属性的图经过聊天软件等渠道的处理,也会丢失exif属性。

至于朝向是0的图片,已知部分安卓手机拍摄的照片会出现这种情况。

开干:

import os
import json
import base64
import io
from PIL import Image, ImageOps

def transpose_img(str_img):
    """
    根据exif信息旋转图片的函数。
    :param str_img: 已经用base64编码之后的图片。
    :return: 图片格式、旋转后的Image对象。
    """
    # 解码成二进制图片,如果你的图片本来就是二进制的,可以跳过这步
    bytes_img = base64.b64decode(str_img)
    # 实例化Image对象
    new_img = Image.open(io.BytesIO(bytes_img), mode='r')
    # 调用exif_transpose()函数,自动根据exif属性旋转
    # 这里是情况特殊,如果你不需要图片格式,可以不返回.format
    return new_img.format, ImageOps.exif_transpose(new_img)
def to_bytes_img(pil_img, fmt):
    """
    将图片重新二进制化的函数。
    :param pil_img: Image对象。
    :fmt: 图片的format属性。
    :return: 二进制形式的图片。
    """
    secondIO = io.BytesIO()
    # 注意这里的quality默认是75,95为最佳;dpi得指定成原图的dpi;这种无图片路径的情况format必须指定;exif也得指定成原图的exif,否则会丢失
    pil_img.save(
        secondIO, 
        format=fmt, 
        quality=95, 
        dpi=pil_img.info.get('dpi', (96.0, 96.0)), 
        exif=pil_img.info.get('exif', b""))
    return secondIO.getvalue()

补充说明一些细节:

  • Image.info可以查看很多图片属性,包括exif属性,但这样得到的exif属性是二进制形式的。也可以使用Image.getexif()获取,但这样获取的属性是不全的。如果想看完整的exif属性,可以使用exifread库或者piexif库;
  • 原来的exif在调用exif_transpose()函数时会自动删除第274号orientation朝向属性,因为若原图exif里是90 cw(顺时针旋转90度),转完了肯定不能还显示90 cw。所以省得我们手动改exif了;
  • 获取字典中的值都用get,有的图片没有那么多info属性;
  • 第二个函数必须传format参数进来,因为图片调整朝向后可能会丢失format属性

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/52677.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

虚拟化基本知识及virtio-net初探

QEMU/KVM是在Linux中被广泛使用的虚拟化技术之一,而virtio作为一个半虚拟化I/O事实上的标准[1],是QEMU/KVM在I/O虚拟化部分的默认实现。virtio-net是virtio标准中的网卡设备,被广泛应用。本文将会沿着虚拟化,virtio半虚拟化I/O&am…

非对称风险模型

推荐模型:非对称性风险,让自己置身于一个好结果比坏结果影响大得多的环境中 比如投资,将85%~90%投入到极低风险的资产中(国债),来享受确定性收益;剩下的投入的10%~15%投入到极高风险资产中&…

使用FeatureTask多线程优化in,提高查询速度

场景是这样的:使用in查询数据的时候,in的数量越多,效率越低,所以一个优化的思路是,缩小in查询的数量,用多线程的方式查询缩小数量后in的sql,并行查询。 直接上代码: public List&l…

Linux零基础入门(四)Linux实用操作

Linux零基础入门(四)Linux实用操作前言Linux实用操作一 各类小技巧(快捷键)1 ctrl c 强制停止2 ctrl d 退出或登出3 历史命令搜索4 光标移动快捷键5 清屏二 软件安装1 Linux系统的应用商店2 yum命令3 apt命令 - 扩展三 systemct…

全光谱台灯对孩子有伤害吗?儿童用台灯的好处和坏处是什么

全光谱台灯是指灯光色谱丰富度与太阳光一般全面的台灯,这样的灯光照射下的任何物体,不但颜色丰富多彩,而且极其真实,无限接近太阳光下的真实色彩,对人眼舒适度有巨大的提升,所以全光谱台灯不但对孩子无害&a…

5G无线技术基础自学系列 | MU-MIMO原理

素材来源:《5G无线网络规划与优化》 一边学习一边整理内容,并与大家分享,侵权即删,谢谢支持! 附上汇总贴:5G无线技术基础自学系列 | 汇总_COCOgsta的博客-CSDN博客 MU-MIMO是指多个用户在上下行数据传输时…

算法实操:Python代码实现直插排序(含有序在前有序在后)

【学习的细节是欢悦的历程】Python 官网:https://www.python.org/ Free:大咖免费“圣经”教程《 python 完全自学教程》,不仅仅是基础那么简单…… 自学并不是什么神秘的东西,一个人一辈子自学的时间总是比在学校学习的时间长&a…

[附源码]SSM计算机毕业设计音乐网站JAVA

项目运行 环境配置: Jdk1.8 Tomcat7.0 Mysql HBuilderX(Webstorm也行) Eclispe(IntelliJ IDEA,Eclispe,MyEclispe,Sts都支持)。 项目技术: SSM mybatis Maven Vue 等等组成,B/S模式 M…

用结构体解决实际问题中构造数据类型

用结构体解决实际问题中构造数据类型。 实验内容 (1)题目:编写input()和output()函数输入,输出5个学生的数据记录。学生数据定义如下: #define N 5struct student{ char num[6];char name[8];int score[4];} stu[N];…

anaconda安装paddle(安装CUDA,CUDNN)

前言 为什么会写这样一篇呢,应该早晚会用到paddlelite,所以paddle还是要学的,与其在飞桨平台上跑,不如在自己电脑上跑。我以为安装paddle只需要三行代码: # 打开Anaconda Prompt conda create -n paddle python3.9 c…

C++异常及异常优缺点

🧸🧸🧸各位大佬大家好,我是猪皮兄弟🧸🧸🧸 文章目录一、C语言传统的处理错误的方式二、C异常1.throw catch2.异常的抛出和捕获3.异常的抛出和捕获原则4.函数调用链 异常 栈展开匹配原则5.异常的…

微信小程序怎样开发?【小程序开发】

说到微信小程序,很多公司企业商家都会有自己的微信小程序,已经是他们的标配了。那么还没有自己的微信小程序的小伙伴,也在筹备着开发小程序。那么微信小程序怎样开发的呢,今天就教大家一个比较简单的开发方法。 微信小程序怎样开…

Linux进程通信之共享内存

一、共享内存之原理 1.是在物理内存中开辟了一片空间; 2.不同的进程通过页表将物理内存空间映射到自己的进程虚拟地址空间之中 3.不同的进程可以通过操作自己的虚拟地址空间中的虚拟地址去操作共享内存(物理地址) 共享内存是最快的进程之间…

为什么计算机中的负数要用补码表示?

本文已收录到 AndroidFamily,技术和职场问题,请关注公众号 [彭旭锐] 提问。 前言 大家好,我是小彭。 在前面的文章里,我们聊到了计算机的冯诺依曼架构的 3 个基本原则。其中第 1 个原则是计算机中所有信息都是采用二进制格式的编…

【在Spring MVC框架中,关于限制请求方式】

目录 1.关于限制请求方式 2. 附:关于GET和POST请求方式 1.关于限制请求方式 在Spring MVC框架中,RequestMapping注解的主要作用是配置请求路径,除此以外,还可以配置请求方式,例如: RequestMapping(value…

【Linux常见指令1】

目录:前言常用指令ls指令whoami && pwdcdtouch (触摸)mkdir (make directory)rmdir && rm (remove)mv(move 移动)cp(copy 拷贝)stat (统计)nanoechogccman(重要&…

如何在一台服务器同一个端口运行多个pgbouncer

PGbouncer是Postgresql数据库最常用的一款连接池软件,但是它是单进程的,所以只能占用一颗CPU资源,会造成CPU资源的浪费。PGbouncer有方法在同一台服务器的同一个端口运行多个进程实例,可以让资源得到充分利用。 先看下一个pgbounc…

【愚公系列】2022年12月 使用win11系统自带SSH,远程控制VMware中Liunx虚拟机系统

文章目录前言1.cpolar简介2.cpolar功能一、使用win11系统自带SSH,远程控制VMware中Liunx虚拟机系统1.注册cpolar账号2.下载最新版Ubuntu系统3.Ubuntu系统安装curl4.Ubuntu系统安装cpolar5.Ubuntu开启SSH6.WIN11测试SSH总结前言 身为开发人员,虚拟化系统…

Java基础之《netty(6)—NIO快速入门》

一、案例 1、编写一个NIO入门案例,实现服务器端和客户端之间的数据简单通讯(非阻塞) 2、目的:理解NIO非阻塞网络编程机制 3、代码 NIOServer.java package netty.niostart;import java.io.IOException; import java.net.InetSoc…

死锁问题【javaEE初阶】

什么是死锁? 所谓死锁,是指多个进程在运行过程中因争夺资源而造成的一种僵局,当进程处于这种僵持状态时,若无外力作用,它们都将无法再向前推进。 因此我们举个例子来描述,如果此时有一个线程A&…