# -*- coding: utf-8 -*-
"""
Created on Wed Nov 14 12:51:10 2018

@author: jah150330
"""
import sys
sys.path.append("..")

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import seaborn as sns
import scipy.stats

import proj_utils as pu
pdObj = pu.proj_data()
pData = pdObj.get_data()
pdir = pu._get_proj_dir()

dfSpectra = pd.read_pickle(pdir+'\data\MEG_AmpPowPha_spectra.pkl')
dfMeanStd = pd.read_pickle(pdir+'\data\MEG_AmpPowPha_spectra_level2.pkl')

import os
path = pdir+'/data/timeseries_MEG'

labels = pData['roiLabels']
database = pData['database']

filelist = os.listdir(path)
filelist = sorted(filelist,key=str.lower)

subjectList = [filelist[x].split('_') for x in range(0,267)]
sessionList = [subjectList[x][2].split('.') for x in range(0,267)]
subjectList = [subjectList[x][0] for x in range(0,267,3)]
sessionList = [sessionList[x][0] for x in range(0,3)]

if '169040' in subjectList: subjectList.remove('169040')
if '662551' in subjectList: subjectList.remove('662551')

# Define EEG bands
eeg_bands = {'BOLD': (.0005, 1/.72/2),   # Bandpass range for HCP rs-fMRI
             'Slow 4': (.02, .06),
             'Slow 3': (.06, .2),
             'Slow 2': (.2, .5),
             'Slow 1': (.5, 1.5),
             'Delta': (1.5, 4),
             'Theta': (4, 8),
             'Alpha': (8, 12),
             'Beta': (12, 30),
             'Gamma': (30, 55)}

iterables = [sessionList,eeg_bands,['Amplitude','Power','Phase'],['mean','std']]
names = ['Session','Freq. Band','Spectrum','Statistic']

dfIdx = pd.MultiIndex.from_product(iterables,names=names)
df2 = pd.DataFrame(index=dfIdx,columns=labels)

#def plot_phase_amplitude(df,label,session='Session3'):
#    rMatrix = np.zeros((10,10))
#    pMatrix = np.zeros((10,10))
#
#    keyList = list(eeg_bands.keys())
#
#    for bandX in eeg_bands:
#        x = df.loc[pd.IndexSlice[session,:,'Phase',bandX],label]
#
#        for bandY in eeg_bands:
#            y = df.loc[pd.IndexSlice[session,:,'Amplitude',bandY],label]
#
#            (rMatrix[keyList.index(bandX),keyList.index(bandY)],
#            pMatrix[keyList.index(bandX),keyList.index(bandY)])= scipy.stats.pearsonr(x,y)
#
##    sns.set_style("darkgrid", {"axes.facecolor": ".9"})
#    plt.figure()
#    sns.set(rc={'figure.figsize':(9.75,8.27)})
#    sns.set_context("talk")
#
#    ax = sns.heatmap(rMatrix,
#                xticklabels = eeg_bands.keys(),
#                yticklabels = eeg_bands.keys(),
#                vmin=0,vmax=1,annot=True)
#
#    for tick in ax.get_xticklabels():
#        tick.set_rotation(0)
#        tick.set_horizontalalignment('center')
#
#    fsize = 24
#    lpad = 15
#    ax.set_title('ROI: '+label, fontsize=fsize,pad=lpad)
#    ax.set_xlabel("Phase Spectrum",fontsize=fsize,labelpad=lpad)
#    ax.set_ylabel("Amplitude Spectrum",fontsize=fsize,labelpad=lpad)
#
#    fig = ax.get_figure()
#    fig.savefig(pdir+'/figures/PAC/Session 3/PAC_'+label+'.png')
#
#    return None
#
#for ROIindex in range(0,360):
#    plot_phase_amplitude(dfSpectra,label=labels[ROIindex])

def plot_power_power(df,label,session):
    rMatrix = np.zeros((10,10))
    pMatrix = np.zeros((10,10))

    keyList = list(eeg_bands.keys())

    for bandX in eeg_bands:
        x = df.loc[pd.IndexSlice[session,:,'Power',bandX],label]

        for bandY in eeg_bands:
            y = df.loc[pd.IndexSlice[session,:,'Power',bandY],label]

            (rMatrix[keyList.index(bandX),keyList.index(bandY)],
            pMatrix[keyList.index(bandX),keyList.index(bandY)])= scipy.stats.pearsonr(x,y)

#    sns.set_style("darkgrid", {"axes.facecolor": ".9"})
    plt.figure()
    sns.set(rc={'figure.figsize':(9.75,8.27)})
    sns.set_context("talk")

    ax = sns.heatmap(rMatrix,
                xticklabels = eeg_bands.keys(),
                yticklabels = eeg_bands.keys(),
                vmin=0,vmax=1,annot=True)

    for tick in ax.get_xticklabels():
        tick.set_rotation(0)
        tick.set_horizontalalignment('center')

    fsize = 24
    lpad = 15
    ax.set_title('ROI: '+label, fontsize=fsize,pad=lpad)
    ax.set_xlabel("Power Spectrum",fontsize=fsize,labelpad=lpad)
    ax.set_ylabel("Power Spectrum",fontsize=fsize,labelpad=lpad)

    fig = ax.get_figure()
    fig.savefig(pdir+'/figures/Power-Power/'+session+'/PowerPower_'+label+'.png')

    return None

for session in sessionList:
    for ROIindex in range(0,360):
        plot_power_power(dfSpectra,label=labels[ROIindex],session=session)


from bs4 import BeautifulSoup
from urllib.request import urlopen

fout = open('dictionary.txt','w')

url = 'http://dictionary.cambridge.org/dictionary/learner-english/python'

doc = urlopen(url)

web_page = BeautifulSoup(doc, 'html.parser')

word = web_page.find(attrs = {'class' : 'di-title cdo-section-title-hw'})
definition = web_page.find(attrs = {'class': 'def'})

fout.write(word.get_text() + ' : ' + definition.get_text().replace('\u200b', ''))
fout.close()

경영_빅데이터_프로그래밍/정리 전/경영빅데이터/경영빅데이터_20170710/20170710_test4.py


import argparse
import configparser
import os
import re

import numpy as np
from keras.utils import to_categorical
from sklearn import preprocessing

min_max_scaler = preprocessing.MinMaxScaler()
######################################################################

parser = argparse.ArgumentParser()
parser.add_argument('--dataset', type=str, default="MUTAG", dest='dataset')
args = parser.parse_known_args()[0]
######################################################################

os.chdir('./{}'.format(args.dataset))
print('dataset:', args.dataset)
config = configparser.ConfigParser()
config.read('.config')
getConfig = lambda x: config.get('config', x)
ds = getConfig('ds')
SUB_size = int(getConfig('SUB_size'))
min = int(getConfig('min'))

max_graph_nodes = 100
cate = int(getConfig('cate'))
class_size = int(getConfig('class_size'))

######################################################################

class graphParser(object):
    def __init__(self):
        self.ds = ds
        self.SUB_size = SUB_size
        self.min = min
        self.cate = cate
        self.class_size = class_size
        self.max_graph_nodes = max_graph_nodes

        #############################################################
        with open(self.ds + '_A.txt', 'r') as f:
            a = f.readlines()
            self.m = len(a)

        with open(self.ds + '_graph_labels.txt', 'r') as f:
            a = f.readlines()
            self.N = len(a)

        with open(self.ds + '_node_labels.txt', 'r') as f:
            a = f.readlines()
            self.nodeIndex2label = {}
            for index, item in enumerate(a):
                self.nodeIndex2label[index] = int(item)
            self.n = len(a)
        #############################################################

        print("m:{},N:{},n:{}".format(self.m, self.N, self.n))

    def ex_which_graph(self):
        d_n2g = {}
        with open(self.ds + '_graph_indicator.txt') as f:
            index = 0
            line = f.readline()
            while line:
                d_n2g[index] = int(line)
                index += 1
                line = f.readline()
        d_gns = {k: [] for k in range(self.N)}
        for it in d_n2g:
            d_gns[d_n2g[it] - 1].append(it)
        return d_gns

    def ex_edges(self):
        d_es = {k: [] for k in range(self.n)}
        with open(self.ds + '_A.txt') as f:
            line = f.readline()
            while line:
                line = line.replace(' ', '')
                s = re.search('[0-9]{1,}', line).group()
                d = re.search('[,][0-9]{1,}', line).group()
                d = d.strip(',')
                d_es[int(s) - 1].append(int(d) - 1)
                line = f.readline()
        adj = np.zeros((self.n, self.n))
        for it in d_es:
            for its in d_es[it]:
                adj[it][its] = 1
        return d_es, adj

    def ex_labels(self, d_gns):
        d_gl = {}
        glabs = np.zeros((self.N, self.class_size))
        with open(self.ds + '_graph_labels.txt') as f:
            #######################################################################
            old_cate_name2new_cate_new = {}
            new_cate_name = 0
            #######################################################################
            line = f.readline()
            index = 0
            while line:
                d_gl[index] = int(line)
                if d_gl[index] not in old_cate_name2new_cate_new.keys():
                    old_cate_name2new_cate_new[d_gl[index]] = new_cate_name
                    new_cate_name += 1
                glabs[index] = to_categorical(old_cate_name2new_cate_new[d_gl[index]], self.class_size)
                index += 1
                line = f.readline()
        return glabs

    def gen_adjmatrix(self, d_gns, adj_com):
        adjs = []
        index = 0
        for it in d_gns:
            gsize = len(d_gns[it])
            adj = adj_com[index:index + gsize, index:index + gsize]
            adjs.append(adj)
            index += gsize
        return adjs

    def gen_adj_onebyone(self, d_gns, adj_com):
        adjs = []
        index = 0
        for it in d_gns:
            gsize = len(d_gns[it])
            adj = []
            for nodeA in range(index, index + gsize):
                for nodeB in range(index + 1, index + gsize):
                    if adj_com[nodeA, nodeB] == 1:
                        adj.append(tuple(sorted([nodeA, nodeB])))
            index += gsize
            adjs.append(list(set(adj)))
        return adjs

    def nodeIndex2nodelabel(self, d_gns):
        nodeLabel = []

        for graph in d_gns.values():
            tempList = {x: self.nodeIndex2label[x] for x in graph}
            nodeLabel.append(tempList)
        return nodeLabel

    def main(self, save=True):
        d_gns = {}
        d_es = {}
        d_gl = {}
        glabs = []
        adj_com = []

        d_gns = self.ex_which_graph()
        d_es, adj_com = self.ex_edges()
        glabs = self.ex_labels(d_gns)
        nodeLabel = self.nodeIndex2nodelabel(d_gns)
        adjs = []
        adjs = self.gen_adjmatrix(d_gns, adj_com)
        adjs_onebyone = self.gen_adj_onebyone(d_gns, adj_com)

        self.d_gns = d_gns
        if save:
            f = open('edges.txt', 'w')
            f.write(str(d_es))
            f.close()

            f = open('nodes_in_graphs.txt', 'w')
            f.write(str(d_gns))
            f.close()

            np.save('graphs_label.npy', glabs)
            np.save('d_gns.npy', list(d_gns.values()))

            np.save('{}adjs.npy'.format(self.N), adjs)
            if not os.path.exists('graph'):
                os.mkdir('graph')
            np.save('graph/graphs_label.npy', glabs)
            np.save('graph/graph_node_labels.npy', nodeLabel)
            np.save('graph/adjs_onebyone.npy', adjs_onebyone)

        return adjs, d_es

class secondParse(graphParser):
    def __init__(self, adjs=None, dict_edges=None):
        super(secondParse, self).__init__()
        if not adjs:
            self.adjs = np.load('{}adjs.npy'.format(N), allow_pickle=True)
        else:
            self.adjs = adjs

        if not dict_edges:
            with open('edges.txt') as f:
                self.dict_edges = eval(f.read())
        else:
            self.dict_edges = dict_edges

    def bfs(self, adj, s):
        n = self.n
        m = self.m
        N = self.N
        SUB_size = self.SUB_size
        min = self.min
        sub = []

        matrix = adj

        visited = [0 for _ in range(len(adj[0]))]

        queue = [s]

        visited[s] = 1

        node = queue.pop(0)

        sub.append(node)
        while True:
            for x in range(0, len(visited)):

                if matrix[node][x] == 1 and visited[x] == 0:
                    visited[x] = 1

                    queue.append(x)

            if len(queue) == 0:
                break
            else:

                newnode = queue.pop(0)
                node = newnode

                sub.append(node)
                if len(sub) == SUB_size:
                    sub.sort()
                    return sub
        sub.sort()
        return sub

    def gen_subs(self, adj):
        n = self.n
        m = self.m
        N = self.N
        SUB_size = self.SUB_size
        min = self.min
        subs = []

        m = {}
        for s in range(len(adj[0])):
            if tuple(self.bfs(adj, s)) not in m.keys():
                subs.append(self.bfs(adj, s))
                m[tuple(self.bfs(adj, s))] = 1

        subs = subs[:min]
        return subs

    def gen_upperlevel_graph(self, sub):
        n = self.n
        m = self.m
        N = self.N
        SUB_size = self.SUB_size
        min = self.min
        adj = np.zeros((len(sub), len(sub)))
        for i in range(len(sub)):
            for j in range(i + 1, len(sub)):
                same = [it for it in sub[i] if it in sub[j]]

                adj[i][j] = len(same)

            adj[i] = min_max_scaler.fit_transform(
                adj[i].reshape(-1, 1)).reshape(-1)

        for i in range(len(sub)):
            adj[i][i] = 1
        return adj

    def gen_subgraphs_adj_onebyone(self, all_adj, subs, nodeList):
        n = self.n
        m = self.m
        N = self.N
        SUB_size = self.SUB_size
        min = self.min
        minNode = np.min(nodeList)
        adjs = []

        for key, sub in enumerate(subs):

            index = 0
            gsize = len(sub)
            adj = []
            for nodeA in sub:
                for nodeB in sub:
                    if all_adj[nodeA][nodeB] == 1 or all_adj[nodeB][nodeA] == 1:
                        adj.append(tuple(sorted([minNode + nodeA, minNode + nodeB])))
            adjs.append(list(set(adj)))
        return adjs

    def gen_subgraphs_adj(self, all_adj, subs):
        n = self.n
        m = self.m
        N = self.N
        SUB_size = self.SUB_size
        min = self.min

        adjs = []
        for sub in subs:
            adj = np.zeros((SUB_size, SUB_size))
            i = 0
            for i in range(len(sub)):
                for j in range(len(sub)):

                    if all_adj[sub[i]][sub[j]] == 1:
                        adj[i][j] = 1

                adj[i] = min_max_scaler.fit_transform(
                    adj[i].reshape(-1, 1)).reshape(-1)

            for i in range(len(sub)):
                adj[i][i] = 1
            adjs.append(adj.copy())
        return adjs

    def main(self, save=True):
        #######################################################################
        n = self.n
        m = self.m
        N = self.N
        SUB_size = self.SUB_size
        min = self.min
        adjs = self.adjs
        self.d_gns = np.load('d_gns.npy', allow_pickle=True)
        #######################################################################
        ADJs = []
        subs_of_graphs = []
        subadjs_of_graphs = []
        subadjs_of_graphs_onebyone = []
        min_max_scaler = preprocessing.MinMaxScaler()
        #######################################################################
        for i in range(len(adjs)):
            if i % 100 == 0:
                print(i)
            adj = adjs[i]
            sub_of_a_graph = self.gen_subs(adj)

            subs_of_graphs.append(sub_of_a_graph)
            subadjs_of_a_graph = self.gen_subgraphs_adj(
                adj, sub_of_a_graph)
            subadjs_of_a_graph_onebyone = self.gen_subgraphs_adj_onebyone(adj, sub_of_a_graph, self.d_gns[i])
            subadjs_of_graphs_onebyone.append(subadjs_of_a_graph_onebyone)
            subadjs_of_graphs.append(subadjs_of_a_graph)

            ADJ_of_a_graph = self.gen_upperlevel_graph(
                sub_of_a_graph)

            ADJs.append(ADJ_of_a_graph)

        if save:
            np.save('subs_of_graphs.npy', subs_of_graphs)
            np.save('subadjs_of_graphs.npy', subadjs_of_graphs)
            np.save('ADJs.npy', ADJs)
            if not os.path.exists('./graph'):
                os.mkdir('./graph')
            np.save('./graph/subadjs_of_graphs_onebyone.npy', subadjs_of_graphs_onebyone)

        sadjs = np.zeros((N, min, SUB_size, SUB_size))
        sus = np.zeros((N, min, SUB_size))
        As = np.zeros((N, min, min))

        if save:
            subs_of_graphs = np.load('subs_of_graphs.npy', allow_pickle=True)
            subadjs_of_graphs = np.load(
                'subadjs_of_graphs.npy', allow_pickle=True)
            ADJs = np.load('ADJs.npy', allow_pickle=True)

        for i in range(N):

            for j in range(len(ADJs[i])):
                As[i][j][:len(ADJs[i][j])][:len(
                    ADJs[i][j])] = ADJs[i][j].copy()
            for j in range(len(subadjs_of_graphs[i])):
                sadjs[i][j][:len(subadjs_of_graphs[i][j])][:len(
                    subadjs_of_graphs[i][j])] = subadjs_of_graphs[i][j].copy()
            for j in range(len(subs_of_graphs[i])):
                sus[i][j][:len(subs_of_graphs[i][j])
                ] = subs_of_graphs[i][j].copy()

        for i in range(N):
            for j in range(min):
                As[i][j][j] = 1
                for k in range(SUB_size):
                    sadjs[i][j][k][k] = 1

        if save:
            np.save('adj.npy', As)
            np.save('sub_adj.npy', sadjs)
            np.save('subindexs_of_graphs.npy', sus)
        return sus

    def third(self, sus=None):

        if sus == None:
            sus = np.load('subindexs_of_graphs.npy', allow_pickle=True)

        with open('nodes_in_graphs.txt') as f:
            dict_gn = eval(f.read())

        labs = self.ex_labels()
        features = np.zeros((self.N, self.min, self.SUB_size, self.cate))

        for i in range(features.shape[0]):
            for j in range(features.shape[1]):
                for k in range(features.shape[2]):
                    len_x = int(sus[i][j][k])

                    features[i][j][k] = labs[i][len_x]

        np.save('features.npy', features)

    def ex_labels(self):
        labs = {k: [] for k in range(self.N)}
        nodes = np.zeros(self.n)

        with open('nodes_in_graphs.txt') as f:
            dict_gn = eval(f.read())

        with open(self.ds + '_node_labels.txt') as f:
            line = f.readline()
            index = 0
            while line:
                nodes[index] = int(line)
                index += 1
                line = f.readline()

        i = 0
        for graph in dict_gn:
            labs[i] = to_categorical(
                [nodes[it] - 1 for it in dict_gn[graph]], self.cate)
            i += 1
        return labs

if __name__ == "__main__":
    paser = graphParser()

    adjs, d_es = paser.main(save=True)
    a = secondParse(adjs, d_es)

    sus = a.main()

    a.third()
    ######################

    # parser = graphParser()

    # d_gns = parser.ex_which_graph()
    # print(d_gns)


import greet_pb2_grpc
import greet_pb2
import time
import grpc

def run():
    # 1. create channel
    with grpc.insecure_channel('localhost:50051') as channel:
        # create client
        stub = greet_pb2_grpc.GreetingStub(channel)
        print("1. SayHello - Unary")
        rpc_call = input("Which rpc would you like to make: ")

        if rpc_call == "1":
            hello_request = greet_pb2.HelloRequest(name = "YouTube")
            hello_reply = stub.SayHello(hello_request)
            print("SayHello Response Received:")
            print(hello_reply)

if __name__ == "__main__":
    run()


## Simple Surveillance System ##
## Author: Kevin Lu ##
## Start Date: 08/12/2022 ##

from camera import CameraCapture
import cv2
import time
import numpy

def start():
    """
    Starts cameras and connections for surveillance
    """
    camera_count = 2
    cameras = []
    cameras_forcequit = False
    for i in range(camera_count):
        cameras.append(CameraCapture(i))
        print("Initialized " + str(cameras[i]))

    while True:
        cameras_forcequit = True
        for i in range(camera_count):
            # Add time text to camera feed
            font = cv2.FONT_HERSHEY_SIMPLEX
            image = cameras[i].get_frame()
            timeStr = time.asctime().replace("  ", " ").replace(" ", "/").replace(":", ":")
            cv2.putText(image,
                       timeStr,
                       (4, 30),
                       font,
                       1,
                       (255, 255, 255),
                       1,
                       cv2.LINE_AA)

            # Show video feed to output window
            cv2.imshow(f"Camera {cameras[i].index}", image)
            cameras_forcequit = cameras_forcequit and cameras[i].forcequit
        if cv2.waitKey(1) & 0xFF == ord('q') or cameras_forcequit:
            for i in range(camera_count):
                cameras[i].forcequit = True
            break

    cv2.destroyAllWindows()

if __name__ == "__main__":
    start()


import keras
#keras.__version__

from keras.models import load_model
from keras import backend as K

#from resizeimage import resizeimage

from keras.preprocessing import image
import numpy as np
import requests
from io import BytesIO
from PIL import Image

from keras import models

import matplotlib.pyplot as plt

from keras.applications.vgg16 import VGG16
from keras.preprocessing import image
from keras.applications.vgg16 import preprocess_input, decode_predictions
import numpy as np
import cv2

import copy

video_stream = cv2.VideoCapture(0)
video_stream.set(3,224)
video_stream.set(4,224)

model = VGG16(weights='imagenet')

LAYER_TO_VISUALIZE = 2

while(True):

    key = cv2.waitKey(1) & 0xFF
    if key == ord('q'):
        exit()
    if key == ord('w'):
        LAYER_TO_VISUALIZE -= 1
        cv2.destroyAllWindows()
    if key == ord('e'):
        LAYER_TO_VISUALIZE += 1
        cv2.destroyAllWindows()
    if LAYER_TO_VISUALIZE == 15:
        LAYER_TO_VISUALIZE = 2
    if LAYER_TO_VISUALIZE == 1:
        LAYER_TO_VISUALIZE = 14

    # Extracts the outputs of the top 8 layers:
    layer_outputs = [layer.output for layer in model.layers[LAYER_TO_VISUALIZE-1:LAYER_TO_VISUALIZE+1]]
    # Creates a model that will return these outputs, given the model input:
    activation_model = models.Model(inputs=model.input, outputs=layer_outputs)

    DOWNSAMPLE_LEVEL = [0, 4, 4, 4, 2,  2,  2,  1,  1,  1,  1,  1,  1,  1,  1,  1][LAYER_TO_VISUALIZE]
    IMAGES_PER_ROW =   [0, 8, 8, 8, 8, 16, 16, 16, 16, 16, 16, 16, 32, 32, 32, 32][LAYER_TO_VISUALIZE]

    ret, img_original = video_stream.read()
    img_original = cv2.resize(img_original, dsize=(224, 224), interpolation=cv2.INTER_CUBIC)

    img = img_original.astype(np.float64)

    # We add a dimension to transform our array into a "batch"
    # of size (1, 224, 224, 3)
    x = np.expand_dims(img, axis=0)

    # Finally we preprocess the batch
    # (this does channel-wise color normalization)
    x = preprocess_input(x)

    activations = activation_model.predict(x)
    #first_layer_activation = activations[0]

    # These are the names of the layers, so can have them as part of our plot
    layer_names = []
    for layer in model.layers[LAYER_TO_VISUALIZE-1:LAYER_TO_VISUALIZE+1]:
        layer_names.append(layer.name)

    images_per_row = IMAGES_PER_ROW

    # Now let's display our feature maps
    for layer_name, layer_activation in zip(layer_names, activations):
        #print(layer_name, layer_activation.shape)
        n_features = layer_activation.shape[-1]

        # The feature map has shape (1, size, size, n_features)
        size = layer_activation.shape[1]

        # We will tile the activation channels in this matrix
        n_cols = n_features // images_per_row
        display_grid = np.zeros((size//DOWNSAMPLE_LEVEL * n_cols, images_per_row * size//DOWNSAMPLE_LEVEL, 3))

        # We'll tile each filter into this big horizontal grid
        for col in range(n_cols):
            for row in range(images_per_row):
                channel_image = layer_activation[0,
                                                 :, :,
                                                 col * images_per_row + row]
                # Post-process the feature to make it visually palatable
                channel_image -= channel_image.mean()
                channel_image /= channel_image.std()
                channel_image *= 64
                channel_image += 128
                channel_image = np.clip(channel_image, 0, 255).astype('uint8')
                channel_image = channel_image[::DOWNSAMPLE_LEVEL, ::DOWNSAMPLE_LEVEL]
                channel_image = cv2.applyColorMap(channel_image, cv2.COLORMAP_OCEAN)
                display_grid[col * size//DOWNSAMPLE_LEVEL : (col + 1) * size//DOWNSAMPLE_LEVEL,
                             row * size//DOWNSAMPLE_LEVEL : (row + 1) * size//DOWNSAMPLE_LEVEL] = channel_image

        # Display the grid
        cv2.imshow(layer_name, display_grid.astype(np.uint8))
        break

exit()


from django import forms

from .models import Post, Content, Theme

class PostForm(forms.ModelForm):
    pictures = forms.FileField(required=False, widget=forms.ClearableFileInput(attrs={'multiple': True}))
    is_published = forms.BooleanField(initial=True, widget=forms.CheckboxInput(attrs={'class': 'js-switch'}))
    text = forms.CharField(label='Short Message', required=False,
            widget=forms.Textarea(attrs={
                    'class': 'post-new-content',
                    'rows': 3,
            })
    )
    class Meta:
        model = Post
        fields = ['theme', 'pictures', 'text',  'is_published']

    def __init__(self, user, *args, **kwargs):
        super(PostForm, self).__init__(*args, **kwargs)
        if self.instance:
            self.fields['theme'].queryset = Theme.objects.filter(author=user)


import re
import ast

from eth_utils import (
    force_text,
)

def process_type(typ):
    if typ == 'function':
        return process_strict_type('bytes24')
    elif typ in ('int', 'uint'):
        return process_strict_type(typ + '256')
    else:
        return process_strict_type(typ)

def process_strict_type(typ):
    # Crazy reg expression to separate out base type component (eg. uint),
    # size (eg. 256, 128x128, none), array component (eg. [], [45], none)
    regexp = '([a-z]*)([0-9]*x?[0-9]*)((\[[0-9]*\])*)'
    base, sub, arr, _ = re.match(regexp, force_text(typ)).groups()
    arrlist = re.findall('\[[0-9]*\]', arr)
    if len(''.join(arrlist)) != len(arr):
        raise ValueError("Unknown characters found in array declaration")
    # Check validity of string type
    if base == 'string' or base == 'bytes':
        if not re.match('^[0-9]*$', sub):
            raise ValueError("String type must have no suffix or numerical suffix")
        if sub and int(sub) > 32:
            raise ValueError("Maximum 32 bytes for fixed-length str or bytes")
    # Check validity of integer type
    elif base == 'uint' or base == 'int':
        if not re.match('^[0-9]+$', sub):
            raise ValueError("Integer type must have numerical suffix")
        if 8 > int(sub) or int(sub) > 256:
            raise ValueError("Integer size out of bounds")
        if int(sub) % 8 != 0:
            raise ValueError("Integer size must be multiple of 8")
    # Check validity of real type
    elif base == 'ureal' or base == 'real':
        if not re.match('^[0-9]+x[0-9]+$', sub):
            raise ValueError("Real type must have suffix of form <high>x<low>, eg. 128x128")
        high, low = [int(x) for x in sub.split('x')]
        if 8 > (high + low) or (high + low) > 256:
            raise ValueError("Real size out of bounds (max 32 bytes)")
        if high % 8 != 0 or low % 8 != 0:
            raise ValueError("Real high/low sizes must be multiples of 8")
    # Check validity of hash type
    elif base == 'hash':
        if not re.match('^[0-9]+$', sub):
            raise ValueError("Hash type must have numerical suffix")
    # Check validity of address type
    elif base == 'address':
        if sub != '':
            raise ValueError("Address cannot have suffix")
    return base, sub, [ast.literal_eval(x) for x in arrlist]


from serapis import config
from serapis.storage.irods.api import IrodsBasicAPI, CollectionAPI, DataObjectAPI
from serapis.storage.irods.exceptions import ACLRetrievalException, ACLRemovingException
from serapis.storage.irods.entities import ACL
from serapis.storage.irods.baton_mappings import ACLMapping, MetadataMapping

from baton.api import connect_to_irods_with_baton
from baton.models import SearchCriterion, User, AccessControl
from baton.collections import IrodsMetadata

import typing

class BatonBasicAPI(IrodsBasicAPI):
    BATON_BIN_PATH = config.BATON_BIN

    @classmethod
    def _get_connection(cls):
        raise NotImplementedError

    @classmethod
    def _get_acls(cls, path: str):
        """
        This private method is fetching all the ACLs from iRODS.
        :param path: str - the path to the subject (data object/collection)
        :param subject_type: str - can be either data_object or collection
        :return:
        """
        try:
            connection = cls._get_connection()
            baton_acls = connection.access_control.get_all(path)
        except Exception as e:
            raise ACLRetrievalException from e
        return {ACLMapping.from_baton(acl) for acl in baton_acls}

    @classmethod
    def get_acls(cls, path: str):
        return cls._get_acls(path)

    @classmethod
    def add_or_replace_acl(cls, path: str, acl):
        """
        This method sets acls
        :param path:
        :param acl: an ACL object of type serapis.storage.irods.entities.ACL
        :return:
        """
        try:
            connection = cls._get_connection()
            baton_acl = ACLMapping.to_baton(acl)
            connection.access_control.add_or_replace(path, baton_acl)
        except FileNotFoundError as e:
            raise e
        except Exception as e:
            raise ACLRetrievalException() from e
        return True

    @classmethod
    def add_or_replace_a_list_of_acls(cls, path: str, acls: typing.List):
        """
        This method sets acls
        :param path:
        :param acl: an ACL object of type serapis.storage.irods.entities.ACL
        :return:
        """
        try:
            connection = cls._get_connection()
            baton_acls = []
            for acl in acls:
                b_acl = ACLMapping.to_baton(acl)
                baton_acls.append(b_acl)
            connection.access_control.add_or_replace(path, baton_acls)
        except Exception as e:
            raise e
            #raise ACLRetrievalException(e.args) from e
        return True

    @classmethod
    def remove_acl_for_user(cls, path, username, zone):
        try:
            connection = cls._get_connection()
            baton_user = ACLMapping.build_baton_user(username, zone)
            connection.access_control.revoke(path, baton_user)
        except Exception as e:
            raise ACLRemovingException() from e
        return True

    @classmethod
    def remove_acls_for_a_list_of_users(cls, path, user_zone_tuples:typing.List):
        try:
            connection = cls._get_connection()
            baton_users = []
            for username, zone in user_zone_tuples:
                user = ACLMapping.build_baton_user(username, zone)
                baton_users.append(user)
            connection.access_control.revoke(path, baton_users)
        except Exception as e:
            print(e)
            raise ACLRemovingException(e)
        return True

    @classmethod
    def remove_all_acls(cls, path):
        try:
            connection = cls._get_connection()
            connection.access_control.revoke_all(path)
        except Exception as e:
            raise ACLRemovingException(e.args) from e

    @classmethod
    def get_all_metadata(cls, path):
        try:
            connection = cls._get_connection()
            metadata = connection.metadata.get_all(path)
        except Exception as e:
            raise e # TODO: some exception, to see which one
        return MetadataMapping.from_baton(metadata)

    @classmethod
    def remove_all_metadata(cls, path):
        try:
            connection = cls._get_connection()
            connection.metadata.remove_all()
        except Exception as e:
            raise e # TODO: check what exc to raise
        return True

    @classmethod
    def add_metadata(cls, path, avu_dict):
        baton_avus = MetadataMapping.to_baton(avu_dict)
        try:
            connection = cls._get_connection()
            connection.metadata.add(path, baton_avus)
        except KeyError as e:
            #  Already existing key -- I don't really care, all I want is for the key - val to be there.
            # In this case returns: KeyError: "Failed to add metadata <key> -> <val> on <path>: error -809000 CATALOG_ALREADY_HAS_ITEM_BY_THAT_NAME"
            pass
        return True

    @classmethod
    def remove_metadata(cls, path, avu_dict):
        baton_avus = MetadataMapping.to_baton(avu_dict)
        try:
            connection = cls._get_connection()
            connection.metadata.remove(path, baton_avus)
        except KeyError:
            # Thrown when the key doesn't exist, i am silencing it for now
            pass
        return True

    @classmethod
    def update_metadata(cls, path, key, new_values):
        baton_metadata = MetadataMapping.to_baton({key: new_values})
        try:
            connection = cls._get_connection()
            connection.metadata.set(path, baton_metadata)
        except Exception as e:
            raise e
        return True

class BatonCollectionAPI(BatonBasicAPI):

    @classmethod
    def _get_connection(cls):
        connection = connect_to_irods_with_baton(cls.BATON_BIN_PATH)
        return connection.collection

    @classmethod
    def list_contents(cls, path):
        pass

    @classmethod
    def list_data_objects(cls, path):
        """
        This method lists the contents of a collection. It creates a different type of connection instead of using the class-defined one,
        because Colin refused to change the implementation so that connection.collection return stuff belonging
        to that collection, but instead one has to create a connection.data_object and then call get_all_in_collection
        in order to get the contents of a collection.
        :return: list of file paths (strings)
        """
        try:
            connection = connect_to_irods_with_baton(cls.BATON_BIN_PATH)
            data_objects = connection.data_object.get_all_in_collection(path)
        except Exception as e:
            raise e
        file_paths = [do.path for do in data_objects]
        return file_paths

    @classmethod
    def list_collections(cls, path):
        """
        This method lists the contents of a collection. It creates a different type of connection instead of using the class-defined one,
        because Colin refused to change the implementation so that connection.collection return stuff belonging
        to that collection, but instead one has to create a connection.data_object and then call get_all_in_collection
        in order to get the contents of a collection.
        :return: list of file paths (strings)
        """
        try:
            connection = connect_to_irods_with_baton(cls.BATON_BIN_PATH)
            collections = connection.collection.get_all_in_collection(path)
        except Exception as e:
            raise e
        file_paths = [coll.path for coll in collections]
        return file_paths

class BatonDataObjectAPI(BatonBasicAPI):

    @classmethod
    def _get_connection(cls):
        connection = connect_to_irods_with_baton(cls.BATON_BIN_PATH)
        return connection.data_object

    @classmethod
    def checksum(cls, path, checksum_type='md5'):
        raise NotImplementedError("BATON does not support recalculating checksums operation")

    @classmethod
    def get_checksum(cls, path):
        pass

# IRODS_ERROR_USER_FILE_DOES_NOT_EXIST, IRODS_ERROR_CAT_INVALID_ARGUMENT+permission issue  -> FileNotFoundError(error_message)
# IRODS_ERROR_CATALOG_ALREADY_HAS_ITEM_BY_THAT_NAME, IRODS_ERROR_CAT_SUCCESS_BUT_WITH_NO_INFO -> KeyError(error_message)
# These are not exclusive to operations involving ACLs.
# Anything that can cause the iRODS library that baton uses to raise a `USER_FILE_DOES_NOT_EXIST` exception (for example), will get wrapped into a `FileNotFound` python error.


from numpy.linalg import inv, pinv
from scipy import optimize
from mpmath import acot
from estimation.utils import _check_data_dim, _check_data_atleast_2D, _norm_along_axis, _musigma_norm, _maxmin_norm
from dataset_generation.utils import _is_in_bbox

import math
#import torch
#import kornia
import numpy as np

class BaseModel(object):

    def __init__(self, params = None):
        self.params = params

class LineModelND(BaseModel):
    """Total least squares estimator for N-dimensional lines.
    In contrast to ordinary least squares line estimation, this estimator
    minimizes the orthogonal distances of points to the estimated line.
    Lines are defined by a point (origin) and a unit vector (direction)
    according to the following vector equation::
        X = origin + lambda * direction
    Attributes
    ----------
    params : tuple
        Line model parameters in the following order `origin`, `direction`.
    """

    def estimate(self, data):
        """Estimate line model from data.
        This minimizes the sum of shortest (orthogonal) distances
        from the given data points to the estimated line.
        Parameters
        ----------
        data : (N, dim) array
            N points in a space of dimensionality dim >= 2.
        Returns
        -------
        success : bool
            True, if model estimation succeeds.
        """
        _check_data_atleast_2D(data)

        origin = data.mean(axis=0)
        data = data - origin

        if data.shape[0] == 2:  # well determined
            direction = data[1] - data[0]
            norm = np.linalg.norm(direction)
            if norm != 0:  # this should not happen to be norm 0
                direction /= norm
        elif data.shape[0] > 2:  # over-determined
            # Note: with full_matrices=1 Python dies with joblib parallel_for.
            _,_, v = np.linalg.svd(data, full_matrices=False)
            direction = v[0]
        else:  # under-determined
            raise ValueError('At least 2 input points needed.')

        self.params = (origin, direction)

        return True

    def residuals(self, data, params=None):
        """Determine residuals of data to model.
        For each point, the shortest (orthogonal) distance to the line is
        returned. It is obtained by projecting the data onto the line.
        Parameters
        ----------
        data : (N, dim) array
            N points in a space of dimension dim.
        params : (2, ) array, optional
            Optional custom parameter set in the form (`origin`, `direction`).
        Returns
        -------
        residuals : (N, ) array
            Residual for each data point.
        """
        _check_data_atleast_2D(data)
        if params is None:
            if self.params is None:
                raise ValueError('Parameters cannot be None')
            params = self.params
        if len(params) != 2:
            raise ValueError('Parameters are defined by 2 sets.')

        origin, direction = params
        norm = np.linalg.norm(direction)
        direction /= norm

        res = (data - origin) - \
              ((data - origin) @ direction)[..., np.newaxis] * direction
        return _norm_along_axis(res, axis=1)

    def predict(self, x, axis=0, params=None):
        """Predict intersection of the estimated line model with a hyperplane
        orthogonal to a given axis.
        Parameters
        ----------ip
        x : (n, 1) array
            Coordinates along an axis.
        axis : int
            Axis orthogonal to the hyperplane intersecting the line.
        params : (2, ) array, optional
            Optional custom parameter set in the form (`origin`, `direction`).
        Returns
        -------
        data : (n, m) array
            Predicted coordinates.
        Raises
        ------
        ValueError
            If the line is parallel to the given axis.
        """
        if params is None:
            if self.params is None:
                raise ValueError('Parameters cannot be None')
            params = self.params
        if len(params) != 2:
            raise ValueError('Parameters are defined by 2 sets.')

        origin, direction = params
        norm = np.linalg.norm(direction)
        direction /= norm

        if direction[axis] == 0:
            # line parallel to axis
            raise ValueError('Line parallel to axis %s' % axis)

        l = (x - origin[axis]) / direction[axis]
        data = origin + l[..., np.newaxis] * direction
        return data

    def predict_x(self, y, params=None):
        """Predict x-coordinates for 2D lines using the estimated model.
        Alias for::
            predict(y, axis=1)[:, 0]
        Parameters
        ----------
        y : array
            y-coordinates.
        params : (2, ) array, optional
            Optional custom parameter set in the form (`origin`, `direction`).
        Returns
        -------
        x : array
            Predicted x-coordinates.
        """
        x = self.predict(y, axis=1, params=params)[:, 0]
        return x

    def predict_y(self, x, params=None):
        """Predict y-coordinates for 2D lines using the estimated model.
        Alias for::
            predict(x, axis=0)[:, 1]
        Parameters
        ----------
        x : array
            x-coordinates.
        params : (2, ) array, optional
            Optional custom parameter set in the form (`origin`, `direction`).
        Returns
        -------
        y : array
            Predicted y-coordinates.
        """
        y = self.predict(x, axis=0, params=params)[:, 1]
        return y

class PlaneModelND(BaseModel):
    """Total least squares estimator for N-dimensional lines.
    In contrast to ordinary least squares line estimation, this estimator
    minimizes the orthogonal distances of points to the estimated line.
    Planes are defined by a point (origin) and a unit vector (normal vector)
    according to the following vector equation::
        w'x + w_o = 0
    where w' is np.transpose(normal_vector) and w_o = w' * x_origin
    and x_origin is a point contained in the plane
    Attributes
    ----------
    params : tuple
        Line model parameters in the following order (`origin`, `direction`).
    """

    def estimate(self, data, w = None):
        """Estimate line model from data.
        This minimizes the sum of shortest (orthogonal) distances
        from the given data points to the estimated line.
        Parameters
        ----------
        data : (N, dim) array
            N points in a space of dimensionality dim >= 2.
        Returns
        -------
        success : bool
            True, if model estimation succeeds.
        """
        _check_data_atleast_2D(data)
        """
        #TLS
        if data.shape[0] == 2:  # well determined
            direction = data[1] - data[0]
            normal_vector = direction
            normal_vector[1] = -normal_vector[1] # perpendicular counter clockwise

            # as unitary vectors
            norm_direction = np.linalg.norm(direction)
            norm_normal_vector = np.linalg.norm(normal_vector)
            if (norm_direction != 0 and norm_normal_vector != 0):  # this should not happen to be norm 0
                normal_vector /= norm_normal_vector
                direction /= norm_direction

        elif data.shape[0] > 2:  # over-determined
            # Note: with full_matrices=1 Python dies with joblib parallel_for.
            _,_, v = np.linalg.svd(data, full_matrices=False)
            normal_vector = v[(np.shape(v)[1]-1)]
        else:  # under-determined
            raise ValueError('At least 2 input points needed.')

        self.params = (origin, normal_vector)
        """

        #OWLS (if w = np.ones((n_points,)) results will be the same as TLS)
        if w is None:
            w = np.ones(np.shape(data[:,0]), dtype = int)
        elif w.shape[0] != data.shape[0]:
            raise ValueError(f'Weigths array shape does not match data shape. w.shape[0] = {w.shape[0]} | data.shape[0] = {data.shape[0]}')
        # normalize weights
        w = w / np.sum(w)

        """
        # particular solution 2D hiperplane: 2D line
        x = data[:,0]
        y = data[:,1]
        # Weighted orthogonal least squares fit of line a*x+b*y+c=0 to a set of 2D points with coordiantes given by x and y and weights w
        n = np.sum(w)
        meanx = np.sum(w*x)/n
        meany = np.sum(w*y)/n
        x = x - meanx
        y = y - meany

        y2x2 = np.sum(w*(y ** 2 - x ** 2))
        xy = np.sum(w*x*y)
        alpha = 0.5 * acot(0.5 * y2x2 / xy) + np.pi/2*(y2x2 > 0)
        alpha = float(alpha)
        #if y2x2 > 0, alpha = alpha + pi/2; end

        a = np.sin(alpha)
        b = np.cos(alpha)
        origin = np.array([meanx,meany])
        self.params = (origin, np.array([a, b]))
        """

        # weighted mean (np.sum(w) = 1)
        origin = (data * w[:, np.newaxis]).sum(axis=0)
        data = data - origin

        # normalize data
        #data, H = musigma_norm(data)
        # general solution
        w = np.diag(w)
        Sw = np.dot(np.dot(np.transpose(data), w), data)
        w, v = np.linalg.eig(Sw)
        normal_vector = v[:,np.argmin(w)]

        self.params = (origin, normal_vector/np.linalg.norm(normal_vector))
        # denormalizes origin and normal_vector

        #self.denorm_params(H)

        return True

    def residuals(self, data, params=None):
        """Determine residuals of data to model.
        For each point, the shortest (orthogonal) distance to the plane is
        returned. It is obtained by projecting the data onto the plane.
        Parameters
        ----------
        data : (N, dim) array
            N points in a space of dimension dim.
        params : (2, ) array, optional
            Optional custom parameter set in the form (`origin`, `subspace`).
        Returns
        -------
        residuals : (N, ) array
            Residual for each data point.
        """

        _check_data_atleast_2D(data)
        if params is None:
            if self.params is None:
                raise ValueError('Parameters cannot be None')
            params = self.params
        if len(params) != 2:
            raise ValueError('Parameters are defined by 2 sets.')

        origin, normal_vector = params
        normal_vector /= np.linalg.norm(normal_vector)
        normal_vector = np.transpose(normal_vector)
        res = (np.dot(data, normal_vector) - np.dot(origin, normal_vector)) / np.linalg.norm(normal_vector)
        return res

    def predict(self, model_bbox, n_points, seed = None, params=None):
        """ Builds a hyperplane given the model_bbox of the subspace
            of the hyperplane.
        Parameters
        ----------
        model_bbox : (n, m) array
            Coordinate limits along an axis.
        n_points : int
            Number of model samples
        params : (2, ) array, optional
            Optional custom parameter set in the form (`origin`, `direction`).
        Returns
        -------
        data : (n, m) array
            Predicted coordinates.
        """
        if params is None:
            if self.params is None:
                raise ValueError('Parameters cannot be None')
            params = self.params
        if len(params) != 2:
            raise ValueError('Parameters are defined by 2 sets.')

        origin, normal_vector = params
        normal_vector /= np.linalg.norm(normal_vector)

        # Gram–Schmidt orthonormalizing process
        def gs_cofficient(v1, v2):
            return np.dot(v2, v1) / np.dot(v1, v1)

        def multiply(cofficient, v):
          return map((lambda x : x * cofficient), v)

        def proj(v1, v2):
          return multiply(gs_cofficient(v1, v2) , v1)

        def gs(X, row_vecs=True, norm = True):
            if not row_vecs:
                X = X.T
            Y = X[0:1,:].copy()
            for i in range(1, X.shape[0]):
                proj = np.diag((X[i,:].dot(Y.T)/np.linalg.norm(Y,axis=1)**2).flat).dot(Y)
                Y = np.vstack((Y, X[i,:] - proj.sum(0)))
            if norm:
                Y = np.diag(1/np.linalg.norm(Y,axis=1)).dot(Y)
            if row_vecs:
                return Y
            else:
                return Y.T

        dim = np.size(normal_vector)
        if seed is not None: seed = int(seed)
        np.random.seed(seed)
        # build hyperplane subspace (based on Gram-Schmidt)
        v = gs(np.array([normal_vector, *(np.random.rand(dim - 1, dim))]))
        subspace = v[1:dim,]

        data = np.zeros((n_points, dim), dtype = float)
        for V, rng in zip(subspace, model_bbox):
            for coord in data:
                coord += np.random.uniform(*rng) * V
        data += origin
        return data

    def set_params(self, coeffs):

        normal_vector = coeffs[:-1]
        normal_vector = np.asarray(normal_vector) / np.linalg.norm(normal_vector)

        dim = np.size(normal_vector)
        w_o = coeffs[dim]
        # some_coord[last_axis] != 0
        first_components = np.zeros(dim - 1)
        last_component = w_o/normal_vector[dim - 1]
        origin = np.append(first_components, np.array(last_component))

        self.params = [origin, normal_vector]
        return True

    def denorm_params(self, H):
        """ Denormalizes general params and
        return denormalized general params"""
        origin, normal_vector = self.params
        dim = np.size(origin)
        origin = np.dot(np.append(origin,1).T, H)

        normal_vector = np.dot(np.append(normal_vector,1).T, H)
        origin = origin/origin[dim]
        normal_vector = normal_vector/normal_vector[dim]
        self.params = (origin[:-1], normal_vector[:-1])
        return True

    def get_general_params(self, params = None):
        """ return hyperplane general parameters
        from [normal_vector, origin] set
        Parameters
        ----------
        (optional) params : (2, N) array
        set of normal_vector and origin
        with N dimensions
        Returns
        -------
        general_params : (N + 1, ) array
        """
        if params is not None:
            self.params = params

        self.params = np.asarray(self.params).reshape(2, -1) # ensure set of [normal_vector, origin]
        origin, normal_vector = self.params
        normal_vector /= np.linalg.norm(normal_vector)
        x_n = np.dot(np.transpose(normal_vector), origin)
        return np.append(normal_vector, x_n)

class CircleModel(BaseModel):

    """Total least squares estimator for 2D circles.
    The functional model of the circle is::
        r**2 = (x - xc)**2 + (y - yc)**2
    This estimator minimizes the squared distances from all points to the
    circle::
        min{ sum((r - sqrt((x_i - xc)**2 + (y_i - yc)**2))**2) }
    A minimum number of 3 points is required to solve for the parameters.
    Attributes
    ----------
    params : tuple
        Circle model parameters in the following order `xc`, `yc`, `r`.
    """

    def estimate(self, data):
        """Estimate circle model from data using total least squares.
        Parameters
        ----------
        data : (N, 2) array
            N points with ``(x, y)`` coordinates, respectively.
        Returns
        -------
        success : bool
            True, if model estimation succeeds.
        """

        _check_data_dim(data, dim=2)

        x = data[:, 0]
        y = data[:, 1]

        # http://www.had2know.com/academics/best-fit-circle-least-squares.html
        x2y2 = (x ** 2 + y ** 2)
        sum_x = np.sum(x)
        sum_y = np.sum(y)
        sum_xy = np.sum(x * y)
        m1 = np.stack([[np.sum(x ** 2), sum_xy, sum_x],
                       [sum_xy, np.sum(y ** 2), sum_y],
                       [sum_x, sum_y, float(len(x))]])
        m2 = np.stack([[np.sum(x * x2y2),
                        np.sum(y * x2y2),
                        np.sum(x2y2)]], axis=-1)
        a, b, c = pinv(m1) @ m2
        a, b, c = a[0], b[0], c[0]
        xc = a / 2
        yc = b / 2
        r = np.sqrt(4 * c + a ** 2 + b ** 2) / 2

        self.params = (xc, yc, r)

        return True

    def residuals(self, data, params=None):
        """Determine residuals of data to model.
        For each point the shortest distance to the circle is returned.
        Parameters
        ----------
        data : (N, 2) array
            N points with ``(x, y)`` coordinates, respectively.
        Returns
        -------
        residuals : (N, ) array
            Residual for each data point.
        """

        _check_data_dim(data, dim=2)
        if params is None:
            if self.params is None:
                raise ValueError('Parameters cannot be None')
            params = self.params

        xc, yc, r = params

        x = data[:, 0]
        y = data[:, 1]

        return r - np.sqrt((x - xc)**2 + (y - yc)**2)

    def predict_xy(self, t, params=None):
        """Predict x- and y-coordinates using the estimated model.
        Parameters
        ----------
        t : array
            Angles in circle in radians. Angles start to count from positive
            x-axis to positive y-axis in a right-handed system.
        params : (3, ) array, optional
            Optional custom parameter set.
        Returns
        -------
        xy : (..., 2) array
            Predicted x- and y-coordinates.
        """
        if params is None:
            params = self.params
        xc, yc, r = params

        x = xc + r * np.cos(t)
        y = yc + r * np.sin(t)

        return np.concatenate((x[..., None], y[..., None]), axis=t.ndim)

    def get_general_params(self, params = None):
        if params is not None:
            self.params = params
        xc, yc, r = self.params
        return np.array([-2*xc, -2*yc, ((xc ** 2) + (yc ** 2) - (r ** 2))])

class EllipseModel(BaseModel):

    """Total least squares estimator for 2D ellipses.
    The functional model of the ellipse is::
        xt = xc + a*cos(theta)*cos(t) - b*sin(theta)*sin(t)
        yt = yc + a*sin(theta)*cos(t) + b*cos(theta)*sin(t)
        d = sqrt((x - xt)**2 + (y - yt)**2)
    where ``(xt, yt)`` is the closest point on the ellipse to ``(x, y)``. Thus
    d is the shortest distance from the point to the ellipse.
    The estimator is based on a least squares minimization. The optimal
    solution is computed directly, no iterations are required. This leads
    to a simple, stable and robust fitting method.
    The ``params`` attribute contains the implicit parameters in the following order::
        xc, yc, a, b, theta
    Attributes
    ----------
    params : tuple
        Ellipse model parameters in the following order `xc`, `yc`, `a`, `b`,
        `theta`.
    params_general : tuple
        Ellipse model general parameters in the following order `a`, `b`, `c`, `d`,
        `f`, `g`.
    """

    def __init__(self, params = None, params_general = None):
        self.params = params
        self.params_general = params_general

    def estimate(self, data, w = None):
        """Estimate circle model from data using total least squares.
        Parameters
        ----------
        data : (N, 2) array
            N points with ``(x, y)`` coordinates, respectively.
        Returns
        -------
        success : bool
            True, if model estimation succeeds.
        References
        ----------
        .. [1] Halir, R.; Flusser, J. "Numerically stable direct least squares
               fitting of ellipses". In Proc. 6th International Conference in
               Central Europe on Computer Graphics and Visualization.
               WSCG (Vol. 98, pp. 125-132).
        """

        #https://es.mathworks.com/matlabcentral/fileexchange/3215-fit_ellipse?s_tid=prof_contriblnk
        # file:///tmp/mozilla_esau0/Weighted_Least_Squares_Fit_of_an_Ellipse_to_Descri.pdf

        # Original Implementation: Ben Hammel, Nick Sullivan-Molina
        # another REFERENCE: [2] http://mathworld.wolfram.com/Ellipse.html
        _check_data_dim(data, dim=2)

        data, H = _maxmin_norm(data)

        x = data[:, 0]
        y = data[:, 1]

        # Quadratic part of design matrix [eqn. 15] from [1]
        D1 = np.vstack([x ** 2, x * y, y ** 2]).T
        # Linear part of design matrix [eqn. 16] from [1]
        D2 = np.vstack([x, y, np.ones(len(x))]).T

        if w is None:
            w = np.diag(np.ones(D1.shape[0]))
        else:
            w = np.diag(w)

        # normalize weights
        w = w / np.sum(w)

        # forming scatter matrix [eqn. 17] from [1]
        S1 = D1.T @ w @ D1
        S2 = D1.T @ w @ D2
        S3 = D2.T @ w @ D2

        # Constraint matrix [eqn. 18]
        C1 = np.array([[0., 0., 2.], [0., -1., 0.], [2., 0., 0.]])

        try:
            # Reduced scatter matrix [eqn. 29]
            M = inv(C1) @ (S1 - S2 @ inv(S3) @ S2.T)
        except np.linalg.LinAlgError:  # LinAlgError: Singular matrix
            return False

        # M*|a b c >=l|a b c >. Find eigenvalues and eigenvectors
        # from this equation [eqn. 28]
        eig_vals, eig_vecs = np.linalg.eig(M)

        # eigenvector must meet constraint 4ac - b^2 to be valid.
        cond = 4 * np.multiply(eig_vecs[0, :], eig_vecs[2, :]) \
               - np.power(eig_vecs[1, :], 2)
        a1 = eig_vecs[:, (cond > 0)]
        # seeks for empty matrix
        if 0 in a1.shape or len(a1.ravel()) != 3:
            return False
        a, b, c = a1.ravel()

        # |d f g> = -S3^(-1)*S2^(T)*|a b c> [eqn. 24]
        a2 = -inv(S3) @ S2.T @ a1
        d, f, g = a2.ravel()

        # eigenvectors are the coefficients of an ellipse in general form
        # a*x^2 + 2*b*x*y + c*y^2 + 2*d*x + 2*f*y + g = 0 (eqn. 15) from [2]
        b /= 2.
        d /= 2.
        f /= 2.

        self.params_general = [a,b,c,d,f,g]
        # denormalizes general and implicit params
        self.denorm_params(H)

        return True

    def residuals(self, data, params=None):
        """Determine residuals of data to model.
        For each point the shortest distance to the ellipse is returned.
        Parameters
        ----------
        data : (N, 2) array
            N points with ``(x, y)`` coordinates, respectively.
        Returns
        -------
        residuals : (N, ) array
            Residual for each data point.
        """

        _check_data_dim(data, dim=2)

        if params is None:
            xc, yc, a, b, theta = self.params
        else:
            xc, yc, a, b, theta = params

        ctheta = math.cos(theta)
        stheta = math.sin(theta)

        x = data[:, 0]
        y = data[:, 1]

        N = data.shape[0]

        def fun(t, xi, yi):
            ct = math.cos(t)
            st = math.sin(t)
            xt = xc + a * ctheta * ct - b * stheta * st
            yt = yc + a * stheta * ct + b * ctheta * st
            return (xi - xt) ** 2 + (yi - yt) ** 2

        # def Dfun(t, xi, yi):
        #     ct = math.cos(t)
        #     st = math.sin(t)
        #     xt = xc + a * ctheta * ct - b * stheta * st
        #     yt = yc + a * stheta * ct + b * ctheta * st
        #     dfx_t = - 2 * (xi - xt) * (- a * ctheta * st
        #                                - b * stheta * ct)
        #     dfy_t = - 2 * (yi - yt) * (- a * stheta * st
        #                                + b * ctheta * ct)
        #     return [dfx_t + dfy_t]

        residuals = np.empty((N, ), dtype=np.double)

        # initial guess for parameter t of closest point on ellipse
        t0 = np.arctan2(y - yc, x - xc) - theta

        # determine shortest distance to ellipse for each point
        for i in range(N):
            xi = x[i]
            yi = y[i]
            # faster without Dfun, because of the python overhead
            t, _ = optimize.leastsq(fun, t0[i], args=(xi, yi))
            residuals[i] = np.sqrt(fun(t, xi, yi))

        return residuals

    def predict_xy(self, t, params=None, model_bbox = None):
        """Predict x- and y-coordinates using the estimated model.
        Parameters
        ----------
        t : array
            Angles in circle in radians. Angles start to count from positive
            x-axis to positive y-axis in a right-handed system.
        params : (5, ) array, optional
            Optional custom parameter set.
        Returns
        -------
        xy : (..., 2) array
            Predicted x- and y-coordinates.
        """

        if params is None:
            params = self.params

        xc, yc, a, b, theta = params

        ct = np.cos(t)
        st = np.sin(t)
        ctheta = math.cos(theta)
        stheta = math.sin(theta)

        x = xc + a * ctheta * ct - b * stheta * st
        y = yc + a * stheta * ct + b * ctheta * st

        model_dataset = np.concatenate((x[..., None], y[..., None]), axis=t.ndim)

        if model_bbox is not None:
            filtered_dataset = []
            for coord in model_dataset:
                if _is_in_bbox(coord, model_bbox):
                    filtered_dataset.append(coord)
            model_dataset = np.asarray(filtered_dataset)

        return model_dataset

    def get_general_params(self, params = None):

        if params is not None:
            xc, yc, ax, bx, theta = params
        else:
            xc, yc, ax, bx, theta = self.params

        z = np.array([[xc], [yc]])
        Q = np.array([[np.cos(theta), -np.sin(theta)], [np.sin(theta), np.cos(theta)]])
        # A = Q'*diag(a^-2,b^-2)*Q
        A = np.matmul(np.matmul(Q, np.diag([ax**(-2), bx**(-2)])), np.transpose(Q))
        # C = -2*A*z
        C = -2*(np.matmul(A, z))
        # D = z'*A*z - 1
        D = np.matmul(np.matmul(np.transpose(z), A), z) - 1
        a, b, c, d, f, g = [A[0,0], A[0,1], A[1,1], C[0,0] / 2, C[1,0] / 2, D[0,0]]

        #general eq. of an ellipse: a*x^2 + 2*b*x*y + c*y^2 + 2*d*x + 2*f*y + g = 0
        return np.array([a, b, c, d, f, g])

    def get_implicit_params(self, params_general = None):

        if params_general is not None:
            a, b, c, d, f, g = params_general
        else:
            a, b, c, d, f, g = self.params_general

        # finding center of ellipse [eqn.19 and 20] from [2]
        x0 = (c * d - b * f) / (b ** 2. - a * c)
        y0 = (a * f - b * d) / (b ** 2. - a * c)

        # Find the semi-axes lengths [eqn. 21 and 22] from [2]
        numerator = a * f ** 2 + c * d ** 2 + g * b ** 2 \
                    - 2 * b * d * f - a * c * g
        term = np.sqrt((a - c) ** 2 + 4 * b ** 2)
        denominator1 = (b ** 2 - a * c) * (term - (a + c))
        denominator2 = (b ** 2 - a * c) * (- term - (a + c))
        width = np.sqrt(2 * numerator / denominator1)
        height = np.sqrt(2 * numerator / denominator2)

        # angle of counterclockwise rotation of major-axis of ellipse
        # to x-axis [eqn. 23] from [2].
        phi = 0.5 * np.arctan((2. * b) / (a - c))
        if a > c:
            phi += 0.5 * np.pi

        ax = bx = None # 'a' is major axis and 'b' minor axis
        if width >= height:
            ax = width
            bx = height
        else:
            ax = height
            bx = width
            phi -= 0.5 * np.pi

        if phi < 0:
            phi += np.pi

        params = np.nan_to_num([x0, y0, ax, bx, phi]).tolist()
        params = [float(np.real(x)) for x in params]

        return params

    def denorm_params(self, H):
        """ Denormalizes general params and
        return implicit params"""
        a, b, c, d, f, g = self.params_general
        coeffs = np.array([ [a, b, d],
                            [b, c, f],
                            [d, f, g]])
        coeffs = np.dot(np.dot(H.T, coeffs), H)
        a, b, c, d, f, g = [coeffs[0,0], coeffs[0,1], coeffs[1,1], coeffs[0,2], coeffs[1,2], coeffs[2,2]]
        self.params_general = (a,b,c,d,f,g)
        self.params = self.get_implicit_params([a,b,c,d,f,g])
        return True

class SimilarityHomographyModel(BaseModel):

    """Least squares estimator for similarity Homography.
    Similarity Homography is defined by the following matrix:
        Hsim = [[s*cos(phi), -s*sin(phi), tx],
                [s*sin(phi), s*cos(phi),  tx],
                [0,          0,           1]]
    Attributes
    ----------
    params : tuple
        Similarity Homography parameters in the following order:
        `s*cos(phi)`, `s*sin(phi)`, `tx`, `ty`.
    H : (3,3) array
        Similarity homography based on params
    """

    def __init__(self):
        BaseModel.__init__(self)
        self.H = None

    def estimate(self, data, w = None):
        """ Fit homography to selected correspondences
        Parameters
        ----------
        data : (2, N, 2) array
            2 sets of N points in a space of dimensionality dim = 2.
        Returns
        -------
        success : bool
            True, if model estimation succeeds.
        """
        # target points
        tp = data[:,2:]
        # from points
        fp = data[:,:2]

        if fp.shape != tp.shape:
            raise RuntimeError('number of points or cardinality do not match')
        if fp.shape[0] + tp.shape[0] < 4:
            raise ValueError(f'At least 4 corresponding points are needed - fp.shape: {fp.shape} - tp.shape: {tp.shape}')

        fp, tp, denorm_params = self.normalize_points(fp,tp)
        self.params = self.HLS_from_points(fp,tp,w)

        #self.params = self.Haffine_from_points(fp,tp)
        #self.set_H()

        # denormalizes similarity homgraphy params
        # and sets the denormalized params self.params = denormalized_params
        # also sets self.H to H based on self.params

        self.denormalize_similarity(denorm_params)
        self.set_H()
        return True

    def residuals(self, data, params = None):
        """ Apply homography to all correspondences,
            return error for each transformed point. """

        def add_ones_column(data):
            """Adds ones column in data
            data.shape is suposed to be (n_points, dim)
            """
            n_points, dim = data.shape
            ones = np.ones((n_points, dim+1))
            ones[:,:-1] = data
            return ones.T

        def normalize(points):
            """ Normalize a collection of points in
                homogeneous coordinates so that last row = 1. """

            for row in points:
                row /= points[-1]
            return points

        # from points
        tp = add_ones_column(data[:,2:])
        # target points
        fp = add_ones_column(data[:,:2])

        if fp.shape != tp.shape:
            raise RuntimeError('number of points or cardinality do not match')

        if params is not None:
            self.set_H(params)
        H = self.H

        # transform fp
        fp_transformed = np.dot(H,fp)
        # normalize hom. coordinates
        fp_transformed = normalize(fp_transformed)

        # compute the reprojection error
        residuals = np.sqrt(np.sum((tp-fp_transformed)**2,axis=0))
        return residuals

    def H_from_points(self,fp,tp):
        """ Find homography H, such that fp is mapped to tp
            using the linear DLT method. Points are conditioned
            automatically. NOT UPDATED"""

        # create matrix for linear method, 2 rows for each correspondence pair
        nbr_correspondences = fp.shape[1]
        A = np.zeros((2*nbr_correspondences,9))
        for i in range(nbr_correspondences):
            A[2*i] = [-fp[0][i],-fp[1][i],-1,0,0,0,
                        tp[0][i]*fp[0][i],tp[0][i]*fp[1][i],tp[0][i]]
            A[2*i+1] = [0,0,0,-fp[0][i],-fp[1][i],-1,
                        tp[1][i]*fp[0][i],tp[1][i]*fp[1][i],tp[1][i]]

        U,S,V = np.linalg.svd(A)
        H = V[8].reshape((3,3))

        # decondition
        # H = np.dot(np.linalg.inv(C2),np.dot(H,C1))

        # normalize and return
        return H / H[2,2]

    def HLS_from_points(self,fp,tp, w = None):
        """ Find homography H, such that fp is mapped to tp
            using the linear DLT method. A particular solution
            to find similarity homography is used.
        Parameters
        ----------
        fp : (N, 2) array
            N `from points` in a space of dimensionality dim = 2.
        tp : (N, 2) array
            N `to points` in a space of dimensionality dim = 2.
        Returns
        -------
        params : tuple
            Params in the following order `s*cos(phi)`, `s*sin(phi)`, `tx`, `ty`.
        """

        if w is None:
            w = np.ones((tp.shape[0],))

        # normalize weights
        w = w / np.sum(w)

        # see simestimator.pdf for detailed derivation

        N = tp.shape[0]
        A = np.sum(w**2*(np.sum(fp**2, axis = -1)))
        B = np.sum(w*fp[:,0])
        C = np.sum(w*fp[:,1])

        # ((M'W'W)Q)'= [D, E, F, G]
        D = np.array([fp[:,0]*tp[:,0], fp[:,1]*tp[:,1]])
        D = np.sum(w**2*(np.sum(D.T, axis = -1)))

        E = np.array([fp[:,0]*tp[:,1], -fp[:,1]*tp[:,0]])
        E = np.sum(w**2*(np.sum(E.T, axis = -1)))

        F = np.sum(w*tp[:,0])
        G = np.sum(w*tp[:,1])

        # solving Ô = [s_cos_phi, s_sin_phi, tx, ty]
        #den = B**2 + C**2 - A*N
        #s_cos_phi = B*F + C*G - D*N / den
        #s_sin_phi = B*G - C*F - E*N / den
        #tx = B*D - A*F - C*E / den
        #ty = B*E + C*D - A*G / den

        # considering mu_tp = (0,0) and mu_fp = (0,0)
        s_cos_phi = D/A
        s_sin_phi = E/A
        tx = 0
        ty = 0
        """
        cx, sx, xx = [0, 0, 0]

        for P, Q, w_i in zip(fp, tp, w):
            cx += (P[0] * Q[0] + P[1] * Q[1])*w_i
            sx += (P[0] * Q[1] - P[1] * Q[0])*w_i
            xx += (P[0] * P[0] + P[1] * P[1])*w_i

        s_cos_phi = cx / xx
        s_sin_phi = sx / xx
        tx = 0
        ty = 0
        """
        return [s_cos_phi, s_sin_phi, tx, ty]

    def Hsim_from_points(self,fp,tp, w = None):
        """ Find homography H, such that fp is mapped to tp
            using LS. A particular solution to find similarity
            homography is used.
        Parameters
        ----------
        fp : (2, 2) array
            2 `from points` in a space of dimensionality dim = 2.
        tp : (2, 2) array
            2 `to points` in a space of dimensionality dim = 2.
        Returns
        -------
        params : tuple
            Params in the following order `s*cos(phi)`, `s*sin(phi)`, `tx`, `ty`.
        """

        fp1, fp2 = [fp[0],fp[1]]
        tp1, tp2 = [tp[0],tp[1]]
        dfp = fp1 - fp2
        dtp = tp1 - tp2

        den = dfp[0]**2 + dfp[1]**2
        s_cos_phi = (dfp[0] * dtp[0] + dfp[1] * dtp[1]) / den
        s_sin_phi = (dfp[0] * dtp[1] - dfp[1] * dtp[0]) / den
        tx = tp2[0] - fp2[0] * s_cos_phi + fp2[1] * s_sin_phi
        ty = tp2[1] - fp2[0] * s_sin_phi - fp2[1] * s_cos_phi

        return [s_cos_phi, s_sin_phi, tx, ty]

    def Haffine_from_points(self, fp,tp):
        """ Find H, affine transformation, such that
            tp is affine transf of fp. NOT UPDATED"""

        ones = np.ones((fp.shape[0],))
        fp = np.vstack((fp.T,ones))
        tp = np.vstack((tp.T,ones))

        if fp.shape != tp.shape:
            raise RuntimeError('number of points do not match')

        # condition points
        # --from points--
        m = np.mean(fp[:2], axis=1)
        maxstd = np.max(np.std(fp[:2], axis=1)) + 1e-9
        C1 = np.diag([1/maxstd, 1/maxstd, 1])
        C1[0][2] = -m[0]/maxstd
        C1[1][2] = -m[1]/maxstd
        fp_cond = np.dot(C1,fp)

        # --to points--
        m = np.mean(tp[:2], axis=1)
        C2 = C1.copy() #must use same scaling for both point sets
        C2[0][2] = -m[0]/maxstd
        C2[1][2] = -m[1]/maxstd
        tp_cond = np.dot(C2,tp)

        # conditioned points have mean zero, so translation is zero
        A = np.concatenate((fp_cond[:2],tp_cond[:2]), axis=0)
        U,S,V = np.linalg.svd(A.T)

        # create B and C matrices as Hartley-Zisserman (2:nd ed) p 130.
        tmp = V[:2].T
        B = tmp[:2]
        C = tmp[2:4]

        tmp2 = np.concatenate((np.dot(C,np.linalg.pinv(B)),np.zeros((2,1))), axis=1)
        H = np.vstack((tmp2,[0,0,1]))

        # decondition
        H = np.dot(np.linalg.inv(C2),np.dot(H,C1))

        myH = np.array(H/H[2,2])
        myH = myH.reshape(3,3)
        s_cos_phi = myH[0,0]
        s_sin_phi = myH[1,0]
        tx = myH[1,0]
        ty = myH[1,1]

        return [s_cos_phi, s_sin_phi, tx, ty]
        #return H / H[2,2]

    def normalize_points(self,fp,tp):
        """ Normalize points (normalization proposed
        in "Multiple View Geometry in Computer Vision" by Hartley - Zisserman)
        Parameters
        ----------
        fp : (N, 2) array
            N `from points` in a space of dimensionality dim = 2.
        tp : (N, 2) array
            N `to points` in a space of dimensionality dim = 2.
        Returns
        -------
        fp : (N, 2) array
            Normalized `from points`
        tp : (N, 2) array
            Normalized `to points`
        denorm_params: tuple
            Denormalization params to denormalize similarity homography
        """
        n = fp.shape[0]
        Pc = np.mean(fp, axis = 0)
        Qc = np.mean(tp, axis = 0)

        Ps = np.sum(np.abs(fp - Pc)**2, axis = -1)
        Qs = np.sum(np.abs(tp - Qc)**2, axis = -1)

        sqrt2_n = np.sqrt(2.0)*n

        Ps = sqrt2_n/np.sum(np.sqrt(Ps))
        Qs = sqrt2_n/np.sum(np.sqrt(Qs))

        return (fp-Pc)*Ps, (tp-Qc)*Qs, [Ps, Qs, Pc, Qc]

    def denormalize_similarity(self, denorm_params):
        """ Denormlizes similarity matrix based on
        denormalization params: scale and centroid
        Parameters
        ----------
        denorm_params: tuple
            Denormalization params in the following order
            denorm_params = [Ps, Qs, Pc, Qc]
        Returns
        -------
        success : bool
            True, if denormalization succeeds.
        """

        Ps, Qs, Pc, Qc = denorm_params
        s_cos_phi, s_sin_phi, tx, ty = self.params

        scale_ratio = Ps/Qs

        s_cos_phi *= scale_ratio
        s_sin_phi *= scale_ratio
        tx = Qc[0] + tx/Qs - (Pc[0] * s_cos_phi - Pc[1] * s_sin_phi)
        ty = Qc[1] + ty/Qs - (Pc[0] * s_sin_phi + Pc[1] * s_cos_phi)

        self.params = (s_cos_phi, s_sin_phi, tx, ty)
        self.set_H()

        return True

    def set_H(self, params = None):
        """ Sets self.H based on self.params
        Parameters
        ----------
        params (optional): tuple
            Params in the following order `s*cos(phi)`, `s*sin(phi)`, `tx`, `ty`
        Returns
        -------
        success : bool
            True, if setting succeeds.
        """
        if params is not None:
            s_cos_phi, s_sin_phi, tx, ty = params
        else:
            s_cos_phi, s_sin_phi, tx, ty = self.params

        self.H = [  [s_cos_phi, -s_sin_phi, tx],
                    [s_sin_phi, s_cos_phi,  ty],
                    [0,         0,          1]]

        return True

    def get_projection(self, data):
        """ returns projection x' = Hx
        """
        N, dim = data.shape
        ones = np.ones((N, dim+1))
        ones[:,:-1] = data
        data = np.dot(self.H, ones.T)
        for row in data:
            row /= data[-1]
            print(np.sum(row))
        return data[:,:-1].T

    def get_general_params(self, params = None):
        if params is not None:
            self.params = params
        s_cos_phi, s_sin_phi, tx, ty = self.params
        """
        H = np.array([[s_cos_phi, -s_sin_phi, tx],
                    [s_sin_phi, s_cos_phi,  ty],
                    [0,         0,          1]])
        H = np.linalg.inv(H)
        H /= H[2,2]
        s_cos_phi = H[0,0]
        s_sin_phi = H[1,0]
        tx = H[0,2]
        ty = H[1,2]
        """
        s = np.sqrt(s_cos_phi**2 + s_sin_phi**2)
        phi = np.arctan(s_sin_phi/s_cos_phi)
        params = np.array([s, phi, tx, ty])
        return params

class HomographyModel(BaseModel):

    """ Homography estimator class based on
        Kornia DLT implementation

    Attributes
    ----------
    params : (3,3) array
        Homography matrix
    """

    def __init__(self):
        BaseModel.__init__(self)

    def estimate(self, data, w = None):
        """ Fit homography to selected correspondences
        Parameters
        ----------
        data : (2, N, 2) array
            2 sets of N points in a space of dimensionality dim = 2.
        Returns
        -------
        success : bool
            True, if model estimation succeeds.
        """
        # target points
        src_pts = data[:,2:]
        # from points
        dst_pts = data[:,:2]

        if src_pts.shape != dst_pts.shape:
            raise RuntimeError('number of points or cardinality do not match')
        if src_pts.shape[0] + dst_pts.shape[0] < 4:
            raise ValueError(f'At least 4 corresponding points are needed - src_pts.shape: {src_pts.shape} - dst_pts.shape: {dst_pts.shape}')

        # The following commented block is to prevent torch and kornia import
        # please if using
        """
        # estimation based on Kornia implementation
        src_pts = torch.from_numpy(src_pts.reshape(1,*src_pts.shape))
        dst_pts = torch.from_numpy(dst_pts.reshape(1,*dst_pts.shape))

        if w is not None:
            w = torch.from_numpy(w.reshape(1,*w.shape))

        H_est =  kornia.geometry.homography.find_homography_dlt(src_pts, dst_pts, w) # shape (1, 3, 3)
        H_est = H_est.cpu().detach().numpy()
        self.params = H_est[0]
        """
        raise ImportError(f'Homography could not be estimated\nPlease install and import torch and torch. Additionally, uncomment code block placed in HomographyModel.estimate')

        return True

    def residuals(self, data, params = None):
        """ Apply homography to all correspondences,
            return error for each transformed point. """

        def add_ones_column(data):
            """Adds ones column in data
            data.shape is suposed to be (n_points, dim)
            """
            n_points, dim = data.shape
            ones = np.ones((n_points, dim+1))
            ones[:,:-1] = data
            return ones.T

        def normalize(points):
            """ Normalize a collection of points in
                homogeneous coordinates so that last row = 1. """

            for row in points:
                row /= points[-1]
            return points

        # from points
        src_pts = add_ones_column(data[:,2:])
        # target points
        dst_pts = add_ones_column(data[:,:2])

        if src_pts.shape != dst_pts.shape:
            raise RuntimeError('number of points or cardinality do not match')

        if params is not None: H = params
        else: H = self.params

        # transform fp
        src_transformed = np.dot(H, src_pts)
        # normalize hom. coordinates
        src_transformed = normalize(src_transformed)

        # compute the reprojection error
        residuals = np.sqrt(np.sum((dst_pts-src_transformed)**2,axis=0))
        return residuals

    def get_projection(self, data):
        """ returns projection x' = Hx
        """
        N, dim = data.shape
        ones = np.ones((N, dim+1))
        ones[:,:-1] = data
        data = np.dot(self.params, ones.T)
        for row in data:
            row /= data[-1]
        return data.T

    def get_general_params(self, params = None):
        if params is not None: self.params = params
        return self.params