OpenCV

OpenCV - Mat, 간단한 영상 처리

haniru 2025. 12. 19. 22:34

OpenCV의 cv::Mat 객체 활용한 메모리 관리, 행렬 연산, 픽셀 접근 및 이미지 변형

#include "opencv2/opencv.hpp"

// 클래스는 순서 필요 없지만 함수는 순서가 중요하다
void printMat(cv::InputArray _mat1);

void show1() {
	cv::Mat image1 = cv::imread("lenna.png");
	cv::Mat image2 = cv::imread("dog.bmp");
	cv::Mat image3;

	if (image1.empty() or image2.empty()) {
		std::cerr << "파일들이 없습니다" << "\\n";
		return;
	}

	image3 = image1.clone();

	std::cout << "lena 는 몇 차원?: " << image1.dims << "\\n";
	std::cout << "lena 는 몇 컬럼?: " << image1.cols << "\\n";
	std::cout << "lena 는 몇 행?: " << image1.rows << "\\n";

	std::cout << "dog 는 몇 차원?: " << image2.dims << "\\n";
	std::cout << "dog 는 몇 컬럼?: " << image2.cols << "\\n";
	std::cout << "dog 는 몇 행?: " << image2.rows << "\\n";

	std::cout << "lena clone 는 몇 차원?: " << image3.dims << "\\n";
	std::cout << "lena clone 는 몇 컬럼?: " << image3.cols << "\\n";
	std::cout << "lena clone 는 몇 행?: " << image3.rows << "\\n";

	cv::namedWindow("LENA");
	cv::imshow("LENA", image1);

	cv::namedWindow("LENA CLONE");
	cv::imshow("LENA CLONE", image3);

	cv::namedWindow("DOG");
	cv::imshow("DOG", image2);

	cv::waitKey();
	cv::destroyAllWindows();
}

void show2() {
	cv::namedWindow("Color");
	for (int i = 0; i <= 255; ++i) {
		// cv::Mat image(512, 512, CV_8UC3, cv::Scalar(0, 0, i)); // 빨강
		cv::Mat image(512, 512, CV_8UC3, cv::Scalar(i, i, i));
		cv::imshow("Color", image);
		cv::waitKey(10);
	}
	cv::waitKey(0);

}

void show2_gray() {
	// cv::Mat image2(1024, 1024, CV_8UC1); // 회색
	// cv::Mat image2(1024, 1024, CV_8UC3); // 회색
	cv::Mat image2(cv::Size(512, 512), CV_8UC3, cv::Scalar(0, 255, 255)); // 회색
	cv::imshow("Color", image2);
	cv::waitKey(0);
	cv::destroyAllWindows();
}

void show2_smooth() {
	cv::namedWindow("Color");
	// 0부터 510까지 순회 (255까지 내려갔다가 255만큼 다시 올라오는 거리)
	for (int i = 0; i <= 510; ++i) {
		// 255에서 i를 뺀 값의 절댓값을 취하면 255 -> 0 -> 255로 바뀜
		int brightness = std::abs(255 - i);

		// (CV_8U, 3): C++ 의 콤마 연산자
		cv::Mat image(512, 512, (CV_8U, 3), cv::Scalar(0, 0, brightness)); // 빨간색 왕복
		cv::imshow("Color", image);
		if (cv::waitKey(5) == 27) break;
	}
}

// 특수 행렬 생성과 타입
// zeros, ones, eye: 각각 영행렬, 모든 요소가 1인 행렬, 항등 행렬을 만든다
// CV_32SC1: 32비트 정수형 1채널을 의미한다. 단순 출력용이 아닌, 정밀한 수치 계산이나 머신러닝 데이터셋 구성시 사용
// 스칼라곱: cv::Mat::eye(...) * 255 와 같이 행렬 전체에 상수를 곱해 대각선만 하얗게 만드는 처리가 가능하다
void show3() {
	// cv::Mat::zeros(512, 512, CV_32SC1);
	// CV_32SC1: 계산을 하려는 용도로 사용하는 것
	cv::Mat img1 = cv::Mat::zeros(512, 512, CV_8UC1); // 까맣게 보임
	cv::Mat img2 = cv::Mat::ones(512, 512, CV_8UC1); // 까맣게 보임
	cv::Mat img3 = cv::Mat::eye(512, 512, CV_8UC1) * 255; // 대각선에 255 곱해서 하얗게 보이게끔 하기
	std::cout << "img1: " << img3 << std::endl;
	cv::namedWindow("WINDOW");
	cv::imshow("WINDOW", img3);
	cv::waitKey();
	cv::destroyAllWindows();
}

// 얕은복사 vs 깊은 복사
// 얕은 복사: img4 = img1은 데이터의 주소값만 복사한다. 따라서 img1을 수정하면 img4도 함께 변한다
// 깊은 복사: clone()이나 copyTo()는 메모리에 새로운 공간을 할당하고 실제 값을 복사한다. 원본과 독립적인 객체가 된다.
void show4() {
	cv::Mat img1 = cv::imread("dog.bmp");

	if (img1.empty()) {
		std::cerr << "파일이 없습니다." << std::endl;
		return;
	}

	cv::Mat img2 = img1.clone();

	cv::Mat img3; // 임시 저장 Matrix
	img2.copyTo(img3);

	cv::Mat img4 = img1; // 복사 생성자가 생김 (copy constructor)

	img1.setTo(cv::Scalar(0, 255, 255)); // img4, img1이 둘다 노랑으로 나온다, 메모리를 서로 공유하기 때문이다, shallow copy

	cv::namedWindow("DOG");
	cv::imshow("DOG", img1);

	cv::namedWindow("CLONE_DOG");
	cv::imshow("CLONE_DOG", img2);

	cv::namedWindow("COPY_DOG");
	cv::imshow("COPY_DOG", img3);

	cv::namedWindow("SHALLOW_COPY_DOG");
	cv::imshow("SHALLOW_COPY_DOG", img4);
	cv::waitKey();
	cv::destroyAllWindows();
}

// 이미지 반전 및 부분 추출(ROI)
// ~img1: 비트 연산자 NOT을 이용하여 이미지의 색상을 반전시킨다
// cv::Rect: 이미지의 특정 영역만 잘라낸다. 이때 잘라낸 이미지(img3)도 원본의 메모리를 공유하는 얕은 복사 방식이므로 주의해야 한다.
void show5() {
	cv::Mat img1 = cv::imread("cat.bmp");
	cv::Mat img2 = ~img1;
	cv::Mat img3 = img1(cv::Rect(220, 120, 340, 240));

	cv::namedWindow("CAT");
	cv::imshow("CAT", img1);

	cv::namedWindow("NOT_CAT");
	cv::imshow("NOT_CAT", img2);

	cv::namedWindow("CUT_CAT");
	cv::imshow("CUT_CAT", img3);

	cv::waitKey();
	cv::destroyAllWindows();
}

// 픽셀 직접 접근
// at<uchar>(i, j): 특정 좌표의 픽셀 값에 직접 접근한다
// c/c++은 행 우선 방식이므로 외곽 루프를 rows, 내부 루프를 cols로 설정하는 것이 캐시 효율상 훨씬 빠르다
void show6() {
	cv::Mat img1 = cv::Mat::zeros(256, 256, CV_8UC1);
	uchar value = 0;
	for (int i = 0; i < img1.rows; ++i) {
		std::cout << "i: " << i << std::endl;
		// 포트란: for 이 row 로 증가함, c는 컬럼으로 증가함
		for (int j = 0; j < img1.cols; ++j) {
			std::cout << "j: " << j << std::endl;
			++value;
			img1.at<uchar>(i, j) = value;
		}
	}
	cv::namedWindow("IMAGE1");
	cv::imshow("IMAGE1", img1);
	cv::waitKey();
	cv::destroyAllWindows();
}

// 이미지 정보와 채널
// elemSize(): 픽셀 하나가 차지하는 바이트 수
// IMREAD_UNCHANGED 로 읽었을 때 채널이 4라면, 이는 BGRA 구조를 가진것이다. Alpha는 투명도를 의미한다
void show7() {
	cv::Mat img1 = cv::imread("coins.png", cv::IMREAD_UNCHANGED);
	if (img1.empty()) return;
	std::cout << "이미지 폭: " << img1.cols << std::endl;
	std::cout << "이미지 높이 : " << img1.rows << std::endl;
	std::cout << "이미지 사이즈 : " << img1.size << std::endl;
	std::cout << "픽셀 한개의 사이즈 : " << img1.elemSize() << std::endl;
	std::cout << "채널 : " << img1.channels() << std::endl; // png는 채널이 4개이다
	std::cout << "이미지 타입 : " << img1.type() << std::endl;
	
	if (img1.type() == CV_8UC1) std::cout << "그레이 " << std::endl;
	else if (img1.type() == CV_8UC3) std::cout << "컬러 " << std::endl;
	else std::cout << "PNG " << std::endl;
}

// cv::Vec: cv::Vec2b (uchar 2개), cv::Vec2d(double 2개) 등은 점의 좌표나 픽셀의 색상 값을 담는 작은 벡터 객체이다
// 이런 벡터 연산들이 모여 이미지 전체의 픽셀을 동시에 계산할 때 GPU의 성능이 극대화 된다
// mat1 * mat1.inv() 의 결과가 항등 행렬(I)이 되는 것을 통해 역행렬의 원리를 코드로 확인할 수 있다.
void show8() {

	cv::Vec<uchar, 2> p1(20,255);
	cv::Vec2b p2(20, 255);
	cv::Vec<double, 2> p3(100.0, 10.0);
	cv::Vec2d p4(0.0, 0.0); // 크기와 방향을 가지는 벡터 GPU-병렬, 선형대수, 스칼라, 벡터

	float data[] = { 1.0f, 2.0f, 3.0f, 4.0f }; // 역행렬이 없으면 안나온다
	cv::Mat mat1(2, 2, CV_32FC1, data); // 2 * 2 형태의 행렬
	// float data[] = { 1.0f, 2.0f, 3.0f, 4.0f, 5.0f, 6.0f }; // 역행렬이 없으면 안나온다
	// cv::Mat mat1(3, 3, CV_32FC1, data); // 3 * 3 형태의 행렬
	cv::Mat mat2 = mat1.inv(); // 역행렬 구하기
	cv::Mat mat3 = mat1* mat2; // A*A^-1 하면 항등행렬 나온다 (eye 행렬 나온다)

	std::cout << mat1 << std::endl;
	std::cout << mat2 << std::endl;
	std::cout << mat3 << std::endl;

	uchar data2[] = { 1,2,3,4,5,6,7,8,9,10,11,12 };
	cv::Mat mat4(3, 4, CV_8UC1, data2);
	std::cout << mat4 << std::endl;
	cv::Mat mat5 = mat4.reshape(0, 4); // 행렬의 데이터는 그대로 두고 모양만 바꾼다(3*4 행렬을 4*3으로 바꾸거나 1차원 배열로 펼칠 때 사용)
	std::cout << mat5 << std::endl;

	cv::Mat img1 = cv::imread("lenna.png", cv::IMREAD_GRAYSCALE);
	cv::namedWindow("LENNA");
	cv::imshow("LENNA", img1);
	cv::Mat img2 = img1.t() + 50; // 밝아짐, 각 픽셀이 50씩 증가한다. 행렬에 상수를 직접 더할 수 있는 것은 OpenCV가 내부적으로 연산자 오버로딩을 통해 모든 원소에 해당 값을 더하도록 설계했기 때문이다. t(): 행과 열을 뒤바꾼다. 이미지를 시계 방향으로 90도 회전시킨 후 대칭시킨 효과와 같다.
	// cv::Mat img2 = img1 - 50; // 어두워짐

	cv::namedWindow("LENNA2");
	cv::imshow("LENNA2", img2);
	cv::waitKey();
	cv::destroyAllWindows();
}

void inputArrayOf() {
	uchar data1[] = { 1,2,3,4,5,6 };
	// uchar data1[] = { 1,2,3,4,5,6,256 }; // unsigned char, 256 넣으면 255 넘어가서 다시 0으로 세팅 됨 (오버플로우)
	cv::Mat mat1(2,3,CV_8U, data1);
	printMat(mat1);
	// 1 2 3
	// 4 5 6
}

// void printMat(cv::Mat _mat1) {
// 다형성
// cv::InputArray 는 OpenCV에서 가장 많이 쓰이는 인터페이스 클래스이다
// 다형성: printMat 함수처럼 매개변수를 cv::InputArray로 받으면, 사용자가 cv::Mat, cv::Scalar, std::vector 등 어떤 타입을 넣어도 다 받아낼 수 있다
// getMat()의 역할: 부모 인터페이스 상태에서는 세부적인 행렬 연산을 할 수 없으므로 내부에서 .getMat()을 호출하여 실제 cv::Mat 객체로 형변환 하여 사용한다
// uchar 데이터에 256을 넣으면 8비트 범위를 초과하여 다시 0으로 돌아간다 (오버플로우)
void printMat(cv::InputArray _mat1) { // 클래스가 상위 클래스이면 문제 없다
	cv::Mat mat = _mat1.getMat(); // 하위클래스에 맞게끔 바꿔준다, 리턴하는 애가 mat 클래스이다 부모 클래스는 자식을 못봄, 형변환
	std::cout << mat << std::endl;
}

void show9() {
	inputArrayOf();
}
// #include <opencv2/opencv.hpp> // 미리 컴파일 됨, 라이브러리 통해서 시스템 쪽으로 접근
#include "opencv2/opencv.hpp"
// 동영상 및 카메라 스트림 처리
void show10() {
    std::cout << "OpenCV Version: " << CV_VERSION << std::endl;
    
    // 0번 카메라 연결
    cv::VideoCapture cap(0); // 시스템의 0번 카메라 가져오기
    if (!cap.isOpened()) {
        std::cerr << "카메라를 열 수 없다." << std::endl;
        return;
    }

    // 카메라 설정값 읽기
    int width = cvRound(cap.get(cv::CAP_PROP_FRAME_WIDTH)); // cap.get(): 폭, 높이, FPS, 총 프레임 수를 가져올 수 있다
    int height = cvRound(cap.get(cv::CAP_PROP_FRAME_HEIGHT));
    std::cout << "가로: " << width << ", 세로: " << height << std::endl;

    // 창 생성 및 위치 지정 (겹치지 않게 분산)
    cv::namedWindow("Original");        cv::moveWindow("Original", 50, 50); // moveWindow: 여러 결과물을 동시에 모니터링 하기 위해 moveWindow를 사용하여 윈도우 창이 겹치지 않게 좌표를 지정한다. 멀티 카메라 시스템이나 단계별 필터링 결과를 확인할 때 유용하다.
    cv::namedWindow("Inversed");        cv::moveWindow("Inversed", 700, 50);
    cv::namedWindow("Mirror");          cv::moveWindow("Mirror", 1350, 50);
    cv::namedWindow("Edges");           cv::moveWindow("Edges", 50, 550);
    cv::namedWindow("Blurred");         cv::moveWindow("Blurred", 700, 550);
    cv::namedWindow("Gray");            cv::moveWindow("Gray", 1350, 550);
    cv::namedWindow("Cartoonish");      cv::moveWindow("Cartoonish", 700, 300);

    cv::Mat frame, inversed, edges, blurred, mirrored, gray, cartoon;

    while (true) {
        cap >> frame; // 카메라로부터 한 프레임 읽기
        if (frame.empty()) break;

        // 1. 반전 (Inversed)
        inversed = ~frame;

        // 2. 엣지 검출 (Edges) - 픽셀값의 변화량이 급격한 부분(경계선)을 찾아낸다. 두 개의 임계값(50, 150)을 사용해 강한 엣지와 약한 엣지를 구분한다.
        cv::Canny(frame, edges, 50, 150);

        // 3. 가우시안 블러 (Blurred) - 가우스 분포를 이용해 영상을 부드럽게 만든다. 노이즈를 제거할 때 주로 사용하며 cv::Size(15, 15)는 커널 크기로 값이 클수록 더 많이 뭉개진다
        cv::GaussianBlur(frame, blurred, cv::Size(15, 15), 0);

        // 4. 좌우 반전 (Mirror) - 1은 좌우반전, 0은 상하반전, -1은 상하좌우 모두 반전을 의미한다
        cv::flip(frame, mirrored, 1);

        // 5. 그레이스케일 (Gray) - BGR 컬러를 Gray(흑백)로 바꾼다. 정보량을 줄여 연산속도를 높여야 하는 연산(엣지 검출 등) 전단계에 필수적이다.
        cv::cvtColor(frame, gray, cv::COLOR_BGR2GRAY);

        // 6. 흑백 + 엣지 (Cartoonish)
        cv::cvtColor(frame, cartoon, cv::COLOR_BGR2GRAY);
        cv::Canny(cartoon, cartoon, 50, 150);

        // 각 창에 결과 표시
        cv::imshow("Original", frame);
        cv::imshow("Inversed", inversed);
        cv::imshow("Edges", edges);
        cv::imshow("Blurred", blurred);
        cv::imshow("Mirror", mirrored);
        cv::imshow("Gray", gray);
        cv::imshow("Cartoonish", cartoon);

        // ESC(27) 누르면 종료
        if (cv::waitKey(10) == 27) break; // waitKey(10):화면을 실제로 갱신할 시간을 준다. 사용자 입력을 대기한다. 10ms는 동영상이 자연스럽게 재생되도록 조절하는 대기 시간이다.
    }

    // 자원 해제 (Mat 객체들은 소멸자에서 자동 해제되지만 명시적으로 release 가능)
    cap.release(); // 카메라나 동영상 파일 핸들을 닫고 메모리를 해제한다. cv::Mat 객체는 스코프가 끝나면 자동으로 해제되지만, 명시적으로 release()를 호출해 관리하는 습관은 좋다.
    cv::destroyAllWindows();
}

void show11() {
    std::cout << CV_VERSION << std::endl;

    cv::VideoCapture capture("stopwatch.avi"); // 동영상 파일 가져오기
    if (!capture.isOpened()) {
        std::cerr << "동영상이 없습니다." << std::endl;
        return;
    }

    // cvRound : 소수점 잘라달라 (정수로 바꿔달라)
    std::cout << "MOVIE HEIGHT: " << cvRound(capture.get(cv::CAP_PROP_FRAME_HEIGHT)) << std::endl;
    std::cout << "MOVIE WIDTH: " << cvRound(capture.get(cv::CAP_PROP_FRAME_WIDTH)) << std::endl;
    std::cout << "MOVIE FRAME COUNT: " << cvRound(capture.get(cv::CAP_PROP_FRAME_COUNT)) << std::endl;
    std::cout << "MOVIE FRAME FPS: " << cvRound(capture.get(cv::CAP_PROP_FPS)) << std::endl; // 1초에 몇 프레임?

    cv::Mat screen, inverse_screen;
    // cv::Mat screen(480, 640);
    while (true) { // 끊임없이 영상을 받아 넣어야 한다
        capture >> screen; // >>: 프레임 읽기 연산자. 한 프레임의 행렬 데이터를 cv::Mat 객체에 담는 동작이다. 내부적으로는 다음 프레임으로 포인터를 이동시킨다.
        if (screen.empty()) {
            // std::cin 과 비슷
            std::cerr << "프레임이 들어오지 않습니다." << std::endl;
            break;
        }
        inverse_screen = ~screen;

        cv::imshow("MOVIE", screen);
        cv::imshow("INVERSE_SCREEN", inverse_screen);
        if (cv::waitKey(10) == 27) break; // ESC 키를 누르면 무한루프 종료
    }
    screen.release(); // 리소스 해제해야 함
    inverse_screen.release();
    cv::destroyAllWindows();
}
extern void show1();
extern void show2();
extern void show2_gray();
extern void show2_smooth();
extern void show3();
extern void show4();
extern void show5();
extern void show6();
extern void show7();
extern void show8();
extern void show9();
#include <iostream>

int main()
{
    std::cout << "OpenCV!\n";
    // show1();
    // show2_gray();
    // show2_smooth();
    // show3();
    // show4();
    // show5();
    // show6();
    // show7();
    // show8();
    show9();
}

 

 

1. cv::Mat의 구조와 정보 추출 (show1)

컴퓨터 입장에서 이미지는 단순한 그림이 아니라 숫자가 들어있는 행렬이다.

  • image.dims: 차원을 의미하며, 일반적인 2D 이미지는 2를 반환한다.
  • image.rows / image.cols: 행렬의 세로(행)와 가로(열) 크기다.
  • clone(): 단순 대입(image3 = image1)은 주소값만 복사하는 **얕은 복사(Shallow Copy)**이지만, .clone()은 메모리 공간을 새로 할당하여 데이터를 통째로 복사하는 **깊은 복사(Deep Copy)**다.

2. Scalar와 색상 체계 (show2, show2_gray)

OpenCV는 기본적으로 BGR(Blue, Green, Red) 순서를 사용한다.

  • CV_8UC3: 8비트(unsigned char), 3채널(Color)을 의미한다. 각 채널은 0~255의 값을 가진다.
  • cv::Scalar(B, G, R):
    • cv::Scalar(i, i, i): 세 값이 같으면 **Gray Scale(회색조)**이 된다.
    • cv::Scalar(0, 0, i): Red 채널만 변화하므로 점점 밝은 빨간색이 된다.
    • cv::Scalar(0, 255, 255): Green과 Red가 섞여 노란색이 출력된다.

3. 루프와 동적 시각화 (show2_smooth)

이미지 데이터의 값을 시간에 따라 변화시키면 애니메이션 효과를 줄 수 있다.

  • std::abs(255 - i): 수학적으로 밝기 값을 255 → 0 → 255로 변하게 하여, 색이 서서히 어두워졌다가 다시 밝아지는 효과를 만든다.
  • cv::waitKey(5): 인자로 들어가는 숫자는 밀리초(ms) 단위다. 5ms 동안 대기하며 화면을 갱신하라는 의미다.
  • 27: ASCII 코드에서 ESC 키를 의미한다. 실행 중 ESC를 누르면 루프를 빠져나오게 설계되어 있다.

4. 메모리 관리와 윈도우 컨트롤

  • namedWindow: 윈도우 창의 이름을 미리 정의한다. imshow에서 동일한 이름을 사용하면 해당 창에 이미지가 덮어씌워진다.
  • destroyAllWindows: main 함수 스택이 끝나기 전, 사용했던 모든 윈도우 리소스를 해제하여 메모리 누수를 방지한다.

 

 

--

1. 빌드 과정: 소스 코드에서 실행 파일까지

소스 코드가 기계어로 변환되어 실행되기까지는 여러 단계를 거친다.

  1. 컴파일 (Compile): .cpp 같은 텍스트 파일을 컴파일러가 읽어 기계어와 가까운 **오브젝트 파일(.obj)**로 변환하는 과정이다.
  2. ASM (Assembly): 컴파일러는 내부적으로 소스 코드를 **어셈블리어(Assembly)**로 바꾸고, 이를 다시 어셈블러가 기계어로 바꾼다.
  3. 링커 (Linker): 여러 개의 오브젝트 파일과 라이브러리 파일들을 하나로 묶어 최종적인 실행 파일(.exe, .out) 또는 **바이너리(.bin)**를 만든다.

링커가 필요한 이유

컴파일만 해서는 실행 파일을 만들 수 없다. 현대의 프로그램은 수많은 소스 파일로 나뉘어 있기 때문이다.

  • 분할 컴파일: 코드를 수정할 때 전체를 다시 컴파일하지 않고 변경된 파일만 컴파일하여 시간을 절약한다.
  • 참조 해결: A 파일에서 B 파일의 함수를 쓸 때, 링커가 그 주소를 연결해 준다. 이때 사용하는 키워드가 **extern**이다. extern은 "이 변수나 함수는 다른 파일에 있다"고 알려주는 역할을 한다.

2. 파일 형식과 기계어

  • 기계어 (Machine Code): CPU가 직접 이해하는 0과 1의 이진수 조합이다.
  • BIN (Binary): 데이터 그 자체를 담고 있는 파일 형식이다. OS가 없는 임베디드 시스템(리모컨 등)은 메모리에 직접 올릴 수 있는 .bin 형태를 주로 사용한다.
  • HEXA (Hexadecimal): 기계어를 사람이 보기 편하게 16진수로 표현한 것이다. (예: 010110 → 0x16)

3. 텍스트가 깨지는 이유 (Encoding)

.obj 파일이나 실행 파일을 메모장으로 열면 이상한 한글이나 한자가 출력되는 이유는 다음과 같다.

  1. 데이터 해석의 차이: 해당 파일은 문자가 아니라 2진 데이터(기계어)를 담고 있다. 메모장은 이 숫자를 억지로 문자로 변환하려다 보니 엉뚱한 한자가 나온다.
  2. 개행 문자의 부재: 텍스트 파일은 문장 끝을 알리는 개행 문자(\n)가 있지만, 바이너리 데이터는 끊어 읽는 기준이 다르기 때문에 텍스트 편집기에서는 줄바꿈 없이 깨진 글자가 나열된다.