Computer Vision: Code Overview

 Now that I've implemented a convolutional neural network in this tutorial. Now, I'm gonna start looking into the code. 

What I'm going to do today is to go over all the code lines, and try and pickpocket and "figure out" what each of the lines of each of the classes do. 

The first file that I'm going to go over is utils.py and document the pseudocode for this. 

import numpy as np

import cv2

import matplotlib.pyplot as plt

def convert_exr_and_write(exr, dest_path, img_format, default_height = 400, default_width = 900):

    im=cv2.imread(exr,-1)

    height, width = im.shape[:2]

    # convert to jpg

    if img_format == 'jpg':

        tonemap = cv2.createTonemap(gamma=1)

        im = tonemap.process(im)

        im = im[0:height-600,:]

        im = cv2.normalize(im, None, alpha=0, beta=20000, norm_type=cv2.NORM_MINMAX, dtype=cv2.CV_32F)

        im=np.uint16(im)

        im = cv2.resize(im, (default_width, default_height))

        cv2.imwrite(dest_path, im)

    elif img_format == 'exr': # convert to exr

        tonemap = cv2.createTonemap(gamma=1)

        im = tonemap.process(im)

        im = im[0:height-600,:]

        im = cv2.resize(im, (default_width, default_height))

        cv2.imwrite(dest_path, im)

    else:

        raise TypeError('Supported format jpg and exr')

def imshow(img):

    cv2.imshow('image', img)

    cv2.waitKey(0)

The first line loads an image. 
The second line is named im.shape. This line returns the dimensions of this image.

createToneMap() creates a simple linear map with gamma correction. Whatever that means.



The process call merges the image and then the image is called from index 0 to the 600th last index. 

After which the image is normalized where the minimum distance is 0 and the maximum distance is 20,000.  I convert everything to integer values and subsequently resize the image and write the image to a certain destination path. This normalization is required for .img file, but not for the .exr file, as this is already compressed. 


Now for the next file. It's called generate-feature-matrix.py. Here is the following code: 


import os

import cv2

import json

import argparse

import numpy as np


from dataset.preprocess import DataGenerator



if __name__ == '__main__':

    parser = argparse.ArgumentParser(description='Generate Feature Matrix')

    parser.add_argument('-f', '--imgformat', type=str,

                    help='generate annotated images of a specific type: jpg or exr', default='jpg')

    parser.add_argument('-sp', '--sourcepath', type=str, help='data source path', default=os.path.join('data', 'jpg_sample'))

    parser.add_argument('-dp', '--destpath', type=str, help='data destination path', default='data')


    # format: jpg or exr

    args = parser.parse_args()

    img_format = args.imgformat

    img_format = str.lower(img_format)

    if img_format == 'jpg' or img_format == 'jpeg' or img_format == 'exr':

        pass

    else:

        raise TypeError("Supported format: jpg and exr")

    

    # paths

    source_path = args.sourcepath

    dest_path = args.destpath


    # generate feature matrix

    matrix_generator = DataGenerator()

    matrix_generator.generate_feature_and_label(os.path.join('data', 'jpg_sample'), 'data', img_format)


The first line is just to generate the description feature matrix.

3 arguments take strings, specifying the image format, the source path, and the destination path. 

We want to make sure the image format is .exr and .jpg. The source path and destination path is subsequently generated and the feature and label is finally generated. How does this happen? Well, the labels of the image are generated



This is what happens when the feature and label are generated. 

The next file I want to discuss is convert_exr.py. The file is as follows: 


import os
import argparse

from utils import convert_exr_and_write


if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='Convert exr to exr or jpg both of size (400, 900)')
    parser.add_argument('-f', '--imgformat', type=str,
                    help='The type images would be converted to', default='jpg')
    parser.add_argument('-dp', '--datapath', type=str, help='data source path', default='/home/adrian/Downloads/laval-dataset')
    parser.add_argument('-s', '--size', type=int, 
                    help='numebr of images converted, if is -1, convert all the image', default=30)
    args = parser.parse_args()

    data_path = args.datapath
    img_format = args.imgformat
    size = args.size

    # format check
    if str.lower(img_format) != 'jpg' and str.lower(img_format) != 'jpeg' and str.lower(img_format) != 'exr':
        raise TypeError("Format supported: jpg and exr")

    if str.lower(img_format) == 'jpeg':
        img_format= 'jpg'
    img_format = str.lower(img_format)

    # store images in different dirs based on format
    dest_path = os.path.join('data', img_format)
    if not os.path.isdir(dest_path):
        os.mkdir(dest_path)

    file_ind = 1
    exr_lists = os.listdir(data_path)
    # number of images is size 
    if size != -1 and size < len(exr_lists):
        exr_lists = exr_lists[0:size]

    # convert and write
    for exr_file_name in exr_lists:
        converted_file = os.path.join(dest_path, str(file_ind) + '.' + img_format)
        convert_exr_and_write(os.path.join(data_path, exr_file_name), converted_file, img_format)
        file_ind = file_ind + 1

This file converts .exr file to .jpg, since .jpg is much more efficient than .exr. If the format of the image isn't .jpg or .exr, then nothing will be able to be supported. We convert the format of the image into lowercase, and then we store images in different directories based on format into the said vectors. 

The next file before the major Train and the neural network classes is the generated_annotated_examples.py. The following is the code for this class: 

import os
import cv2
import argparse
import numpy as np

from dataset.preprocess import JPGLabeler, EXRLabeler 


if __name__ == '__main__':
    # parse arguments
    parser = argparse.ArgumentParser(description='Generate example images')
    parser.add_argument('-f', '--imgformat', type=str,
                    help='generate annotated images of a specific type: jpg or exr', default='exr')
    parser.add_argument('-dp', '--datapath', type=str, help='data source path', default='exr_sample')
    args = parser.parse_args()

    img_format = args.imgformat

    # decide a labeler
    if str.lower(img_format) == 'jpg' or str.lower(img_format) == 'jpeg':
        labler = JPGLabeler()
    elif str.lower(img_format) == 'exr':
        labler = EXRLabeler()
    else:
        raise TypeError("Format supported: jpg and exr")

    data_path = os.path.join('data', args.datapath)
    example_path = os.path.join('annotated_examples', str.lower(img_format))
    if not os.path.isdir(example_path):
        os.mkdir(example_path)

    # get image files
    img_names = os.listdir(os.path.join(data_path))
    discarded_data_set = set()
    img_names.sort()

    # generate images
    for img_name in img_names:
        img = cv2.imread(os.path.join(data_path, img_name), cv2.IMREAD_UNCHANGED).astype(np.float32)
        img = labler.generate_annotated_img(img)
        # any img with fewer than 3 lights is removed. 
        if img is None:
            discarded_data_set.add(img_name)
            continue

        # write images to the specific location
        cv2.imwrite(os.path.join(example_path, img_name),img)

    
This class, I believe is just a training data class, generating annotated images of a particular dataset. First, we generate a labeler based on the format of the image. JPG_labeler takes the Gaussian form of a particular image and then blur, erodes and dilates an image in order to make it easier to process. This is stored and any image under 3 light sources is removed. 


The next part of the code I want to go through is the program, called train.py. Here is the code: 

import time
import os
import copy
import data
import torch
import torch.optim as optim
import numpy as np
import matplotlib.pyplot as plt
import argparse


import dataset.transformer as transformer

from datetime import datetime
from torch.utils.data import DataLoader
from torch.optim import lr_scheduler
from torchvision import transforms

from model.loss import average_difference_loss, location_success_count
from model.network import IlluminationPredictionNet
from dataset.dataset import EnvironmentJPGDataset


def train_model(model, criterion, optimizer, scheduler, num_epochs=25):
    global train_dataloader
    global test_dataloader

    since = time.time()

    best_model_wts = None
    best_acc = 0.0

    device = torch.device('cuda' if torch.cuda.is_available() else ('cpu'))
    model.to(device)
    
    # to draw figures
    train_loss_epoch = np.zeros((num_epochs, 2))
    train_acc_epoch = np.zeros((num_epochs, 2))

    val_loss_epoch = np.zeros((num_epochs, 2))
    val_acc_epoch = np.zeros((num_epochs, 2))

    for epoch in range(num_epochs):
        print('Epoch {}/{}'.format(epoch, num_epochs - 1))
        print('-' * 10)

        # Each epoch has a training and validation phase
        for phase in ['train', 'validation']:
            if phase == 'train':
                model.train()  # Set model to training mode
                dataloader = train_dataloader
            else:
                model.eval()   # Set model to evaluate mode
                dataloader = test_dataloader

            running_loss = 0.0
            running_corrects = 0

            # Iterate over data.
            for (i, data) in enumerate(dataloader):
                inputs = data[0].to(device)
                labels = data[1].to(device)

                # zero the parameter gradients
                optimizer.zero_grad()

                # forward
                # track history if only in train
                with torch.set_grad_enabled(phase == 'train'):
                    outputs = model(inputs)
                    outputs = torch.reshape(outputs, (-1, 3, 9))
                    loss = criterion(outputs, labels)
                    running_corrects += location_success_count(outputs, labels)

                    # backward + optimize only if in training phase
                    if phase == 'train':
                        loss.backward()
                        optimizer.step()

                # statistics
                running_loss += loss.item() * inputs.size(0)
            if phase == 'train' and scheduler is not None:
                scheduler.step()

            epoch_loss = running_loss / len(dataloader)/dataloader.batch_size
            epoch_acc = running_corrects / len(dataloader)/3/dataloader.batch_size

            # record loss & acc during training
            if phase == 'train':
                train_loss_epoch[epoch][1] = epoch_loss
                train_acc_epoch[epoch][1] = epoch_acc

                train_loss_epoch[epoch][0] = epoch
                train_acc_epoch[epoch][0] = epoch
            else:
                val_loss_epoch[epoch][1] = epoch_loss
                val_acc_epoch[epoch][1] = epoch_acc

                val_loss_epoch[epoch][0] = epoch
                val_acc_epoch[epoch][0] = epoch

            print('{} Loss: {:.4f} Acc: {:.4f}'.format(
                phase, epoch_loss, epoch_acc))

            if epoch_acc > best_acc and phase == 'validation':
                best_model_wts = copy.deepcopy(model)
                best_acc = epoch_acc
    
        print()
    
    time_elapsed = time.time() - since
    print('Training complete in {:.0f}m {:.0f}s'.format(
        time_elapsed // 60, time_elapsed % 60))

    if best_model_wts is None and num_epochs >= 25:
        raise TypeError("Accuracy Metric is Invalid")

    if num_epochs >= 25:
        torch.save(best_model_wts, os.path.join('checkpoint','naive_model_with_activation' + datetime.now().strftime("_%H_%M_%S_%d-%m-%Y")))

    return best_model_wts, train_loss_epoch, train_acc_epoch, val_loss_epoch, val_acc_epoch


def plot_loss_acc(train_loss_epoch, train_acc_epoch, val_loss_epoch, val_acc_epoch):
    epoch_num = len(train_loss_epoch)

    # loss plot
    l1, = plt.plot(train_loss_epoch[:,0], train_loss_epoch[:, 1], color='blue')
    l2, = plt.plot(val_loss_epoch[:,0], val_loss_epoch[:, 1], color ='red')
    plt.legend(handles=[l1,l2],labels=['train','validation'],loc='best')
    plt.title('Training and Validation Loss')
    plt.xlabel('epoch')
    plt.ylabel('loss')
    plt.savefig(os.path.join("figures", "epoch-vs-squared_loss" + datetime.now().strftime("_%H-%M-%S_%d-%m-%Y") + ".png"))
    plt.close('all')

    # acc plot
    l1, = plt.plot(train_acc_epoch[:,0], train_acc_epoch[:,1], color='blue')
    l2, = plt.plot(val_acc_epoch[:,0], val_acc_epoch[:,1], color ='red')
    plt.legend(handles=[l1,l2],labels=['train','validation'],loc='best')
    plt.title('Training and Validation Accuracy')
    plt.xlabel('epoch')
    plt.ylabel('accuracy')
    plt.savefig(os.path.join("figures", "epoch-vs-accuracy"+ datetime.now().strftime("_%H-%M-%S_%d-%m-%Y") + ".png"))


if __name__ == '__main__':
    parser = argparse.ArgumentParser(description='train model')
    parser.add_argument('-opt', '--optimizer', type=str,
                    help='choose SGD or Adam', default='sgd')
    parser.add_argument('-lr', '--learningrate', type=float, help='learning rate', default=0.0001)
    parser.add_argument('-mm', '--momentum', type=float, help='momentum for sgd', default=0.9)
    parser.add_argument('-b1', '--beta1', type=float, help='beta1 parameter for Adam', default=0.9)
    parser.add_argument('-b2', '--beta2', type=float, help='beta2 parameter for Adam', default=0.999)
    parser.add_argument('-e', '--epsilon', type=float, help='eps parameter for Adam', default=1e-8)
    parser.add_argument('-he', '--height', type=int, help = 'height of the input image', default=400)
    parser.add_argument('-w', '--width', type=int, help = 'width of the input image', default=900)
    parser.add_argument('-bs', '--batchsize', type=int, help='batch size', default=16)
    parser.add_argument('-epoch', '--epoch', type=int, help='training epoch', default = 50)
    args = parser.parse_args()

    # args
    choice_of_optimizer = str.lower(str(args.optimizer))
    if choice_of_optimizer != 'sgd' and choice_of_optimizer != 'adam':
        raise TypeError('Optimizer Must be SGD or Adam')
    
    batch_size = args.batchsize

    learning_rate = args.learningrate
    sgd_momentum = args.momentum

    beta1 = args.beta1
    beta2 = args.beta2
    adam_beta = (beta1, beta2)
    adam_epsilon = args.epsilon

    height = args.height
    width = args.width
    #transformer.CustomNormalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])
    # dataset
    train_ds = EnvironmentJPGDataset(os.path.join('data', 'train_feature_matrix.npy'), os.path.join('data', 'train_label.npy'),\
        transform= transforms.Compose([
                                       transformer.ToTensor(),
                                       transformer.CustomNormalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])])
                                       , augmentation=(1, 2))
    test_ds = EnvironmentJPGDataset(os.path.join('data', 'test_feature_matrix.npy'), os.path.join('data', 'test_label.npy'),\
        transform= transforms.Compose([
                                       transformer.ToTensor(),
                                       transformer.CustomNormalize(mean=[0.485, 0.456, 0.406], std=[0.229, 0.224, 0.225])])
                                       , augmentation=(1, 2))
    train_dataloader = DataLoader(train_ds, batch_size)
    test_dataloader = DataLoader(test_ds, batch_size)

    # model
    model = IlluminationPredictionNet()
    model.double()

    # optimizer
    if optim == 'sgd':
        optimizer = optim.SGD(model.parameters(), lr=learning_rate, momentum=sgd_momentum)
    else:
        optimizer = optim.Adam(model.parameters(), lr=learning_rate, betas=adam_beta, eps=adam_epsilon)

    scheduler = lr_scheduler.StepLR(optimizer, step_size=10, gamma=0.01)
    
    # train
    model, train_loss_epoch, train_acc_epoch, val_loss_epoch, val_acc_epoch = train_model(model, average_difference_loss, optimizer, scheduler, 1)

    plot_loss_acc(train_loss_epoch, train_acc_epoch, val_loss_epoch, val_acc_epoch)


There is a line of code that keeps track of the time, and dedicates the code to the CPU or GPU depending on what part of the model the code is located in. There is a training set and a value set that is made, with num_epoch rows and 2 columns. 

Each epoch has both a training and validation phase. If the epoch is in the training phase, then set the model into training mode. Else, set the model into evaluation/testing mode. The loss and corrects are set to 0 and then we start enumerating all of the data. First, we zero the parameter, and track the parameter history if only in training mode. 

Torch.reshape reshapes the vectors to a 3x9 with 3 lights and 9 features for each light, including the RGB value. Get everything from the dataloader, and get the inputs and labels, and model everything with a convolutional neural network. We then compute the loss and implement the number of correct labels after training the network. Loss.backward() computes the optimizer loss and stores the gradient into tensors(vectors). Then, all the gradients are updated with the function optimizer.step(); We increment the total loss and then have an average epoch loss and epoch accuracy. There is a training loss epoch and value loss epoch. An unknown command is copy.deepcopy(). The best model and accuracy is copied and tricked, and a graph is ploted in case of epoch and accuracy, and saved based on the date. 


In the main function, there's a lot of arguments, including, beta, and momentum, to include a few of them. We initialize a prediction network and run the model-update method. However, before this update is run the optimizer is initialized for either stochastic gradient descent or Adam optimization. The SGD is the gradient descent with momentum. Adam optimization is a stochastic gradient descent method that is based on adaptive estimation of first-order and second-order moments.


The next file I want to discuss here is the dataset.py. There are 2 methods in this class, EnvironmentJPGDataset, and EnvironmentEXRDataset. There are 2 files here, img_npy_file and label_npy_file. We shape the environment and the labels. After which comes the augmentation part. Augmentation is a tuple of 2 dimensions, first dimension is the number of rows, and the second dimension is the number of columns, as follows:

[num_rows, num_cols]. First we assume an environmental frame, and then a label frame. The for loops separates things into width/column number and height / row number, respectively. I'm not entirely sure what transform means, but I am not entirely sure, probably things to set up the api. Transform converts everything into a tensor vector which needs to be this way for pytorch to operate on it.

The next class is the transformer class and it is as follows: 


import torch
import numpy as np

from skimage import transform

class Normalize(object):
    '''Normalize image matrix'''

    def __init__(self):
        pass

    def __call__(self, input):
        image, labels = input[0], input[1]
        h, w = image.shape[:2]
        if isinstance(self.output_size, int):
            if h > w:
                new_h, new_w = self.output_size * h / w, self.output_size
            else:
                new_h, new_w = self.output_size, self.output_size * w / h
        else:
            new_h, new_w = self.output_size

        new_h, new_w = int(new_h), int(new_w)

        img = transform.resize(image, (new_h, new_w))

        # h and w are swapped for landmarks because for images,
        # x and y axes are axis 1 and 0 respectively
        # landmarks = landmarks * [new_w / w, new_h / h]

        return img, labels

''' source code from pytorch '''

class Rescale(object):
    """Rescale the image in a sample to a given size.

    Args:
        output_size (tuple or int): Desired output size. If tuple, output is
            matched to output_size. If int, smaller of image edges is matched
            to output_size keeping aspect ratio the same.
    """

    def __init__(self, output_size):
        assert isinstance(output_size, (int, tuple))
        self.output_size = output_size

    def __call__(self, input):
        image, labels = input[0], input[1]
        h, w = image.shape[:2]
        if isinstance(self.output_size, int):
            if h > w:
                new_h, new_w = self.output_size * h / w, self.output_size
            else:
                new_h, new_w = self.output_size, self.output_size * w / h
        else:
            new_h, new_w = self.output_size

        new_h, new_w = int(new_h), int(new_w)

        img = transform.resize(image, (new_h, new_w))

        # h and w are swapped for landmarks because for images,
        # x and y axes are axis 1 and 0 respectively
        # landmarks = landmarks * [int(new_w / w), int(new_h / h)]

        return img, labels


class ToTensor(object):
    """Convert ndarrays in sample to Tensors."""

    def __call__(self, input):
        image, labels = input[0], input[1]
        # swap color axis because
        # numpy image: H x W x C
        # torch image: C X H X 
        image = image.transpose((2, 0, 1))
        return torch.from_numpy(image), torch.from_numpy(labels)


''' Normalize Images in torch.tensor'''
class CustomNormalize(object):
    def __init__(self, mean, std):
        self.mean = torch.from_numpy(np.resize(mean, (3, 1, 1)))
        self.std = torch.from_numpy(np.resize(std, (3, 1, 1)))

    def __call__(self, input):
        image, labels = input[0], input[1]
        output = (image - self.mean) / self.std

        return output, labels


this class rearranges the size of the image, and normalizes this image to make it easier to process. There are also classes called CustomNormalize and ToTensor, and these classes. A numpy image is height x width x color, while torch image is color x height x width, which is what the transpose line of code does. Normalizing images resizes them and result in the difference from the mean. The loss is represented as a basic squared error difference approach, and the location accuracy count is based on the distance between the output and the labels. Network.py sets a simple neural network, and has a dense net network where each layer is interconnected with each other. 

The last class I want to discuss is preprocessing, which performs everything that needs to be done for a neural network to run. 



import os
import cv2
import math
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.image as mpimg
import imutils

from scipy.interpolate import interp2d
from functools import cmp_to_key

from imutils import contours
from skimage import measure

import utils


# extract max

# grow the seed

# find illuminated part

# repeat


class JPGLabeler():
    def _get_threshold_img(self, img):
        gray_img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
        blurred_img = cv2.GaussianBlur(gray_img, (11, 11), 0)
        extreme = blurred_img.max()

        img_mean = np.mean(img)

        if img_mean < 85:
            thresh_img = cv2.threshold(blurred_img, extreme*5/12, extreme, cv2.THRESH_BINARY)[1]
        else:
            thresh_img = cv2.threshold(blurred_img, extreme*2/3, extreme, cv2.THRESH_BINARY)[1]
        
        thresh_img = cv2.erode(thresh_img, None, iterations=2)
        thresh_img = cv2.dilate(thresh_img, None, iterations=4)

        return thresh_img


    def _find_connected_components(self, img, thresh_img, N):
        labels = measure.label(thresh_img, connectivity=1, background=0)
        masks = []
        # loop over the unique components
        for label in np.unique(labels):
            # if this is the background label, ignore it
            if label == 0:
                continue

            # otherwise, construct the label mask and count the
            # number of pixels 
            labelMask = np.zeros(thresh_img.shape, dtype="uint8")
            labelMask[labels == label] = 255
            numPixels = cv2.countNonZero(labelMask)
            # if the number of pixels in the component is sufficiently
            # large, then add it to our mask of "large blobs"
            if numPixels > 300:
                masks.append([np.resize(labelMask, thresh_img.shape), np.sum(img[labelMask == 255])/np.count_nonzero(img[labelMask == 255]), numPixels])
        
        # If not enough number of light, discard the image
        if len(masks) < N:
            return None
        
        def compare_connected_components(x, y):
            if x[1] > y[1]:
                return 1
            elif x[1] < y[1]:
                return -1
            elif x[2] > y[2]:
                return 1
            elif x[2] < y[2]:
                return -1
            else:
                return 0

        # otherwise, return the first N largest light
        masks = sorted(masks, key =cmp_to_key(compare_connected_components), reverse=True)
        return masks[0:N]

    
    def _label_img(self, masks, img):
        # for l in labels: 
        # l = [[(x1, y1, 1), (ax1, ax2), rotation1, ('R', 'G', 'B')], 
        #      [(x2, y2, 1), (ax1, ax2), rotation2, ('R', 'G', 'B')],
        #      ...,
        #      [(xN, yN, 1), (ax1, ax2), rotationN, ('R', 'G', 'B')]]

        labels = []
        # for each illumination (denoted by mask)
        for mask, _, _ in masks:
            # find the contour
            cnts = cv2.findContours(mask.copy(), cv2.RETR_EXTERNAL,
            cv2.CHAIN_APPROX_SIMPLE)
            cnts = imutils.grab_contours(cnts)
            cnts = contours.sort_contours(cnts)[0]
            
            # loop over the contours
            for (i, c) in enumerate(cnts):
                
                # draw the bright spot on the image
                rect = cv2.minAreaRect(c)
                x, y = rect[0]
                
                r, g, b =np.divide(np.sum(img[mask == 255], axis=(0)), \
                    np.count_nonzero(img[mask==255], axis=(0)))

                labels.append([x, y, 1 , rect[1][0], rect[1][1], rect[2], r, g, b])

        return labels


    def _annotate_img(self, masks, img):

        for mask, _, _ in masks:
            cnts = cv2.findContours(mask.copy(), cv2.RETR_EXTERNAL,
            cv2.CHAIN_APPROX_SIMPLE)
            cnts = imutils.grab_contours(cnts)
            cnts = contours.sort_contours(cnts)[0]

            # loop over the contours
            for (i, c) in enumerate(cnts):
                # draw the bright spot on the image
                rect = cv2.minAreaRect(c)
                elp = cv2.ellipse(img, rect, color = (0, 0, 255), thickness=2)

        return img


    def generate_annotated_img(self, img, N=3):
        # preprocessing
        thresh_img = self._get_threshold_img(img)
        masks = self._find_connected_components(img, thresh_img, N)
        
        if masks is None:
            return None
        
        img = self._annotate_img(masks, img)

        return img


    def generate_labels(self, img, N=3):
        # preprocessing
        thresh_img = self._get_threshold_img(img)
        masks = self._find_connected_components(img, thresh_img, N)
        if masks is None:
            return None
        
        label = self._label_img(masks, img)
        return label


class EXRLabeler():
    def _get_threshold_img(self, img):
        # gray_img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)
        # blurred_img = cv2.GaussianBlur(gray_img, (11, 11), 0)
        img_one_channel = np.sum(img, axis=-1)
        extreme = img_one_channel.max()
        

        # img_mean = np.mean(img)

        # if img_mean < 85:
        #     thresh_img = cv2.threshold(blurred_img, extreme*5/12, extreme, cv2.THRESH_BINARY)[1]
        # else:
        #     thresh_img = cv2.threshold(blurred_img, extreme*2/3, extreme, cv2.THRESH_BINARY)[1]
        
        # thresh_img = cv2.erode(thresh_img, None, iterations=2)
        # thresh_img = cv2.dilate(thresh_img, None, iterations=4)
        thresh_img = np.zeros(shape=img_one_channel.shape, dtype='uint8')
        thresh_img[img_one_channel >  extreme*1/100] = 255

        return thresh_img


    def _find_connected_components(self, img, thresh_img, N):
        labels = measure.label(thresh_img, connectivity=1, background=0)
        masks = []
        # loop over the unique components
        for label in np.unique(labels):
            # if this is the background label, ignore it
            if label == 0:
                continue

            # otherwise, construct the label mask and count the
            # number of pixels 
            labelMask = np.zeros(thresh_img.shape, dtype="uint8")
            labelMask[labels == label] = 255
            numPixels = cv2.countNonZero(labelMask)
            # if the number of pixels in the component is sufficiently
            # large, then add it to our mask of "large blobs"
            if numPixels > 300:
                masks.append([np.resize(labelMask, thresh_img.shape), np.sum(img[labelMask == 255])/np.count_nonzero(img[labelMask == 255]), numPixels])
        
        # If not enough number of light, discard the image
        if len(masks) < N:
            return None
        
        def compare_connected_components(x, y):
            if x[1] > y[1]:
                return 1
            elif x[1] < y[1]:
                return -1
            elif x[2] > y[2]:
                return 1
            elif x[2] < y[2]:
                return -1
            else:
                return 0

        # otherwise, return the first N largest light
        masks = sorted(masks, key =cmp_to_key(compare_connected_components), reverse=True)
        return masks[0:N]

    
    def _label_img(self, masks, img):
        # for l in labels: 
        # l = [[(x1, y1, 1), (ax1, ax2), rotation1, ('R', 'G', 'B')], 
        #      [(x2, y2, 1), (ax1, ax2), rotation2, ('R', 'G', 'B')],
        #      ...,
        #      [(xN, yN, 1), (ax1, ax2), rotationN, ('R', 'G', 'B')]]

        labels = []
        
        # for each illumination (denoted by mask)
        for mask, _, _ in masks:
            # find the contour
            cnts = cv2.findContours(mask.copy(), cv2.RETR_EXTERNAL,
            cv2.CHAIN_APPROX_SIMPLE)
            cnts = imutils.grab_contours(cnts)
            cnts = contours.sort_contours(cnts)[0]

            # loop over the contours
            for (i, c) in enumerate(cnts):
                # find a rectangle (ellipse)
                rect = cv2.minAreaRect(c)
                x, y = rect[0]
                
                r, g, b =np.divide(np.sum(img[mask == 255], axis=(0)), \
                    np.count_nonzero(img[mask==255], axis=(0)))

                labels.append([x, y, 1 , rect[1][0], rect[1][1], rect[2], r, g, b])

        return labels


    def _annotate_img(self, masks, img):
        max_intensity = np.max(img)
        for mask, _, _ in masks:
            cnts = cv2.findContours(mask.copy(), cv2.RETR_EXTERNAL,
            cv2.CHAIN_APPROX_SIMPLE)
            cnts = imutils.grab_contours(cnts)
            cnts = contours.sort_contours(cnts)[0]

            # loop over the contours
            for (i, c) in enumerate(cnts):
                # draw the bright spot on the image
                rect = cv2.minAreaRect(c)
                elp = cv2.ellipse(img, rect, color = (0, 0, 255), thickness=2)

        return img


    def generate_annotated_img(self, img, N=3):
        # preprocessing
        thresh_img = self._get_threshold_img(img)
        masks = self._find_connected_components(img, thresh_img, N)
        
        if masks is None:
            return None
        
        img = self._annotate_img(masks, img)

        return img


    def generate_labels(self, img, N=3):
        # preprocessing
        thresh_img = self._get_threshold_img(img)
        masks = self._find_connected_components(img, thresh_img, N)
        if masks is None:
            return None
        
        label = self._label_img(masks, img)
        return label


class DataGenerator():
    def __init__(self):
        pass

    def generate_feature_and_label(self, source_path, dest_path, img_format='jpg'):
        img_format = str.lower(img_format)
        if img_format == 'jpg' or img_format == 'jpeg':
            labeler = JPGLabeler()
        elif img_format == 'exr':
            labeler = EXRLabeler()
        
        img_names = os.listdir(os.path.join(source_path))
        discarded_data_set = set()
        img_names.sort()
        label_dict = dict()
        
        img_index = 0
        
        labeled_images = []
        labels = []

        # generate labels
        for img_name in img_names:
            img = cv2.imread(os.path.join(source_path, img_name))
            label = labeler.generate_labels(img)

            if label is None:
                discarded_data_set.add(img_name)
                continue

            # label_dict[img_index] = label

            labels.append(label)
            labeled_images.append(img)

            img_index += 1

        # store labeled images
        # with open(os.path.join(label_path, 'labels.txt'), 'w') as f:
        #    f.write(json.dumps(label_dict))

        labeled_images = np.array(labeled_images)
        labels = np.array(labels, dtype='float64')

        # num of X == num of Y
        assert labels.shape[0] == labeled_images.shape[0]

        # ratio of test sample
        ratio = 0.2
        num_of_samples = labeled_images.shape[0]
        num_of_test_samples = int(ratio * num_of_samples)
        num_of_train_samples = int(num_of_samples - num_of_test_samples)

        train_matrix = labeled_images[0:num_of_train_samples]
        train_labels = labels[0:num_of_train_samples]

        test_matrix = labeled_images[num_of_train_samples:-1]
        test_labels = labels[num_of_train_samples:-1]

        np.save(os.path.join(dest_path, 'train_feature_matrix.npy'), train_matrix, allow_pickle=True)
        np.save(os.path.join(dest_path, 'train_label.npy'), train_labels, allow_pickle=True)

        np.save(os.path.join(dest_path, 'test_feature_matrix.npy'), test_matrix, allow_pickle=True)
        np.save(os.path.join(dest_path, 'test_label.npy'), test_labels, allow_pickle=True)

class NaiveCropper():
    def __init__(self, environment_map):
        self.map = environment_map
        self.height = environment_map.shape[0]
        self.width = environment_map.shape[1]
        self.type = environment_map.dtype
        self.channel = environment_map.shape[2]

    
    def generate_image(self, view_direction_theta, view_direction_phi, FOV_horizontal, image_width, image_height):
        """
        Return a cropped image

        Parameters
        ----------
        view_direction_theta : float.
            The theta of the view vector, which defines in spherical coordinate

        view_direction_phi : float.
            The phi of the view vector, which defines in spherical coordinate

        FOV_horizontal : float.
            The field of view of the cropped image horizontally

        image_height, image_width : int.

        Returns
        -------
        out : 2darray
            A 2darray with size = image_heigh x image_width

        Examples
        --------
        image = handle.generate_image(0, 0, math.pi/3, 1080, 720)

        """
        FOV_vertical = FOV_horizontal/image_width * image_height
        theta_start = view_direction_theta - FOV_horizontal/2
        phi_start = view_direction_phi - FOV_vertical/2

        image_center = np.array([1 * np.cos(view_direction_theta) * np.sin(view_direction_phi), \
            1 * np.sin(view_direction_theta) * np.sin(view_direction_phi),1 * np.cos(view_direction_phi)])
        horizontal_size_half = np.tan(FOV_horizontal/2)
        vertical_size_half = np.tan(FOV_vertical/2)
        
        horizontal_displacement = np.array([-horizontal_size_half * np.sin(view_direction_theta), horizontal_size_half * np.cos(view_direction_theta), 0])
        vertical_displacement = np.array([-vertical_size_half*np.cos(view_direction_phi)*np.cos(view_direction_theta), -vertical_size_half * np.cos(view_direction_phi) * np.sin(view_direction_theta), \
            vertical_size_half*np.sin(view_direction_phi)])

        top_left = image_center + vertical_displacement - horizontal_displacement
        top_right = image_center + vertical_displacement + horizontal_displacement
        bot_left = image_center - vertical_displacement - horizontal_displacement
        bot_right = image_center - vertical_displacement + horizontal_displacement

        # form the image plane
        
        img_rect = np.zeros(shape=(image_height,image_width, 3), dtype=np.float32)
        left = top_left
        right = top_right
        height_array = np.linspace(top_left ,bot_left, image_height)
        for i in range(image_height):
            left = height_array[i]
            right = left + horizontal_displacement*2
            img_rect[i,:,:] = np.linspace(left,right,image_width)
        # transfer the image plane back to spherical 
        theta = np.arctan2(img_rect[:,:,1], img_rect[:,:,0])
        theta[theta < 0] = theta[theta < 0] + math.pi*2
        phi = np.arccos(img_rect[:,:,2]/np.linalg.norm(img_rect, axis=2))

        # Read from the original image
        # u = np.floor(theta  * self.width / math.pi)
        # v = np.floor(phi * self.height / (math.pi/2))
        img = np.zeros(shape=(image_height,image_width, self.channel), dtype=self.type)

        for i in range(self.channel):
            f = interp2d(np.linspace(0, math.pi * 2,self.width), np.linspace(0,math.pi, self.height), self.map[:,:,i], kind='linear')
            for j in range(theta.shape[0]):
                for k in range(theta.shape[1]):
                    img[j,k,i]  = (f(theta[j,k], phi[j,k]))[0]
        return img

    def generate_image_detail(self, starting_theta, starting_phi, theta_size, phi_size, height_resolution, width_resolution):
        # create four points on the sphere
        top_left_sph = np.array([1, starting_theta, starting_phi], dtype = np.float32)
        top_right_sph = np.array([1, starting_theta + theta_size, starting_phi], dtype = np.float32)
        bot_left_sph = np.array([1, starting_theta, starting_phi+phi_size], dtype = np.float32)
        bot_right_sph = np.array([1, starting_theta + theta_size, starting_phi+phi_size], dtype = np.float32)

        # transfer them to rect system
        top_left = np.array([top_left_sph[0] * np.cos(top_left_sph[1]) * np.sin(top_left_sph[2]), \
            top_left_sph[0] * np.sin(top_left_sph[1]) * np.sin(top_left_sph[2]), top_left_sph[0] * np.cos(top_left_sph[2])])
        top_right = np.array([top_right_sph[0] * np.cos(top_right_sph[1]) * np.sin(top_right_sph[2]), \
            top_right_sph[0] * np.sin(top_right_sph[1]) * np.sin(top_right_sph[2]), top_right_sph[0] * np.cos(top_right_sph[2])])
        bot_left = np.array([bot_left_sph[0] * np.cos(bot_left_sph[1]) * np.sin(bot_left_sph[2]), \
            bot_left_sph[0] * np.sin(bot_left_sph[1]) * np.sin(bot_left_sph[2]), bot_left_sph[0] * np.cos(bot_left_sph[2])])
        bot_right = np.array([bot_right_sph[0] * np.cos(bot_right_sph[1]) * np.sin(bot_right_sph[2]), \
            bot_right_sph[0] * np.sin(bot_right_sph[1]) * np.sin(bot_right_sph[2]), bot_right_sph[0] * np.cos(bot_right_sph[2])])

        # form the image plane
        img_width_difference = top_right - top_left
        img_height_difference = bot_left - top_left
        img_rect = np.zeros(shape=(height_resolution,width_resolution, 3), dtype=np.float32)

        left = top_left
        right = top_right
        height_array = np.linspace(top_left ,bot_left, height_resolution)
        for i in range(height_resolution):
            left = height_array[i]
            right = left + img_width_difference
            img_rect[i,:,:] = np.linspace(left,right,width_resolution)

        # transfer the image plane back to spherical 
        theta = np.arctan2(img_rect[:,:,1], img_rect[:,:,0])
        theta[theta < 0] = theta[theta < 0] + math.pi/2
        phi = np.arccos(img_rect[:,:,2]/np.linalg.norm(img_rect, axis=2))

        # print(theta)
        # print(phi)

        # Read from the original image
        u = np.floor(theta  * self.width / math.pi)
        v = np.floor(phi * self.height / (math.pi/2))

        img = np.zeros(shape=(height_resolution,width_resolution, self.channel), dtype=self.type)

        # x = np.linspace(0, math.pi,self.width)
        # y = np.linspace(0, math.pi/2,self.height)
        # points_x, points_y = np.meshgrid(x, y)
        # points_x_lin = np.reshape(points_x, -1)
        # points_y_lin = np.reshape(points_y, -1)
        
        # points = np.stack((points_x_lin, points_y_lin), axis = 1)
        # print(points.shape)

        # grid_x, grid_y = np.meshgrid(u,v)
        # print(u.shape)   
        for i in range(self.channel):
            f = interp2d(np.linspace(0, math.pi * 2,self.width), np.linspace(0,math.pi, self.height), self.map[:,:,i], kind='linear')
            for j in range(theta.shape[0]):
                img[j,:,i]  = (f(theta[j,:], phi[j,:]))[0,:]
            # print(img[:,:,i])
        # for i in range(self.channel):
        #     img[:,:,i] = self.map[v,u,i]
        #     print(img[:,:,i]) 
        return img


# test
if __name__ == '__main__':
    # preprocessing
    img = cv2.imread('./test_conversion/5.jpg')
    l = ImageLabeler()
    thresh_img = l._get_threshold_img(img)
    masks = l._find_connected_components(img, thresh_img, 3)
    img = l._annotate_image(masks, img)

    for mask in masks:
        plt.imshow(mask)
        plt.show()

    # exr => label, jpeg => train
    # ambient light
    # chopped image


get_threshold_image erodes, create, and dilates an image. Find_connected components assigns a full value to the array of anything that is connected. The label img labels stuff for each illumination. 



Comments

Popular Posts