0

I'm trying to pass a frame read from a camera using opencv as an argument to another python script but I cannot get it to work.

Running the following script stores a camera frame on my network every 5 seconds:


#!/usr/bin/python

import cv2
import threading
import subprocess
import time
import datetime

def camera():
    camera = cv2.VideoCapture("/dev/video0")
    if not camera.isOpened():
        print("Cannot open camera.")
        raise SystemExit
    camera.set(cv2.CAP_PROP_FRAME_WIDTH, 320)
    camera.set(cv2.CAP_PROP_FRAME_HEIGHT, 320)
    camera.set(cv2.CAP_PROP_FPS, 15)
    return camera

try:
    camera = camera()
    while True:
        frame = camera.read()[1]
        #subprocess.call(["python", "/home/user/test2.py", frame])
        server = "/net/192.168.4.51/mnt/LAN4Storage/EC/Camera/"
        cv2.imwrite(server + "temp.jpg", frame)
        timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        cv2.imwrite(server + timestamp + ".jpg", frame)
        time.sleep(5)

except Exception as error:
    print(error)

finally:
    camera.release()

If I uncomment the subprocess.call line and comment the next 4 lines I expect the frame to be passed to my test2.py script:

`

    #/usr/bin/env python

    import cv2
    import sys
    import os
    import time
    import datetime

    try:
        frame = cv2.imread(sys.argv[1])
        server = "/net/192.168.4.51/mnt/LAN4Storage/EC/Camera/"
        cv2.imwrite(server + "temp.jpg", frame)
        timestamp = datetime.datetime.now().strftime("%Y-%m-%d %H:%M:%S")
        cv2.imwrite(server + timestamp + ".jpg", frame)

    except Exception as error:
        print(error)

    finally:
        pass

` But on starting test1.py, I get the following error:

`expected str, bytes or os.PathLike object, not ndarray`
  • You can't pass arbitrary objects on the command line of another program, only strings. You probably want to write the file first, and then pass the file name to `test2.py`. – Thomas Nov 02 '22 at 13:33
  • Take a step back... what are you really trying to do? Why is there another program involved? Are you really trying to pass it a frame of video, or actually the name of a JPEG? – Mark Setchell Nov 02 '22 at 14:29
  • @Thomas: I try to avoid writing to file but perhaps this is the only way. – unformatted Nov 02 '22 at 16:15
  • @MarkSetchell: this is a stripped down version or a bigger script, I just want to know how I can pass a frame as an argument. – unformatted Nov 02 '22 at 16:16
  • A 1920x1080 RGB image is 6MB, which is too much to pass to a script. Also, it may contain a zero byte if there is a black pixel, and the shell will interpret that as the end of the string. So, you would have to base64-encode the image to be sure there were no zero bytes - this will make it 30% larger and it already exceeds the limit. So, you'll need to pass the name of a file that contains the image - probably a JPEG or a PNG. If you explained what you are trying to do, we could maybe help you better. – Mark Setchell Nov 02 '22 at 16:32
  • @MarkSetchell: I have a streaming server which reads camera frames using opencv. It detects motion and I want to save the frame with the motion part (including a green rectangle aroud the moving object) to my server. This actually works fine from within the main script. But I want to use another script for handling the motion frame (not only saving to server but also sending an email) because the main script would get too "complex". Hence the question how to pass a frame from the main script to another script WITHOUT saving it to disk first. And BTW: the frames are 320x320 pixels. – unformatted Nov 02 '22 at 16:47
  • Ok, and what is the objection to saving to disk, please? It takes around 1.2 milliseconds to write a 320x320 image to disk on my (decent specification) computer. And are you going to be able to send an Email attachment without a disk file? – Mark Setchell Nov 02 '22 at 17:56
  • Starting a new process for every frame (even if once every 5 seconds) seems rather wasteful. Fire off a persistent process and use whatever IPC method (pipes, sockets, shared memory,...) suits your taste to communicate between them. | Also keep in mind that at least some `VideoCapture` do their own buffering, so sleeping 5 seconds and then just reading one frame may not necessarily get you the latest one. – Dan Mašek Nov 02 '22 at 18:53
  • @MarkSetchell: the code runs on RPi3 with SD card, I haven't though about the email yet. – unformatted Nov 03 '22 at 07:04
  • @DanMašek: the code above is not the final code, just stripped down for demonstration purpose. – unformatted Nov 03 '22 at 07:06
  • `But I want to use another script for handling the motion frame [...] because the main script would get too "complex".` Put it in another Python module! It's easier to code, easier to maintain, and more efficient. – Thomas Nov 04 '22 at 07:09
  • @Thomas: I run into a problem when I import the script as a module. The script contains `def read(): url = "http://127.0.0.1:8000/image.jpg" image = urllib.request.urlopen(url) image = numpy.asarray(bytearray(image.read()), dtype="uint8") image = cv2.imdecode(image, cv2.IMREAD_COLOR) return image` and upon importing from my main script it gives a bunch of "url" errors, probably because the variables from the imported script are evaluated before the mains script has completely run, which contains a streaming server supplying the url. – unformatted Nov 05 '22 at 12:02
  • Put the functionality in a function in the module, import the module, call the function. Maybe do a tutorial about Python modules if this doesn't make sense to you. – Thomas Nov 07 '22 at 11:39

2 Answers2

1

There seem to be a couple of misconceptions here that are leading things astray...

  • You can't sensibly pass images (i.e. their complete pixel data) as command-line arguments/parameters. They are generally too long and may contain nulls.

  • It is grossly inefficient to think about writing an image for every frame of a video. That's actually why we have video - because we can difference and do motion vectors between frames to enable better compression.

  • You can write an image file to RAM without causing wear to an SD card.

In light of what I understand so far, I would suggest either:

  • writing your image to a tmpfs filesystem, a.k.a. RAM drive, or
  • look at this answer, and write the images to Redis where you can pick them up from any machine or process in your network, using Python, C/C++, Ruby or bash scripts.

Note that if you use the file-based approach and unsynchronised access from multiple processes, you may get corruption if a file is overwritten during a read. One way to avoid this is to leverage the fact that renames are atomic. So, write the image to a file called something like InProgress.jpg and when complete, rename InProgress.jpg as Latest.jpg and let readers always read Latest.jpg. That way readers will always get one of the two files and never two inconsistent halves of different versions.

Also, bear in mind Dan's comment about buffering. More material here.

Mark Setchell
  • 191,897
  • 31
  • 273
  • 432
  • Like I said earlier, I already have it working in the main script. I only asked about passing a frame as an argument because I wanted to split the main script. I learned now that this is not good practice since it produces all kind of quirks. BTW, I am only saving a couple of frames to my server when motion is detected. Thanks you guys for all your help. – unformatted Nov 03 '22 at 08:29
0

the frame object is a ndarray, you need to convert to string. if you do:

subprocess.call(["python", "/home/user/test2.py", frame.tostring()]) should work.

but you shouldn't use cv2.imread, because you are not reading the image from a file, you are reading froma string in command

So you should use

frame = np.fromstring(sys.argv[1])

i'm not sure if the conversion from ndarray to string and in the other way work fine, probabbly you need to adjust the parameters

Pablo Estevez
  • 186
  • 1
  • 8
  • [user@PC16 ~]$ python test1.py /home/user/test1.py:23: DeprecationWarning: tostring() is deprecated. Use tobytes() instead. subprocess.call(["python", "/home/user/test2.py", frame.tostring()]) embedded null byte. Using "tobytes()" gives "embedded null byte" – unformatted Nov 02 '22 at 16:18
  • You can't pass a null byte (00) as argument. maybe encoding the bytes as a string and decoding in the other end. like frame.tobytes().decode('utf-8') and in the other end np.frombuffer(sys.argv[1].encode('utf8')) but that has problem https://stackoverflow.com/questions/53376786/convert-byte-array-back-to-numpy-array – Pablo Estevez Nov 02 '22 at 19:11
  • It looks like @Thomas was right after all. I just cannot get the encoding and decoding stuff right, if one problem is solved, another pops up. – unformatted Nov 03 '22 at 07:12