2020년 4월 15일 수요일

JetsonNano - Facial Makeup Transfer with Deep GAN(Generative Adversarial Network)

I used Jetson Nano, JetPack 4.3 Official image with root account.

What is BeautyGAN?

 BeautyGAN is Instance-level Facial Makeup Transfer with Deep Generative Adversarial Network. Official website(http://liusi-group.com/projects/BeautyGAN) summarizes BeautyGAN as follows.
Facial makeup transfer aims to translate the makeup style from a given reference makeup face image to another non-makeup one while preserving face identity. Such an instance-level transfer problem is more challenging than conventional domain-level transfer tasks, especially when paired data is unavailable. Makeup style is also different from global styles (e.g., paintings) in that it consists of several local styles/cosmetics, including eye shadow, lip-stick, foundation, and so on. Extracting and transferring such local and delicate makeup information is infeasible for existing style transfer methods. We address the issue by incorporating both global domain-level loss and local instance-level loss in an dual input/output Generative Adversarial Network, called BeautyGAN. Specically, the domain-level transfer is ensured by discriminators that distinguish generated images from domains’ real samples. The instance-level loss is calculated by pixel-level histogram loss on separate local facial regions. We further introduce perceptual loss and cycle consistency loss to generate high quality faces and preserve identity. The overall objective function enables the network to learn translation on instance-level through unsupervised adversarial learning. We also build up a new makeup dataset that consists of 3834 high-resolution face images. Extensive experiments show 



 The following figure is a schematic diagram of the operation flow of BeautyGAN.


Figure : Framework of the proposed BeautyGAN. The upper pipeline shows the overall system.Gaccpets two images asinputs: non-makeup imageIsr c, reference makeup imageIr e f, and generates two outputs: transferred makeup imageIBsr c, anti-makeup imageIAr e f. The generated images are fed into the sameGto build up reconstruction results:Ir ecsr c,Ir ecr e f. There are fourloss terms for trainingG: cycle consistency loss, perceptual loss, adversarial loss (denoted asDAandDB) and makeup loss. Thelower pipeline shows the details of makeup loss. It consists of three local histogram loss terms acted on face, eye shadow andlips, respectively. We first utilize face parsing model to separate each cosmetic region ofIsr c,Ir e f,IBsr c. Then, for each region,we employ histogram matching betweenIsr candIr e fto obtain a histogram remapping facial region as ground truth. The localloss term calculates pixel-level differences between such ground truth and corresponding cosmetic region ofIBsr c.


If you want to know more about BeautyGAN's theory, please refer to the article on this page(http://liusi-group.com/pdf/BeautyGAN-camera-ready_2.pdf).

And I have referenced a lot of pages at https://github.com/kairess/BeautyGAN which forked the github site (https://github.com/Honlan/BeautyGAN) that introduced BeautyGAN.

Prerequisites

Tensorflow is required to operate BeautyGAN. If TensorFlow is not installed on your Jetson Nano, install TensorFlow first. And you need OpenCV to run GitHub's example code that introduced BeautyGAN. If you are using JetPack4.3, OpenCV 4.1 will be installed by default. If you are using JetPack 4.2 or earlier, you must install OpenCV separately.

Install OpenCV

If you use JetPack 4.3, OpenCV 4.1 is preinstalled so skip this process.
If you are using JetPack 4.2 or below, see my other post on OpenCV installation https://spyjetson.blogspot.com/2019/09/jetsonnano-opencv-411-build.html.

Install Tensorflow

Do not follow the installation process of the TensorFlow homepage. Nvidia provides the optimized Tensorflow package available in JetPack. To install Tensorflow provided by Nvidia, see my other post https://spyjetson.blogspot.com/2019/09/jetsonnano-installing-tensorflow-114.html.

If you want to run Taehee Lee kairess 's code, install these packages too.
  • matplotlib
  • dlib
Installing dlib is described at https://spyjetson.blogspot.com/2019/11/jetsonnano-face-recognition-using-dlib.html.

To install matplotlib,

pip3 install -U matplotlib

And you may need to install imageio packages to run main.py example.


pip3 install imageio


Install BeautyGAN

Select and download one of the two Githubs introduced above. I will download https://github.com/kairess/BeautyGAN. This site provides excellent Jupyter Notebook examples and additional Youtube videos.

<Youtube video provided by kairess>


cd /usr/local/src
git clone https://github.com/kairess/BeautyGAN.git



Download the pre-trained models

You can download models that have already been trained from the following two sites.


Download these files and copy them to the "/usr/local/src/BeautyGAN/models" directory.



Testing main.py

BeautyGAN provides main.py for testing. With this program, you can take a brief look at the operation of BeautyGAN.

Be Careful: If you downloaded source codes from https://github.com/kairess/BeautyGAN, you need to slightly modify the model path in the main.py file. Honlan's original source code uses the model directory name, while kairess's fork version uses the models directory. Therefore, change the "model" directory of the main.py file to "models".


# -*- coding: utf-8 -*-

import tensorflow as tf
import numpy as np
import os
import glob
from imageio import imread, imsave
import cv2
import argparse

parser = argparse.ArgumentParser()
parser.add_argument('--no_makeup', type=str, default=os.path.join('imgs', 'no_makeup', 'xfsy_0068.png'), help='path to the no_makeup image')
args = parser.parse_args()

def preprocess(img):
    return (img / 255. - 0.5) * 2

def deprocess(img):
    return (img + 1) / 2

batch_size = 1
img_size = 256
no_makeup = cv2.resize(imread(args.no_makeup), (img_size, img_size))
X_img = np.expand_dims(preprocess(no_makeup), 0)
makeups = glob.glob(os.path.join('imgs', 'makeup', '*.*'))
result = np.ones((2 * img_size, (len(makeups) + 1) * img_size, 3))
result[img_size: 2 *  img_size, :img_size] = no_makeup / 255.

tf.reset_default_graph()
sess = tf.Session()
sess.run(tf.global_variables_initializer())

# I modified model ->models
saver = tf.train.import_meta_graph(os.path.join('models', 'model.meta'))
saver.restore(sess, tf.train.latest_checkpoint('models'))

graph = tf.get_default_graph()
X = graph.get_tensor_by_name('X:0')
Y = graph.get_tensor_by_name('Y:0')
Xs = graph.get_tensor_by_name('generator/xs:0')

for i in range(len(makeups)):
    makeup = cv2.resize(imread(makeups[i]), (img_size, img_size))
    Y_img = np.expand_dims(preprocess(makeup), 0)
    Xs_ = sess.run(Xs, feed_dict={X: X_img, Y: Y_img})
    Xs_ = deprocess(Xs_)
    result[:img_size, (i + 1) * img_size: (i + 2) * img_size] = makeup / 255.
    result[img_size: 2 * img_size, (i + 1) * img_size: (i + 2) * img_size] = Xs_[0]

imsave('result.jpg', result)
<main.py>

Before going through the code, let's run it once to see the results.

Tips: The TensorFlow example above requires a lot of memory. If an error occurs due to insufficient memory, changing Ubuntu Desktop to LXDE can secure an additional 1 GB of memory. I also got the correct results after using this method. Please refer to my other post https://spyjetson.blogspot.com/2019/09/jetson-nano-useful-tips-before-you.html for LXDE Desktop changes.



root@spypiggy-nano:/usr/local/src/BeautyGAN# python3 main.py
2020-04-15 15:50:11.401865: I tensorflow/stream_executor/platform/default/dso_loader.cc:44] Successfully opened dynamic library libcudart.so.10.0
WARNING:tensorflow:From main.py:29: The name tf.reset_default_graph is deprecated. Please use tf.compat.v1.reset_default_graph instead.
WARNING:tensorflow:From main.py:30: The name tf.Session is deprecated. Please use tf.compat.v1.Session instead.
2020-04-15 15:50:23.173272: I tensorflow/stream_executor/platform/default/dso_loader.cc:44] Successfully opened dynamic library libcuda.so.1
.......
.......
2020-04-15 15:51:27.574461: I tensorflow/stream_executor/platform/default/dso_loader.cc:44] Successfully opened dynamic library libcudnn.so.7
2020-04-15 15:51:51.405994: I tensorflow/stream_executor/platform/default/dso_loader.cc:44] Successfully opened dynamic library libcublas.so.10.0
Lossy conversion from float64 to uint8. Range [0, 1]. Convert image to uint8 prior to saving to suppress this warning.

If it ended without error, the result.jpg file would be created in the current working directory.

<result.jpg>

The image in the top row is a makeup image, and the images are stored in the imgs / makeup directory. The bottom left image is the original image without makeup. This file can be changed with the --no_makeup option. The default is imgs / no_makeup / xfsy_0068.png file. And the rest of the lower images are the result of applying the upper makeup image. You can see that it has become a fairly natural makeup.


def preprocess(img):
    return (img / 255. - 0.5) * 2

The pixels of the image have RGB values ​​from 0 to 255 (uint 8). The preprocess function adjusts the range of pixel values ​​from 0 to 255 to a range between -1 and 1. BeautyGAN models use values ​​from this value range.



def deprocess(img):
    return (img + 1) / 2

The ndarray that has passed the preprocess function is output to the makeup ndarray using the model. The range of this value is also between -1 and 1. Again, we need to change these values ​​from 0 to 255. However, the deprocess function does not convert to uint8 from 0 to 255, but to a float value between 0 and 1. The reason for this conversion is that when the imageio module's imsave function receives an ndarray with a float value between 0 and 1, it is automatically converted to 0 to 255 uint8 and stored. However, this method is confusing and the postprocess function used by kairess is much easier to understand.



def deprocess(img):
    return ((img + 1.) * 127.5).astype(np.uint8)
<new deprocess function that is easy to understand>

The part of reading the model already learned in the tensor flow is omitted because there are many examples on the Internet. Also, it is easy to understand the arrangement of the output image after makeup on the result ndarray by reading the code.

Make up your face

Now, let's select a random input image and a desired makeup model and output the makeup image.

The makeup image exists in the imgs / makeup directory.


root@spypiggy-nano:/usr/local/src/BeautyGAN/imgs# ls -al makeup/
total 1852
drwxr-xr-x 2 root root   4096  4 15 01:59 .
drwxr-xr-x 4 root root   4096  4 15 16:41 ..
-rw-r--r-- 1 root root 228499  4 15 01:59 vFG112.png
-rw-r--r-- 1 root root 195287  4 15 01:59 vFG137.png
-rw-r--r-- 1 root root 187376  4 15 01:59 vFG56.png
-rw-r--r-- 1 root root 254934  4 15 01:59 vFG756.png
-rw-r--r-- 1 root root 197081  4 15 01:59 vRX916.png
-rw-r--r-- 1 root root 208965  4 15 01:59 XMY-014.png
-rw-r--r-- 1 root root 230706  4 15 01:59 XMY-074.png
-rw-r--r-- 1 root root 189209  4 15 01:59 XMY-136.png
-rw-r--r-- 1 root root 172506  4 15 01:59 XMY-266.png

I removed imageio package from the source code and used only OpenCV. OpenCV uses BGR color model, so before pass the image to the model, convert to RGB model. And before saving, convert RGB color to BGR model once more.


import tensorflow as tf
import numpy as np
import os
import glob
from imageio import imread, imsave
import cv2
import argparse

parser = argparse.ArgumentParser()
parser.add_argument('--no_makeup', type=str, default=os.path.join('imgs', 'no_makeup', 'xfsy_0068.png'), help='path to the no_makeup image')
parser.add_argument('--makeup', type=str, default=os.path.join('imgs', 'makeup', 'XMY-014.png'), help='path to the makeup image')
args = parser.parse_args()

def preprocess(img):
    return (img / 255. - 0.5) * 2

def deprocess(img):
    return ((img + 1.) * 127.5).astype(np.uint8)

batch_size = 1
img_size = 256
no_makeup = cv2.resize(cv2.imread(args.no_makeup, cv2.IMREAD_COLOR), (img_size, img_size))
no_makeup = cv2.cvtColor(no_makeup, cv2.COLOR_BGR2RGB)

X_img = np.expand_dims(preprocess(no_makeup), 0)
makeup_image = args.makeup

tf.reset_default_graph()
sess = tf.Session()
sess.run(tf.global_variables_initializer())

saver = tf.train.import_meta_graph(os.path.join('models', 'model.meta'))
saver.restore(sess, tf.train.latest_checkpoint('models'))

graph = tf.get_default_graph()
X = graph.get_tensor_by_name('X:0')
Y = graph.get_tensor_by_name('Y:0')
Xs = graph.get_tensor_by_name('generator/xs:0')

makeup = cv2.resize(imread(makeup_image), (img_size, img_size))
Y_img = np.expand_dims(preprocess(makeup), 0)
Xs_ = sess.run(Xs, feed_dict={X: X_img, Y: Y_img})
Xs_ = deprocess(Xs_)
img = cv2.cvtColor(Xs_[0], cv2.COLOR_RGB2BGR)
cv2.imwrite('makeup.jpg', img)
<makeup.py>

Run the code, and you will get the makeup.jpg files.


python3 makeup.py --no_makeup=./imgs/no_makeup/lee.jpg



Cropping the face and apply the BeautyGAN

https://github.com/kairess/BeautyGAN/blob/master/test.ipynb file introduces an example of applying BeautyGAN with horizontal alignment after trimming the face area from the image.

<face cropping and horizontal alignment>

This is a good example to learn by using a Jupyter Nodebook.

Wrapping up


Currently, only trained models provided by BeautyGAN can use 256X256X3 images. Therefore, it is impossible to create a high-resolution makeup image at this time. I hope that the source code related to the training has been released so that this part can be modified and improved by many people.

You can download the source code at https://github.com/raspberry-pi-maker/NVIDIA-Jetson










2020년 2월 21일 금요일

Camera - CSI Camera (Raspberry Pi camera V2)

So far, I have been using USB cameras mainly with Jetson Nano. In this post, I'll show you how to use the Raspberry Pi camera V2 and see how it differs from USB cameras.

Raspberry Pi Camera V2 connection


Raspberry Pi cameras connect using the Camera Serial Interface (CSI).
The video from JetsonHack can help you a lot. Make sure to turn off the Jetson Nano before connecting. When connecting, pay attention to the direction of the ribbon cable.



<Raspberry Pi Camera V2 and Jetson Nano>

After connecting, boot your Jetson Nano. Then check if the camera is connected with the following command. This command can check both USB camera and CSI camera.
In the previous post, I explained in detail how to check the camera.

root@spypiggy-nano:~# ls -al /dev/video*
crw-rw----+ 1 root video 81, 0  2 20 01:33 /dev/video0
root@spypiggy-nano:~# v4l2-ctl --list-devices
vi-output, imx219 6-0010 (platform:54080000.vi:0):
        /dev/video0

You can check the camera stats on the Raspberry Pi website:


Raspberry Camera supports V4L2 driver, so you can use v4l2-ctl command to check the camera. The image sensor of Raspberry Pi camera V2 is "SONY IMX219", and you can find this  in the ablove command output. So the Raspberry Pi camera is working correctly.

Test the camera

gstreamer usage

Jetson Nano uses a gstreamer to output camera input to the screen. We will test the camera using the gst-launch-1.0 program, the gstreamer test tool.


gst-launch passes the result of the plug-in corresponding to elements to the next element via a link.
For example, in the figure, Element1 can be either a camera plug-in or a source video file, and Element2 can be a plug-in that changes the size of the frame received through Element1, and mirrors front and back. And finally, Element3 can be a plug-in that receives the frame changed by Element2 and prints it to the screen or saves it to a file.

Test the Raspberry Pi camera V2

Run this command, you can see the camera video.

root@spypiggy-nano:~#gst-launch-1.0 nvarguscamerasrc ! 'video/x-raw(memory:NVMM),width=1280, height=720, framerate=30/1, format=NV12' ! nvegltransform ! nveglglessink -e


This command has the following meaning:

 

 Adjust camera settingsvi readme.txt

Perhaps the camera you see is upside down or left, right changed. In this case, you can do this by inserting a plug-in that controls flip.
The NVIDIA proprietary nvvidconv Gstreamer-1.0 plug-in also allows you to perform video rotation operations


flip-method
Property value
no flip
0
counterclockwise - 90 degrees
1
rotate - 180 degrees
2
clockwise - 90 degrees
3
 
Before passing to the plug-in responsible for the display, you can add a flip plug in as follows:

gst-launch-1.0 nvarguscamerasrc ! 'video/x-raw(memory:NVMM),width=1280, height=720, framerate=30/1, format=NV12' ! nvvidconv flip-method=1 ! nvegltransform ! nveglglessink -e

The NVIDIA proprietary nvvidconv Gstreamer-1.0 plug-in allows you to convert between OSS (raw) video formats and NVIDIA video formats. The nvvidconv plug-in currently supports the format conversions described in this section

Therefore, make sure to go through the nvvidconv plugin before passing it to the display like this.

gst-launch-1.0 nvarguscamerasrc ! 'video/x-raw(memory:NVMM),width=1280, height=720, framerate=30/1, format=NV12' ! nvvidconv flip-method=0 ! nvvidconv !nvegltransform ! nveglglessink -e

Supported resolutions

When you run the program above, the modes supported by the current camera are displayed on the screen as follows.

GST_ARGUS: Available Sensor modes :
GST_ARGUS: 3264 x 2464 FR = 21.000000 fps Duration = 47619048 ; Analog Gain range min 1.000000, max 10.625000; Exposure Range min 13000, max 683709000;
GST_ARGUS: 3264 x 1848 FR = 28.000001 fps Duration = 35714284 ; Analog Gain range min 1.000000, max 10.625000; Exposure Range min 13000, max 683709000;
GST_ARGUS: 1920 x 1080 FR = 29.999999 fps Duration = 33333334 ; Analog Gain range min 1.000000, max 10.625000; Exposure Range min 13000, max 683709000;
GST_ARGUS: 1280 x 720 FR = 59.999999 fps Duration = 16666667 ; Analog Gain range min 1.000000, max 10.625000; Exposure Range min 13000, max 683709000;
GST_ARGUS: 1280 x 720 FR = 120.000005 fps Duration = 8333333 ; Analog Gain range min 1.000000, max 10.625000; Exposure Range min 13000, max 683709000;


Using Raspberry Pi camera V2 with python OpenCV

We are familiar with camera processing using OpenCV. So let's see how to manipulate the Raspberry Pi camera in OpenCV.
Jetson Nano's OpenCV also supports cameras using gstreamer. Therefore, once you understand the pipeline described above, you can easily manipulate the Raspberry Pi camera in OpenCV.

# MIT License
# Copyright (c) 2019 JetsonHacks
# See license
# Using a CSI camera (such as the Raspberry Pi Version 2) connected to a
# NVIDIA Jetson Nano Developer Kit using OpenCV
# Drivers for the camera and OpenCV are included in the base image

import cv2

# gstreamer_pipeline returns a GStreamer pipeline for capturing from the CSI camera
# Defaults to 1280x720 @ 60fps
# Flip the image by setting the flip_method (most common values: 0 and 2)
# display_width and display_height determine the size of the window on the screen


def gstreamer_pipeline(
    capture_width=1280,
    capture_height=720,
    display_width=1280,
    display_height=720,
    framerate=60,
    flip_method=0,
):
    return (
        "nvarguscamerasrc ! "
        "video/x-raw(memory:NVMM), "
        "width=(int)%d, height=(int)%d, "
        "format=(string)NV12, framerate=(fraction)%d/1 ! "
        "nvvidconv flip-method=%d ! "
        "video/x-raw, width=(int)%d, height=(int)%d, format=(string)BGRx ! "
        "videoconvert ! "
        "video/x-raw, format=(string)BGR ! appsink"
        % (
            capture_width,
            capture_height,
            framerate,
            flip_method,
            display_width,
            display_height,
        )
    )


def show_camera():
    # To flip the image, modify the flip_method parameter (0 and 2 are the most common)
    print(gstreamer_pipeline(flip_method=0))
    cap = cv2.VideoCapture(gstreamer_pipeline(flip_method=0), cv2.CAP_GSTREAMER)
    if cap.isOpened():
        window_handle = cv2.namedWindow("CSI Camera", cv2.WINDOW_AUTOSIZE)
        # Window
        while cv2.getWindowProperty("CSI Camera", 0) >= 0:
            ret_val, img = cap.read()
            cv2.imshow("CSI Camera", img)
            # This also acts as
            keyCode = cv2.waitKey(30) & 0xFF
            # Stop the program on the ESC key
            if keyCode == 27:
                break
        cap.release()
        cv2.destroyAllWindows()
    else:
        print("Unable to open camera")


if __name__ == "__main__":
    show_camera()


The OpenCV installed on Jetson Nano is built to work with gstreamer, so the code above runs fine.

Note :

  • The use of cv2.CAP_GSTREAMER as the second parameter in the cv2.VideoCapture function is optional. The second parameter can be omitted.
  • In the gstreamer pipline string, last video format is "BGR", because the OpenCV's default color map is BGR.
  • Notice that the last pipeline is appsink, not the screen output(nvegltransform ! nveglglessink -e).

 

Using Raspberry Pi camera and Webcam simultaneously

First connect the USB webcam to the Jetson Nano, and check the cameras.

root@spypiggy-nano:/usr/local/src/study/csi_cam# ls -al /dev/video*
crw-rw----+ 1 root video 81, 0  2 20 01:33 /dev/video0
crw-rw----+ 1 root video 81, 3  2 21 21:49 /dev/video1
root@spypiggy-nano:/usr/local/src/study/csi_cam# v4l2-ctl --list-devices
vi-output, imx219 6-0010 (platform:54080000.vi:0):
        /dev/video0

UVC Camera (046d:0825) (usb-70090000.xusb-2.1):
        /dev/video1

Notice that the USB webcam is video1. I will compare two cameras at the same time.

# MIT License
# Copyright (c) 2019 JetsonHacks
# See license
# Using a CSI camera (such as the Raspberry Pi Version 2) connected to a
# NVIDIA Jetson Nano Developer Kit using OpenCV
# Drivers for the camera and OpenCV are included in the base image

import cv2

# gstreamer_pipeline returns a GStreamer pipeline for capturing from the CSI camera
# Defaults to 1280x720 @ 60fps
# Flip the image by setting the flip_method (most common values: 0 and 2)
# display_width and display_height determine the size of the window on the screen


def gstreamer_pipeline(
    capture_width=1280,
    capture_height=720,
    display_width=640,
    display_height=480,
    framerate=60,
    flip_method=0,
):
    return (
        "nvarguscamerasrc ! "
        "video/x-raw(memory:NVMM), "
        "width=(int)%d, height=(int)%d, "
        "format=(string)NV12, framerate=(fraction)%d/1 ! "
        "nvvidconv flip-method=%d ! "
        "video/x-raw, width=(int)%d, height=(int)%d, format=(string)BGRx ! "
        "videoconvert ! "
        "video/x-raw, format=(string)BGR ! appsink"
        % (
            capture_width,
            capture_height,
            framerate,
            flip_method,
            display_width,
            display_height,
        )
    )

camSet='nvarguscamerasrc !  video/x-raw(memory:NVMM), width=3264, height=2464, format=NV12, framerate=21/1 ! nvvidconv flip-method=0 ! video/x-raw, width=640, height=480, format=BGRx ! videoconvert ! video/x-raw, format=BGR ! appsink'

def show_camera():
    # To flip the image, modify the flip_method parameter (0 and 2 are the most common)
    print(gstreamer_pipeline(flip_method=0))
    #cap = cv2.VideoCapture(gstreamer_pipeline(flip_method=0), cv2.CAP_GSTREAMER)
    csi_cap = cv2.VideoCapture(camSet)
    web_cap = cv2.VideoCapture(1)
    web_cap.set(cv2.CAP_PROP_FRAME_WIDTH, 640)
    web_cap.set(cv2.CAP_PROP_FRAME_HEIGHT, 480)

    if csi_cap.isOpened() and web_cap.isOpened():
        while True:
            ret_valw, imgw = web_cap.read()
            ret_val, img = csi_cap.read()
            cv2.imshow("Web Camera", imgw)
            cv2.imshow("CSI Camera", img)
           # This also acts as
            keyCode = cv2.waitKey(30) & 0xFF
            # Stop the program on the ESC key
            if keyCode == 27:
                break
        csi_cap.release()
        web_cap.release()
        cv2.destroyAllWindows()
    else:
        print("Unable to open camera")


if __name__ == "__main__":
    show_camera()
<csi_web_cam.py>



Run the code, you can see two windows for each camera.



Be careful : When I tested an old Logitech C270 Webcam, the Raspberry Pi camera output is delayed. I can't find the reason, but when I changed the webcam to the newest one, the delay has disappeared and two cameras were output at about the same time.