6

I tried filling a numpy array with the foreach_get() method on an image, but get the following error:
foreach_get/set() takes exactly 1 argument (2 given)

def read_pixels(img):
    resolution = img.size[0] * img.size[1]  
    colors = np.zeros(resolution*4, dtype=np.float)
    img.pixels.foreach_get("pixels", colors)
    colors = np.reshape(colors, (resolution, 4))
    return colors

img = bpy.data.images['TestImg.png'] print(read_pixels(img))

foreach_get() should take 2 arguments, doesn't it? The name of the attribute ('pixels') and the array to be filled?!

Tortenrandband
  • 771
  • 6
  • 13
  • 2
    I'm not sure, but you call foreach_get method on the pixels object, so the method uses its self argument which is set automatically to the pixels object. From what I could google, other people use foreach_get by only passing the numpy array as a temporary buffer, so you should try img.pixels.foreach_get(colors) – Markus von Broady Mar 09 '21 at 10:33

2 Answers2

10

"The python console check"

In case Markus is busy can get a lot of info using autocomplete Tab in the python console, with an image as img

>>> img.pixels.foreach_get(
foreach_get(seq)
.. method:: foreach_get(seq)
This is a function to give fast access to array data.

which is plainly different from, for instance a mesh me vertices, which asks for an attribute attr , eg for vert coordinates "co"

>>> me.vertices.foreach_get(
foreach_get(attr, seq)
.. method:: foreach_get(attr, seq)
This is a function to give fast access to attributes within a collection.

A very slight improvement in speed can be attained by using np.empty to initialize the array, since any garbage will be filled by the foreach get.

import time
import numpy as np

REPS = 100

def time_it(func): def wrapper(arg, kw): t1 = time.time() for i in range(REPS): func(arg, **kw) t2 = time.time() print(func.name, (t2 - t1)) return wrapper

@time_it def read_pixels(img): resolution = img.size[0] * img.size[1]
colors = np.zeros(resolution*4, dtype=np.float32) img.pixels.foreach_get(colors) colors = np.reshape(colors, (resolution, 4)) return colors

@time_it def read_pixels2(img): x, y = img.size pixels = np.empty(x * y << 2, dtype=np.float32) img.pixels.foreach_get(pixels) return pixels.reshape((x, y, 4))

@time_it def nptest1(img): x, y = img.size return np.array(img.pixels, dtype=np.float32).reshape((x, y, 4))

@time_it
def nptest2(img): x, y = img.size return np.array(img.pixels[:], dtype=np.float32).reshape((x, y, 4))

import bpy for img in bpy.data.images: nptest1(img) nptest2(img) read_pixels(img) read_pixels2(img) #break

Results

nptest1 8.4240
nptest2 7.0605
read_pixels 0.2264
read_pixels2 0.2222

Note on posting noticed I had reshaped to image x, y, 4, however reshape is an almost time "free" method, will get back if otherwise. (Got a squeak more using bitwise shift to quadruple)

batFINGER
  • 84,216
  • 10
  • 108
  • 233
3

as Markus von Broady pointed out, ommiting the attribute name and just passing the numpy array works. Thank you.

I was confused, because specifying the the attribute name worked in other areas, e.g.

sculpt_vertex_colors[name].data.foreach_get('color', colors)

But I guess that's a different structure.
Had to change the dtype to float32 instead of just float.
Here is the corrected example

def read_pixels(img):
    resolution = img.size[0] * img.size[1]  
    colors = np.zeros(resolution*4, dtype=np.float32)
    img.pixels.foreach_get(colors)
    colors = np.reshape(colors, (resolution, 4))
    return colors

img = bpy.data.images['TestImg.png'] print(read_pixels(img))

Tortenrandband
  • 771
  • 6
  • 13