2013-12-01 22:50:24 +01:00
|
|
|
//
|
|
|
|
// Don't use as a standalone file
|
|
|
|
//
|
|
|
|
|
|
|
|
#include "opencv2/core.hpp"
|
|
|
|
#include "opencv2/core/utility.hpp" // cv::format
|
|
|
|
#include "opencv2/imgproc.hpp" // cvtColor
|
|
|
|
#include "opencv2/imgproc/types_c.h" // cvtColor
|
|
|
|
#include "opencv2/highgui.hpp" // imread
|
|
|
|
#include "opencv2/core/directx.hpp"
|
2014-07-04 16:48:15 +02:00
|
|
|
#include "opencv2/imgcodecs.hpp"
|
2013-12-01 22:50:24 +01:00
|
|
|
|
|
|
|
#include <iostream>
|
|
|
|
#include <queue>
|
|
|
|
|
|
|
|
using namespace cv;
|
|
|
|
using namespace cv::directx;
|
|
|
|
static const int fontFace = cv::FONT_HERSHEY_DUPLEX;
|
|
|
|
#if !defined(USE_D3D9) && !defined(USE_D3DEX)
|
|
|
|
const cv::Scalar frameColor(255,128,0,255);
|
|
|
|
#else
|
|
|
|
const cv::Scalar frameColor(0,128,255,255); // BGRA for D3D9
|
|
|
|
#endif
|
|
|
|
|
|
|
|
#define SAFE_RELEASE(p) if (p) { p->Release(); p = NULL; }
|
|
|
|
|
|
|
|
const int WIDTH = 1024;
|
|
|
|
const int HEIGHT = 768;
|
|
|
|
|
|
|
|
HINSTANCE hInstance;
|
|
|
|
HWND hWnd;
|
|
|
|
|
|
|
|
// external declaration
|
|
|
|
bool initDirect3D(void);
|
|
|
|
bool initDirect3DTextures(void);
|
|
|
|
void render(void);
|
|
|
|
void cleanUp (void);
|
|
|
|
|
|
|
|
#define USAGE_DESCRIPTION_0 "1 - CPU write via LockRect/Map"
|
|
|
|
#define USAGE_DESCRIPTION_1 "2* - Mat->D3D"
|
|
|
|
#define USAGE_DESCRIPTION_2 "3* - D3D->UMat / change UMat / UMat->D3D"
|
|
|
|
#define USAGE_DESCRIPTION_3 "0 - show input texture without any processing"
|
|
|
|
#define USAGE_DESCRIPTION_SPACE "SPACE - toggle frame processing (only data transfers)"
|
|
|
|
|
|
|
|
static int g_sampleType = 0;
|
|
|
|
static int g_disableProcessing = false;
|
|
|
|
|
|
|
|
// forward declaration
|
|
|
|
static LRESULT CALLBACK WndProc(HWND, UINT, WPARAM, LPARAM);
|
|
|
|
|
|
|
|
static bool initWindow()
|
|
|
|
{
|
|
|
|
WNDCLASSEX wcex;
|
|
|
|
|
|
|
|
wcex.cbSize = sizeof(WNDCLASSEX);
|
|
|
|
wcex.style = CS_HREDRAW | CS_VREDRAW;
|
|
|
|
wcex.lpfnWndProc = WndProc;
|
|
|
|
wcex.cbClsExtra = 0;
|
|
|
|
wcex.cbWndExtra = 0;
|
|
|
|
wcex.hInstance = hInstance;
|
|
|
|
wcex.hIcon = LoadIcon(0, IDI_APPLICATION);
|
|
|
|
wcex.hCursor = LoadCursor(0, IDC_ARROW);
|
|
|
|
wcex.hbrBackground = 0;
|
|
|
|
wcex.lpszMenuName = 0L;
|
|
|
|
wcex.lpszClassName = "OpenCVDirectX";
|
|
|
|
wcex.hIconSm = 0;
|
|
|
|
|
|
|
|
RegisterClassEx(&wcex);
|
|
|
|
|
|
|
|
RECT rc = {0, 0, WIDTH, HEIGHT};
|
|
|
|
AdjustWindowRect(&rc, WS_OVERLAPPEDWINDOW, false);
|
|
|
|
hWnd = CreateWindow("OpenCVDirectX", WINDOW_NAME,
|
|
|
|
WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, rc.right - rc.left, rc.bottom - rc.top, NULL, NULL, hInstance, NULL);
|
|
|
|
|
|
|
|
if (!hWnd)
|
|
|
|
return false;
|
|
|
|
|
|
|
|
ShowWindow(hWnd, SW_SHOW);
|
|
|
|
UpdateWindow(hWnd);
|
|
|
|
|
|
|
|
return true;
|
|
|
|
}
|
|
|
|
|
|
|
|
static LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
|
|
|
|
{
|
|
|
|
switch (message)
|
|
|
|
{
|
|
|
|
case WM_DESTROY:
|
|
|
|
PostQuitMessage(0);
|
|
|
|
return 0;
|
|
|
|
|
|
|
|
case WM_CHAR:
|
|
|
|
if (wParam >= '0' && wParam <= '3')
|
|
|
|
{
|
|
|
|
g_sampleType = (char)wParam - '0';
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
else if (wParam == ' ')
|
|
|
|
{
|
|
|
|
g_disableProcessing = !g_disableProcessing;
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
else if (wParam == VK_ESCAPE)
|
|
|
|
{
|
|
|
|
DestroyWindow(hWnd);
|
|
|
|
return 0;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
return DefWindowProc(hWnd, message, wParam, lParam);
|
|
|
|
}
|
|
|
|
|
|
|
|
static float getFps()
|
|
|
|
{
|
|
|
|
static std::queue<int64> time_queue;
|
|
|
|
|
|
|
|
int64 now = cv::getTickCount(), then = 0;
|
|
|
|
time_queue.push(now);
|
|
|
|
|
|
|
|
if (time_queue.size() >= 2)
|
|
|
|
then = time_queue.front();
|
|
|
|
|
|
|
|
if (time_queue.size() >= 25)
|
|
|
|
time_queue.pop();
|
|
|
|
|
|
|
|
return time_queue.size() * (float)cv::getTickFrequency() / (now - then);
|
|
|
|
}
|
|
|
|
|
|
|
|
static int bgColor[4] = {0, 0, 0, 0};
|
|
|
|
static cv::Mat* inputMat = NULL;
|
|
|
|
|
|
|
|
static void renderToD3DObject(void)
|
|
|
|
{
|
|
|
|
static int frame = 0;
|
|
|
|
|
|
|
|
const float fps = getFps();
|
|
|
|
|
2014-01-31 17:23:01 +01:00
|
|
|
String deviceName = cv::ocl::useOpenCL() ? cv::ocl::Context::getDefault().device(0).name() : "No OpenCL device";
|
2013-12-01 22:50:24 +01:00
|
|
|
|
|
|
|
if ((frame % std::max(1, (int)(fps / 25))) == 0)
|
|
|
|
{
|
|
|
|
String msg = format("%s%s: %s, Sample %d, Frame %d, fps %g (%g ms)",
|
|
|
|
g_disableProcessing ? "(FRAME PROCESSING DISABLED) " : "",
|
|
|
|
WINDOW_NAME, deviceName.c_str(), g_sampleType,
|
|
|
|
frame, fps, (int(10 * 1000.0 / fps)) * 0.1);
|
|
|
|
SetWindowText(hWnd, msg.c_str());
|
|
|
|
}
|
|
|
|
|
|
|
|
// 0..255
|
|
|
|
int c[4] =
|
|
|
|
{
|
|
|
|
std::abs((frame & 0x1ff) - 0x100),
|
|
|
|
std::abs(((frame * 2) & 0x1ff) - 0x100),
|
|
|
|
std::abs(((frame / 2) & 0x1ff) - 0x100),
|
|
|
|
0
|
|
|
|
};
|
|
|
|
|
|
|
|
int c1 = c[0] / 2 - 0x40 - bgColor[0];
|
|
|
|
int c2 = c[1] / 2 - 0x40 - bgColor[1];
|
|
|
|
int c3 = c[2] / 2 - 0x40 - bgColor[2];
|
|
|
|
|
|
|
|
switch (g_sampleType)
|
|
|
|
{
|
|
|
|
case 0:
|
|
|
|
#if defined(USE_D3D9) || defined (USE_D3DEX)
|
|
|
|
if (FAILED(dev->StretchRect(pReadOnlySurface, NULL, pBackBuffer, NULL, D3DTEXF_NONE)))
|
|
|
|
{
|
|
|
|
std::cerr << "Can't StretchRect()" << std::endl;
|
|
|
|
}
|
|
|
|
#elif defined(USE_D3D10)
|
|
|
|
dev->CopyResource(pBackBufferTexture, pInputTexture);
|
|
|
|
#elif defined(USE_D3D11)
|
|
|
|
devcon->CopyResource(pBackBufferTexture, pInputTexture);
|
|
|
|
#else
|
|
|
|
#error "Invalid USE_D3D value"
|
|
|
|
#endif
|
|
|
|
break;
|
|
|
|
|
|
|
|
case 1:
|
|
|
|
{
|
|
|
|
int BOXSIZE = 50;
|
|
|
|
int x = std::abs(((frame * 1) % (2 * (WIDTH - BOXSIZE))) - (WIDTH - BOXSIZE));
|
|
|
|
int y = std::abs(((frame / 2) % (2 * (HEIGHT - BOXSIZE))) - (HEIGHT - BOXSIZE));
|
|
|
|
cv::Rect boxRect(x, y, BOXSIZE, BOXSIZE);
|
|
|
|
#if defined(USE_D3D9) || defined (USE_D3DEX)
|
|
|
|
D3DLOCKED_RECT memDesc = {0, NULL};
|
|
|
|
RECT rc = {0, 0, WIDTH, HEIGHT};
|
|
|
|
if (SUCCEEDED(pCPUWriteSurface->LockRect(&memDesc, &rc, 0)))
|
|
|
|
{
|
|
|
|
if (!g_disableProcessing)
|
|
|
|
{
|
|
|
|
Mat m(Size(WIDTH, HEIGHT), CV_8UC4, memDesc.pBits, (int)memDesc.Pitch);
|
|
|
|
inputMat->copyTo(m);
|
|
|
|
m(boxRect).setTo(Scalar(c[0], c[1], c[2], 255));
|
|
|
|
}
|
|
|
|
pCPUWriteSurface->UnlockRect();
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
std::cerr << "Can't LockRect() on surface" << std::endl;
|
|
|
|
}
|
|
|
|
#elif defined(USE_D3D10)
|
|
|
|
D3D10_MAPPED_TEXTURE2D mappedTex;
|
|
|
|
if (SUCCEEDED(pCPUWriteTexture->Map( D3D10CalcSubresource(0, 0, 1), D3D10_MAP_WRITE_DISCARD, 0, &mappedTex)))
|
|
|
|
{
|
|
|
|
if (!g_disableProcessing)
|
|
|
|
{
|
|
|
|
Mat m(Size(WIDTH, HEIGHT), CV_8UC4, mappedTex.pData, (int)mappedTex.RowPitch);
|
|
|
|
inputMat->copyTo(m);
|
|
|
|
m(boxRect).setTo(Scalar(c[0], c[1], c[2], 255));
|
|
|
|
}
|
|
|
|
pCPUWriteTexture->Unmap(D3D10CalcSubresource(0, 0, 1));
|
|
|
|
dev->CopyResource(pBackBufferTexture, pCPUWriteTexture);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
std::cerr << "Can't Map() texture" << std::endl;
|
|
|
|
}
|
|
|
|
#elif defined(USE_D3D11)
|
|
|
|
D3D11_MAPPED_SUBRESOURCE mappedTex;
|
|
|
|
if (SUCCEEDED(devcon->Map(pCPUWriteTexture, D3D11CalcSubresource(0, 0, 1), D3D11_MAP_WRITE_DISCARD, 0, &mappedTex)))
|
|
|
|
{
|
|
|
|
if (!g_disableProcessing)
|
|
|
|
{
|
|
|
|
Mat m(Size(WIDTH, HEIGHT), CV_8UC4, mappedTex.pData, (int)mappedTex.RowPitch);
|
|
|
|
inputMat->copyTo(m);
|
|
|
|
m(boxRect).setTo(Scalar(c[0], c[1], c[2], 255));
|
|
|
|
}
|
|
|
|
devcon->Unmap(pCPUWriteTexture, D3D11CalcSubresource(0, 0, 1));
|
|
|
|
devcon->CopyResource(pBackBufferTexture, pCPUWriteTexture);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
std::cerr << "Can't Map() texture" << std::endl;
|
|
|
|
}
|
|
|
|
#else
|
|
|
|
#error "Invalid USE_D3D value"
|
|
|
|
#endif
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case 2:
|
|
|
|
{
|
|
|
|
static Mat m;
|
|
|
|
if (!g_disableProcessing)
|
|
|
|
{
|
|
|
|
#if 1
|
|
|
|
cv::add(*inputMat, Scalar(c1, c2, c3, 255), m);
|
|
|
|
#else
|
|
|
|
inputMat->copyTo(m);
|
|
|
|
#endif
|
|
|
|
cv::putText(m,
|
|
|
|
cv::format("Frame %d, fps %g (%g ms)",
|
|
|
|
frame, fps, (int(10 * 1000.0 / fps)) * 0.1),
|
|
|
|
cv::Point(8, 80), fontFace, 1, frameColor, 2);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
m.create(Size(WIDTH, HEIGHT), CV_8UC4);
|
|
|
|
}
|
|
|
|
try
|
|
|
|
{
|
|
|
|
#if defined(USE_D3D9) || defined (USE_D3DEX)
|
|
|
|
convertToDirect3DSurface9(m, pSurface, (void*)surfaceShared);
|
|
|
|
#elif defined(USE_D3D10)
|
|
|
|
convertToD3D10Texture2D(m, pBackBufferTexture);
|
|
|
|
#elif defined(USE_D3D11)
|
|
|
|
convertToD3D11Texture2D(m, pBackBufferTexture);
|
|
|
|
#else
|
|
|
|
#error "Invalid USE_D3D value"
|
|
|
|
#endif
|
|
|
|
}
|
|
|
|
catch (cv::Exception& e)
|
|
|
|
{
|
|
|
|
std::cerr << "Can't convert to D3D object: exception: " << e.what() << std::endl;
|
|
|
|
}
|
|
|
|
catch (...)
|
|
|
|
{
|
|
|
|
std::cerr << "Can't convert to D3D object" << std::endl;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
case 3:
|
|
|
|
{
|
|
|
|
static UMat tmp;
|
|
|
|
try
|
|
|
|
{
|
|
|
|
#if defined(USE_D3D9) || defined (USE_D3DEX)
|
|
|
|
convertFromDirect3DSurface9(pReadOnlySurface, tmp, (void*)readOnlySurfaceShared);
|
|
|
|
#elif defined(USE_D3D10)
|
|
|
|
convertFromD3D10Texture2D(pInputTexture, tmp);
|
|
|
|
#elif defined(USE_D3D11)
|
|
|
|
convertFromD3D11Texture2D(pInputTexture, tmp);
|
|
|
|
#else
|
|
|
|
#error "Invalid USE_D3D value"
|
|
|
|
#endif
|
|
|
|
}
|
|
|
|
catch (cv::Exception& e)
|
|
|
|
{
|
|
|
|
std::cerr << "Can't convert from D3D object: exception: " << e.what() << std::endl;
|
|
|
|
}
|
|
|
|
catch (...)
|
|
|
|
{
|
|
|
|
std::cerr << "Can't convert from D3D object" << std::endl;
|
|
|
|
}
|
|
|
|
static UMat res;
|
|
|
|
if (!g_disableProcessing)
|
|
|
|
{
|
|
|
|
cv::add(tmp, Scalar(c1, c2, c3, 255), res);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
res = tmp;
|
|
|
|
}
|
|
|
|
try
|
|
|
|
{
|
|
|
|
#if defined(USE_D3D9) || defined (USE_D3DEX)
|
|
|
|
convertToDirect3DSurface9(res, pSurface, (void*)surfaceShared);
|
|
|
|
#elif defined(USE_D3D10)
|
|
|
|
convertToD3D10Texture2D(res, pBackBufferTexture);
|
|
|
|
#elif defined(USE_D3D11)
|
|
|
|
convertToD3D11Texture2D(res, pBackBufferTexture);
|
|
|
|
#else
|
|
|
|
#error "Invalid USE_D3D value"
|
|
|
|
#endif
|
|
|
|
}
|
|
|
|
catch (cv::Exception& e)
|
|
|
|
{
|
|
|
|
std::cerr << "Can't convert to D3D object: exception: " << e.what() << std::endl;
|
|
|
|
}
|
|
|
|
catch (...)
|
|
|
|
{
|
|
|
|
std::cerr << "Can't convert to D3D object" << std::endl;
|
|
|
|
}
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
}
|
|
|
|
frame++;
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
static cv::Mat getInputTexture()
|
|
|
|
{
|
|
|
|
cv::Mat inputMat = cv::imread("input.bmp", cv::IMREAD_COLOR);
|
|
|
|
|
|
|
|
if (inputMat.depth() != CV_8U)
|
|
|
|
{
|
|
|
|
inputMat.convertTo(inputMat, CV_8U);
|
|
|
|
}
|
|
|
|
if (inputMat.type() == CV_8UC3)
|
|
|
|
{
|
|
|
|
cv::cvtColor(inputMat, inputMat, CV_RGB2BGRA);
|
|
|
|
}
|
|
|
|
if (inputMat.type() != CV_8UC4 || inputMat.size().area() == 0)
|
|
|
|
{
|
|
|
|
std::cerr << "Invalid input image format. Generate other" << std::endl;
|
|
|
|
inputMat.create(cv::Size(WIDTH, HEIGHT), CV_8UC4);
|
|
|
|
inputMat.setTo(cv::Scalar(0, 0, 255, 255));
|
|
|
|
bgColor[0] = -128; bgColor[1] = -128; bgColor[2] = 127; bgColor[3] = -128;
|
|
|
|
}
|
|
|
|
if (inputMat.size().width != WIDTH || inputMat.size().height != HEIGHT)
|
|
|
|
{
|
|
|
|
cv::resize(inputMat, inputMat, cv::Size(WIDTH, HEIGHT));
|
|
|
|
}
|
2014-01-31 17:23:01 +01:00
|
|
|
String deviceName = cv::ocl::useOpenCL() ? cv::ocl::Context::getDefault().device(0).name() : "No OpenCL device";
|
2013-12-01 22:50:24 +01:00
|
|
|
cv::Scalar color(64, 255, 64, 255);
|
|
|
|
cv::putText(inputMat,
|
|
|
|
cv::format("OpenCL Device name: %s", deviceName.c_str()),
|
|
|
|
cv::Point(8,32), fontFace, 1, color);
|
|
|
|
cv::putText(inputMat, WINDOW_NAME, cv::Point(50, HEIGHT - 32), fontFace, 1, color);
|
|
|
|
cv::putText(inputMat, USAGE_DESCRIPTION_0, cv::Point(30, 128), fontFace, 1, color);
|
|
|
|
cv::putText(inputMat, USAGE_DESCRIPTION_1, cv::Point(30, 192), fontFace, 1, color);
|
|
|
|
cv::putText(inputMat, USAGE_DESCRIPTION_2, cv::Point(30, 256), fontFace, 1, color);
|
|
|
|
cv::putText(inputMat, USAGE_DESCRIPTION_3, cv::Point(30, 320), fontFace, 1, color);
|
|
|
|
cv::putText(inputMat, USAGE_DESCRIPTION_SPACE, cv::Point(30, 448), fontFace, 1, color);
|
|
|
|
|
|
|
|
#if defined(USE_D3D9) || defined (USE_D3DEX)
|
|
|
|
cv::cvtColor(inputMat, inputMat, CV_RGBA2BGRA);
|
|
|
|
std::swap(bgColor[0], bgColor[2]);
|
|
|
|
#endif
|
|
|
|
|
|
|
|
// Make a global copy
|
|
|
|
::inputMat = new cv::Mat(inputMat);
|
|
|
|
|
|
|
|
return inputMat;
|
|
|
|
}
|
|
|
|
|
|
|
|
static int mainLoop()
|
|
|
|
{
|
|
|
|
hInstance = GetModuleHandle(NULL);
|
|
|
|
|
|
|
|
if (!initWindow())
|
|
|
|
CV_Error(cv::Error::StsError, "Can't create window");
|
|
|
|
|
|
|
|
if (!initDirect3D())
|
|
|
|
CV_Error(cv::Error::StsError, "Can't create D3D object");
|
|
|
|
|
|
|
|
if (cv::ocl::haveOpenCL())
|
|
|
|
{
|
|
|
|
#if defined(USE_D3D9)
|
2014-01-31 17:23:01 +01:00
|
|
|
cv::ocl::Context& ctx = cv::directx::ocl::initializeContextFromDirect3DDevice9(dev);
|
2013-12-01 22:50:24 +01:00
|
|
|
#elif defined (USE_D3DEX)
|
2014-01-31 17:23:01 +01:00
|
|
|
cv::ocl::Context& ctx = cv::directx::ocl::initializeContextFromDirect3DDevice9Ex(dev);
|
2013-12-01 22:50:24 +01:00
|
|
|
#elif defined(USE_D3D10)
|
2014-01-31 17:23:01 +01:00
|
|
|
cv::ocl::Context& ctx = cv::directx::ocl::initializeContextFromD3D10Device(dev);
|
2013-12-01 22:50:24 +01:00
|
|
|
#elif defined(USE_D3D11)
|
2014-01-31 17:23:01 +01:00
|
|
|
cv::ocl::Context& ctx = cv::directx::ocl::initializeContextFromD3D11Device(dev);
|
2013-12-01 22:50:24 +01:00
|
|
|
#else
|
|
|
|
#error "Invalid USE_D3D value"
|
|
|
|
#endif
|
|
|
|
std::cout << "Selected device: " << ctx.device(0).name().c_str() << std::endl;
|
|
|
|
g_sampleType = 2;
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
std::cerr << "OpenCL is not available. DirectX - OpenCL interop will not work" << std::endl;
|
|
|
|
}
|
|
|
|
|
|
|
|
if (!initDirect3DTextures())
|
|
|
|
CV_Error(cv::Error::StsError, "Can't create D3D texture object");
|
|
|
|
|
|
|
|
MSG msg;
|
|
|
|
ZeroMemory(&msg, sizeof(msg));
|
|
|
|
|
|
|
|
while (msg.message != WM_QUIT)
|
|
|
|
{
|
|
|
|
if (PeekMessage(&msg, NULL, 0U, 0U, PM_REMOVE))
|
|
|
|
{
|
|
|
|
TranslateMessage(&msg);
|
|
|
|
DispatchMessage(&msg);
|
|
|
|
}
|
|
|
|
else
|
|
|
|
{
|
|
|
|
render();
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
cleanUp();
|
|
|
|
|
|
|
|
return static_cast<int>(msg.wParam);
|
|
|
|
}
|
|
|
|
|
|
|
|
int main(int /*argc*/, char ** /*argv*/)
|
|
|
|
{
|
|
|
|
try
|
|
|
|
{
|
|
|
|
return mainLoop();
|
|
|
|
}
|
|
|
|
catch (cv::Exception& e)
|
|
|
|
{
|
|
|
|
std::cerr << "Exception: " << e.what() << std::endl;
|
|
|
|
return 10;
|
|
|
|
}
|
|
|
|
catch (...)
|
|
|
|
{
|
|
|
|
std::cerr << "FATAL ERROR: Unknown exception" << std::endl;
|
|
|
|
return 11;
|
|
|
|
}
|
|
|
|
}
|