Avoiding numpy with PythonCall

I’m trying to avoid using numpy in the following MWE.

The main goal here is to construct the object-points argument, objectPoints, needed in OpenCV’s camera calibration function, calibrateCamera.

The following creates the correct objectPoints object:

using PythonCall

using CondaPkg
CondaPkg.add("numpy")

np = pyimport("numpy")

nc = (2, 3)
n = 4

objp = np.zeros((1, *(nc...), 3), np.float32)
objectPoints = PyList(fill(objp, n))

Is there a way, and if so how, to create a similar objectPoints without using numpy? My main test for if the alternative is similar or not is if calibrateCamera accepts it or not…

When a Julia array is passed to Python with PythonCall, it is given the numpy array interface. This means that you can pass Julia arrays to most functions which accept numpy arrays.

So replacing np.zeros with zeros should work:

objp = zeros(Float32, (1, *(nc...), 3))
objectPoints = pylist(fill(objp, n))

Furthermore, the conversion to a Python list might be unnecessary:

objectPoints = fill(objp, n)

Thanks for the suggestion!

However, here is a more complete MWE that illustrates a problem with your suggestion.

The following works:

using PythonCall

using CondaPkg
CondaPkg.add.(["numpy", "opencv"])

np = pyimport("numpy")
cv2 = pyimport("cv2")

nc = (5, 8) # number of corners in the calibration checkerboard
n = 9 # number of images of the checkerboard
sz = (10, 10) # image dimensions

objp = np.zeros((1, *(nc...), 3), np.float32)
# the following just populates the object points with real-world coordinates
for (i, v) in enumerate(repeat(1:nc[1], outer = nc[2]))
    objp[0][i - 1][0] = v - 1
end
for (i, v) in enumerate(repeat(1:nc[2], inner = nc[1]))
    objp[0][i - 1][1] = v - 1
end
objectPoints = fill(objp, n)

imgp = np.zeros((*(nc...), 1, 2), np.float32)
# the following just populates the image points with random pixel coordinates
for i in 1:*(nc...)
    imgp[i - 1] = rand()
end
imagePoints = fill(imgp, n)

# the following call works
cv2.calibrateCamera(objectPoints, imagePoints, sz, nothing, nothing)

But if I replace objp with your suggestion:

objp = zeros(Float32, (1, *(nc...), 3))
# and fill it with the same real-world coordinates
objp[1, :, 1] .= repeat(0:nc[1] - 1, outer = nc[2])
objp[1, :, 2] .= repeat(0:nc[2] - 1, inner = nc[1])

the call to cv2.calibrateCamera fails with:

julia> cv2.calibrateCamera(objectPoints, imagePoints, sz, nothing, nothing)
ERROR: Python: error: OpenCV(4.7.0) :-1: error: (-5:Bad argument) in function 'calibrateCamera'
> Overload resolution failed:
>  - Can't parse 'objectPoints'. Sequence item with index 0 has a wrong type
>  - Can't parse 'objectPoints'. Sequence item with index 0 has a wrong type

Python stacktrace: none
Stacktrace:
 [1] pythrow()
   @ PythonCall ~/.julia/packages/PythonCall/3GRYN/src/err.jl:94
 [2] errcheck
   @ ~/.julia/packages/PythonCall/3GRYN/src/err.jl:10 [inlined]
 [3] pycallargs(f::Py, args::Py)
   @ PythonCall ~/.julia/packages/PythonCall/3GRYN/src/abstract/object.jl:210
 [4] pycall(::Py, ::Vector{Array{Float32, 3}}, ::Vararg{Any}; kwargs::Base.Pairs{Symbol, Union{}, Tuple{}, NamedTuple{(), Tuple{}}})
   @ PythonCall ~/.julia/packages/PythonCall/3GRYN/src/abstract/object.jl:228
 [5] pycall(::Py, ::Vector{Array{Float32, 3}}, ::Vararg{Any})
   @ PythonCall ~/.julia/packages/PythonCall/3GRYN/src/abstract/object.jl:218
 [6] (::Py)(::Vector{Array{Float32, 3}}, ::Vararg{Any}; kwargs::Base.Pairs{Symbol, Union{}, Tuple{}, NamedTuple{(), Tuple{}}})
   @ PythonCall ~/.julia/packages/PythonCall/3GRYN/src/Py.jl:352
 [7] (::Py)(::Vector{Array{Float32, 3}}, ::Vararg{Any})
   @ PythonCall ~/.julia/packages/PythonCall/3GRYN/src/Py.jl:352
 [8] top-level scope
   @ REPL[103]:1

To me, Can't parse 'objectPoints'. Sequence item with index 0 has a wrong type seems to suggest that the type of the elements in objectPoints is wrong.

It looks like OpenCV only accepts actual numpy arrays, and not array-like objects: opencv/cv2_convert.cpp at 8bd17163c79231a4afbbdf978864cd2d0c6b1f67 · opencv/opencv · GitHub

You could make a PR to have it accept anything satisfying the array interface.

1 Like

Ah, I see. OK, I think I’ll live with numpy then. Changing opencv just so I won’t need to use numpy with PythonCall seems a bit extreme (unless there are other legitimate reasons to change that type check?).

Thanks for the detective work though!

No problem. You can nevertheless do more in Julia - you can construct objp as an ordinary Julia array and convert it to numpy afterwards.

Stuff like this is a bit of a gripe of mine. Python is supposed to be “duck typed” meaning you rely on interfaces (e.g. does it iterate, can it convert to array) than checking for specific types, which is what OpenCV does here.

1 Like

I managed to do exactly what you suggested. The ugly part is that I have these conversion functions that convert julia things into python things just because of those type-checks. It’s fine. Thanks again!