【问题标题】:Render NumPy array in FastAPI在 FastAPI 中渲染 NumPy 数组
【发布时间】:2022-07-02 12:48:06
【问题描述】:

我找到了How to return a numpy array as an image using FastAPI?,但是,我仍在努力显示图像,它看起来只是一个白色方块。

我将一个数组读入io.BytesIO,如下所示:

def iterarray(array):
    output = io.BytesIO()
    np.savez(output, array)
    yield output.get_value()

在我的终点,我的回报是StreamingResponse(iterarray(), media_type='application/octet-stream')

当我将 media_type 留空时,会推断出一个 zip 文件已下载。

如何让数组显示为图像?

【问题讨论】:

  • 文件下载后的阅读效果如何?
  • @richardec 我正在通过查找一组数组(来自 grib 文件)的平均值来生成数组,所以我并没有真正下载文件?如果我误解了,请澄清一下
  • 哦,对不起。你有一个fastapi 服务器,对吧?并且您正在从中返回一个数组,那么您如何在另一端获取该数组?
  • 哦,我明白了,它们是我计算机上的本地文件,我刚刚在我的代码中硬编码了它们的路径。使用 GDAL 库读取它们以提取数组
  • @Chris 感谢您非常全面的回答!

标签: python numpy fastapi bytesio


【解决方案1】:

选项 1 - 以字节形式返回图像

以下示例显示如何将从磁盘加载的图像或内存中的图像(numpy 数组)转换为字节(使用 PILOpenCV 库)并使用自定义 Response 返回它们.出于本演示的目的,以下代码用于创建基于 this answer 的内存示例图像(numpy 数组)。

# Function to create a sample RGB image
def create_img():
    w, h = 512, 512
    arr = np.zeros((h, w, 3), dtype=np.uint8)
    arr[0:256, 0:256] = [255, 0, 0] # red patch in upper left
    return arr

使用 PIL


服务器端:

您可以从磁盘加载图片,也可以使用Image.fromarray来加载内存中的图片(注意:出于演示目的,当案例是从磁盘加载图片时,以下演示该操作在路由内部进行。但是,如果要多次提供同一图像,则可以在startupstore it on the app instance 仅加载一次图像,如this answer 所述)。接下来,将图像写入缓冲流,即BytesIO,并使用getvalue() 方法获取缓冲区的全部内容。即使缓冲的流在超出范围时被垃圾回收,通常也是better to call close() or use the with statement,如图here

from fastapi import Response
from PIL import Image
import numpy as np
import io

@app.get("/image", response_class=Response)
def get_image():
    # loading image from disk
    # im = Image.open('test.png')
    
    # using an in-memory image
    arr = create_img()
    im = Image.fromarray(arr)
    
    # save image to an in-memory bytes buffer
    with io.BytesIO() as buf:
        im.save(buf, format='PNG')
        im_bytes = buf.getvalue()
        
    headers = {'Content-Disposition': 'inline; filename="test.png"'}
    return Response(im_bytes, headers=headers, media_type='image/png')
客户端:

下面演示了如何使用 Python requests 模块向上述端点发送请求,并将接收到的字节写入文件,或将字节转换回 PIL Image,如 here 所述。

import requests
from PIL import Image

url = 'http://127.0.0.1:8000/image'
r = requests.get(url=url)

# write raw bytes to file
with open("test.png", 'wb') as f:
    f.write(r.content)

# or, convert back to PIL Image
# im = Image.open(io.BytesIO(r.content))
# im.save("test.png") 

使用 OpenCV


服务器端:

您可以使用cv2.imread() 函数从磁盘加载图像,或者使用内存中的图像,如果它是RGB 的顺序,如下例所示 - 需要转换为OpenCV uses BGR as its default colour order for images。接下来,使用cv2.imencode() 函数压缩图像数据(基于您传递的定义输出格式的文件扩展名 - 即.png.jpg 等)并将其存储在内存缓冲区中用于通过网络传输数据。

import cv2

@app.get("/image", response_class=Response)
def get_image():
    # loading image from disk
    # arr = cv2.imread('test.png', cv2.IMREAD_UNCHANGED)
    
    # using an in-memory image
    arr = create_img()
    arr = cv2.cvtColor(arr, cv2.COLOR_RGB2BGR)
    # arr = cv2.cvtColor(arr, cv2.COLOR_RGBA2BGRA) # if dealing with 4-channel RGBA (transparent) image

    success, im = cv2.imencode('.png', arr)
    headers = {'Content-Disposition': 'inline; filename="test.png"'}
    return Response(im.tobytes() , headers=headers, media_type='image/png')
客户端:

在客户端,您可以将原始字节写入文件,或使用numpy.frombuffer() 函数和cv2.imdecode() 函数将缓冲区解压缩为图像格式(类似于this) - cv2.imdecode() 不需要一个文件扩展名,因为正确的编解码器将从缓冲区中压缩图像的第一个字节推导出来。

url = 'http://127.0.0.1:8000/image'
r = requests.get(url=url) 

# write raw bytes to file
with open("test.png", 'wb') as f:
    f.write(r.content)

# or, convert back to image format    
# arr = np.frombuffer(r.content, np.uint8)
# img_np = cv2.imdecode(arr, cv2.IMREAD_UNCHANGED)
# cv2.imwrite('test.png', img_np)

更多信息

由于您注意到您希望显示图像 - 类似于 FileResponse - 使用自定义 Response 返回字节应该是执行此操作的方法,而不是使用 StreamingResponse。为了向浏览器指示应在浏览器中查看图像,HTTP 响应应包含以下标头,如 here 所述和上述示例中所示(filename 周围的引号是必需的,如果filename 包含特殊字符):

headers = {'Content-Disposition': 'inline; filename="test.png"'}

然而,下载而不是查看图像(使用 attachment 代替):

headers = {'Content-Disposition': 'attachment; filename="test.png"'}

如果您想使用 Javascript 接口(例如 Fetch API 或 Axios)显示(或下载)图像,请查看答案 herehere

至于StreamingResponse,如果numpy数组从一开始就完全加载到内存中,StreamingResponse就完全没有必要了。 StreamingResponse 通过迭代 iter() 函数提供的块来流式传输(如果 Content-Length 未在标头中设置,与 StreamingResponse 不同,其他 Response 类为您设置该标头,以便浏览器会知道数据在哪里结束)。如this answer中所述:

当您不知道数据的大小时,分块传输编码是有意义的 你的输出提前,你不想等待收集它 在您开始将其发送给客户之前找出所有内容。这样可以 适用于提供慢速数据库查询结果之类的东西,但是 它通常不适用于提供图片

即使您想流式传输保存在磁盘上的图像文件(您不应该这样做,除非它是一个无法放入内存的相当大的文件。相反,您应该使用 FileResponse) , file-like 对象,如open() 创建的对象,是普通的迭代器;因此,您可以直接在StreamingResponse 中返回它们,如documentation 中所述,如下所示:

@app.get("/image")
def get_image():
    def iterfile():  
        with open("test.png", mode="rb") as f:  
            yield from f  
            
    return StreamingResponse(iterfile(), media_type="image/png")

或者,如果图像被加载到内存中,然后被保存到BytesIO 缓冲流中以返回字节,BytesIO,就像io module 的所有具体类一样,是@987654350 @object,表示也可以直接返回:

@app.get("/image")
def get_image():
    arr = create_img()
    im = Image.fromarray(arr)
    buf = BytesIO()
    im.save(buf, format='PNG')
    buf.seek(0)
    return StreamingResponse(buf, media_type="image/png")

因此,对于您的情况,最好使用您的自定义contentmedia_type 返回一个Response,并设置Content-Disposition 标头,如上所述,以便查看图像在浏览器中。

选项 2 - 将图像返回为 JSON 编码的 numpy 数组

下面不应该用于在浏览器中显示图像,而是在这里添加 - 为了完整起见 - 显示如何将图像转换为 numpy 数组(preferably, using asarray() function),返回 numpy 数组并将其转换回客户端的图像,如thisthis 回答中所述。

使用 PIL


服务器端:
@app.get("/image")
def get_image():
    im = Image.open('test.png')
    # im = Image.open("test.png").convert("RGBA") # if dealing with 4-channel RGBA (transparent) image 
    arr = np.asarray(im)
    return json.dumps(arr.tolist())
客户端:
url = 'http://127.0.0.1:8000/image'
r = requests.get(url=url) 
arr = np.asarray(json.loads(r.json())).astype(np.uint8)
im = Image.fromarray(arr)
im.save("test.png")

使用 OpenCV


服务器端:
@app.get("/image")
def get_image():
    arr = cv2.imread('test.png', cv2.IMREAD_UNCHANGED)
    return json.dumps(arr.tolist())
客户端:
url = 'http://127.0.0.1:8000/image'
r = requests.get(url=url) 
arr = np.asarray(json.loads(r.json())).astype(np.uint8)
cv2.imwrite('test.png', arr)

【讨论】:

    猜你喜欢
    • 2018-05-01
    • 2019-05-10
    • 2019-10-21
    • 2018-02-07
    • 2021-03-16
    • 2014-11-10
    • 1970-01-01
    • 1970-01-01
    • 2018-03-05
    相关资源
    最近更新 更多