반응형

CPU와 GPU의 이미지 처리 속도를 비교해 보자.

 

#include <iostream>
#include <opencv2/opencv.hpp>
#include <opencv2/cudafilters.hpp>
#include <opencv2/cudaimgproc.hpp>

#pragma comment(lib, "opencv_core4d.lib")
#pragma comment(lib, "opencv_highgui4d.lib")
#pragma comment(lib, "opencv_imgcodecs4d.lib")
#pragma comment(lib, "opencv_imgproc4d.lib")
#pragma comment(lib, "opencv_cudaimgproc4d.lib")
#pragma comment(lib, "opencv_cudafilters4d.lib")

int main() {
	cv::utils::logging::setLogLevel(cv::utils::logging::LOG_LEVEL_ERROR);

	cv::Mat image = cv::imread("palvin1.png");
	if (image.empty()) {
		std::cerr << "Error: Could not open image file" << std::endl;

		return -1;
	}

	int64 start;
	double timeSec;
	std::vector<cv::Vec4i> lines;

	// For CPU processing
	cv::Mat grayImage;
	cv::Mat resultImage;

	// For GPU processing
	cv::cuda::GpuMat gpuImage;
	cv::cuda::GpuMat gpuResultImage;
	cv::cuda::GpuMat gpuLines;
	cv::cuda::GpuMat gpuGrayImage;

	/////////////// CPU Processing ///////////////
	start = cv::getTickCount();

	cv::cvtColor(image, grayImage, cv::COLOR_BGR2GRAY);
	cv::HoughLinesP(grayImage, lines, 1, CV_PI / 180, 50, 50, 10); // 이 함수의 연산량이 굉장히 크다
	cv::Sobel(image, resultImage, CV_8U, 1, 0, 3);
	cv::Canny(grayImage, resultImage, 50, 150, 3);

	timeSec = (cv::getTickCount() - start) / cv::getTickFrequency();
	std::cout << "CPU Processing Time : " << timeSec << " sec" << std::endl;
	///////////////////////////////////////////////

	//////////////// GPU Processing ///////////////
	start = cv::getTickCount();

	gpuImage.upload(image);
    
	cv::cuda::cvtColor(gpuImage, gpuGrayImage, cv::COLOR_BGR2GRAY);

	cv::Ptr<cv::cuda::HoughSegmentDetector> houghDetector = cv::cuda::createHoughSegmentDetector(1, CV_PI / 180, 50, 50, 10);
	houghDetector->detect(gpuGrayImage, gpuLines);
	gpuLines.download(lines);

	cv::Ptr<cv::cuda::Filter> sobelFilter = cv::cuda::createSobelFilter(gpuImage.type(), CV_8UC3, 1, 0, 3);
	sobelFilter->apply(gpuImage, gpuResultImage);

	cv::Ptr<cv::cuda::CannyEdgeDetector> cannyDetector = cv::cuda::createCannyEdgeDetector(50, 150, 3);
	cannyDetector->detect(gpuGrayImage, gpuResultImage);

	gpuImage.download(image);

	timeSec = (cv::getTickCount() - start) / cv::getTickFrequency();
	std::cout << "GPU Processing Time : " << timeSec << " sec" << std::endl;
	////////////////////////////////////////////////

	cv::imshow("Original Image", image);

	cv::waitKey(0);

	cv::destroyAllWindows();

	return 0;
}

 

이미지 사이즈: 840 X 1260

 

 

나름 공정하게 비교했는데 정확한지는 모르겠다.

어쨌든 10배 이상의 속도 차이가 난다.

 

 

HoughLinesP()의 연산량이 커서 속도에 큰 차이가 벌어지는데, HoughLinesP()를 몇 번 더 호출하면 더 큰 차이를 보이게 된다.

 

	/////////////// CPU Processing ///////////////
	start = cv::getTickCount();

	cv::cvtColor(image, grayImage, cv::COLOR_BGR2GRAY);
	cv::HoughLinesP(grayImage, lines, 1, CV_PI / 180, 50, 50, 10);
	cv::HoughLinesP(grayImage, lines, 1, CV_PI / 180, 50, 50, 10);
	cv::HoughLinesP(grayImage, lines, 1, CV_PI / 180, 50, 50, 10);
	cv::Sobel(image, resultImage, CV_8U, 1, 0, 3);
	cv::Canny(grayImage, resultImage, 50, 150, 3);

	timeSec = (cv::getTickCount() - start) / cv::getTickFrequency();
	std::cout << "CPU Processing Time : " << timeSec << " sec" << std::endl;
	///////////////////////////////////////////////

	//////////////// GPU Processing ///////////////
	start = cv::getTickCount();

	gpuImage.upload(image);
    
	cv::cuda::cvtColor(gpuImage, gpuGrayImage, cv::COLOR_BGR2GRAY);

	cv::Ptr<cv::cuda::HoughSegmentDetector> houghDetector = cv::cuda::createHoughSegmentDetector(1, CV_PI / 180, 50, 50, 10);
	houghDetector->detect(gpuGrayImage, gpuLines);
	gpuLines.download(lines);

	houghDetector->detect(gpuGrayImage, gpuLines);
	gpuLines.download(lines);

	houghDetector->detect(gpuGrayImage, gpuLines);
	gpuLines.download(lines);

	cv::Ptr<cv::cuda::Filter> sobelFilter = cv::cuda::createSobelFilter(gpuImage.type(), CV_8UC3, 1, 0, 3);
	sobelFilter->apply(gpuImage, gpuResultImage);

	cv::Ptr<cv::cuda::CannyEdgeDetector> cannyDetector = cv::cuda::createCannyEdgeDetector(50, 150, 3);
	cannyDetector->detect(gpuGrayImage, gpuResultImage);

	gpuImage.download(image);

	timeSec = (cv::getTickCount() - start) / cv::getTickFrequency();
	std::cout << "GPU Processing Time : " << timeSec << " sec" << std::endl;
	////////////////////////////////////////////////

 

GPU가 약 37배 더 빠르다

 

반대로 HoughLinesP() 호출을 삭제하면 오히려 GPU보다 CPU가 더 빠른 결과를 보인다.

GPU 연산을 위한 메모리 복사 등의 오버헤드가 크기 때문이다.

	/////////////// CPU Processing ///////////////
	start = cv::getTickCount();

	cv::cvtColor(image, grayImage, cv::COLOR_BGR2GRAY);
	//cv::HoughLinesP(grayImage, lines, 1, CV_PI / 180, 50, 50, 10);
	cv::Sobel(image, resultImage, CV_8U, 1, 0, 3);
	cv::Canny(grayImage, resultImage, 50, 150, 3);

	timeSec = (cv::getTickCount() - start) / cv::getTickFrequency();
	std::cout << "CPU Processing Time : " << timeSec << " sec" << std::endl;
	///////////////////////////////////////////////

	//////////////// GPU Processing ///////////////
	start = cv::getTickCount();

	gpuImage.upload(image);
    
	cv::cuda::cvtColor(gpuImage, gpuGrayImage, cv::COLOR_BGR2GRAY);

	//cv::Ptr<cv::cuda::HoughSegmentDetector> houghDetector = cv::cuda::createHoughSegmentDetector(1, CV_PI / 180, 50, 50, 10);
	//houghDetector->detect(gpuGrayImage, gpuLines);
	//gpuLines.download(lines);

	cv::Ptr<cv::cuda::Filter> sobelFilter = cv::cuda::createSobelFilter(gpuImage.type(), CV_8UC3, 1, 0, 3);
	sobelFilter->apply(gpuImage, gpuResultImage);

	cv::Ptr<cv::cuda::CannyEdgeDetector> cannyDetector = cv::cuda::createCannyEdgeDetector(50, 150, 3);
	cannyDetector->detect(gpuGrayImage, gpuResultImage);

	gpuImage.download(image);

	timeSec = (cv::getTickCount() - start) / cv::getTickFrequency();
	std::cout << "GPU Processing Time : " << timeSec << " sec" << std::endl;
	////////////////////////////////////////////////

 

CPU가 약 두 배 더 빠르다

 

반응형
Posted by J-sean
:

[OpenCV] OpenCV with CUDA Build

2026. 4. 26. 00:16

보호되어 있는 글입니다.
내용을 보시려면 비밀번호를 입력하세요.

반응형

영상을 리사이즈하면 어떻게 변화하는지 살펴보자. (불필요한 로그 메세지도 없애보자)

 

2X2 데이터

#include <iostream>
#include <opencv2/opencv.hpp>

int main()
{
	// OpenCV 로그 레벨을 WARNING 또는 ERROR로 설정하여 불필요한 INFO 메시지를 숨김
	cv::utils::logging::setLogLevel(cv::utils::logging::LOG_LEVEL_WARNING);
	//LOG_LEVEL_VERBOSE: 모든 로그 출력(매우 상세)
	//LOG_LEVEL_INFO : 일반적인 정보 메시지만 출력
	//LOG_LEVEL_WARNING : 경고 메시지만 출력
	//LOG_LEVEL_ERROR : 실제 에러 발생 시에만 출력
	//LOG_LEVEL_FATAL : 프로그램이 중단될 정도의 치명적 에러만 출력
	//LOG_LEVEL_SILENT : 모든 로그 끄기

	char data[4] = {
		30, 60,
		90, 120
	};
	cv::Mat mat(2, 2, CV_8UC1, data);

	std::cout << "Original Mat: " << std::endl << mat << std::endl;

	cv::Mat resizedMat;

	cv::resize(mat, resizedMat, cv::Size(), 2, 2, cv::INTER_NEAREST);
	// INTER_NEAREST는 가장 가까운 픽셀의 값을 사용하여 이미지를 확대하는 방법이다.
	// 이 방법은 빠르지만, 확대된 이미지가 계단 현상(픽셀화)으로 보일 수 있다.
	std::cout << "Resized Mat (INTER_NEAREST): " << std::endl << resizedMat << std::endl;

	cv::resize(mat, resizedMat, cv::Size(), 2, 2, cv::INTER_LINEAR);
	// INTER_LINEAR는 주변 픽셀의 값을 선형 보간하여 이미지를 확대하는 방법이다.
	// 이 방법은 INTER_NEAREST보다 부드러운 결과를 제공하지만, 계산 비용이 더 높다.
	// 가장 일반적으로 사용되는 보간 방법으로, 대부분의 경우에 적절한 결과를 제공한다.
	std::cout << "Resized Mat (INTER_LINEAR): " << std::endl << resizedMat << std::endl;

	cv::resize(mat, resizedMat, cv::Size(), 2, 2, cv::INTER_CUBIC);
	// INTER_CUBIC는 주변 픽셀의 값을 3차 보간하여 이미지를 확대하는 방법이다.
	// 이 방법은 INTER_LINEAR보다 더 부드러운 결과를 제공하지만, 계산 비용이 더 높다.
	// 특히, 이미지가 크게 확대될 때 더 좋은 결과를 제공할 수 있다.
	std::cout << "Resized Mat (INTER_CUBIC): " << std::endl << resizedMat << std::endl;

	cv::resize(mat, resizedMat, cv::Size(), 2, 2, cv::INTER_LANCZOS4);
	// INTER_LANCZOS4는 주변 픽셀의 값을 Lanczos 보간을 사용하여 이미지를 확대하는 방법이다.
	// 이 방법은 가장 부드러운 결과를 제공하지만, 계산 비용이 가장 높다.
	// 특히, 이미지가 크게 확대될 때 가장 좋은 결과를 제공할 수 있다.
	std::cout << "Resized Mat (INTER_LANCZOS4): " << std::endl << resizedMat << std::endl;

	return 0;
}

 

4X4 크기의 이미지를 네 가지 방법으로 리사이즈한 결과

 

4X4 데이터

...
	char data[16] = {
		30, 30, 60, 60,
		30, 30, 60, 60,
		90, 90, 120, 120,
		90, 90, 120, 120
	};
	cv::Mat mat(4, 4, CV_8UC1, data);
...

 

 

8X8 데이터

...
	char data[64] = {
		30, 30, 30, 30, 60, 60, 60, 60,
		30, 30, 30, 30, 60, 60, 60, 60,
		30, 30, 30, 30, 60, 60, 60, 60,
		30, 30, 30, 30, 60, 60, 60, 60,
		90, 90, 90, 90, 120, 120, 120, 120,
		90, 90, 90, 90, 120, 120, 120, 120,
		90, 90, 90, 90, 120, 120, 120, 120,
		90, 90, 90, 90, 120, 120, 120, 120
	};
	cv::Mat mat(8, 8, CV_8UC1, data);
...

 

 

 

반응형
Posted by J-sean
:
반응형

히스토그램을 그리고 중간 빈도수(중앙값)의 위치를 찾아보자.

 

#include <iostream>
#include <opencv2/opencv.hpp>

// src의 히스토그램을 계산하고 중간 빈도수의 값(위치)을 반환하는 함수
int Median(const cv::Mat& src)
{
	cv::Mat hist;
	int nImages = 1;
	int channels[] = { 0 };
	int dims = 1;
	int histSize[] = { 256 };
	float graylevel[] = { 0, 256 };
	const float* ranges[] = { graylevel };

	cv::calcHist(&src, nImages, channels, cv::noArray(), hist, dims, histSize, ranges);

	// 히스토그램에서 각 밝기 값의 빈도수를 모두 더해서 총 빈도수를 계산한다
	//int total = 0;
	//for (int i = 0; i < hist.rows; ++i)
	//	total += hist.at<float>(i, 0);
	// 그러나 위 코드는 비효율적이다. src 이미지의 총 픽셀 수를 직접 계산하여 총 빈도수로 사용할 수 있다.
	int total = src.total(); // src.total()는 src 이미지의 총 픽셀 수를 반환한다.

	int median = 0;
	int cumulative = 0;
	// 누적 빈도수가 총 빈도수의 절반에 도달하는 위치값을 찾는다
	for (int i = 0; i < hist.rows; ++i) {
		cumulative += hist.at<float>(i, 0);
		if (cumulative >= total / 2) {
			median = i;
			break;
		}
	}

	std::cout << "Total: " << total << std::endl <<
		"Cumulative: " << cumulative << std::endl <<
		"Median Index: " << median << std::endl;

	return median;
}

cv::Mat getHistImage(const cv::Mat& src)
{
	int nImages = 1;
	int channels[] = { 0 };
	cv::Mat hist; // calcHist()에서 반환되는 hist는 256X1 크기의 행렬로, 각 행은 해당 밝기 값의 빈도수를 나타낸다.
	int dims = 1;
	const int histSize[] = { 256 };
	float graylevel[] = { 0, 256 };
	const float* ranges[] = { graylevel };

	cv::calcHist(&src, nImages, channels, cv::noArray(), hist, dims, histSize, ranges);

	double maxVal;
	// 히스토그램에서 최대 빈도수 값을 찾는다
	cv::minMaxLoc(hist, nullptr, &maxVal);

	cv::Mat histImage(100, 256, CV_8UC3, cv::Scalar(255, 255, 255));
	for (int i = 0; i < 256; ++i)
		// 각 빈도수를 최대 빈도수로 정규화하여 100픽셀 높이로 표현한다
		cv::line(histImage, cv::Point(i, 100), cv::Point(i, 100 - cvRound(hist.at<float>(i, 0) / maxVal * 100)), cv::Scalar(0));

	// 옵션: 히스토그램에서 중간빈도수 위치에 빨간색 선을 그려서 시각적으로 표시한다
	int midFreq = Median(src);
	cv::line(histImage, cv::Point(midFreq, 100), cv::Point(midFreq, 0), cv::Scalar(0, 0, 255));

	return histImage;
}

int main() {
	cv::Mat image = cv::imread("palvin1.png");
	if (image.empty()) {
		std::cerr << "Could not read the image" << std::endl;
		return 1;
	}

	cv::cvtColor(image, image, cv::COLOR_BGR2GRAY);
	cv::Mat histImage = getHistImage(image);

	cv::imshow("Image", image);
	cv::imshow("Histogram", histImage);

	cv::waitKey(0);

	cv::destroyAllWindows();

	return 0;
}

 

 

 

빈도수의 중앙값 위치를 빨간색 선으로 표시한다.

 

반응형
Posted by J-sean
:

보호되어 있는 글입니다.
내용을 보시려면 비밀번호를 입력하세요.

보호되어 있는 글입니다.
내용을 보시려면 비밀번호를 입력하세요.

보호되어 있는 글입니다.
내용을 보시려면 비밀번호를 입력하세요.

반응형

1개 이상의 동영상 파일을 로드하고 재생해 보자.

 

아래 코드는 각각의 영상을 독립적으로 처리하기 때문에 시간 표시도 개별적으로 되고 앞뒤 이동도 제한된다.

 

#include <iostream>
#include <vector>
#include <filesystem>
#include <opencv2/opencv.hpp>

int main()
{
	std::vector<std::string> videoFiles;
	std::string folderPath = "./";

	for (const std::filesystem::directory_entry& entry : std::filesystem::directory_iterator(folderPath))
	{
		if (entry.is_regular_file() && entry.path().extension() == ".mkv")
		{
			videoFiles.push_back(entry.path().string());
		}
	}

	std::cout << "Found " << videoFiles.size() << " .mkv files in the folder:" << std::endl;
	for (const std::string& file : videoFiles)
	{
		std::cout << file << std::endl;
	}

	std::vector<cv::VideoCapture> videoCaptures;
	for (const std::string& file : videoFiles)
	{
		cv::VideoCapture cap(file);
		if (!cap.isOpened())
		{
			std::cerr << "Error opening video file: " << file << std::endl;
			continue;
		}
		videoCaptures.push_back(std::move(cap)); // Move the VideoCapture object into the vector
	}

	std::cout << "Successfully opened " << videoCaptures.size() << " video files." << std::endl;

	cv::Mat frame;
	double timestamp = 0.0;
	bool quit = false;
	std::string time;
	int key = 0;

	for (size_t i = 0; i < videoCaptures.size(); ++i)
	{
		std::cout << "Playing video: " << videoFiles[i] << std::endl;
		videoCaptures[i] >> frame;

		while (!frame.empty())
		{
			timestamp = videoCaptures[i].get(cv::CAP_PROP_POS_MSEC); // Get the current timestamp
			time = std::to_string(timestamp / 1000.0);
			time = time.substr(0, time.find(".") + 3); // Keep only 2 decimal places

			cv::putText(frame, time, cv::Point(50, 50), cv::FONT_HERSHEY_SIMPLEX, 1.0, cv::Scalar(0, 255, 0), 2);
			cv::imshow("Video", frame);

			key = cv::waitKey(33);
			if (key == 27)
			{
				quit = true;
				break;
			}
			else if (key == 'f')
				// 5초 앞으로
				videoCaptures[i].set(cv::CAP_PROP_POS_MSEC, timestamp + 5.0 * 1000.0);
			else if (key == 'b')
				 // 5초 뒤로
				videoCaptures[i].set(cv::CAP_PROP_POS_MSEC, timestamp - 5.0 * 1000.0);

			videoCaptures[i] >> frame;
		}

		if (quit)
			break;
	}

	for (cv::VideoCapture& cap : videoCaptures)
		cap.release();

	cv::destroyAllWindows();

	return 0;
}

 

f: 5초 앞으로

b: 5초 뒤로

 

 

아래 코드는 각각의 영상을 하나의 영상인 것처럼 시간을 표시하고 5초 앞뒤로 이동할 때도 각 영상을 자연스럽게 이동한다.

 

#include <iostream>
#include <vector>
#include <filesystem>
#include <opencv2/opencv.hpp>

int main()
{
	std::vector<std::string> videoFiles;
	std::string folderPath = "./";

	for (const std::filesystem::directory_entry& entry : std::filesystem::directory_iterator(folderPath))
	{
		if (entry.is_regular_file() && entry.path().extension() == ".mkv")
		{
			videoFiles.push_back(entry.path().string());
		}
	}

	std::cout << "Found " << videoFiles.size() << " .mkv files in the folder:" << std::endl;
	for (const std::string& file : videoFiles)
	{
		std::cout << file << std::endl;
	}

	std::vector<cv::VideoCapture> videoCaptures;
	for (const std::string& file : videoFiles)
	{
		cv::VideoCapture cap(file);
		if (!cap.isOpened())
		{
			std::cerr << "Error opening video file: " << file << std::endl;
			continue;
		}
		videoCaptures.push_back(std::move(cap)); // Move the VideoCapture object into the vector
	}

	std::cout << "Successfully opened " << videoCaptures.size() << " video files." << std::endl;

	cv::Mat frame;
	double timestamp = 0.0;
	bool quit = false;
	std::string time;
	double duration = 0.0;
	double move = 5000.0; // 5 seconds in milliseconds
	int key = 0;

	for (size_t i = 0; i < videoCaptures.size(); ++i)
	{
		std::cout << "Playing video: " << videoFiles[i] << std::endl;
		videoCaptures[i] >> frame;

		duration = 0.0;
		for (int j = 0; j < i; j++) {
			duration += (videoCaptures[j].get(cv::CAP_PROP_FRAME_COUNT) / videoCaptures[j].get(cv::CAP_PROP_FPS));
		}
		duration *= 1000.0; // Convert to milliseconds

		while (!frame.empty())
		{
			timestamp = duration + videoCaptures[i].get(cv::CAP_PROP_POS_MSEC); // Get the current timestamp
			time = std::to_string(timestamp / 1000.0);
			time = time.substr(0, time.find(".") + 3); // Keep only 2 decimal places

			cv::putText(frame, time, cv::Point(50, 50), cv::FONT_HERSHEY_SIMPLEX, 1.0, cv::Scalar(0, 255, 0), 2);
			cv::imshow("Video", frame);

			key = cv::waitKey(33);
			if (key == 27)
			{
				quit = true;
				break;
			}
			else if (key == 'f')
			{
				// 현재 영상에서 5초 앞으로 가는 것이 가능한 경우, 5초 앞으로 이동
				if (videoCaptures[i].get(cv::CAP_PROP_POS_MSEC) + move < videoCaptures[i].get(cv::CAP_PROP_FRAME_COUNT) / videoCaptures[i].get(cv::CAP_PROP_FPS) * 1000.0)
					videoCaptures[i].set(cv::CAP_PROP_POS_MSEC, videoCaptures[i].get(cv::CAP_PROP_POS_MSEC) + move);
				// 현재 영상에서 5초 앞으로 가는 것이 불가능한 경우, 가능한 만큼만 이동하고 다음 영상으로 이동해서 남은 시간만큼 앞으로 이동
				else if (i < videoCaptures.size() - 1)
				{
					videoCaptures[i + 1].set(cv::CAP_PROP_POS_MSEC, move - (videoCaptures[i].get(cv::CAP_PROP_FRAME_COUNT) / videoCaptures[i].get(cv::CAP_PROP_FPS) * 1000.0 - videoCaptures[i].get(cv::CAP_PROP_POS_MSEC)));

					// 현재 영상(i)을 나중에 다시 재생할 때 처음부터 재생될 수 있도록 위치를 0으로 초기화하는건 불필요.
					//videoCaptures[i].set(cv::CAP_PROP_POS_MSEC, 0.0);

					break;
				}
				else
				{
					//videoCaptures[i].set(cv::CAP_PROP_POS_MSEC, videoCaptures[i].get(cv::CAP_PROP_FRAME_COUNT) / videoCaptures[i].get(cv::CAP_PROP_FPS) * 1000.0);
				}

			}
			else if (key == 'b')
			{
				// 현재 영상에서 5초 뒤로 가는 것이 가능한 경우, 5초 뒤로 이동
				if (videoCaptures[i].get(cv::CAP_PROP_POS_MSEC) > move)
					videoCaptures[i].set(cv::CAP_PROP_POS_MSEC, videoCaptures[i].get(cv::CAP_PROP_POS_MSEC) - move);
				// 현재 영상에서 5초 뒤로 가는 것이 불가능한 경우, 가능한 만큼만 이동하고 이전 영상으로 이동해서 남은 시간만큼 뒤로 이동
				else if (i > 0)
				{
					videoCaptures[i - 1].set(cv::CAP_PROP_POS_MSEC, videoCaptures[i - 1].get(cv::CAP_PROP_FRAME_COUNT) / videoCaptures[i - 1].get(cv::CAP_PROP_FPS) * 1000.0 - (move - videoCaptures[i].get(cv::CAP_PROP_POS_MSEC)));

					// 현재 영상(i)을 곧(5초 이내) 다시 재생할 때 처음부터 재생될 수 있도록 위치를 0으로 초기화.
					videoCaptures[i].set(cv::CAP_PROP_POS_MSEC, 0.0);

					i -= 2; // 다음 루프에서 i++ 되므로 -2

					break;
				}
				else
				{
					videoCaptures[i].set(cv::CAP_PROP_POS_MSEC, 0.0);
				}
			}

			videoCaptures[i] >> frame;
		}

		if (quit)
			break;
	}

	for (cv::VideoCapture& cap : videoCaptures)
		cap.release();

	cv::destroyAllWindows();

	return 0;
}

 

반응형
Posted by J-sean
: