Commit e92394d0 authored by Bellaktris's avatar Bellaktris

fixed bugs in WH transform, some code compression

parent 79020cf7
......@@ -62,6 +62,6 @@ createStructuredEdgeDetection
:param model: model file name
.. [Dollar2013] Dollár P., Zitnick C. L., "Structured forests for fast edge detection",
.. [Dollar2013] P. Dollár, C. L. Zitnick, "Structured forests for fast edge detection",
IEEE International Conference on Computer Vision (ICCV), 2013,
pp. 1841-1848. `DOI <http://dx.doi.org/10.1109/ICCV.2013.231>`_
set(the_description "Addon to basic photo module")
ocv_define_module(xphoto opencv_core opencv_imgproc opencv_photo OPTIONAL opencv_photo opencv_highgui)
\ No newline at end of file
ocv_define_module(xphoto opencv_core opencv_imgproc OPTIONAL opencv_photo opencv_highgui opencv_photo)
\ No newline at end of file
Single image inpainting
***********************
.. highlight:: cpp
Inpainting
----------
.. ocv:function:: void inpaint(const Mat &src, const Mat &mask, Mat &dst, const int algorithmType)
The function implements different single-image inpainting algorithms.
:param src : source image, it could be of any type and any number of channels from 1 to 4. In case of 3- and 4-channels images the function expect them in CIELab colorspace or similar one, where first color component shows intensity, while second and third shows colors. Nonetheless you can try any colorspaces.
:param mask : mask (CV_8UC1), where non-zero pixels indicate valid image area, while zero pixels indicate area to be inpainted
:param dst : destination image
:param algorithmType : expected noise standard deviation
* INPAINT_SHIFTMAP: This algorithm searches for dominant correspondences (transformations) of image patches and tries to seamlessly fill-in the area to be inpainted using this transformations. Look in the original paper [He2012]_ for details.
.. [He2012] K. He, J. Sun., "Statistics of Patch Offsets for Image Completion",
IEEE European Conference on Computer Vision (ICCV), 2012,
pp. 16-29. `DOI <http://dx.doi.org/10.1007/978-3-642-33709-3_2>`_
\ No newline at end of file
......@@ -7,3 +7,4 @@ xphoto. Addon to basic photo modul
Color balance <colorbalance/whitebalance>
Denoising <denoising/denoising>
Inpainting <inpainting/inpainting>
......@@ -46,6 +46,8 @@ int main( int argc, const char** argv )
return -1;
}
cv::cvtColor(src, src, CV_RGB2Lab);
cv::Mat mask = cv::imread(maskFilename, 0);
if ( mask.empty() )
{
......@@ -54,13 +56,13 @@ int main( int argc, const char** argv )
}
cv::Mat res(src.size(), src.type());
cv::inpaint( src, mask, res, cv::INPAINT_SHIFTMAP );
cv::cvtColor(res, res, CV_Lab2RGB);
if ( outFilename == "" )
{
cv::namedWindow("denoising result", 1);
cv::imshow("denoising result", res);
cv::namedWindow("inpainting result", 1);
cv::imshow("inpainting result", res);
cv::waitKey(0);
}
......
......@@ -76,15 +76,18 @@ private:
: main(_main), dimIdx(_dimIdx) {}
};
const int height, width;
const int leafNumber;
std::vector <cv::Vec <Tp, cn> > data;
std::vector <int> idx;
std::vector <cv::Point2i> nodes;
int getMaxSpreadN(const int left, const int right) const;
void operator =(const KDTree <Tp, cn> &) const {};
public:
std::vector <cv::Vec <Tp, cn> > data;
std::vector <int> idx;
std::vector <cv::Point2i> nodes;
void updateDist(const int leaf, const int &idx0, int &bestIdx, double &dist);
KDTree(const cv::Mat &data, const int leafNumber = 8);
~KDTree(){};
......@@ -109,15 +112,16 @@ getMaxSpreadN(const int _left, const int _right) const
template <typename Tp, int cn> KDTree <Tp, cn>::
KDTree(const cv::Mat &img, const int _leafNumber)
: leafNumber(_leafNumber)
: height(img.rows), width(img.cols),
leafNumber(_leafNumber)
///////////////////////////////////////////////////
{
for (int i = 0; i < img.rows; ++i)
for (int j = 0; j < img.cols; ++j)
data.push_back(img.template at<cv::Vec <Tp, cn> >(i, j));
std::copy( (cv::Vec <Tp, cn> *) img.data,
(cv::Vec <Tp, cn> *) img.data + img.total(),
std::back_inserter(data) );
generate_seq( std::back_inserter(idx), 0, int(data.size()) );
fill_n( std::back_inserter(nodes), int(data.size()), cv::Point2i(0, 0) );
std::fill_n( std::back_inserter(nodes),
int(data.size()), cv::Point2i(0, 0) );
std::stack <int> left, right;
left.push( 0 );
......@@ -125,75 +129,72 @@ KDTree(const cv::Mat &img, const int _leafNumber)
while ( !left.empty() )
{
int _left = left.top(); left.pop();
int _left = left.top(); left.pop();
int _right = right.top(); right.pop();
if ( _right - _left <= leafNumber)
{
for (int i = _left; i < _right; ++i)
{
nodes[idx[i]].x = _left;
nodes[idx[i]].y = _right;
}
nodes[idx[i]] = cv::Point2i(_left, _right);
continue;
}
std::vector <int>::iterator begIt = idx.begin();
int nth = _left + (_right - _left)/2;
std::nth_element(/**/ begIt + _left,
begIt + nth, begIt + _right,
KDTreeComparator( this,
getMaxSpreadN(_left, _right) ) /**/);
int dimIdx = getMaxSpreadN(_left, _right);
KDTreeComparator comp( this, dimIdx );
std::nth_element(/**/
idx.begin() + _left,
idx.begin() + nth,
idx.begin() + _right, comp
/**/);
left.push(_left); right.push(nth + 1);
left.push(nth + 1); right.push(_right);
left.push(_left); right.push(nth + 1);
left.push(nth + 1); right.push(_right);
}
}
/************************** ANNF search **************************/
template <typename Tp, int cn>
static void updateDist(const KDTree <Tp, cn> &kdTree, const cv::Point2i &I, const int height,
const int width, const int &currentIdx, int &bestIdx, double &dist)
template <typename Tp, int cn> void KDTree <Tp, cn>::
updateDist(const int leaf, const int &idx0, int &bestIdx, double &dist)
{
for (int k = I.x; k < I.y; ++k)
for (int k = nodes[leaf].x; k < nodes[leaf].y; ++k)
{
int newIdx = kdTree.idx[k];
int y = idx0/width, ny = idx[k]/width;
int x = idx0%width, nx = idx[k]%width;
if (newIdx%width == width - 1)
if (abs(ny - y) + abs(nx - x) < 32)
continue;
if (newIdx/width == height - 1)
if (nx == width - 1 || ny == height - 1)
continue;
int dx = currentIdx%width - newIdx%width;
int dy = currentIdx/width - newIdx/width;
if (abs(dx) + abs(dy) < 32)
continue;
double ndist = norm2(data[idx0], data[idx[k]]);
double ndist = norm2(kdTree.data[newIdx],
kdTree.data[currentIdx]);
if (ndist < dist)
{
dist = ndist;
bestIdx = newIdx;
bestIdx = idx[k];
}
}
}
static void getANNF(const cv::Mat &img, std::vector <cv::Matx33f> &transforms,
const int nTransform, const int psize)
/************************** ANNF search **************************/
static void dominantTransforms(const cv::Mat &img, std::vector <cv::Matx33f> &transforms,
const int nTransform, const int psize)
{
/** Walsh-Hadamard Transformation **/
std::vector <cv::Mat> channels;
cv::split(img, channels);
const int np[] = {16, 4, 4};
int cncase = std::max(img.channels() - 2, 0);
const int np[] = {cncase == 0 ? 12 : (cncase == 1 ? 16 : 10),
cncase == 0 ? 12 : (cncase == 1 ? 04 : 02),
cncase == 0 ? 00 : (cncase == 1 ? 04 : 02),
cncase == 0 ? 00 : (cncase == 1 ? 00 : 10)};
for (int i = 0; i < img.channels(); ++i)
getWHSeries(channels[i], channels[i], np[i], psize);
rgb2whs(channels[i], channels[i], np[i], psize);
cv::Mat whs; // Walsh-Hadamard series
cv::merge(channels, whs);
......@@ -209,22 +210,16 @@ static void getANNF(const cv::Mat &img, std::vector <cv::Matx33f> &transforms,
double dist = std::numeric_limits <double>::max();
int current = i*whs.cols + j;
cv::Point2i I = kdTree.nodes[i*whs.cols + j];
updateDist(kdTree, I, whs.rows, whs.cols, current, annf[i*whs.cols + j], dist);
if (i != 0)
{
int idx = annf[(i - 1)*whs.cols + j] + whs.cols;
cv::Point2i I = kdTree.nodes[idx];
updateDist(kdTree, I, whs.rows, whs.cols, current, annf[i*whs.cols + j], dist);
}
if (j != 0)
{
int idx = annf[i*whs.cols + (j - 1)] + 1;
cv::Point2i I = kdTree.nodes[idx];
updateDist(kdTree, I, whs.rows, whs.cols, current, annf[i*whs.cols + j], dist);
}
int dy[] = {0, 1, 0}, dx[] = {0, 0, 1};
for (int k = 0; k < sizeof(dy)/sizeof(int); ++k)
if (i - dy[k] >= 0 && j - dx[k] >= 0)
{
int neighbor = (i - dy[k])*whs.cols + (j - dx[k]);
int leafIdx = k == 0 ? neighbor :
annf[neighbor] + dy[k]*whs.cols + dx[k];
kdTree.updateDist(leafIdx, current,
annf[i*whs.cols + j], dist);
}
}
/** Local maxima extraction **/
......@@ -233,12 +228,12 @@ static void getANNF(const cv::Mat &img, std::vector <cv::Matx33f> &transforms,
_annfHist(2*whs.rows, 2*whs.cols, 0.0);
for (size_t i = 0; i < annf.size(); ++i)
++annfHist( (annf[i] - int(i))/whs.cols + whs.rows,
(annf[i] - int(i))%whs.cols + whs.cols);
(annf[i] - int(i))%whs.cols + whs.cols );
cv::GaussianBlur( annfHist, annfHist,
cv::Size(9, 9), 1.41, 0.0, cv::BORDER_CONSTANT);
cv::dilate(annfHist, _annfHist,
cv::Matx<uchar, 9, 9>::ones());
cv::dilate( annfHist, _annfHist,
cv::Matx<uchar, 9, 9>::ones() );
std::vector < std::pair<double, int> > amount;
std::vector <cv::Point2i> shiftM;
......
......@@ -57,6 +57,9 @@
#include "opencv2/core/types.hpp"
#include "opencv2/core/types_c.h"
#include "opencv2/highgui.hpp"
namespace xphotoInternal
{
# include "photomontage.hpp"
......@@ -66,22 +69,18 @@ namespace xphotoInternal
namespace cv
{
template <typename Tp, unsigned int cn>
static void shiftMapInpaint(const Mat &src, const Mat &mask, Mat &dst)
static void shiftMapInpaint(const Mat &src, const Mat &mask, Mat &dst,
const int nTransform = 60, const int psize = 8)
{
const int nTransform = 60; // number of dominant transforms for stitching
const int psize = 8; // single ANNF patch size
/** Preparing input **/
cv::Mat img;
cvtColor(src, img, CV_RGB2Lab);
src.convertTo( img, CV_32F );
img.setTo(0, 255 - mask);
img.convertTo( img, CV_32F );
/** ANNF computation **/
std::vector <Matx33f> transforms( nTransform );
xphotoInternal::getANNF(img, transforms,
nTransform, psize);
xphotoInternal::dominantTransforms(img,
transforms, nTransform, psize);
/** Warping **/
std::vector <Mat> images( nTransform + 1 ); // source image transformed with transforms
......@@ -109,8 +108,7 @@ namespace cv
.assignResImage(photomontageResult);
/** Writing result **/
photomontageResult.convertTo( photomontageResult, dst.type() );
cvtColor(photomontageResult, dst, CV_Lab2RGB);
photomontageResult.convertTo( dst, dst.type() );
}
template <typename Tp, unsigned int cn>
......@@ -143,66 +141,90 @@ namespace cv
switch ( src.type() )
{
//case CV_8UC1:
// inpaint <uchar, 1>( src, mask, dst, algorithmType );
// break;
//case CV_8UC2:
// inpaint <uchar, 2>( src, mask, dst, algorithmType );
// break;
case CV_8SC1:
inpaint <char, 1>( src, mask, dst, algorithmType );
break;
case CV_8SC2:
inpaint <char, 2>( src, mask, dst, algorithmType );
break;
case CV_8SC3:
inpaint <char, 3>( src, mask, dst, algorithmType );
break;
case CV_8SC4:
inpaint <char, 4>( src, mask, dst, algorithmType );
break;
case CV_8UC1:
inpaint <uchar, 1>( src, mask, dst, algorithmType );
break;
case CV_8UC2:
inpaint <uchar, 2>( src, mask, dst, algorithmType );
break;
case CV_8UC3:
inpaint <uchar, 3>( src, mask, dst, algorithmType );
break;
//case CV_8UC4:
// inpaint <uchar, 4>( src, mask, dst, algorithmType );
// break;
//case CV_16SC1:
// inpaint <short, 1>( src, mask, dst, algorithmType );
// break;
//case CV_16SC2:
// inpaint <short, 2>( src, mask, dst, algorithmType );
// break;
case CV_8UC4:
inpaint <uchar, 4>( src, mask, dst, algorithmType );
break;
case CV_16SC1:
inpaint <short, 1>( src, mask, dst, algorithmType );
break;
case CV_16SC2:
inpaint <short, 2>( src, mask, dst, algorithmType );
break;
case CV_16SC3:
inpaint <short, 3>( src, mask, dst, algorithmType );
break;
//case CV_16SC4:
// inpaint <short, 4>( src, mask, dst, algorithmType );
// break;
//case CV_32SC1:
// inpaint <int, 1>( src, mask, dst, algorithmType );
// break;
//case CV_32SC2:
// inpaint <int, 2>( src, mask, dst, algorithmType );
// break;
case CV_16SC4:
inpaint <short, 4>( src, mask, dst, algorithmType );
break;
case CV_16UC1:
inpaint <ushort, 1>( src, mask, dst, algorithmType );
break;
case CV_16UC2:
inpaint <ushort, 2>( src, mask, dst, algorithmType );
break;
case CV_16UC3:
inpaint <ushort, 3>( src, mask, dst, algorithmType );
break;
case CV_16UC4:
inpaint <ushort, 4>( src, mask, dst, algorithmType );
break;
case CV_32SC1:
inpaint <int, 1>( src, mask, dst, algorithmType );
break;
case CV_32SC2:
inpaint <int, 2>( src, mask, dst, algorithmType );
break;
case CV_32SC3:
inpaint <int, 3>( src, mask, dst, algorithmType );
break;
//case CV_32SC4:
// inpaint <int, 4>( src, mask, dst, algorithmType );
// break;
//case CV_32FC1:
// inpaint <float, 1>( src, mask, dst, algorithmType );
// break;
//case CV_32FC2:
// inpaint <float, 2>( src, mask, dst, algorithmType );
// break;
case CV_32SC4:
inpaint <int, 4>( src, mask, dst, algorithmType );
break;
case CV_32FC1:
inpaint <float, 1>( src, mask, dst, algorithmType );
break;
case CV_32FC2:
inpaint <float, 2>( src, mask, dst, algorithmType );
break;
case CV_32FC3:
inpaint <float, 3>( src, mask, dst, algorithmType );
break;
//case CV_32FC4:
// inpaint <float, 4>( src, mask, dst, algorithmType );
// break;
//case CV_64FC1:
// inpaint <double, 1>( src, mask, dst, algorithmType );
// break;
//case CV_64FC2:
// inpaint <double, 2>( src, mask, dst, algorithmType );
// break;
case CV_32FC4:
inpaint <float, 4>( src, mask, dst, algorithmType );
break;
case CV_64FC1:
inpaint <double, 1>( src, mask, dst, algorithmType );
break;
case CV_64FC2:
inpaint <double, 2>( src, mask, dst, algorithmType );
break;
case CV_64FC3:
inpaint <double, 3>( src, mask, dst, algorithmType );
break;
//case CV_64FC4:
// inpaint <double, 4>( src, mask, dst, algorithmType );
// break;
case CV_64FC4:
inpaint <double, 4>( src, mask, dst, algorithmType );
break;
default:
CV_Error_( CV_StsNotImplemented,
("Unsupported source image format (=%d)",
......
......@@ -40,24 +40,52 @@
#ifndef __NORM2_HPP__
#define __NORM2_HPP__
static inline int sqr(uchar x) { return x*x; }
/************************ General template *************************/
template <typename Tp> static inline Tp sqr(Tp x) { return x*x; }
template <typename Tp, int cn> static inline Tp sqr( cv::Vec<Tp, cn> x) { return x.dot(x); }
template <typename Tp> static inline Tp norm2(const Tp &a, const Tp &b) { return sqr(a - b); }
template <typename Tp, int cn> static inline
Tp norm2(const cv::Vec <Tp, cn> &a, const cv::Vec<Tp, cn> &b) { return sqr(a - b); }
template <int cn> static inline int sqr( cv::Vec<uchar, cn> x) { return x.dot(x); }
static inline int norm2(const uchar &a, const uchar &b) { return sqr(a - b); }
/******************* uchar, char, ushort, uint *********************/
static inline int norm2(const uchar &a, const uchar &b) { return sqr(int(a) - int(b)); }
template <int cn> static inline
int norm2(const cv::Vec <uchar, cn> &a, const cv::Vec<uchar, cn> &b) { return sqr(a - b); }
int norm2(const cv::Vec <uchar, cn> &a, const cv::Vec<uchar, cn> &b)
{
return sqr( cv::Vec<int, cn>(a) - cv::Vec<int, cn>(b) );
}
static inline int norm2(const char &a, const char &b) { return sqr(int(a) - int(b)); }
template <int cn> static inline
int norm2(const cv::Vec <char, cn> &a, const cv::Vec<char, cn> &b)
{
return sqr( cv::Vec<int, cn>(a) - cv::Vec<int, cn>(b) );
}
template <typename Tp> static inline Tp sqr(Tp x) { return x*x; }
static inline short norm2(const ushort &a, const ushort &b) { return sqr <short>(short(a) - short(b)); }
template <typename Tp, int cn> static inline Tp sqr( cv::Vec<Tp, cn> x) { return x.dot(x); }
template <int cn> static inline
short norm2(const cv::Vec <ushort, cn> &a, const cv::Vec<ushort, cn> &b)
{
return sqr( cv::Vec<short, cn>(a) - cv::Vec<short, cn>(b) );
}
template <typename Tp> static inline Tp norm2(const Tp &a, const Tp &b) { return sqr(a - b); }
static inline int norm2(const uint &a, const uint &b) { return sqr(int(a) - int(b)); }
template <int cn> static inline
int norm2(const cv::Vec <uint, cn> &a, const cv::Vec<uint, cn> &b)
{
return sqr( cv::Vec<int, cn>(a) - cv::Vec<int, cn>(b) );
}
template <typename Tp, int cn> static inline
Tp norm2(const cv::Vec <Tp, cn> &a, const cv::Vec<Tp, cn> &b) { return sqr(a - b); }
#endif /* __NORM2_HPP__ */
\ No newline at end of file
......@@ -93,7 +93,8 @@ private:
protected:
virtual double dist(const Tp &l1p1, const Tp &l1p2, const Tp &l2p1, const Tp &l2p2);
virtual void setWeights(GCGraph <double> &graph, const cv::Point &pA, const cv::Point &pB, const int lA, const int lB, const int lX);
virtual void setWeights(GCGraph <double> &graph, const cv::Point &pA,
const cv::Point &pB, const int lA, const int lB, const int lX);
public:
Photomontage(const std::vector <cv::Mat> &images, const std::vector <cv::Mat> &masks);
......
......@@ -51,69 +51,95 @@ static inline int hl(int x)
return res;
}
static inline int rp2(int x)
{
int res = 1;
while (res < x)
res <<= 1;
return res;
}
template <typename ForwardIterator>
static void generate_snake(ForwardIterator snake, const int n)
{
cv::Point previous;
if (n > 0)
{
previous = cv::Point(0, 0);
*snake = previous;
}
for (int k = 1, num = 1; num <= n; ++k)
{
const cv::Point2i dv[] = { cv::Point2i( !(k&1), (k&1) ),
cv::Point2i( -(k&1), -!(k&1) ) };
*snake = previous = previous - dv[1];
++num;
for (int i = 0; i < 2; ++i)
for (int j = 0; j < k && num < n; ++j)
{
*snake = previous = previous + dv[i];
++num;
}
}
}
static void nextProjection(std::vector <cv::Mat> &projections, const cv::Point &A,
const cv::Point &B, const int psize)
{
int xsign = (A.x != B.x)*(hl(A.x&B.x) + (B.x > A.x))&1;
int ysign = (A.y != B.y)*(hl(A.y&B.y) + (B.y > A.y))&1;
bool plusToMinusUpdate = std::max(xsign, ysign);
bool plusToMinusUpdate = xsign || ysign;
int dx = (A.x != B.x) << hl(psize - 1) - hl(A.x ^ B.x);
int dy = (A.y != B.y) << hl(psize - 1) - hl(A.y ^ B.y);
int dx = (A.x != B.x) << ( hl(psize - 1) - hl(A.x ^ B.x) );
int dy = (A.y != B.y) << ( hl(psize - 1) - hl(A.y ^ B.y) );
cv::Mat proj = projections[projections.size() - 1];
cv::Mat nproj( proj.size(), proj.type(), cv::Scalar::all(0) );
cv::Mat proj = projections[projections.size() - 1],
nproj = -proj.clone();
for (int i = dy; i < nproj.rows; ++i)
{
float *vCurrent = proj.template ptr<float>(i);
float *vxCurrent = proj.template ptr<float>(i - dy);
float *vxNext = nproj.template ptr<float>(i - dy);
float *vNext = nproj.template ptr<float>(i);
float *vxNext = nproj.template ptr<float>(i - dy);
float *vNext = nproj.template ptr<float>(i);
float *vxCurrent = proj.template ptr<float>(i - dy);
if (plusToMinusUpdate)
for (int j = dx; j < nproj.cols; ++j)
vNext[j] = -vxNext[j - dx] + vCurrent[j] - vxCurrent[j - dx];
vNext[j] += vxCurrent[j - dx] - vxNext[j - dx];
else
for (int j = dx; j < nproj.cols; ++j)
vNext[j] = +vxNext[j - dx] + vCurrent[j] + vxCurrent[j - dx];
vNext[j] -= vxCurrent[j - dx] - vxNext[j - dx];
}
projections.push_back(nproj);
}
static void getWHSeries(const cv::Mat &src, cv::Mat &dst, const int nProjections, const int psize)
static void rgb2whs(const cv::Mat &src, cv::Mat &dst, const int nProjections, const int psize)
{
CV_Assert(nProjections <= psize*psize && src.type() == CV_32FC1);
CV_Assert( hl(psize) == 1 );
const int npsize = rp2(psize);
std::vector <cv::Mat> projections;
cv::Mat proj;
cv::boxFilter(src, proj, CV_32F, cv::Size(psize, psize),
cv::Point(-1,-1), true, cv::BORDER_REFLECT);
cv::Mat img, proj;
cv::copyMakeBorder(src, img, npsize, npsize, npsize, npsize,
cv::BORDER_CONSTANT, 0);
cv::boxFilter(img, proj, CV_32F, cv::Size(npsize, npsize),
cv::Point(-1, -1), true, cv::BORDER_REFLECT);
projections.push_back(proj);
std::vector <cv::Point2i> snake_idx( 1, cv::Point2i(0, 0) );
for (int k = 1, num = 1; k < psize && num <= nProjections; ++k)
{
const cv::Point2i dv[] = { cv::Point2i( !(k&1), (k&1) ),
cv::Point2i( -(k&1), -!(k&1) ) };
snake_idx.push_back(snake_idx[num++ - 1] - dv[1]);
for (int i = 0; i < k && num < nProjections; ++i)
snake_idx.push_back(snake_idx[num++ - 1] + dv[0]);
for (int i = 0; i < k && num < nProjections; ++i)
snake_idx.push_back(snake_idx[num++ - 1] + dv[1]);
}
std::vector <cv::Point2i> snake_idx;
generate_snake(std::back_inserter(snake_idx), nProjections);
for (int i = 1; i < nProjections; ++i)
nextProjection(projections, snake_idx[i - 1],
snake_idx[i], psize);
snake_idx[i], npsize);
cv::merge(projections, dst);
cv::merge(projections, img);
img(cv::Rect(npsize, npsize, src.cols, src.rows)).copyTo(dst);
}
#endif /* __WHS_HPP__ */
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment