From e8faced822e90f228c585d7899c0b693a73da74b Mon Sep 17 00:00:00 2001 From: visionworkz <31665404+visionworkz@users.noreply.github.com> Date: Wed, 18 Apr 2018 10:49:25 +0800 Subject: [PATCH] Add basic image io and remove python C-API refs from numpy_returns.cpp (#1258) * Fixed reference count issue * Fixed refcount issue in Python dlib.jitter_image and dlib.get_face_chips * Consolidation of https://github.com/davisking/dlib/pull/1249 * Fixed build issue * Fixed: Paths in a pytest file should be relative to dlib root * Skip numpy return tests for Python 2.7 or if Numpy is not installed * Enabled numpy returns tests on Python 2.7 using cPickle.dumps --- python_examples/cnn_face_detector.py | 8 +- python_examples/correlation_tracker.py | 8 +- python_examples/face_alignment.py | 32 ++--- python_examples/face_clustering.py | 8 +- python_examples/face_detector.py | 11 +- python_examples/face_jitter.py | 34 ++--- python_examples/face_landmark_detection.py | 8 +- python_examples/face_recognition.py | 8 +- .../find_candidate_object_locations.py | 10 +- .../opencv_webcam_face_detection.py | 59 ++++++++ python_examples/requirements.txt | 1 - python_examples/train_object_detector.py | 21 +-- python_examples/train_shape_predictor.py | 9 +- tools/python/CMakeLists.txt | 16 +-- tools/python/src/numpy_returns.cpp | 134 +++++++++++------- tools/python/src/numpy_returns_stub.cpp | 59 -------- .../test/generate_numpy_returns_test_data.py | 33 +++++ tools/python/test/shape.pkl | 3 + tools/python/test/test_face_chip.npy | Bin 0 -> 67628 bytes tools/python/test/test_numpy_returns.py | 66 +++++++++ tools/python/test/utils.py | 48 +++++++ 21 files changed, 346 insertions(+), 230 deletions(-) create mode 100644 python_examples/opencv_webcam_face_detection.py delete mode 100644 tools/python/src/numpy_returns_stub.cpp create mode 100644 tools/python/test/generate_numpy_returns_test_data.py create mode 100644 tools/python/test/shape.pkl create mode 100644 tools/python/test/test_face_chip.npy create mode 100644 tools/python/test/test_numpy_returns.py create mode 100644 tools/python/test/utils.py diff --git a/python_examples/cnn_face_detector.py b/python_examples/cnn_face_detector.py index 75357a62f..b7037dab5 100755 --- a/python_examples/cnn_face_detector.py +++ b/python_examples/cnn_face_detector.py @@ -33,14 +33,12 @@ # command: # sudo apt-get install cmake # -# Also note that this example requires scikit-image which can be installed +# Also note that this example requires Numpy which can be installed # via the command: -# pip install scikit-image -# Or downloaded from http://scikit-image.org/download.html. +# pip install numpy import sys import dlib -from skimage import io if len(sys.argv) < 3: print( @@ -55,7 +53,7 @@ win = dlib.image_window() for f in sys.argv[2:]: print("Processing file: {}".format(f)) - img = io.imread(f) + img = dlib.load_rgb_image(f) # The 1 in the second argument indicates that we should upsample the image # 1 time. This will make everything bigger and allow us to detect more # faces. diff --git a/python_examples/correlation_tracker.py b/python_examples/correlation_tracker.py index 4493a55b7..8d902cc77 100755 --- a/python_examples/correlation_tracker.py +++ b/python_examples/correlation_tracker.py @@ -32,16 +32,14 @@ # command: # sudo apt-get install cmake # -# Also note that this example requires scikit-image which can be installed +# Also note that this example requires Numpy which can be installed # via the command: -# pip install scikit-image -# Or downloaded from http://scikit-image.org/download.html. +# pip install numpy import os import glob import dlib -from skimage import io # Path to the video frames video_folder = os.path.join("..", "examples", "video_frames") @@ -54,7 +52,7 @@ win = dlib.image_window() # We will track the frames as we load them off of disk for k, f in enumerate(sorted(glob.glob(os.path.join(video_folder, "*.jpg")))): print("Processing Frame {}".format(k)) - img = io.imread(f) + img = dlib.load_rgb_image(f) # We need to initialize the tracker on the first frame if k == 0: diff --git a/python_examples/face_alignment.py b/python_examples/face_alignment.py index 53df7a3e1..8de2043d0 100755 --- a/python_examples/face_alignment.py +++ b/python_examples/face_alignment.py @@ -21,16 +21,13 @@ # command: # sudo apt-get install cmake # -# Also note that this example requires OpenCV and Numpy which can be installed +# Also note that this example requires Numpy which can be installed # via the command: -# pip install opencv-python numpy -# Or downloaded from http://opencv.org/releases.html +# pip install numpy import sys import dlib -import cv2 -import numpy as np if len(sys.argv) != 3: print( @@ -48,14 +45,8 @@ face_file_path = sys.argv[2] detector = dlib.get_frontal_face_detector() sp = dlib.shape_predictor(predictor_path) -# Load the image using OpenCV -bgr_img = cv2.imread(face_file_path) -if bgr_img is None: - print("Sorry, we could not load '{}' as an image".format(face_file_path)) - exit() - -# Convert to RGB since dlib uses RGB images -img = cv2.cvtColor(bgr_img, cv2.COLOR_BGR2RGB) +# Load the image using Dlib +img = dlib.load_rgb_image(face_file_path) # Ask the detector to find the bounding boxes of each face. The 1 in the # second argument indicates that we should upsample the image 1 time. This @@ -72,20 +63,17 @@ faces = dlib.full_object_detections() for detection in dets: faces.append(sp(img, detection)) +window = dlib.image_window() + # Get the aligned face images # Optionally: # images = dlib.get_face_chips(img, faces, size=160, padding=0.25) images = dlib.get_face_chips(img, faces, size=320) for image in images: - cv_bgr_img = cv2.cvtColor(image, cv2.COLOR_RGB2BGR) - cv2.imshow('image',cv_bgr_img) - cv2.waitKey(0) + window.set_image(image) + dlib.hit_enter_to_continue() # It is also possible to get a single chip image = dlib.get_face_chip(img, faces[0]) -cv_bgr_img = cv2.cvtColor(image, cv2.COLOR_RGB2BGR) -cv2.imshow('image',cv_bgr_img) -cv2.waitKey(0) - -cv2.destroyAllWindows() - +window.set_image(image) +dlib.hit_enter_to_continue() diff --git a/python_examples/face_clustering.py b/python_examples/face_clustering.py index 362613871..f4769b11e 100755 --- a/python_examples/face_clustering.py +++ b/python_examples/face_clustering.py @@ -28,16 +28,14 @@ # command: # sudo apt-get install cmake # -# Also note that this example requires scikit-image which can be installed +# Also note that this example requires Numpy which can be installed # via the command: -# pip install scikit-image -# Or downloaded from http://scikit-image.org/download.html. +# pip install numpy import sys import os import dlib import glob -from skimage import io if len(sys.argv) != 5: print( @@ -66,7 +64,7 @@ images = [] # Now find all the faces and compute 128D face descriptors for each face. for f in glob.glob(os.path.join(faces_folder_path, "*.jpg")): print("Processing file: {}".format(f)) - img = io.imread(f) + img = dlib.load_rgb_image(f) # Ask the detector to find the bounding boxes of each face. The 1 in the # second argument indicates that we should upsample the image 1 time. This diff --git a/python_examples/face_detector.py b/python_examples/face_detector.py index eed3732b0..0d48fb822 100755 --- a/python_examples/face_detector.py +++ b/python_examples/face_detector.py @@ -37,23 +37,20 @@ # command: # sudo apt-get install cmake # -# Also note that this example requires scikit-image which can be installed +# Also note that this example requires Numpy which can be installed # via the command: -# pip install scikit-image -# Or downloaded from http://scikit-image.org/download.html. +# pip install numpy import sys import dlib -from skimage import io - detector = dlib.get_frontal_face_detector() win = dlib.image_window() for f in sys.argv[1:]: print("Processing file: {}".format(f)) - img = io.imread(f) + img = dlib.load_rgb_image(f) # The 1 in the second argument indicates that we should upsample the image # 1 time. This will make everything bigger and allow us to detect more # faces. @@ -76,7 +73,7 @@ for f in sys.argv[1:]: # Also, the idx tells you which of the face sub-detectors matched. This can be # used to broadly identify faces in different orientations. if (len(sys.argv[1:]) > 0): - img = io.imread(sys.argv[1]) + img = dlib.load_rgb_image(sys.argv[1]) dets, scores, idx = detector.run(img, 1, -1) for i, d in enumerate(dets): print("Detection {}, score: {}, face_type:{}".format( diff --git a/python_examples/face_jitter.py b/python_examples/face_jitter.py index ee959846d..a729e7dda 100755 --- a/python_examples/face_jitter.py +++ b/python_examples/face_jitter.py @@ -25,26 +25,23 @@ # command: # sudo apt-get install cmake # -# Also note that this example requires OpenCV and Numpy which can be installed +# Also note that this example requires Numpy which can be installed # via the command: -# pip install opencv-python numpy +# pip install numpy # # The image file used in this example is in the public domain: # https://commons.wikimedia.org/wiki/File:Tom_Cruise_avp_2014_4.jpg import sys import dlib -import cv2 -import numpy as np -def show_jittered_images(jittered_images): +def show_jittered_images(window, jittered_images): ''' Shows the specified jittered images one by one ''' for img in jittered_images: - cv_bgr_img = cv2.cvtColor(img, cv2.COLOR_RGB2BGR) - cv2.imshow('image',cv_bgr_img) - cv2.waitKey(0) + window.set_image(img) + dlib.hit_enter_to_continue() if len(sys.argv) != 2: print( @@ -62,14 +59,8 @@ face_file_path = "../examples/faces/Tom_Cruise_avp_2014_4.jpg" detector = dlib.get_frontal_face_detector() sp = dlib.shape_predictor(predictor_path) -# Load the image using OpenCV -bgr_img = cv2.imread(face_file_path) -if bgr_img is None: - print("Sorry, we could not load '{}' as an image".format(face_file_path)) - exit() - -# Convert to RGB since dlib uses RGB images -img = cv2.cvtColor(bgr_img, cv2.COLOR_BGR2RGB) +# Load the image using dlib +img = dlib.load_rgb_image(face_file_path) # Ask the detector to find the bounding boxes of each face. dets = detector(img) @@ -83,15 +74,14 @@ for detection in dets: # Get the aligned face image and show it image = dlib.get_face_chip(img, faces[0], size=320) -cv_bgr_img = cv2.cvtColor(image, cv2.COLOR_RGB2BGR) -cv2.imshow('image',cv_bgr_img) -cv2.waitKey(0) +window = dlib.image_window() +window.set_image(image) +dlib.hit_enter_to_continue() # Show 5 jittered images without data augmentation jittered_images = dlib.jitter_image(image, num_jitters=5) -show_jittered_images(jittered_images) +show_jittered_images(window, jittered_images) # Show 5 jittered images with data augmentation jittered_images = dlib.jitter_image(image, num_jitters=5, disturb_colors=True) -show_jittered_images(jittered_images) -cv2.destroyAllWindows() +show_jittered_images(window, jittered_images) diff --git a/python_examples/face_landmark_detection.py b/python_examples/face_landmark_detection.py index 351941317..e7bda3412 100755 --- a/python_examples/face_landmark_detection.py +++ b/python_examples/face_landmark_detection.py @@ -45,16 +45,14 @@ # command: # sudo apt-get install cmake # -# Also note that this example requires scikit-image which can be installed +# Also note that this example requires Numpy which can be installed # via the command: -# pip install scikit-image -# Or downloaded from http://scikit-image.org/download.html. +# pip install numpy import sys import os import dlib import glob -from skimage import io if len(sys.argv) != 3: print( @@ -76,7 +74,7 @@ win = dlib.image_window() for f in glob.glob(os.path.join(faces_folder_path, "*.jpg")): print("Processing file: {}".format(f)) - img = io.imread(f) + img = dlib.load_rgb_image(f) win.clear_overlay() win.set_image(img) diff --git a/python_examples/face_recognition.py b/python_examples/face_recognition.py index da2bdbc55..c7f5437a7 100755 --- a/python_examples/face_recognition.py +++ b/python_examples/face_recognition.py @@ -40,16 +40,14 @@ # command: # sudo apt-get install cmake # -# Also note that this example requires scikit-image which can be installed +# Also note that this example requires Numpy which can be installed # via the command: -# pip install scikit-image -# Or downloaded from http://scikit-image.org/download.html. +# pip install numpy import sys import os import dlib import glob -from skimage import io if len(sys.argv) != 4: print( @@ -76,7 +74,7 @@ win = dlib.image_window() # Now process all the images for f in glob.glob(os.path.join(faces_folder_path, "*.jpg")): print("Processing file: {}".format(f)) - img = io.imread(f) + img = dlib.load_rgb_image(f) win.clear_overlay() win.set_image(img) diff --git a/python_examples/find_candidate_object_locations.py b/python_examples/find_candidate_object_locations.py index a5c386425..aee46e356 100755 --- a/python_examples/find_candidate_object_locations.py +++ b/python_examples/find_candidate_object_locations.py @@ -31,18 +31,14 @@ # command: # sudo apt-get install cmake # -# Also note that this example requires scikit-image which can be installed +# Also note that this example requires Numpy which can be installed # via the command: -# pip install scikit-image -# Or downloaded from http://scikit-image.org/download.html. - - +# pip install numpy import dlib -from skimage import io image_file = '../examples/faces/2009_004587.jpg' -img = io.imread(image_file) +img = dlib.load_rgb_image(image_file) # Locations of candidate objects will be saved into rects rects = [] diff --git a/python_examples/opencv_webcam_face_detection.py b/python_examples/opencv_webcam_face_detection.py new file mode 100644 index 000000000..be0e322b8 --- /dev/null +++ b/python_examples/opencv_webcam_face_detection.py @@ -0,0 +1,59 @@ +#!/usr/bin/python +# The contents of this file are in the public domain. See LICENSE_FOR_EXAMPLE_PROGRAMS.txt +# +# This example program shows how to find frontal human faces in a webcam stream using OpenCV. +# It is also meant to demonstrate that rgb images from Dlib can be used with opencv by just +# swapping the Red and Blue channels. +# +# You can run this program and see the detections from your webcam by executing the +# following command: +# ./opencv_face_detection.py +# +# This face detector is made using the now classic Histogram of Oriented +# Gradients (HOG) feature combined with a linear classifier, an image +# pyramid, and sliding window detection scheme. This type of object detector +# is fairly general and capable of detecting many types of semi-rigid objects +# in addition to human faces. Therefore, if you are interested in making +# your own object detectors then read the train_object_detector.py example +# program. +# +# +# COMPILING/INSTALLING THE DLIB PYTHON INTERFACE +# You can install dlib using the command: +# pip install dlib +# +# Alternatively, if you want to compile dlib yourself then go into the dlib +# root folder and run: +# python setup.py install +# or +# python setup.py install --yes USE_AVX_INSTRUCTIONS +# if you have a CPU that supports AVX instructions, since this makes some +# things run faster. +# +# Compiling dlib should work on any operating system so long as you have +# CMake installed. On Ubuntu, this can be done easily by running the +# command: +# sudo apt-get install cmake +# +# Also note that this example requires Numpy which can be installed +# via the command: +# pip install numpy + +import sys +import dlib +import cv2 + +detector = dlib.get_frontal_face_detector() +cam = cv2.VideoCapture(0) +color_green = (0,255,0) +line_width = 3 +while True: + ret_val, img = cam.read() + rgb_image = cv2.cvtColor(img, cv2.COLOR_BGR2RGB) + dets = detector(rgb_image) + for det in dets: + cv2.rectangle(img,(det.left(), det.top()), (det.right(), det.bottom()), color_green, line_width) + cv2.imshow('my webcam', img) + if cv2.waitKey(1) == 27: + break # esc to quit +cv2.destroyAllWindows() \ No newline at end of file diff --git a/python_examples/requirements.txt b/python_examples/requirements.txt index 8fa92c8a0..71ec12eda 100644 --- a/python_examples/requirements.txt +++ b/python_examples/requirements.txt @@ -1,3 +1,2 @@ -scikit-image>=0.9.3 opencv-python numpy diff --git a/python_examples/train_object_detector.py b/python_examples/train_object_detector.py index 4713365e3..c9f324fa6 100755 --- a/python_examples/train_object_detector.py +++ b/python_examples/train_object_detector.py @@ -25,18 +25,15 @@ # command: # sudo apt-get install cmake # -# Also note that this example requires scikit-image which can be installed +# Also note that this example requires Numpy which can be installed # via the command: -# pip install scikit-image -# Or downloaded from http://scikit-image.org/download.html. +# pip install numpy import os import sys import glob import dlib -from skimage import io - # In this example we are going to train a face detector based on the small # faces dataset in the examples/faces directory. This means you need to supply @@ -116,7 +113,7 @@ print("Showing detections on the images in the faces folder...") win = dlib.image_window() for f in glob.glob(os.path.join(faces_folder, "*.jpg")): print("Processing file: {}".format(f)) - img = io.imread(f) + img = dlib.load_rgb_image(f) dets = detector(img) print("Number of faces detected: {}".format(len(dets))) for k, d in enumerate(dets): @@ -128,9 +125,6 @@ for f in glob.glob(os.path.join(faces_folder, "*.jpg")): win.add_overlay(dets) dlib.hit_enter_to_continue() - - - # Next, suppose you have trained multiple detectors and you want to run them # efficiently as a group. You can do this as follows: detector1 = dlib.fhog_object_detector("detector.svm") @@ -140,22 +134,19 @@ detector2 = dlib.fhog_object_detector("detector.svm") # make a list of all the detectors you wan to run. Here we have 2, but you # could have any number. detectors = [detector1, detector2] -image = io.imread(faces_folder + '/2008_002506.jpg') +image = dlib.load_rgb_image(faces_folder + '/2008_002506.jpg') [boxes, confidences, detector_idxs] = dlib.fhog_object_detector.run_multiple(detectors, image, upsample_num_times=1, adjust_threshold=0.0) for i in range(len(boxes)): print("detector {} found box {} with confidence {}.".format(detector_idxs[i], boxes[i], confidences[i])) - - - # Finally, note that you don't have to use the XML based input to # train_simple_object_detector(). If you have already loaded your training # images and bounding boxes for the objects then you can call it as shown # below. # You just need to put your images into a list. -images = [io.imread(faces_folder + '/2008_002506.jpg'), - io.imread(faces_folder + '/2009_004587.jpg')] +images = [dlib.load_rgb_image(faces_folder + '/2008_002506.jpg'), + dlib.load_rgb_image(faces_folder + '/2009_004587.jpg')] # Then for each image you make a list of rectangles which give the pixel # locations of the edges of the boxes. boxes_img1 = ([dlib.rectangle(left=329, top=78, right=437, bottom=186), diff --git a/python_examples/train_shape_predictor.py b/python_examples/train_shape_predictor.py index 23758b2ce..e07f8299a 100755 --- a/python_examples/train_shape_predictor.py +++ b/python_examples/train_shape_predictor.py @@ -33,18 +33,15 @@ # command: # sudo apt-get install cmake # -# Also note that this example requires scikit-image which can be installed +# Also note that this example requires Numpy which can be installed # via the command: -# pip install scikit-image -# Or downloaded from http://scikit-image.org/download.html. +# pip install numpy import os import sys import glob import dlib -from skimage import io - # In this example we are going to train a face detector based on the small # faces dataset in the examples/faces directory. This means you need to supply @@ -110,7 +107,7 @@ print("Showing detections and predictions on the images in the faces folder...") win = dlib.image_window() for f in glob.glob(os.path.join(faces_folder, "*.jpg")): print("Processing file: {}".format(f)) - img = io.imread(f) + img = dlib.load_rgb_image(f) win.clear_overlay() win.set_image(img) diff --git a/tools/python/CMakeLists.txt b/tools/python/CMakeLists.txt index 9b929c1ed..b30e5ea62 100644 --- a/tools/python/CMakeLists.txt +++ b/tools/python/CMakeLists.txt @@ -37,16 +37,12 @@ find_package(PythonInterp) if(PYTHONINTERP_FOUND) execute_process( COMMAND ${PYTHON_EXECUTABLE} -c "import numpy" OUTPUT_QUIET ERROR_QUIET RESULT_VARIABLE NUMPYRC) if(NUMPYRC EQUAL 1) - message(WARNING "Numpy not found. Functions that return numpy arrays will throw exceptions!") + message(WARNING "Numpy not found. Functions that return numpy arrays will not work without Numpy installed!") else() message(STATUS "Found Python with installed numpy package") - execute_process( COMMAND ${PYTHON_EXECUTABLE} -c "import sys; from numpy import get_include; sys.stdout.write(get_include())" OUTPUT_VARIABLE NUMPY_INCLUDE_PATH) - message(STATUS "Numpy include path '${NUMPY_INCLUDE_PATH}'") - include_directories(${NUMPY_INCLUDE_PATH}) endif() else() - message(WARNING "Numpy not found. Functions that return numpy arrays will throw exceptions!") - set(NUMPYRC 1) + message(WARNING "Numpy not found. Functions that return numpy arrays will not work without Numpy installed!") endif() add_definitions(-DDLIB_VERSION=${DLIB_VERSION}) @@ -73,15 +69,9 @@ set(python_srcs src/cnn_face_detector.cpp src/global_optimization.cpp src/image_dataset_metadata.cpp + src/numpy_returns.cpp ) -# Only add the Numpy returning functions if Numpy is present -if(NUMPYRC EQUAL 1) - list(APPEND python_srcs src/numpy_returns_stub.cpp) -else() - list(APPEND python_srcs src/numpy_returns.cpp) -endif() - # Only add the GUI module if requested if(NOT ${DLIB_NO_GUI_SUPPORT}) list(APPEND python_srcs src/gui.cpp) diff --git a/tools/python/src/numpy_returns.cpp b/tools/python/src/numpy_returns.cpp index 4f14fa93c..2e1f0733b 100644 --- a/tools/python/src/numpy_returns.cpp +++ b/tools/python/src/numpy_returns.cpp @@ -2,16 +2,76 @@ #include #include "dlib/pixel.h" #include - -#define NPY_NO_DEPRECATED_API NPY_1_7_API_VERSION -#include - +#include +#include using namespace dlib; using namespace std; namespace py = pybind11; +py::array_t convert_to_numpy(matrix &rgb_image) +{ + const size_t dtype_size = sizeof(uint8_t); + const auto rows = static_cast(num_rows(rgb_image)); + const auto cols = static_cast(num_columns(rgb_image)); + const size_t channels = 3; + const size_t image_size = dtype_size * rows * cols * channels; + + unique_ptr arr_ptr = rgb_image.steal_memory(); + uint8_t* arr = (uint8_t *) arr_ptr.release(); + + return pybind11::array_t( + {rows, cols, channels}, // shape + {dtype_size * cols * channels, dtype_size * channels, dtype_size}, // strides + arr, // pointer + pybind11::capsule{ + arr, [](void *arr_p) { + delete[] reinterpret_cast(arr_p); + } + } + ); +} + +// -------------------------------- Basic Image IO ---------------------------------------- + +py::array_t load_rgb_image (const std::string &path) +{ + matrix img; + load_image(img, path); + return convert_to_numpy(img); +} + +bool has_ending (std::string const full_string, std::string const &ending) { + if(full_string.length() >= ending.length()) { + return (0 == full_string.compare(full_string.length() - ending.length(), ending.length(), ending)); + } else { + return false; + } +} + +void save_rgb_image(py::object img, const std::string &path) +{ + if (!is_rgb_python_image(img)) + throw dlib::error("Unsupported image type, must be RGB image."); + + std::string lowered_path = path; + std::transform(lowered_path.begin(), lowered_path.end(), lowered_path.begin(), ::tolower); + + if(has_ending(lowered_path, ".bmp")) { + save_bmp(numpy_rgb_image(img), path); + } else if(has_ending(lowered_path, ".dng")) { + save_dng(numpy_rgb_image(img), path); + } else if(has_ending(lowered_path, ".png")) { + save_png(numpy_rgb_image(img), path); + } else if(has_ending(lowered_path, ".jpg") || has_ending(lowered_path, ".jpeg")) { + save_jpeg(numpy_rgb_image(img), path); + } else { + throw dlib::error("Unsupported image type, image path must end with one of [.bmp, .png, .dng, .jpg, .jpeg]"); + } + return; +} + // ---------------------------------------------------------------------------------------- py::list get_jitter_images(py::object img, size_t num_jitters = 1, bool disturb_colors = false) @@ -27,12 +87,6 @@ py::list get_jitter_images(py::object img, size_t num_jitters = 1, bool disturb_ // The top level list (containing 1 or more images) to return to python py::list jitter_list; - size_t rows = num_rows(img_mat); - size_t cols = num_columns(img_mat); - - // Size of the numpy array - npy_intp dims[3] = { num_rows(img_mat), num_columns(img_mat), 3}; - for (int i = 0; i < num_jitters; ++i) { // Get a jittered crop matrix crop = dlib::jitter_image(img_mat, rnd_jitter); @@ -40,14 +94,11 @@ py::list get_jitter_images(py::object img, size_t num_jitters = 1, bool disturb_ if(disturb_colors) dlib::disturb_colors(crop, rnd_jitter); - PyObject *arr = PyArray_SimpleNew(3, dims, NPY_UINT8); - npy_uint8 *outdata = (npy_uint8 *) PyArray_DATA((PyArrayObject*) arr); - memcpy(outdata, image_data(crop), rows * width_step(crop)); + // Convert image to Numpy array + py::array_t arr = convert_to_numpy(crop); - py::handle handle = arr; // Append image to jittered image list - jitter_list.append(handle); - Py_DECREF(arr); + jitter_list.append(arr); } return jitter_list; @@ -77,27 +128,18 @@ py::list get_face_chips ( dlib::array> face_chips; extract_image_chips(numpy_rgb_image(img), dets, face_chips); - npy_intp rows = size; - npy_intp cols = size; - - // Size of the numpy array - npy_intp dims[3] = { rows, cols, 3}; - for (auto& chip : face_chips) { - PyObject *arr = PyArray_SimpleNew(3, dims, NPY_UINT8); - npy_uint8 *outdata = (npy_uint8 *) PyArray_DATA((PyArrayObject*) arr); - memcpy(outdata, image_data(chip), rows * width_step(chip)); - py::handle handle = arr; + // Convert image to Numpy array + py::array_t arr = convert_to_numpy(chip); // Append image to chips list - chips_list.append(handle); - Py_DECREF(arr); + chips_list.append(arr); } return chips_list; } -py::object get_face_chip ( +py::array_t get_face_chip ( py::object img, const full_object_detection& face, size_t size = 150, @@ -109,36 +151,22 @@ py::object get_face_chip ( matrix chip; extract_image_chip(numpy_rgb_image(img), get_face_chip_details(face, size, padding), chip); - - // Size of the numpy array - npy_intp dims[3] = { num_rows(chip), num_columns(chip), 3}; - - PyObject *arr = PyArray_SimpleNew(3, dims, NPY_UINT8); - npy_uint8 *outdata = (npy_uint8 *) PyArray_DATA((PyArrayObject *) arr); - memcpy(outdata, image_data(chip), num_rows(chip) * width_step(chip)); - return py::reinterpret_steal(arr); + return convert_to_numpy(chip); } // ---------------------------------------------------------------------------------------- -// we need this wonky stuff because different versions of numpy's import_array macro -// contain differently typed return statements inside import_array(). -#if PY_VERSION_HEX >= 0x03000000 -#define DLIB_NUMPY_IMPORT_ARRAY_RETURN_TYPE void* -#define DLIB_NUMPY_IMPORT_RETURN return 0 -#else -#define DLIB_NUMPY_IMPORT_ARRAY_RETURN_TYPE void -#define DLIB_NUMPY_IMPORT_RETURN return -#endif -DLIB_NUMPY_IMPORT_ARRAY_RETURN_TYPE import_numpy_stuff() -{ - import_array(); - DLIB_NUMPY_IMPORT_RETURN; -} - void bind_numpy_returns(py::module &m) { - import_numpy_stuff(); + m.def("load_rgb_image", &load_rgb_image, + "Takes a path and returns a numpy array (RGB) containing the image", + py::arg("path") + ); + + m.def("save_rgb_image", &save_rgb_image, + "Saves the given (RGB) image to the specified path. Determines the file type from the file extension specified in the path", + py::arg("img"), py::arg("path") + ); m.def("jitter_image", &get_jitter_images, "Takes an image and returns a list of jittered images." diff --git a/tools/python/src/numpy_returns_stub.cpp b/tools/python/src/numpy_returns_stub.cpp deleted file mode 100644 index 07d38ceac..000000000 --- a/tools/python/src/numpy_returns_stub.cpp +++ /dev/null @@ -1,59 +0,0 @@ -#include "opaque_types.h" -#include -#include "dlib/pixel.h" -#include - -using namespace dlib; -using namespace std; -namespace py = pybind11; - -// ---------------------------------------------------------------------------------------- - -py::list get_jitter_images(py::object img, size_t num_jitters = 1, bool disturb_colors = false) -{ - throw dlib::error("jitter_image is only supported if you compiled dlib with numpy installed!"); -} - -// ---------------------------------------------------------------------------------------- - -py::list get_face_chips ( - py::object img, - const std::vector& faces, - size_t size = 150, - float padding = 0.25 -) -{ - throw dlib::error("get_face_chips is only supported if you compiled dlib with numpy installed!"); -} - -py::object get_face_chip ( - py::object img, - const full_object_detection& face, - size_t size = 150, - float padding = 0.25 -) -{ - throw dlib::error("get_face_chip is only supported if you compiled dlib with numpy installed!"); -} - -// ---------------------------------------------------------------------------------------- - -void bind_numpy_returns(py::module &m) -{ - m.def("jitter_image", &get_jitter_images, - "Takes an image and returns a list of jittered images." - "The returned list contains num_jitters images (default is 1)." - "If disturb_colors is set to True, the colors of the image are disturbed (default is False)", - py::arg("img"), py::arg("num_jitters")=1, py::arg("disturb_colors")=false - ); - - m.def("get_face_chip", &get_face_chip, - "Takes an image and a full_object_detection that references a face in that image and returns the face as a Numpy array representing the image. The face will be rotated upright and scaled to 150x150 pixels or with the optional specified size and padding.", - py::arg("img"), py::arg("face"), py::arg("size")=150, py::arg("padding")=0.25 - ); - - m.def("get_face_chips", &get_face_chips, - "Takes an image and a full_object_detections object that reference faces in that image and returns the faces as a list of Numpy arrays representing the image. The faces will be rotated upright and scaled to 150x150 pixels or with the optional specified size and padding.", - py::arg("img"), py::arg("faces"), py::arg("size")=150, py::arg("padding")=0.25 - ); -} diff --git a/tools/python/test/generate_numpy_returns_test_data.py b/tools/python/test/generate_numpy_returns_test_data.py new file mode 100644 index 000000000..1a85f5e8e --- /dev/null +++ b/tools/python/test/generate_numpy_returns_test_data.py @@ -0,0 +1,33 @@ +#!/usr/bin/python +# The contents of this file are in the public domain. See LICENSE_FOR_EXAMPLE_PROGRAMS.txt +# +# This utility generates the test data required for the tests contained in test_numpy_returns.py +# +# Also note that this utility requires Numpy which can be installed +# via the command: +# pip install numpy +import sys +import dlib +import numpy as np +import utils + +if len(sys.argv) != 2: + print( + "Call this program like this:\n" + " ./generate_numpy_returns_test_data.py shape_predictor_5_face_landmarks.dat\n" + "You can download a trained facial shape predictor from:\n" + " http://dlib.net/files/shape_predictor_5_face_landmarks.dat.bz2\n") + exit() + + +detector = dlib.get_frontal_face_detector() +predictor = dlib.shape_predictor(sys.argv[1]) + +img = dlib.load_rgb_image("../../../examples/faces/Tom_Cruise_avp_2014_4.jpg") +dets = detector(img) +shape = predictor(img, dets[0]) + +utils.save_pickled_compatible(shape, "shape.pkl") + +face_chip = dlib.get_face_chip(img, shape) +np.save("test_face_chip", face_chip) \ No newline at end of file diff --git a/tools/python/test/shape.pkl b/tools/python/test/shape.pkl new file mode 100644 index 000000000..b656440c7 --- /dev/null +++ b/tools/python/test/shape.pkl @@ -0,0 +1,3 @@ +cdlib +full_object_detection +q)qU+'Ow qb. \ No newline at end of file diff --git a/tools/python/test/test_face_chip.npy b/tools/python/test/test_face_chip.npy new file mode 100644 index 0000000000000000000000000000000000000000..900e36dbe0026d4a03483bc9ce3ac82d45c3247f GIT binary patch literal 67628 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 HX)D`<6r(H@Tr@%g^rqmhO&-~guLig(Vs5? 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 VqrW4_6-0tD*kM#U+(B|F@jn2=5yk)j literal 0 HcmV?d00001 diff --git a/tools/python/test/test_numpy_returns.py b/tools/python/test/test_numpy_returns.py new file mode 100644 index 000000000..7917cd4f0 --- /dev/null +++ b/tools/python/test/test_numpy_returns.py @@ -0,0 +1,66 @@ +import sys +import pickle + +import dlib +import pytest + +import utils + +# Paths are relative to dlib root +image_path = "examples/faces/Tom_Cruise_avp_2014_4.jpg" +shape_path = "tools/python/test/shape.pkl" +face_chip_path = "tools/python/test/test_face_chip.npy" + +def get_test_image_and_shape(): + img = dlib.load_rgb_image(image_path) + shape = utils.load_pickled_compatible(shape_path) + return img, shape + +def get_test_face_chips(): + rgb_img, shape = get_test_image_and_shape() + shapes = dlib.full_object_detections() + shapes.append(shape) + return dlib.get_face_chips(rgb_img, shapes) + +def get_test_face_chip(): + rgb_img, shape = get_test_image_and_shape() + return dlib.get_face_chip(rgb_img, shape) + +# The tests below will be skipped if Numpy is not installed +@pytest.mark.skipif(not utils.is_numpy_installed(), reason="requires numpy") +def test_get_face_chip(): + import numpy + face_chip = get_test_face_chip() + expected = numpy.load(face_chip_path) + assert numpy.array_equal(face_chip, expected) + +@pytest.mark.skipif(not utils.is_numpy_installed(), reason="requires numpy") +def test_get_face_chips(): + import numpy + face_chips = get_test_face_chips() + expected = numpy.load(face_chip_path) + assert numpy.array_equal(face_chips[0], expected) + +@pytest.mark.skipif(not utils.is_numpy_installed(), reason="requires numpy") +def test_regression_issue_1220_get_face_chip(): + """ + Memory leak in Python get_face_chip + https://github.com/davisking/dlib/issues/1220 + """ + face_chip = get_test_face_chip() + # we expect two references: + # 1.) the local variable + # 2.) the temporary passed to getrefcount + assert sys.getrefcount(face_chip) == 2 + +@pytest.mark.skipif(not utils.is_numpy_installed(), reason="requires numpy") +def test_regression_issue_1220_get_face_chips(): + """ + Memory leak in Python get_face_chip + https://github.com/davisking/dlib/issues/1220 + """ + face_chips = get_test_face_chips() + count = sys.getrefcount(face_chips) + assert count == 2 + count = sys.getrefcount(face_chips[0]) + assert count == 2 \ No newline at end of file diff --git a/tools/python/test/utils.py b/tools/python/test/utils.py new file mode 100644 index 000000000..0f97ba75b --- /dev/null +++ b/tools/python/test/utils.py @@ -0,0 +1,48 @@ +import pkgutil +import sys + +def save_pickled_compatible(obj_to_pickle, file_name): + ''' + Save an object to the specified file in a backward compatible + way for Pybind objects. See: + http://pybind11.readthedocs.io/en/stable/advanced/classes.html#pickling-support + and https://github.com/pybind/pybind11/issues/271 + ''' + try: + import cPickle as pickle # Use cPickle on Python 2.7 + except ImportError: + import pickle + data = pickle.dumps(obj_to_pickle, 2) + with open(file_name, "wb") as handle: + handle.write(data) + +def load_pickled_compatible(file_name): + ''' + Loads a pickled object from the specified file + ''' + try: + import cPickle as pickle # Use cPickle on Python 2.7 + except ImportError: + import pickle + + with open(file_name, "rb") as handle: + data = handle.read() + if not is_python3(): + return pickle.loads(data) + else: + return pickle.loads(data, encoding="bytes") + +def is_numpy_installed(): + ''' + Returns True if Numpy is installed otherwise False + ''' + if pkgutil.find_loader("numpy"): + return True + else: + return False + +def is_python3(): + ''' + Returns True if using Python 3 or above, otherwise False + ''' + return sys.version_info >= (3, 0)