// This file is part of OpenCV project.
// It is subject to the license terms in the LICENSE file found in the top-level directory
// of this distribution and at http://opencv.org/license.html.

#include "precomp.hpp"
#include <math.h>
#include <vector>
#include <iostream>

/*
If you use this code please cite this @cite BergerRaghunathan1998
Coalescence in 2 dimensions: experiments on thin copolymer films and numerical simulations  The European Physical Journal B - Condensed Matter and Complex Systems  (Volume:2 ,  Issue: 1 )  1998
*/

namespace cv {
namespace ximgproc {


void ContourFitting::setCtrSize(int n)
{
    CV_Assert(n>0);
    ctrSize = n;
}

void ContourFitting::setFDSize(int n)
{
    CV_Assert(n>0);
    fdSize=n;
}

void ContourFitting::frequencyInit()
{
    int nbElt = ctrSize;
    frequence.resize(ctrSize);

    for (int i = 0; i <= nbElt / 2; i++)
        frequence[i] = 2 * M_PI*(float)i / nbElt;
    for (int i = nbElt / 2 + 1; i<nbElt; i++)
        frequence[i] = 2 * M_PI*(float)(i - nbElt) / nbElt;
}

void ContourFitting::fAlpha(double x, double &fn, double &df)
{
    int 	nbElt = static_cast<int>(rho.size());
    double	s1 = 0, s2 = 0, s3 = 0, s4 = 0;
    double	ds1 = 0, ds2 = 0, ds3 = 0, ds4 = 0;

    for (int n = 1; n <= fdSize; n++)
    {
        s1 += rho[n] * sin(psi[n] + frequence[n] * x) +
            rho[nbElt - n] * sin(psi[nbElt - n] + frequence[nbElt - n] * x);
        s2 += frequence[n] * rho[n] * cos(psi[n] + frequence[n] * x) +
            frequence[nbElt - n] * rho[nbElt - n] * cos(psi[nbElt - n] + frequence[nbElt - n] * x);
        s3 += rho[n] * cos(psi[n] + frequence[n] * x) +
            rho[nbElt - n] * cos(psi[nbElt - n] + frequence[nbElt - n] * x);
        s4 += frequence[n] * rho[n] * sin(psi[n] + frequence[n] * x) +
            frequence[nbElt - n] * rho[nbElt - n] * sin(psi[nbElt - n] + frequence[nbElt - n] * x);
        ds1 += frequence[n] * rho[n] * cos(psi[n] + frequence[n] * x) +
            frequence[nbElt - n] * rho[nbElt - n] * cos(psi[nbElt - n] + frequence[nbElt - n] * x);
        ds2 += -frequence[n] * frequence[n] * rho[n] * sin(psi[n] + frequence[n] * x) -
            frequence[nbElt - n] * frequence[nbElt - n] * rho[nbElt - n] * sin(psi[nbElt - n] + frequence[nbElt - n] * x);
        ds3 += -frequence[n] * rho[n] * sin(psi[n] + frequence[n] * x) -
            frequence[nbElt - n] * rho[nbElt - n] * sin(psi[nbElt - n] + frequence[nbElt - n] * x);
        ds4 += frequence[n] * frequence[n] * rho[n] * cos(psi[n] + frequence[n] * x) +
            frequence[nbElt - n] * frequence[nbElt - n] * rho[nbElt - n] * cos(psi[nbElt - n] + frequence[nbElt - n] * x);
    }
    fn = s1 * s2 - s3 *s4;
    df = ds1 * s2 + s1 * ds2 - ds3 * s4 - s3 * ds4;
}

double ContourFitting::distance(std::complex<double> r, double alpha)
{
    std::complex<double>		j(0, 1);
    double 		d = 0;

    for (int i = 1; i <= fdSize; i++)
        d += abs(a[i] - b[i] * r*exp(j*alpha*frequence[i])) + abs(a[a.size() - i] - b[a.size() - i] * r*exp(j*alpha*frequence[a.size() - i]));
    return d/fdSize/2;
};

double  ContourFitting::newtonRaphson(double x1, double x2)
{
    double f1,df1;
    fAlpha(x1,f1,df1);
    if (f1 < 0)
    {
        x1=x2;
        fAlpha(x1, f1, df1);
    }
    CV_Assert(f1>=0);
    if (f1==0)
        return x1;
    for (int i = 0; i < 5; i++)
    {
        x1 = x1 -f1/df1;
        fAlpha(x1, f1, df1);
        if (f1 == 0)
            return x1;
    }
    return x1;
}

void ContourFitting::estimateTransformation(InputArray _src, InputArray _ref, OutputArray _alphaPhiST, double &distFin, bool fdContour)
{
    estimateTransformation(_src, _ref, _alphaPhiST, &distFin, fdContour);
}
void ContourFitting::estimateTransformation(InputArray _src, InputArray _ref, OutputArray _alphaPhiST,double *distFin, bool fdContour)
{
    if (!fdContour)
        CV_Assert( _src.kind() == _InputArray::STD_VECTOR && _ref.kind() == _InputArray::STD_VECTOR);
    else
        CV_Assert(fdContour && _src.kind() == _InputArray::MAT && _ref.kind() == _InputArray::MAT);
    CV_Assert(_src.channels() == 2 && _ref.channels() == 2);
    Mat fdCtr1,fdCtr2;
    if (!fdContour)
    {
        Mat newCtr1,newCtr2;
        contourSampling(_src, newCtr1, ctrSize);
        contourSampling(_ref, newCtr2, ctrSize);
        fourierDescriptor(newCtr1, fdCtr1);
        fourierDescriptor(newCtr2, fdCtr2);
    }
    else
    {
        fdCtr1=_src.getMat();
        fdCtr2= _ref.getMat();
        CV_Assert(fdCtr1.rows == fdCtr2.rows);
    }
    CV_Assert( fdSize<= ctrSize / 2 - 1);
    if (fdCtr1.type() != CV_64FC2)
        fdCtr1.convertTo(fdCtr1, CV_64F);
    if (fdCtr2.type() != CV_64FC2)
        fdCtr2.convertTo(fdCtr2, CV_64F);
    rho.resize(ctrSize);
    psi.resize(ctrSize);
    b.resize(ctrSize);
    a.resize(ctrSize);
    frequencyInit();
    double  alphaMin, phiMin, sMin;
    long		    n, nbElt = ctrSize;
    double 		    s1, s2, sign1, sign2, df, x1 = nbElt, x2 = nbElt, dx;
    double		    dist, distMin = 10000, alpha, s, phi;
    std::complex<double> 	j(0, 1), zz;

    for (n = 0; n<nbElt; n++)
    {
        b[n] = std::complex<double>(fdCtr1.at<Vec2d>(n,0)[0], fdCtr1.at<Vec2d>(n, 0)[1]);
        a[n] = std::complex<double>(fdCtr2.at<Vec2d>(n, 0)[0], fdCtr2.at<Vec2d>(n, 0)[1]);
        zz = conj(a[n])*b[n];
        rho[n] = abs(zz);
        psi[n] = arg(zz);
    }

    x1 = nbElt, x2 = nbElt;
    sMin = 1;
    alphaMin = 0;
    phiMin = arg(a[1] / b[1]);
    do
    {
        x2 = x1;
        fAlpha(x2, sign2, df);
        dx = 1;
        x1 = x2;
        do
        {
            x2 = x1;
            x1 -= dx;
            fAlpha(x1, sign1, df);
        }
        while ((sign1*sign2>0) && (x1>-nbElt));
        if (sign1*sign2<0)
        {
            alpha=newtonRaphson(x1,x2);
            s1 = 0;
            s2 = 0;
            for (n = 1; n<nbElt; n++)
            {
                s1 += rho[n] * sin(psi[n] + frequence[n] * alpha);
                s2 += rho[n] * cos(psi[n] + frequence[n] * alpha);
            }
            phi = -atan2(s1, s2);
            s1 = 0;
            s2 = 0;
            for (n = 1; n < nbElt; n++)
            {
                s1 += rho[n] * cos(psi[n] + frequence[n] * alpha + phi);
                s2 += abs(b[n] * conj(b[n]));
            }
            s = s1 / s2;
            zz = s*exp(j * phi);
            if (s>0)
                dist = distance(zz, alpha);
            else
                dist = 10000;
            if (dist<distMin)
            {
                distMin = dist;
                alphaMin = alpha;
                phiMin = phi;
                sMin = s;
            }
        }
    }
    while ((x1>-nbElt));
    Mat x=(Mat_<double>(1,5)<<alphaMin/ nbElt,phiMin,sMin, fdCtr2.at<Vec2d>(0, 0)[0]- fdCtr1.at<Vec2d>(0, 0)[0], fdCtr2.at<Vec2d>(0, 0)[1]- fdCtr1.at<Vec2d>(0, 0)[1]);
    if (distFin)
        *distFin= distMin;
    x.copyTo(_alphaPhiST);
}

void fourierDescriptor(InputArray _src, OutputArray _dst, int nbElt, int nbFD)
{
    CV_Assert(_src.kind() == _InputArray::MAT || _src.kind() == _InputArray::STD_VECTOR);
    CV_Assert(_src.empty() || (_src.channels() == 2 && (_src.depth() == CV_32S || _src.depth() == CV_32F || _src.depth() == CV_64F)));
    Mat  z = _src.getMat();
    CV_Assert(z.rows == 1 || z.cols == 1);
    if (nbElt==-1)
        nbElt = getOptimalDFTSize(max(z.rows, z.cols));
    CV_Assert((nbFD >= 1 && nbFD <=nbElt/2) || nbFD==-1);
    Mat  Z;
    if (z.rows*z.cols!=nbElt)
        contourSampling(_src, z,nbElt);
    dft(z, Z, DFT_SCALE | DFT_REAL_OUTPUT);
    if (nbFD == -1)
    {
        Z.copyTo(_dst);
    }
    else
    {
        int n1 = nbFD / 2, n2 = nbElt - n1;
        Mat d(nbFD, 1, Z.type());
        Z.rowRange(Range(1, n1+1)).copyTo(d.rowRange(Range(0, n1)));
        if (n2>0)
            Z.rowRange(Range(n2, Z.rows)).copyTo(d.rowRange(Range(n1, nbFD)));
        d.copyTo(_dst);
    }
}

void contourSampling(InputArray _src, OutputArray _out, int nbElt)
{
    CV_Assert(_src.kind() == _InputArray::STD_VECTOR || _src.kind() == _InputArray::MAT);
    CV_Assert(_src.empty() || (_src.channels() == 2 && (_src.depth() == CV_32S || _src.depth() == CV_32F || _src.depth() == CV_64F)));
    CV_Assert(nbElt>0);
    Mat ctr;
    _src.getMat().convertTo(ctr,CV_32F);
    if (ctr.rows*ctr.cols == 0)
    {
        _out.release();
        return;
    }
    CV_Assert(ctr.rows==1 || ctr.cols==1);
    double		l1 = 0, l2, p, d, s;
 //   AutoBuffer<Point2d> _buf(nbElt);
    Mat r;
    if (ctr.rows==1)
        ctr=ctr.t();
    int j = 0;
    int 		nb = ctr.rows;
    p = arcLength(_src, true);
    l2 = norm(ctr.row(j) - ctr.row(j + 1)) / p;
    for (int i = 0; i<nbElt; i++)
    {
        s = (float)i / (float)nbElt;
        while (s >= l2)
        {
            j++;
            l1 = l2;
            d = norm(ctr.row(j % nb) - ctr.row((j + 1) % nb));
            l2 = l1 + d / p;
        }
        if ((s >= l1) && (s < l2))
        {
            Mat d1=ctr.row((j + 1) % nb);
            Mat d0=ctr.row(j % nb);
            Mat d10 = d1 - d0;
            Mat pn = d0 + d10 * (s - l1) / (l2 - l1);
            r.push_back(pn);
 //           _buf[i]=Point2d(pn.at<Point2f>(0,0));
        }
    }
    r.copyTo(_out);
}

void transformFD(InputArray _src, InputArray _t,OutputArray _dst,  bool fdContour)
{
    if (!fdContour)
        CV_Assert(_src.kind() == _InputArray::STD_VECTOR);
    else
        CV_Assert( _src.kind() == _InputArray::MAT );
    CV_Assert(_src.channels() == 2);
    CV_Assert(_t.kind() == _InputArray::MAT);
    Mat t=_t.getMat();
    CV_Assert(t.rows == 1 && t.cols==5 && t.depth()==CV_64F);
    Mat Z;
    if (!fdContour)
    {
        Mat ctr1 = _src.getMat();
        if (ctr1.rows==1)
            ctr1 = ctr1.t();
        Mat newCtr1;
        int M = getOptimalDFTSize(ctr1.rows);
        contourSampling(ctr1, newCtr1, M);
        fourierDescriptor(newCtr1, Z);
    }
    else
        Z = _src.getMat();
    if (Z.type()!=CV_64FC2)
        Z.convertTo(Z,CV_64F);
    std::complex<double> expitheta = t.at<double>(0,2)  * std::complex<double>(cos(t.at<double>(0, 1)), sin(t.at<double>(0, 1)));
    for (int j = 1; j<Z.rows; j++)
    {
        std::complex<double> zr(Z.at<Vec2d>(j, 0)[0], Z.at<Vec2d>(j,0)[1]);
        if (j<=Z.rows/2)
            zr = zr*expitheta*exp(t.at<double>(0, 0) * 2 * (M_PI*j) *  std::complex<double>(0, 1));
        else
            zr = zr*expitheta*exp(t.at<double>(0, 0)* 2 * (M_PI*(j - Z.rows))    * std::complex<double>(0, 1));
        Z.at<Vec2d>(j, 0) = Vec2d(zr.real(),zr.imag());
    }
    Z.at<Vec2d>(0, 0) += Vec2d(t.at<double>(0, 3), t.at<double>(0, 4));
    std::vector<Point2d> z;
    dft(Z, z, DFT_INVERSE);
    Mat(z).copyTo(_dst);
}

cv::Ptr<ContourFitting> createContourFitting(int ctr, int fd)
{
    return makePtr<ContourFitting>(ctr, fd);
}

}
}