利用 ImageData 实现图片左右旋转 90°

问题描述

最近在做 OCR 图像识别。大致流程是先拿到预览区图片的 base64 字符串,根据接口要求压缩 base64 字符串大小,再调用 OCR 相关接口获取识别结果。然而,通过文件上传 input 域、FileReader 读到的 base64 字符串直接放入 img 标签后,预览出的图片往往会出现 原本为纵向拍摄的图片默认按横向图片展示,从而导致后续 OCR 识别报错。

方向正确:
在这里插入图片描述
方向错误:
在这里插入图片描述
究其原因,可能是 canvas.toDataURL(type, quality) 生成的 base64 字符串没有图片朝向相关的标识,赋给 img.src 后,img 标签 默认将较长的一边作为宽度、较短的一边作为高度 来显示图片。要解决这个问题,需要在图片加载完毕后,手动调节图片的朝向(右转或左转 90°)。

网上关于图片转向的文章大多通过构造新的 canvas 画布,设置具体的转向角度后重绘图片,最后在写回原图片。结合项目实际需求,只需要简单左右旋转 90° 即可。这可以通过 ImageData 对象的像素变换轻松实现。

基本原理1——像素矩阵变换

ImageData 是图片经数据化处理后的对象,其中包含三个属性:

  • width:图片的总宽度像素值(整数)
  • height:图片的总高度像素值(整数)
  • data:八位无符号整型固定数组、一个特殊的类型数组。该数组每 4 个元素的值,依次描述了对应像素点的 R、G、B、A 的取值,值域均为 [0, 255]。

因此一个 4 × 3 像素的原始图片,可以看作如下形式的像素矩阵 A
A = [ a 11 a 12 a 13 a 14 a 21 a 22 a 23 a 24 a 31 a 32 a 33 a 34 ] (1) A = \left[ \begin{matrix} a_{11} & a_{12} & a_{13} & a_{14} \\ a_{21} & a_{22} & a_{23} & a_{24} \\ a_{31} & a_{32} & a_{33} & a_{34} \end{matrix} \right] \tag{1} A=a11a21a31a12a22a32a13a23a33a14a24a34(1)
图片向右旋转 90°,实质就是设法将 A 变为 A’ ——
A ′ = [ a 31 a 21 a 11 a 32 a 22 a 12 a 33 a 23 a 13 a 34 a 24 a 14 ] (2) A'= \left[ \begin{matrix} a_{31} & a_{21} & a_{11}\\ a_{32} & a_{22} & a_{12}\\ a_{33} & a_{23} & a_{13}\\ a_{34} & a_{24} & a_{14} \end{matrix} \right] \tag{2} A=a31a32a33a34a21a22a23a24a11a12a13a14(2)

这可以通过原矩阵一次 转置、与多次初等 变换(逆序排列各列)得到:

A T = [ a 11 a 12 a 13 a 14 a 21 a 22 a 23 a 24 a 31 a 32 a 33 a 34 ] T = [ a 11 a 21 a 31 a 12 a 22 a 32 a 13 a 23 a 33 a 14 a 24 a 34 ] = > [ a 31 a 21 a 11 a 32 a 22 a 12 a 33 a 23 a 13 a 34 a 24 a 14 ] = A ′ (3) A^T=\left[ \begin{matrix} a_{11} & a_{12} & a_{13} & a_{14} \\ a_{21} & a_{22} & a_{23} & a_{24} \\ a_{31} & a_{32} & a_{33} & a_{34} \end{matrix} \right]^T= \left[ \begin{matrix} a_{11} & a_{21} & a_{31}\\ a_{12} & a_{22} & a_{32}\\ a_{13} & a_{23} & a_{33}\\ a_{14} & a_{24} & a_{34} \end{matrix} \right] => \left[ \begin{matrix} a_{31} & a_{21} & a_{11}\\ a_{32} & a_{22} & a_{12}\\ a_{33} & a_{23} & a_{13}\\ a_{34} & a_{24} & a_{14} \end{matrix} \right] = A' \tag{3} AT=a11a21a31a12a22a32a13a23a33a14a24a34T=a11a12a13a14a21a22a23a24a31a32a33a34=>a31a32a33a34a21a22a23a24a11a12a13a14=A(3)

同理,图片向左旋转 90°,实际上就是得到矩阵 A’'

A ′ ′ = [ a 14 a 24 a 34 a 13 a 23 a 33 a 12 a 22 a 32 a 11 a 21 a 31 ] (4) A''= \left[ \begin{matrix} a_{14} & a_{24} & a_{34}\\ a_{13} & a_{23} & a_{33}\\ a_{12} & a_{22} & a_{32}\\ a_{11} & a_{21} & a_{31} \end{matrix} \right] \tag{4} A=a14a13a12a11a24a23a22a21a34a33a32a31(4)
这可以通过原矩阵一次 转置、与多次初等 变换(逆序排列各行)得到——

A T = [ a 11 a 12 a 13 a 14 a 21 a 22 a 23 a 24 a 31 a 32 a 33 a 34 ] T = [ a 11 a 21 a 31 a 12 a 22 a 32 a 13 a 23 a 33 a 14 a 24 a 34 ] = > [ a 14 a 24 a 34 a 13 a 23 a 33 a 12 a 22 a 32 a 11 a 21 a 31 ] = A ′ ′ (5) A^T = \left[ \begin{matrix} a_{11} & a_{12} & a_{13} & a_{14} \\ a_{21} & a_{22} & a_{23} & a_{24} \\ a_{31} & a_{32} & a_{33} & a_{34} \end{matrix} \right]^T= \left[ \begin{matrix} a_{11} & a_{21} & a_{31}\\ a_{12} & a_{22} & a_{32}\\ a_{13} & a_{23} & a_{33}\\ a_{14} & a_{24} & a_{34} \end{matrix} \right]=> \left[ \begin{matrix} a_{14} & a_{24} & a_{34}\\ a_{13} & a_{23} & a_{33}\\ a_{12} & a_{22} & a_{32}\\ a_{11} & a_{21} & a_{31} \end{matrix} \right] = A'' \tag{5} AT=a11a21a31a12a22a32a13a23a33a14a24a34T=a11a12a13a14a21a22a23a24a31a32a33a34=>a14a13a12a11a24a23a22a21a34a33a32a31=A(5)

基本原理2——像素数组与矩阵的对应关系

由于 ImageData.data 对应一个数组,对于 4 × 3 的图片而言,ImageData.data 就是一个具有 48 个元素的数组 D,不妨每个元素的值就是其下标值,则:
D = [ 0 , 1 , 2 , 3 , 4 , 5 , 6 , 7...44 , 45 , 46 , 47 ] (6) D = \left[0, 1, 2, 3, 4, 5, 6, 7... 44, 45, 46, 47\right]\tag{6} D=[0,1,2,3,4,5,6,7...44,45,46,47](6)
其中:

元组 (0, 1, 2, 3) 表示第 1(= 0 / 4 + 1) 个像素的颜色为 rgba(0, 1, 2, 3/255)
元组 (4, 5, 6, 7) 表示第 2(= 4 / 4 + 1) 个像素的颜色为 rgba(4, 5, 6, 7/255)
元组 (8, 9, 10, 11) 表示第 3(= 8 / 4 + 1) 个像素的颜色为 rgba(8, 9, 10, 11/255)

元组 (i, i+1, i+2, i+3) 表示第 (i / 4 + 1) 个像素的颜色为 rgba(i, i+1, i+2, (i+3)/255)

元组 (44, 45, 46, 47) 表示第 12(= 44 / 4 + 1) 个像素的颜色为 rgba(44, 45, 46, 47/255)

可见从 0 开始遍历 D 数组,每次递增 4 个单位,即可依次得到各个像素的红色值 R,再依次加1、加2、加3,即得到对应的绿色值 G、蓝色值 B、等效 α 通道值 A。

反之,如果知道图片的像素尺寸为 4 × 3,则可以通过下图找到数组 D 的各个元素:

imageMatrix

可见各像素点是按照 从左至右、从上至下 的顺序排列的。设图片总宽度像素为 W,总高度像素为 H,任一像素点 P 的坐标为 (x, y)P 的红色值在数组 D 的下标为 R(x, y),则:
R ( x , y ) = ( x + W ⋅ y ) × 4 (7) R(x, y) = (x + W · y) × 4 \tag{7} R(x,y)=(x+Wy)×4(7)
验证:(x 与 y 均从 0 开始计数)

R(2, 1) = (2 + 1 × 4) × 4 = 24
R(1, 2) = (1 + 2 × 4) × 4 = 36
R(3, 1) = (3 + 1 × 4) × 4 = 28

拿到了 R(x, y),不难求出该像素的纵向中心对称像素 Rh(x, y)、横向中心对称像素 Rw(x, y)、以及主对角线对称像素 Rd(x, y)
R h ( x , y ) = [ x + W ⋅ ( H − 1 − y ) ] × 4 (8-1) Rh(x, y) = [x + W · (H - 1 - y)] × 4 \tag{8-1} Rh(x,y)=[x+W(H1y)]×4(8-1)

R w ( x , y ) = [ ( W − 1 − x ) + W ⋅ y ] × 4 (8-2) Rw(x, y) = [(W - 1 - x) + W · y] × 4 \tag{8-2} Rw(x,y)=[(W1x)+Wy]×4(8-2)

R d ( x , y ) = ( y + H ⋅ x ) × 4 (8-3) Rd(x, y) = (y + H · x) × 4 \tag{8-3} Rd(x,y)=(y+Hx)×4(8-3)

其中,式(8-3)用于 转置 运算;式(8-1)、式(8-2)分别用于 初等行变换初等列变换

具体实现

基本思路:

  1. 通过 canvas 获取目标图片的 ImageData 对象;
  2. 转置原图片数组,得到数组 AT
  3. AT 执行一组初等行变换,使各行逆序排列,得到左旋 90° 效果;
  4. AT 执行一组初等列变换,使各列逆序排列,得到右旋 90° 效果;
  5. 将新的像素数组写回图片源标签。

HTML

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>Rotate by ImageData</title>
    <style>
        .image{ margin-top: 5px; }
    </style>
</head>
<body>
    <div class="btns">
        <input type="button" value="左转 90°" id="turnLeft" />
        <input type="button" value="右转 90°" id="turnRight" />
    </div>
    <div class="image">
        <img id="fruit" src="fruit.jpg" class="image" alt="fruit" title="fruit" />
    </div>
    <script src="imageRotate.js"></script>
</body>

</html>

imageRotate.js

document.querySelector('#turnLeft' ).addEventListener('click', e => rotateImage('l'))
document.querySelector('#turnRight').addEventListener('click', e => rotateImage('r'))

function rotateImage(direction = 'l') {
    // 1. Prepare ImageData
    let img = document.querySelector('#fruit')
    const { width: W, height: H } = img
    let cvs = document.createElement('canvas')
    cvs.width = W
    cvs.height = H
    let ctx = cvs.getContext('2d')
    ctx.drawImage(img, 0, 0)
    let imgDt0 = ctx.getImageData(0, 0, W, H)
    let imgDt1 = new ImageData(H, W)
    let imgDt2 = new ImageData(H, W)
    let dt0 = imgDt0.data
    let dt1 = imgDt1.data
    let dt2 = imgDt2.data

    // 2. Transpose
    let r = r1 = 0  // index of red pixel in old and new ImageData, respectively
    for (let y = 0, lenH = H; y < lenH; y++) {
        for (let x = 0, lenW = W; x < lenW; x++) {
            r  = (x + lenW * y) * 4
            r1 = (y + lenH * x) * 4
            dt1[r1 + 0] = dt0[r + 0]
            dt1[r1 + 1] = dt0[r + 1]
            dt1[r1 + 2] = dt0[r + 2]
            dt1[r1 + 3] = dt0[r + 3]
        }
    }
    
    // 3. Reverse width / height
    for (let y = 0, lenH = W; y < lenH; y++) {
        for (let x = 0, lenW = H; x < lenW; x++) {
            r  = (x + lenW * y) * 4
            r1 = direction === 'l'
                ? (x + lenW * (lenH - 1 - y)) * 4
                : ((lenW - 1 - x) + lenW * y) * 4
            dt2[r1 + 0] = dt1[r + 0]
            dt2[r1 + 1] = dt1[r + 1]
            dt2[r1 + 2] = dt1[r + 2]
            dt2[r1 + 3] = dt1[r + 3]
        }
    }
    
    // 4. Redraw image
    cvs.width = H
    cvs.height = W
    ctx.clearRect(0, 0, W, H)
    ctx.putImageData(imgDt2, 0, 0, 0, 0, H, W)
    img.src = cvs.toDataURL('image/jpeg', 1)
}

运行结果:

原始图片:
原始图片
左转 90°:
在这里插入图片描述
右转 90°:
右转 90°

示例文件

链接: https://pan.baidu.com/s/1w1_5qh3Tg95VUUjLmhrvTA
提取码: v7f6

  • 1
    点赞
  • 5
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
要将旋转后的JPEG图像以流的形式传输到MINIO对象存储,可以使用Java的MINIO客户端库。以下是一个示例代码,它将JPEG图像旋转90度并将旋转后的图像流上传到MINIO: ```java import io.minio.MinioClient; import io.minio.errors.*; import io.minio.messages.Item; import java.awt.geom.AffineTransform; import java.awt.image.AffineTransformOp; import java.awt.image.BufferedImage; import java.io.ByteArrayInputStream; import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; import java.security.InvalidKeyException; import java.security.NoSuchAlgorithmException; import javax.imageio.ImageIO; public class JpegRotationToMinio { public static void main(String[] args) { String endpoint = "http://minio-server:9000"; // MINIO服务器地址 String accessKey = "YOUR_ACCESS_KEY"; // MINIO访问密钥 String secretKey = "YOUR_SECRET_KEY"; // MINIO访问密钥 String bucketName = "your-bucket"; // MINIO存储桶名称 String objectName = "output.jpg"; // MINIO对象名称 try { // 创建一个MinioClient对象 MinioClient minioClient = new MinioClient(endpoint, accessKey, secretKey); // 读取输入图像 BufferedImage inputImage = ImageIO.read(new File("input.jpg")); // 输入JPEG图像路径 // 创建一个AffineTransform对象,用于旋转图像 AffineTransform transform = new AffineTransform(); transform.rotate(Math.toRadians(-90), inputImage.getWidth() / 2, inputImage.getHeight() / 2); // 执行旋转操作 AffineTransformOp op = new AffineTransformOp(transform, AffineTransformOp.TYPE_BILINEAR); BufferedImage outputImage = op.filter(inputImage, null); // 将旋转后的图像保存到字节数组输出流 ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); ImageIO.write(outputImage, "jpeg", outputStream); // 将字节数组输出流转换为字节数组 byte[] imageData = outputStream.toByteArray(); // 将字节数组作为输入流上传到MINIO ByteArrayInputStream inputStream = new ByteArrayInputStream(imageData); minioClient.putObject(bucketName, objectName, inputStream, imageData.length, "image/jpeg"); System.out.println("图像旋转并上传到MINIO完成!"); } catch (IOException | InvalidKeyException | NoSuchAlgorithmException | InsufficientDataException | InvalidResponseException | InternalException | ErrorResponseException | RegionConflictException | XmlParserException | InvalidBucketNameException | InvalidObjectNameException e) { e.printStackTrace(); } } } ``` 在上述代码中,需要将`endpoint`、`accessKey`、`secretKey`、`bucketName`和`objectName`替换为你的MINIO配置信息和存储桶、对象的名称。代码将读取输入图像,执行反向旋转90度,并将旋转后的图像以流的形式上传到MINIO中。请确保在运行代码之前已经正确设置了MINIO服务器和相关凭证信息。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值