반응형

OpenCV에서는 cornerHarris( ) 함수를 통해 쉽게 코너를 검출할 수 있도록 지원하고 있다.

 

하지만 상황에 따라 해당 함수를 사용하여 적용하기보단 나의 상황에 맞도록 코너검출의 과정을 수정하고 싶을 때가 있다. 이를 위해 opencv의 cornerHarris( ) 함수를 사용하지 않고 직접 구현하여 코너를 검출하는 과정에 대해 설명한다.

 

이를 위해 우선 어떻게 코너를 검출하는지 과정부터 살펴볼 필요가 있다.

 

기본적으로 2차원의 GrayScale 이미지는 아래와 같은 형태를 갖고 있다.

위의 그림에서 하나의 네모칸을 '픽셀'이라고 표현하며 GrayScale의 이미지는 0~255의 정수로 표현되는 픽셀값을 갖고 있다.

그럼 여기서 코너를 어떻게 정의할 수 있을까?

 

위의 그림에서 (a)는 그냥 아무런 특징이 나타나지 않는 면에 해당된다.

(b)의 경우 '엣지'라고 불리는 부분이며 말 그대로 모서리에 대한 부분을 나타내고 있다. 이러한 경우 x축의 방향으로는 픽셀값의 차이가 뚜렷하게 나타나지만(영상이 밝은곳에서 어두운곳으로 진행하다가 다시 밝은 부분이 나오게 된다.) y축 방향으로는 영상의 픽셀에 대한 변화가 나타나지 않게 된다. 다시 말해 분홍색 윈도우가 x축으로 이동하게 되면 변화가 발생하지만 y축으로 이동하면 변화가 발생하지 않는다. 이러한 부분을 엣지라고 정의한다.

(c)의 그림이 코너에 해당하는 부분으로 분홍색 윈도우를 x축, y축으로 이동시켜보면 어느 방향으로 이동시켜도 윈도우 내 영상에서 변화가 감지되게 된다.  이를 코너의 특징으로 볼 수 있다.

 

그럼 영상에서 코너를 왜 취득하는걸까? 코너가 갖는 의미는 무엇일까?

 

좌측의 그림과 같은 바다의 풍경이 담긴 사진이 있다고 보자.

우리는 A윈도우의 정보를 갖고 A가 원본 이미지의 어떤 위치를 나타내는지 알 수 있는가?

하늘은 모두 A와 유사하기에 "A가 정확히 여기이다!" 라고 말하기 어렵다.

 

B역시 바다의 수평선에 대한 정보(엣지)만 존재하므로 이 부분이 정확히 바다의 어떤 위치를 나타내는지는 알기 어렵다.

 

 

하지만 C는 어떤가. C의 위치는 원본 이미지에서 정확이 어떠한 부분인지를 나타낼 수 있는 정보를 보여주고 있다. 그리고 이는 '코너'정보를 갖고 있다. 이렇게 코너는 영상의 특징이 될 수 있는 정보를 담고있으므로 Feature Point(특징 점)으로 표현한다.

 

이를 수식으로 표현하면 다음과 같이 나타낼 수 있다.

내용을 보면 영상의 변화율 E는 영상 내 윈도우 w의 (x, y)에 대하여 이미지 I를 x축, y축으로 이동시켰을 때 나타나는 변화율의 크기를 통해 표현하고 있다.

위의 값이 크다면 영상의 x, y에 대한 차이가 크다는 것이고 이는 코너임을 나타낼 수 있다.

 

그럼 이 식을 조금 변형하여 보자.

 

 

반응형
반응형

C++로 코드작업을 할때 배열을 많이 사용하게 된다.

 

기본적으로 배열은 처음 선언할때 크기와 타입을 지정하게 된다.

이를 동적배열이라 한다.

 

그런데 가끔 이 배열의 크기를 임의로 늘리거나 줄이고 싶은 때가 있다.

 

어디서 뭔가 좀 들어본 사람은 "크기를 동적으로 쓰고싶은 상황이니까 동적배열을 쓰는건가?" 할 수 있겠지만

동적배열은 조금 다른 개념이다.

 

동적배열은 배열의 크기로 변수를 받아와 일정한 크기의 배열을 생성하는 것이다.

 

기본 정적배열을 선언할때에는 변수의 값이 들어가지 못하지만 동적배열을 통해 상황에 맞는

변수의 크기로 동적 메모리를 할당할 수 있게 된다.

 

하지만 지금 우리가 하고싶은 것은 배열의 크기를 필요에 따라 늘리고 줄이는 과정을 하고싶은 것이다.

 

이를 위해 보통 '리스트' 혹은 '벡터'의 개념을 많이 사용한다.

 

벡터(vector)는 내부적으로 배열의 구조를 갖고 있다.

따라서 항목이 추가되거나 삭제될 때는 내부적으로 임시 배열을 생성하여 복사한 후

항목들을 이동시키게 된다.

 

즉 쉽게  다룰 수 있으나 메모리의 문제들이 발생할 수 있게 된다.

 

이와 반대로 리스트는 포인터의 개념을 통해 메모리에 직접 접근하는 연결구조를 갖도록 구성한다.

 

즉 항목들은 포인터를 통해 연결되어 있고 항목이 삭제될 경우 연결이 끊어지는 과정을 진행하므로서

불필요한 배열의 복사과정을 없앨 수 있기에 메모리적인 측면에서 장점이 존재한다.

 

하지만 리스트의 경우 원하는 순서의 항목을 출력하거나 할 때 어려움이 발생한다.

반응형
반응형

간단하게 marker array를 출력하여 RVIZ로 시각화할 수 있는 패키지를 만들어보려한다.

 

우선 work space를 하나 생성하고 그 안에 src폴더를 생성한다.

src폴더 안에는 만들고자 하는 패키지의 이름의 파일을 생성해주자.

 

나는 test라는 이름으로 패키지를 생성해보고자 한다.

 

즉 workspace/src/test/ 까지의 폴더를 생성한다.

 

그럼 우선 그 패키지에서 빌드될 소스파일을 담을 수 있는 src폴더를 다시 하나 만든다.

 

그리고 패키지 이름의 src폴더에 작성하고자하는 소스코드를 입력한다.

 

나는 marker array를 출력할 것이기에 publisher라는 이름의 cpp파일을 생성하였다.

#include "ros/ros.h"
#include "std_msgs/String.h"
#include "visualization_msgs/Marker.h"

#include<sstream>

int main(int argc, char **argv)
{
    ros::init(argc, argv, "publisher");
    ros::NodeHandle n;

    ros::Publisher vis_pub = n.advertise<visualization_msgs::Marker>( "visualization_marker", 1000 );

    while(ros::ok()){
    visualization_msgs::Marker marker;
	marker.header.frame_id = "base_link";
	marker.header.stamp = ros::Time();
	marker.ns = "my_namespace";
	marker.id = 0;
	marker.type = visualization_msgs::Marker::SPHERE;
	marker.action = visualization_msgs::Marker::ADD;
	marker.pose.position.x = 1;
	marker.pose.position.y = 1;
	marker.pose.position.z = 1;
	marker.pose.orientation.x = 0.0;
	marker.pose.orientation.y = 0.0;
	marker.pose.orientation.z = 0.0;
	marker.pose.orientation.w = 1.0;
	marker.scale.x = 1;
	marker.scale.y = 1;
	marker.scale.z = 1;
	marker.color.a = 1.0; // Don't forget to set the alpha!
	marker.color.r = 0.0;
	marker.color.g = 1.0;
	marker.color.b = 0.0;
	//only if using a MESH_RESOURCE marker type:
	marker.mesh_resource = "package://pr2_description/meshes/base_v0/base.dae";
    
	vis_pub.publish( marker );
	ros::spinOnce();
    }
    return 0;
}

 

roscpp 코드에 대한 내용은 앞의 게시물을 참고하여 작성하였다.

 

다음으론 이를 빌드하기 위해 필요한 CMakeLists.txt 파일을 작성해주는 것이다.

 

 

 

 

CMakeLists.txt파일은 패키지 이름의 폴더에서 작성해준다.(src폴더 밖에서 작성)

 

CMakeLists.txt 파일의 내용은 아래와 같다.

cmake_minimum_required(VERSION 3.0.2)
project(test)

find_package(catkin REQUIRED COMPONENTS roscpp rospy std_msgs visualization_msgs genmsg)

generate_messages(DEPENDENCIES visualization_msgs)


include_directories(include ${catkin_INCLUDE_DIRS})

add_executable(publisher src/publisher.cpp)
target_link_libraries(publisher ${catkin_LIBRARIES})
#add_dependencies(publisher test_generate_messages_cpp)

 

CMakeLists에서 publisher에 해당되는 부분은 내가 만들고자하는 실행파일의 이름이다.

 

그럼 마지막으로 패키지 생성을 위한 package.xml파일을 작성해보자.

 

이는 CMakeLists.txt를 작성했던 위치와 동일한 위치에서 진행한다.

<package format="2">
<name>test</name>
<version>1.2.4</version>
<description> This package provides foo capability. </description>
<maintainer email="foobar@foo.bar.willowgarage.com">PR-foobar</maintainer>
<license>BSD</license>
<buildtool_depend>catkin</buildtool_depend>
<build_depend>roscpp</build_depend>
<build_depend>std_msgs</build_depend>
<build_depend>visualization_msgs</build_depend>
<exec_depend>roscpp</exec_depend>
<exec_depend>std_msgs</exec_depend>
<exec_depend>visualization_msgs</exec_depend>
</package>

 

이렇게 작성한 후 모든 내용을 저장하고

workspace로 돌아와 catkin_make를 통해 빌드를 진행한다.

 

그리고 source로 마무리 해주면

 

rosrun test publisher  가 실행된다.

 

코드에 대한 자세한 내용은 추후에 수정을 통해 다루도록 하겠다.

 

 

하지만 위의 내용을 진행하고 RVIZ를 통해 marker를 확인해보면 base_link에 대한 정보가 존재하지 않으므로

아래와 같은 문제가 발생함을 확인할 수 있다.

 

이는 말 그대로 고정 좌표계가 없어서 발생하는 문제로 mark array의 좌표를 지정해주긴 하였으나

어느 좌표계를 기준으로 marker를 생성할지 명확하지 않아 발생하는 문제이다.

 

이럴 경우

$ rosrun tf static_transform_publisher 0 0 0 0 0 0 1 map base_link 10

 

위의 코드를 통해 해결할 수 있다.

 

그럼 아래와 같은 이쁜 구형이 publish되어 시각화되는 것을 확인할 수 있다.

반응형
반응형

함수들을 동시에 실행한다.

 

보통 다중 스레드나 멀티 프로세스가 머리속에 떠오를 것이다.

 

그럼 멀티스레드와 멀티프로세스의 차이는 무엇일까?

둘 다 동시에 무엇인가를 실행하는 것 아닌가?

 

설명들을 찾다보면 메모리가 어떻다느니, 시스템 자원이 어떻다느니 어려운 말들이 많다.

물론 좋은 설명들이지만 초보자가 이해하기는 어려운 감이 있다.

 

CPU에는 코어가 있다.

원래 CPU는 병렬처리가 안된다. 하지만 코어수를 늘림으로서 CPU의 역할을 분담하게 되고

이를 통해 병렬처리가 가능하게 된다. 이를 멀티스레드라 한다.

 

멀티프로세스는 하나의 CPU코어에서 다수의 프로세스를 짧게짧게 진행하는 것이다.

이를 통해 작업자는 여러개의 프로세스들이 동시에 진행되는 것 처럼 보이지만 사실은

짧게짧게 여러 프로세스들이 빠르게 진행되는 것이다.

 

그럼 본론으로 들어와 파이썬에서 멀티프로세스를 통해 여러 함수들을 실행시켜보자.

 

 

from multiprocessing import Process
import time


def print_a():
    for i in range(500):
        print("Process A!")
        time.sleep(0.05)


def print_b():
    for i in range(500):
        print("Process B!")
        time.sleep(0.07)


if __name__ == '__main__':

    p_a = Process(target=print_a)
    p_b = Process(target=print_b)
    count = 0
    while True:
        count = count + 1
        print("count: ", count)
        if count == 100000000:
            count = 0
        if count == 50:
            p_a.start()
        if count == 70:
            p_b.start()
        time.sleep(0.1)
    p_a.join()
    p_b.join()

 

함수는 "Process A!"를 출력하는 함수와 "Process B!"를 출력하는 함수가 있으며

이들은 무한루프에서 실행된다.

 

그럼 프로세스는 while을 실행하는 프로세스와 print_a() 함수, print_b() 함수가 실행되게 된다.

 

실행 결과는 좌측의 사진과 같다.

 

count도 출력하고 A, B 함수들이 실행되어 각각의 문자들도 출력된다.

반응형
반응형

포인터 변수는 해당 변수의 주소를 가리킨다고 배웠다.

말 그대로이다.

 

이를 간단히 정리하면 아래와 같다.

int num = 3;
int *ptr = &num;

printf("num: %d", ptr);		// num변수가 저장된 주소를 가리킨다.
printf("arr: %d", *ptr);	// num에 저장된 값인 3을 반환한다.
printf("arr: %d", &ptr);	// ptr변수의 주소를 반환한다.

 

그럼 포인터변수 ptr에 다른 변수의 주소값을 지정하려면 어떻게 해야할까?

아래 코드를 살펴보자.

	int num1 = 3;
	int num2 = 6;
	int* ptr = &num1;
	printf("ptr: %d\n", ptr);	// num1의 주소값을 반환한다.
	printf("*ptr: %d\n", *ptr);	// num1의 변수 값을 반환한다.
	printf("&ptr: %d\n\n", &ptr);	// ptr변수의 주소값을 반환한다.

	ptr = &num2;
	printf("ptr: %d\n", ptr);	// num2의 주소값을 반환한다.
	printf("*ptr: %d\n", *ptr);	// num2의 변수 값을 반환한다.
	printf("&ptr: %d\n", &ptr);	// ptr변수의 주소값을 반환하며 이는 위와 같다.

변수의 주소가 저장되는 포인터변수 p에 다른 변수의 주소를 할당해줌으로서 위와 같은 결과를

확인할 수 있다.

 

 

그럼 const char *p = "Hellow" 로 지정되는 문자열 상수는 어떻게 이해할 수 있을까?

 

위와 같이 포인터변수 p에는 문자열 "Hellow"를 담고있는 주소값이 저장된다.

그럼 포인터변수 p가 다른 문자열을 가리키게 하려면 어떻게 해야할까?

 

이럴 경우 다른 문자열의 값을 p에 할당해주면 된다.

 

	const char *p = "Hello";
	printf("string1: %s\n", p);
	
	p = "Good Bye";
	printf("string2: %s\n", p);

위의 코드에서 p = "Good Bye" 로 지정해주는 과정을 통해

p변수에는 "Good Bye"라는 문자열의 시작 주소가 담기게 된다.

 

int형의 경우 &변수 를 통해 포인터변수에 주소값을 지정해주어야 했지만

문자열의 경우 단순히 문자열을 지정해주면 해당 문자열의 주소값이 대입되는 것으로 보인다.

 

반응형
반응형

앞의 과정에서 github 원격 저장소에 내용을 push하고 원격저장소의 내용을 불러오는 pull과정을 살펴봤다.

 

앞의 과정들은 모두 master branch에서 작업되었다.

 

하지만 프로젝트 작업을 진행하다보면 현재 프로젝트에서 새로운 기능을 넣거나 실험해보고 싶은 내용이 생길 경우가 있다. 이 경우 원본 파일을 수정하게 되면 다시 원점으로 돌아오기 힘든 상황이 발생할 수 있기에 현재까지는 해당 파일을 다른이름으로 복사하여 작업하고 실험해보는 과정을 진행했었다.

 

하지만 Git에서는 이러한 과정을 쉽게 다룰 수 있도록 서비스를 제공한다.

 

Branch가 이러한 내용을 의미한다. 나무에서 가지가 뻗어나가 듯 원본 파일에서 여러 파일들을 뻗어나간다는 의미로 볼 수 있을 것이다.

 

우선 작업환경을 셋팅하자.

디렉토리를 생성한 후 해당 디렉토리에 기존의 master branch에 대한 내용을 불러오자.

나는 test2라는 폴더를 생성한 후 해당 폴더에 master branch의 내용을 불러왔다.

 

 

이제 여기서 Readme.txt파일을 수정한 후 저장하였다.

그리고 추가적은 branch를 생성하기 위해 다음 명령어를 사용하였다.

 

우선 $ git branch 명령어를 통해 현재 어떠한 branch가 존재하는지 알 수 있다.

처음에는 아무런 branch도 생성하지 않았으므로 master branch만 존재하고 branch 생성을 위해

$ git branch another

위의 명령어를 통해 "another"이라는 이름의 branch를 생성하였다.

 

another 이라는 이름의 branch를 생성한 후 제대로 생성되었는지 확인하기 위해

$ git branch  명령어를 다시 입력해보면 위의 그림과 같이 another과 master이 둘 다 출력되고 master에 *표시가 붙은 것을 확인할 수 있다. 즉 현재 master branch 상태의 환경이라는 것이다.

 

$ git checkout another  명령어를 통해 현재 환경을 another branch로 변경해주자.

그럼 커맨드라인의 끝에 ( master ) 로 표시되던 부분이  ( another ) 로 변경되는 것을 확인할 수 있다.

 

그럼 이제 변경된 Readme.txt 파일을 add, commit, push 해주자.

 

 

이 과정은 앞의 과정과 동일하다.

 

그럼 이제 깃허브 저장소로 이동하여 제대로 branch가 생성되고 적용되었는지 살펴보자.

 

변경된 내용이 잘 적용된 것을 확인할 수 있다.

 

반응형
반응형

앞의 내용에서 인터넷의 저장소에 접근하여 로컬 데이터를 업로드(push)하는 과정까지 다뤘다.

 

그럼 깃허브 저장소에 있는 파일을 불러와 수정하고 다시 업로드(push)하는 과정을 진행해보자.

 

우선 새로운 폴더 test1을 생성한 후 git init을 통해 초기화해준다.

 

그럼 이제 해당 txt파일을 수정하자.

원래 아무 내용도 없던 텍스트파일에 임의의 문구를 작성한 후 저장하자.

 

그리고 git status를 확인해보면 다음과 같다.

 

그럼 이제 변경된 파일을 push하여 깃허브 저장소에 업로드하자.

 

그럼 이제 다시 깃허브 저장소로 이동하여 확인하여보자.

 

저장소에서 붉은색으로 표시한 부분을 클릭해보자.

 

그리고 branch들이 표시되는 부분에 master로 설정하면 초기 저장소에 Readme.txt파일을 생성하여 push했을 때, Edit Readme.txt 라는 커밋메시지의 내용을 push했을 때, 그리고 추가적으로 Secondary Edit on Readme.txt 라는 커밋메시지의 내용을 push하였고 모든 history가 확인되는 것을 볼 수 있다.

 

그럼 Readme.txt파일은 가장 마지막에 수정한 내용으로 작성되어 있음을 확인할 수 있다.

반응형
반응형

아래 블로그들을 참고하여 작성하였다.

 

https://blog.naver.com/kdj9502/222199333574

https://blog.weirdx.io/post/816

 

 

가장먼저 깃을 사용하기 위해 아래 사이트로 이동하여 git을 다운로드한다.

 

http://www.git-scm.com/download/win

 

Git - Downloading Package

Downloading Git Now What? Now that you have downloaded Git, it's time to start using it.

www.git-scm.com

 

설치과정은 다음과 같다.(사진은 위의 참고 블로그에 게시된 자료를 사용하였다.)

 

기본 셋팅으로 진행하다가 다음과 같이 편집기를 어떤걸 사용할지 묻는 단계가 나온다.

 

다양한 편집기툴이 있는데 나는 Vim을 사용하지 않고 nano 편집툴을 사용하였다.

vim을 잘 사용하는 사람은 vim이 편하다고 많이 얘기하는데 나는 우분투환경에서도 nano를 많이 사용하였고

vim은 이래저래 단축키들을 사용하여 작업을 해야하는 반면 nano는 어느정도 익숙한 환경이기에 nano로 설정하였다.

위의 내용은 보안서버에 접속하기 위한 방법을 설정하는 것이라고 한다.

기본 설정값인 OpenSSL을 선택하고 진행하였다.

 

위의 내용은 에뮬레이터를 설정하는 부분으로

나의 경우 윈도우 기본 콘솔창으로 설정하였다.

위의 항목에서는 참고 블로그에서 진행한대로 Enable file system caching을 선택하였고

아래 Enable symbolic links는 선택하지 않았다.

새로 추가된 내용들에 대해 (NEW!) 라는 설명으로 항목들을 선택할지 뜨는데

새로생긴 내용들이다 보니 버그가 많을 수 있어 초보자에게는 추천하지 않는다는 설명에 따라

선택하지 않고 진행하였다.

 

그렇게 설치를 진행하면 아래와 같이 시작메뉴에 추가된 것을 확인할 수 있다.

 

그럼 이제 github 저장소에 프로젝트를 load해보자.

생성된 파일 중 Git Bash 프로그램을 실행시킨 후 아래 명령어들을 입력해주자.

 

$ git config --global user.name "Github name here"

$ git config --global user.email "github_account@email.com"

 

우선 test라는 파일을 생성한 후 해당 위치에서

$git init 명령어를 실행한다.

위의 명령어는 이 디렉토리를 로컬 깃 저장소로 지정하겠다는 의미를 갖는다.

 

이 과정을 진행하면 아래와 같이 해당 경로에 숨김파일로 .git 이라는 폴더가 생긴 것을 확인할 수 있다.

 

그럼 이제 pc는 이 디렉토리를 Git-Ready로 인식하여 깃 명령어를 사용할 수 있게 된다.

다음으로 사용된 명령어는 다음과 같다.

 

$ touch Readme.txt

touch 명령어는 create를 의미한다.

 

그리고 $ git status 명령어를 통해 확인하면

현재 master branch에 있으며

아직 커밋된 내용이 없다고 확인된다.

 

untracked로 뜨는 것은 현재 git이 해당 내용들을 무시한다고 볼 수 있다.

그럼 이제 git이 해당 파일에 관심을 갖도록 하기 위해 다음 명령어를 입력하자.

 

$ git add Readme.txt

 

그리고 다시 status를 확인해보면 Changes to be committed 라는 내용과 함께 해당 파일이 확인되는 것을 볼 수 있다.

그럼 이제 해당 파일을 커밋해주자.

 

$ git commit -m "Readme.txt"

여기서 -m 뒤의 " " 안에 들어가는 내용은 커밋메시지라고 하며

어떤 파일에 대한 커밋인지 명확하게 작성하는 것이 좋다고 한다.

이 커밋 메시지를 잘 작성하는 습관이 중요하다고 한다.

명령어의 결과는 좌측 이미지와 같다.

그리고 status를 확인해보면

nothing to commit, working tree clean 이라는

메시지가 출력된다.

 

 

 

 

그럼 이제 해당 환경을 github 저장소에 업로드하자.

 

온라인 저장소에 업로드하기 위해 remote 를 사용하며

origin은 로컬이 아닌 온라인의 주소(origin 뒤에 나오는 주소)를 의미한다.

 

온라인 저장소의 주소는 https://github.com을  입력하고 사용자의 이름을 입력한다.

그리고 저장소의 이름을 입력한 뒤 .git을 입력한다.

그럼 위와 같은 계정확은 과정을 진행하게 되고 Sign in with your browser을 통해

로그인하여 계정확인을 진행하였다.

그럼 위와 같은 창을 확인할 수 있고 그럼 브라우저를 종료해도 된다.

 

위의 명령어는 원격 origin 에 대한 항목을 보여주는 것으로

push와 fetch가 가능한 것을 확인할 수 있다.

 

우선 push는 로컬에 대한 내용을 깃허브상에 업로드하는 과정을 얘기한다.

반대로 fetch는 온라인상의 내용을 로컬로 가져오는 것을 의미하는데

가져오는 명령어로는 fetch와 pull이 존재한다.

 

pull 명령어의 경우 원격 저장소의 소스를 가져오고 가져온 소스를 merge한다고 얘기한다.

merge란 버전을 맞춰주는 과정으로 보이는데 예를들어 원격 저장소의 소스가 현재 내 소스보다 더 최신이라면

지금의 버전을 해당 소스에 맞춰서 올리는 merge를 진행한다고 한다.

반대로 fetch는 merge과정을 진행하지 않는다고 하는데 이는 merge에 대해 조금 더 자세히 알아봐야

정확한 차이를 알 수 있을 것 같다.

 

나는 앞에서 생성한 Readme.txt파일을 온라인 저장소에 업로드 할 것이므로 push 명령어를 사용한다.

저장소의 master branch에 해당 파일이 제대로 업로드 된 것을 확인할 수 있다.

 

 

 

또한 git에 대한 기초적인 내용은 아래의 블로거 분께서 잘 설명해주셨다.

처음 다루는 분들은 한분씩 읽어보면 좋을 것 같다.

https://june98.tistory.com/23

반응형

+ Recent posts