by museonghwang

OpenCV를 위한 NumPy 2

|

해당 게시물은 파이썬으로 만드는 OpenCV 프로젝트(이세우 저) 를 바탕으로 작성되었습니다.


NumPy

NumPy는 OpenCV-Python 모듈의 필수 라이브러리기도 하지만, 데이터 과학 분야와 관련된 일을 파이썬 언어로 할 때 전반적으로 사용하는 라이브러리입니다.

9. 인덱싱과 슬라이싱

NumPy 배열은 파이썬의 리스트처럼 인덱스로 각 요소에 접근할 수 있습니다. 당연히 배열의 차원에 따라서 인덱스의 개수도 달라집니다.

>>> a = np.arange(10)
>>> a
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
>>> a[5]
5
>>> b = np.arange(12).reshape(3,4)
>>> b
array([[ 0,  1,  2,  3],
       [ 4,  5,  6,  7],
       [ 8,  9, 10, 11]])
>>> b[1]
array([4, 5, 6, 7])
>>> b[1, 2]
6

위 코드에서 배열 a는 1차원, b는 2차원입니다. 이때 2차원인 배열 b에 1개의 인덱스만 사용하면 1개의 행 모두가 선택됩니다. 2차원일 때는 인덱스 2개를 사용해서 열과 행을 지정해야 1개의 요소를 선택할 수 있습니다.

>>> a
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
>>> a[5] = 9
>>> a
array([0, 1, 2, 3, 4, 9, 6, 7, 8, 9])
>>> b
array([[ 0,  1,  2,  3],
       [ 4,  5,  6,  7],
       [ 8,  9, 10, 11]])
>>> b[0] = 0
>>> b
array([[ 0,  0,  0,  0],
       [ 4,  5,  6,  7],
       [ 8,  9, 10, 11]])
>>> b[1,2] = 99
>>> b
array([[ 0,  0,  0,  0],
       [ 4,  5, 99,  7],
       [ 8,  9, 10, 11]])

위 코드에서 보여주는 것처럼 값의 변경도 마찬가지로 인덱스로 정확히 1개의 요소를 지정하면 1개의 요소만 변경되지만, 인덱스를 적게 지정해서 행 단위로 지정하면 브로드캐스팅 연산이 일어나서 해당 단위 모두를 같은 값으로 변경합니다.

인덱스 자리에 콜론(:)을 이용해서 범위를 지정하면 슬라이싱(slicing)을 할 수 있습니다. 이때 범위의 끝 인덱스는 슬라이싱 결과에 포함되지 않습니다. 시작과 끝 인덱스를 각각 생략하면 처음부터 끝까지라는 의미입니다.

>>> a = np.arange(10);
>>> a
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
>>> a[2:5]
array([2, 3, 4])
>>> a[5:]
array([5, 6, 7, 8, 9])
>>> a[:]
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
>>> b = np.arange(12).reshape(3,4)
>>> b
array([[ 0,  1,  2,  3],
       [ 4,  5,  6,  7],
       [ 8,  9, 10, 11]])
>>> b[0:2, 1]
array([1, 5])
>>> b[0:2, 1:3]
array([[1, 2],
       [5, 6]])
>>> b[2, :]
array([ 8, 9, 10, 11])
>>> b[:, 1]
array([1, 5, 9])
>>> b[0:2, 1:3] = 0
array([[ 0,  0,  0,  3],
       [ 4,  0,  0,  7],
       [ 8,  9, 10, 11]])

b[0:2, 1] 은 b 배열 0~1행의 1열 요소들을 의미하고, b[0:2, 1:3] 은 b 배열 0~1행의 1~2열 요소들을 의미합니다.

값의 할당은 앞서 살펴본 것처럼 브로드캐스팅 연산으로 이루어집니다. 파이썬 기본형인 리스트와의 가장 큰 차이점은 슬라이싱의 결과가 복제본이 아닌 원본이라는 것입니다. 흔히 다음 코드와 같이 슬라이싱으로 전체 배열 중 일부를 다른 변수에 할당하는 경우 별도의 배열로 착각하는 경우가 많습니다. 그러나 이는 슬라이싱 해당 영역에 대한 참조일 뿐 값의 변경은 원본에도 그대로 반영됩니다.

>>> b
array([[ 0,  0,  0,  3],
       [ 4,  0,  0,  7],
       [ 8,  9, 10, 11]])
>>> bb = b[0:2, 1:3]
>>> bb
array([[0, 0],
       [0, 0]])
>>> bb[0] = 99
>>> b
array([[ 0, 99, 99,  3],
       [ 4,  0,  0,  7],
       [ 8,  9, 10, 11]])

만약 파이썬 리스트처럼 복제본을 얻고 싶다면 ndarray.copy() 함수를 명시적으로 호출해야 합니다.

10. 팬시 인덱싱

배열 인덱스에 다른 배열을 전달해서 원하는 요소를 선택하는 방법팬시 인덱싱(fancy indexing)이라고 합니다. 전달하는 배열에 숫자를 포함하고 있으면 해당 인덱스에 맞게 선택되고, 배열에 불(boolean) 값을 포함하면 True인 값을 갖는 요소만 선택됩니다.

>>> a = np.arange(5)
>>> a
array([0, 1, 2, 3, 4])
>>> a[[1, 3]]
array([1, 3])
>>> a[[True, False, True, False, True]]
array([0, 2, 4])

NumPy 배열에 비교 연산을 하면 개별 요소들이 조건을 만족하는지를 알 수 있습니다. 반대로 불 값을 갖는 배열을 배열의 인덱스 대신 사용하면 True 값 위치의 값들만 얻을 수 있는데, 이 둘을 한번에 합해서 실행하면 원하는 조건의 값만을 얻을 수 있습니다.

>>> a = np.arange(10)
>>> a
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
>>> b = a > 5
>>> b
array([False, False, False, False, False, False, True, True, True, True])
>>> a[b]
array([6, 7, 8, 9])
>>> a[a>5]
array([6, 7, 8, 9])
>>> a[a>5] = 1
>>> a
array([0, 1, 2, 3, 4, 5, 1, 1, 1, 1])

이때에도 새로운 값을 지정하면 브로드캐스팅 연산으로 해당 요소들 모두의 값을 바꿀 수 있습니다.

다차원인 경우 인덱스 배열도 다차원으로 지정할 수 있고, 이때는 교차하는 인덱스의 것이 선택됩니다.

>>> b = np.arange(12).reshape(3,4)
>>> b
array([[ 0,  1,  2,  3],
       [ 4,  5,  6,  7],
       [ 8,  9, 10, 11]])
>>> b[[0,2]]
array([[ 0,  1,  2,  3],
       [ 8,  9, 10, 11]])
>>> b[[0,2], [2,3]]
array([2, 11])

11. 병합과 분리

2개 이상의 NumPy 배열을 병합하는 방법은 크게 두 가지가 있습니다. 단순히 배열들을 이어 붙여서 크기를 키우는 방법새로운 차원을 만들어 서로서로 끼워넣는 방법입니다.

여기서부터는 축(axis)이라는 용어를 꼭 알아야 합니다. NumPy 배열의 shape 속성을 확인하면 튜플 형식으로 몇 개의 숫자가 나오는 것은 이미 알아보았습니다. 각 숫자의 개수는 차원을 의미하며, 맨 앞부터 0, 1, 2,… 순으로 축 번호를 사용합니다. 예를 들어 어느 배열의 shape가 (10,20,3)이면 3개의 축이 있고 10은 0번 축, 20은 1번 축, 3은 2번 축을 나타냅니다. 축을 기준으로 작업을 한다는 뜻은 바로 shape의 각 순서에 따라 작업을 한다는 뜻입니다.

여기서 알아볼 병합에 사용하는 함수는 다음과 같습니다.

  • numpy.hstack(arrays) : arrays 배열을 수평으로 병합
  • numpy.vstack(arrays) : arrays 배열을 수직으로 병합
  • numpy.concatenate(arrays, axis=0) : arrays 배열을 지정한 축 기준으로 병합
  • numpy.stack(arrays, axis=0) : arrays 배열을 새로운 축으로 병합
    • arrays : 병합 대상 배열(튜플)
    • axis : 작업할 대상 축 번호

위 함수 중에 일단 가장 손쉽게 쓸 수 있는 함수는 numpy.vstack()numpy.hstack() 입니다.

>>> a = np.arange(4).reshape(2,2)
>>> a
array([[0, 1],
       [2, 3]])
>>> b = np.arange(10, 14).reshape(2,2)
>>> b
array([[10, 11],
       [12, 13]])
>>> np.vstack((a,b))
array([[ 0,  1],
       [ 2,  3],
       [10, 11],
       [12, 13]])
>>> np.hstack((a,b))
array([[0, 1, 10, 11],
       [2, 3, 12, 13]])
>>> np.concatenate((a,b), 0)
array([[ 0,  1],
       [ 2,  3],
       [10, 11],
       [12, 13]])
>>> np.concatenate((a,b), 1)
array([[ 0, 1, 10, 11],
       [ 2, 3, 12, 13]])

위 코드는 2행 2열인 배열 a와 b를 numpy.vstack() 으로 수직 병합해서 4행 2열 배열로 만들고, numpy.hstack() 으로 수평 병합해서 2행 4열 배열로 만듭니다. numpy.vstack() 함수는 수직으로 배열을 병합하는데, numpy.concatenate() 함수에 축 번호로 0을 지정해서도 같은 결과를 얻을 수 있습니다. 2개의 (2,2) 배열의 0번 축 방향 그러니까 행 방향으로 병합하므로 (4,2)가 되게 하는 것입니다. hstack() 함수는 수평으로 배열을 병합하는데, concatenate() 함수에 축 번호로 1을 지정해서도 (2,4) 크기의 배열을 얻을 수 있습니다.

numpy.stack() 함수는 차원(축)이 새로 늘어나는 방법으로 병합을 하는데, 축 번호를 지정하지 않으면 0번을 의미하고, -1은 마지막 축 번호를 의미합니다.

>>> a = np.arange(12).reshape(4,3)
>>> b = np.arange(10, 130, 10).reshape(4,3)
>>> a
array([[ 0,  1,  2],
       [ 3,  4,  5],
       [ 6,  7,  8],
       [ 9, 10, 11]])
>>> b
array([[ 10, 20, 30],
       [ 40, 50, 60],
       [ 70, 80, 90],
       [100, 110, 120]])
>>> c = np.stack((a,b), 0)
>>> c.shape
(2, 4, 3)
>>> c
array([[[ 0,  1,  2],
        [ 3,  4,  5],
        [ 6,  7,  8],
        [ 9, 10, 11]],
        
       [[ 10, 20, 30],
        [ 40, 50, 60],
        [ 70, 80, 90],
        [100, 110, 120]]])

위 코드는 (4,3) 배열 2개를 np.stack((a, b), 0) 함수로 병합하고 있으므로 원래 4행 3열인 2차원 배열은 병합하고 나면 3차원이 되고 축 번호도 0, 1, 2로 3개가 됩니다. 이때 축 번호로 0을 전달하므로 맨 앞 축 번호가 새로 생성되어 (2, 4, 3)인 배열이 만들어집니다. 이때 축 번호를 생략하면 0과 같습니다.

다음은 같은 배열을 축 번호 1과 2로 각각 생성한 코드입니다.

>>> d = np.stack((a,b), 1)
>>> d.shape
(4, 2, 3)
>>> d
array([[[  0,   1,   2],
        [ 10,  20,  30]],
        
       [[  3,   4,   5],
        [ 40,  50,  60]],
        
       [[  6,   7,   8],
        [ 70,  80,  90]],
        
       [[  9,  10,  11],
        [100, 110, 120]]])
>>> e = np.stack((a,b), 2)
>>> e.shape
(4, 3, 2)
>>> e
array([[[ 0, 10],
        [ 1, 20],
        [ 2, 30]],

       [[ 3, 40],
        [ 4, 50],
        [ 5, 60]],
        
       [[ 6, 70],
        [ 7, 80],
        [ 8, 90]],
        
       [[ 9, 100],
        [10, 110],
        [11, 120]]])
>>> ee = np.stack((a,b), -1)
>>> ee.shape
(4, 3, 2)

위 코드는 앞서 사용했던 코드에서 (4,3) 인 배열 a와 b를 병합합니다. np.stack((a,b), 1) 로 병합할 때 전달한 축 번호는 1이므로 새로운 축은 1번 축에 추가되어 (4,2,3)이 되고, np.stack((a,b), 2) 로 병합할 때 축 번호는 2이므로 새로운 축은 2번 축에 추가되어 (4,3,2)가 됩니다. 이때 축 번호로 -1을 전달하면 마지막 축 번호,즉 여기서는 2와 같은 의미를 갖습니다.

이 함수들은 이미지 작업을 완료하고 작업 전과 후 이미지를 병합해서 나란히 출력할 때 자주 씁니다.

배열을 분리할 때 사용하는 함수는 아래와 같습니다.

  • numpy.vsplit(array, indice) : array 배열을 수평으로 분리
  • numpy.hsplit(array, indice) : array 배열을 수직으로 분리
  • numpy.split(array, indice, axis=0) : array 배열을 axis 축으로 분리
    • array : 분리할 배열
    • indice : 분리할 개수 또는 인덱스
    • axis : 기준 축 번호

indice 는 어떻게 나눌지를 정하는 인자인데, 정수 또는 1차원 배열을 사용할 수 있습니다. 정수를 전달하면 배열을 그 수로 나누고, 1차원 배열을 전달하면 나누고자 하는 인덱스로 사용합니다. indice 인자의 사용 예를 들면 아래와 같습니다.

>>> a = np.arange(12)
>>> a
array([ 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11])
>>> np.hsplit(a, 3)
[array([0, 1, 2, 3]), array([4, 5, 6, 7]), array([ 8, 9, 10, 11])]
>>> np.hsplit(a, [3,6])
[array([0, 1, 2]), array([3, 4, 5]), array([ 6, 7, 8, 9, 10, 11])]
>>> np.hsplit(a, [3,6,9])
[array([0, 1, 2]), array([3, 4, 5]), array([6, 7, 8]), array([ 9, 10, 11])]
>>> np.split(a, 3, 0)
[array([0, 1, 2, 3]), array([4, 5, 6, 7]), array([ 8, 9, 10, 11])]
>>> np.split(a, [3,6,9], 0)
[array([0, 1, 2]), array([3, 4, 5]), array([6, 7, 8]), array([ 9, 10, 11])]

위 코드에서 np.hsplit(a, 3) 은 12개의 요소를 갖는 배열을 수평으로 분리하는데, indice 항목에 3이 전달되었으므로 배열을 3개로 쪼개어 각 배열은 4개 요소씩 갖습니다.

np.hsplit(a, [3,6]) 은 3과 6을 배열로 표시했기 때문에 인덱스로 사용합니다. 이것은 [0:3], [3:6], [6:] 과 같은 의미입니다. 위 코드에서처럼 나누고 싶은 구역의 인덱스를 좀 더 자세히 전달하려면 np.hsplit(a, [3,6,9]) 와 같이 전달할 수도 있습니다.

위 코드에서는 1차원 배열을 사용했으므로 numpy.hsplit() 함수만 사용할 수 있습니다. 1차원인 경우 축 번호도 1개, 즉 0만 사용할 수 있으므로 numpy.split() 함수에서도 축(axis)에 사용할 수 있는 값은 0뿐이며, 같은 결과를 반환합니다.

>>> b = np.arange(12).reshape(4,3)
>>> b
array([[ 0,  1,  2],
       [ 3,  4,  5],
       [ 6,  7,  8],
       [ 9, 10, 11]])
>>> np.vsplit(b, 2)
[array([[ 0,  1,  2],
        [ 3,  4,  5]]),
 array([[ 6,  7,  8],
        [ 9, 10, 11]])]
>>> np.split(b, 2, 0)
[array([[ 0,  1,  2],
        [ 3,  4,  5]]),
 array([[ 6,  7,  8],
        [ 9, 10, 11]])]
>>> np.hsplit(b, [1])
[array([[0],
        [3],
        [6],
        [9]]),
 array([[ 1, 2],
        [ 4, 5],
        [ 7, 8],
        [10, 11]])]
>>> np.split(b, [1], 1)
[array([[0],
        [3],
        [6],
        [9]]),
 array([[ 1, 2],
        [ 4, 5],
        [ 7, 8],
        [10, 11]])]

위 코드는 나누려는 배열이 2차원일 때 numpy.vsplit() 를 사용한 것과 numpy.split() 에 축 번호 0을 지정했을 때 같은 결과가 나오는 것을 보여주고 있습니다. 배열의 shape가 (4,3)이므로 vsplit(b, 2) 는 수직으로 나누어 4행을 2로 나누어 2행씩 갖게하고, split(b, 2, 0) 함수는 0번 축을 기준으로 2로 나누게 됩니다.

4행 3열 배열을 수평으로 나누는 것은 1번 축으로 나누는 것입니다. 3을 2로 나눌수 없으므로 특정 인덱스로 지정해서 나눠보면 indice 인자에 [1] 을 지정한 것은 [1:] 과 같고 numpy.hsplit() 함수를 쓴 것과 numpy.split() 함수에 1을 전달한 결과는 같습니다.

12. 검색

NumPy 배열을 사용하는 이유는 수많은 데이터를 쉽고 빠르게 다루려는 이유가 가장 크며, 이미지 작업도 마찬가지입니다. 그래서 NumPy를 쓰다 보면 배열 안에서 관심 있는 데이터만을 찾거나 찾아서 바꾸는 일이 자주 필요합니다. 이와 관련한 함수는 다음과 같습니다.

  • ret = numpy.where(condition [, t, f]) : 조건에 맞는 요소를 찾기
    • ret : 검색 조건에 맞는 요소의 인덱스 또는 변경된 값으로 채워진 배열(튜플)
    • condition : 검색에 사용할 조건식
    • t, f : 조건에 따라 지정할 값 또는 배열, 배열의 경우 조건에 사용한 배열과 같은 shape
      • t : 조건에 맞는 값에 지정할 값이나 배열
      • f : 조건에 틀린 값에 지정할 값이나 배열
  • numpy.nonzero(array) : array에서 요소 중에 0(영, zero)이 아닌 요소의 인덱스들을 반환(튜플)
  • numpy.all(array [, axis]) : array의 모든 요소가 True인지 검색
    • array : 검색 대상 배열
    • axis : 검색할 기준 축, 생략하면 모든 요소 검색, 지정하면 축 개수별로 결과 반환
  • numpy.any(array [, axis]) : array의 어느 요소이든 True가 있는지 검색

아래 코드는 배열에서 조건에 맞는 인덱스를 찾아오는 사례, 그리고 찾은 값을 새로운 값으로 변경한 배열을 구하는 사례를 보여줍니다.

>>> a = np.arange(10, 20)
>>> a
array([10, 11, 12, 13, 14, 15, 16, 17, 18, 19])
>>> np.where(a > 15)
(array([6, 7, 8, 9]),)
>>> np.where(a > 15, 1, 0)
array([0, 0, 0, 0, 0, 0, 1, 1, 1, 1])
>>> a
array([10, 11, 12, 13, 14, 15, 16, 17, 18, 19])

위 코드 np.where(a > 15) 는 10부터 19까지 값을 갖는 배열 a에서 15보다 큰 값을 갖는 요소의 인덱스를 구합니다. np.where(a > 15, 1, 0) 은 15보다 큰 값을 1로 채우고 그렇지 않은 값은 0으로 채운 새로운 배열을 구합니다.

만약 조건에 맞는 요소만 특정한 값으로 변경하고 맞지 않는 요소는 기존 값을 그대로 갖게 하려면 다음과 같은 코드로 할 수 있습니다.

>>> a
array([10, 11, 12, 13, 14, 15, 16, 17, 18, 19])
>>> np.where(a > 15, 99, a)
array([10, 11, 12, 13, 14, 15, 99, 99, 99, 99])
>>> np.where(a > 15, a, 0)
array([0, 0, 0, 0, 0, 0, 16, 17, 18, 19])
>>> a
array([10, 11, 12, 13, 14, 15, 16, 17, 18, 19])

위 코드는 조건에 맞거나 틀린 경우에 할당할 값으로, 원래의 검색 대상 배열을 그대로 지정해서 조건에 맞는 값만 새로운 값으로 지정하거나 그 반대로도 가능합니다. 결과는 새로운 배열을 반환하므로 원본 배열은 그대로 유지됩니다.

다차원 배열인 경우 원하는 요소를 검색만 한다면 해당하는 요소의 인덱스는 여러 개를 반환합니다.

>>> b = np.arange(12).reshape(3,4)
>>> b
array([[ 0,  1,  2,  3],
       [ 4,  5,  6,  7],
       [ 8,  9, 10, 11]])
>>> coords = np.where(b>6)
>>> coords
(array([1, 2, 2, 2, 2]), array([3, 0, 1, 2, 3]))
>> np.stack((coords[0], coords[1]), -1)
array([[1, 3],
       [2, 0],
       [2, 1],
       [2, 2],
       [2, 3]])

위 코드는 3행 4열의 배열에서 6보다 큰 수만 검색하는 코드인데, 검색 결과는 행 번호(axis=0)만 갖는 배열과 열 번호(axis=1)만 갖는 배열 2개를 반환합니다. 이때 따로 떨어진 2개의 배열을 짝지어진 좌표 (x,y) 모양으로 얻으려면 앞서 살펴본 stack() 함수를 이용해서 병합하면 됩니다. 위 코드 np.stack((coords[0], coords[1]), -1) 에서 -1은 1로 바꾸어도 같습니다.

배열 요소 중에 0(zero)이 아닌 요소를 찾을 때는 numpy.nonzero() 함수를 사용할 수 있습니다. 이 함수는 0이 아닌 요소의 인덱스를 배열로 만들어서 반환합니다.

>>> z = np.array([0,1,2,0,1,2])
>>> np.nonzero(z)
(array([1, 2, 4, 5]),)
>>> zz = np.array([[0,1,2], [1,2,0], [2,0,1]])
>>> zz
array([[0, 1, 2],
       [1, 2, 0],
       [2, 0, 1]])
>>> coords = np.nonzero(zz)
>>> coords
(array([0, 0, 1, 1, 2, 2]), array([1, 2, 0, 1, 0, 2]))
>>> np.stack((coords[0], coords[1]), -1)
array([[0, 1],
       [0, 2],
       [1, 0],
       [1, 1],
       [2, 0],
       [2, 2]])

다차원 배열인 경우 numpy.where() 함수와 마찬가지로 차원 수만큼의 배열로 반환하므로 앞서 설명한 방법대로 필요에 따라 numpy.stack() 함수로 병합하여 좌표 꼴로 만들 수 있습니다.

numpy.nonzero() 함수는 True나 False 같은 불 값에 대해서는 False를 O(영, zero)으로 간주하고 동작하므로 numpy.where() 함수처럼 조건을 만족하는 요소의 인덱스를 찾을 수도 있습니다.

>>> a
array([10, 11, 12, 13, 14, 15, 16, 17, 18, 19])
>>> np.nonzero(a > 15)
(array([6, 7, 8, 9]),)
>>> np.where(a > 15)
(array([6, 7, 8, 9]),)
>>> b
array([[ 0,  1,  2,  3],
       [ 4,  5,  6,  7],
       [ 8,  9, 10, 11]])
>>> np.nonzero(b > 6)
(array([1, 2, 2, 2, 2]), array([3, 0, 1, 2, 3]))
>>> np.where(b > 6)
(array([1, 2, 2, 2, 2]), array([3, 0, 1, 2, 3]))

NumPy 배열에 모든 요소가 참 또는 거짓인지 확인할 때는 all() 함수를 사용할 수 있습니다.

>>> t = np.array([True, True, True])
>>> np.all(t)
True
>>> t[1] = False
>>> t
array([ True, False, True])
>>> np.all(t)
False

위 코드에서 배열의 모든 요소가 True일 때는 np.all(t) 가 True를 반환하지만 1개의 요소를 False로 바꾸자 그 결과가 False로 나타나는 것을 보여줍니다.

>>> tt = np.array([[True, True], [False, True], [True, True]])
>>> tt
array([[ True, True],
       [False, True],
       [ True, True]])
>>> np.all(tt, 0)
array([False, True])
>>> np.all(tt, 1)
array([True, False, True])

all() 함수에 축(axis) 인자를 지정하지 않으면 모든 요소에 대해서 True를 만족하는지 검색하지만, 축 인자를 지정하면 해당 축을 기준으로 True를 만족하는지 반환합니다.

NumPy 배열에 조건식 연산을 하면 True, False를 갖는 배열이 생성되는 것을 앞서 “연산”에서 살펴보았습니다. 이것을 numpy.all()numpy.where() 함수를 이용하면 2개의 배열이 서로 같은지 다른지, 다르다면 어느 항목이 다른지를 찾을 수 있습니다.

>>> a = np.arange(10)
>>> b = np.arange(10)
>>> a
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
>>> b
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
>>> a==b
array([ True, True, True, True, True, True, True, True, True, True])
>>> np.all(a==b)
True
>>> b[5] = -1
>>> a
array([0, 1, 2, 3, 4, 5, 6, 7, 8, 9])
>>> b
array([0, 1, 2, 3, 4, -1, 6, 7, 8, 9])
>>> np.all(a==b)
False
>>> np.where(a==b)
(array([0, 1, 2, 3, 4, 6, 7, 8, 9]),)
>>> np.where(a!=b)
(array([5]), )

위 코드에서 배열 a와 b는 처음에는 동일하지만 b[5] = -1 연산으로 다르게 했습니다. np.all(a==b) 연산으로 두 배열이 서로 같은 값으로 채워졌는지 아닌지를 확인할 수 있고, 만약 아니라면 np.where(a!=b) 로 다른 요소의 인덱스를 찾을 수 있습니다.

이미지 작업에서는 이전 프레임과 다음 프레임 간의 픽셀 값의 변화가 있는지, 변화가 있는 픽셀의 위치가 어디인지를 찾는 방법으로 움직임을 감지하거나 객체 추적과 같은 작업을 하는 데 이 함수들을 사용합니다.

13. 기초 통계 함수

배열의 값이 하나하나를 확인할 수 없을 만큼 많을 때는 평균, 최대 값, 최소 값 같은 통계값들이 의미 있는 정보가 될 때가 많습니다. 대표적인 함수는 다음과 같습니다.

  • numpy.sum(array [, axis]) : 배열의 합계 계산
  • numpy.mean(array [, axis]) : 배열의 평균 계산
  • numpy.amin(array [, axis]) : 배열의 최소 값 계산
  • numpy.min(array [, axis]) : numpy.amin()과 동일
  • numpy.amax(array [, axis]) : 배열의 최대 값 계산
  • numpy.max(array [, axis]) : numpy.amax()와 동일
    • array : 계산의 대상 배열
    • axis : 계산 기준 축, 생략하면 모든 요소를 대상
>>> a = np.arange(12).reshape(3, 4)
>>> a
array([[ 0,  1,  2,  3],
       [ 4,  5,  6,  7],
       [ 8,  9, 10, 11]])
>>> np.sum(a)
66
>>> np.sum(a, 0)
array([12, 15, 18, 21])
>>> np.sum(a, 1)
array([ 6, 22, 38])

위 코드는 0부터 11까지 값으로 갖는 배열을 3행 4열로 만들었습니다. np.sum(a) 함수는 모든 요소 값의 합계를 내줍니다. 축 번호를 지정하면 행과 열을 기준으로 각각 합산하는 것을 알 수 있습니다.

다음 코드는 같은 방법으로 평균, 최소 값, 최대 값을 각각 구하고 있습니다.

>>> np.mean(a)
5.5
>>> np.mean(a, 0)
array([4., 5., 6., 7.])
>>> np.mean(a, 1)
array([1.5, 5.5, 9.5])
>>> np.amin(a)
0
>>> np.amin(a, 0)
array([0, 1, 2, 3])
>>> np.amin(a, 1)
array([0, 4, 8])
>>> np.amax(a)
11
>>> np.amax(a, 0)
array([ 8, 9, 10, 11])
>>> np.amax(a, 1)
array([ 3, 7, 11])

다음 코드는 amin()min(), amax()max() 함수가 내부적으로 동일하다는 것을 보여주고 있습니다. 따라서 각각의 함수는 어느 것을 사용하든 차이가 없습니다.

>>> np.amin is np.min
True
>>> np.max is np.amax
True

14. 이미지 생성

지금까지 NumPy를 사용하기 위한 기본적인 내용을 살펴보았습니다. 다음 코드는 지금까지 다룬 지식으로 간단한 이미지를 생성해 보는 사례입니다.

'''NumPy 배열로 체크무늬 그레이 스케일 이미지 생성(np_gray.py)'''
import cv2
import numpy as np

img = np.zeros((120,120), dtype=np.uint8)   # 120x120 2차원 배열 생성, 검은색 흑백 이미지
img[25:35, :] = 45                          # 25~35행 모든 열에 45 할당 
img[55:65, :] = 115                         # 55~65행 모든 열에 115 할당 
img[85:95, :] = 160                         # 85~95행 모든 열에 160 할당 
img[:, 35:45] = 205                         # 모든행 35~45 열에 205 할당 
img[:, 75:85] = 255                         # 모든행 75~85 열에 255 할당 

cv2.imshow('Gray', img)
cv2.waitKey(0)
cv2.destroyAllWindows()


image

위 코드는 120 × 120 크기의 2차원 배열을 dtype은 uint8로 생성했습니다. 2차원 배열이고 가질 수 있는 값은 0~255 사이가 됩니다. OpenCV에서 이미지를 표현하기 위한 NumPy 배열은 반드시 dtype이 uint8이어야 합니다.

0은 검은색을 의미하고 255는 흰색을 의미하므로 그 사이의 값은 값이 커질수록 밝은 색을 나타내고 값이 작아질수록 어두운 색을 표현하는 1채널 그레이 스케일 이미지를 표현할 수 있습니다. 완전히 검은 바탕에 행 단위와 열 단위로 점점 밝은 값을 지정해서 체크무늬 이미지를 만들었습니다.

'''NumPy 배열로 체크무늬 BGR 스케일 이미지 생성(np_bgr.py)'''
import cv2
import numpy as np

img = np.zeros((120,120, 3), dtype=np.uint8)    # 120x120 2차원 배열 생성, 3채널 컬러 이미지
img[25:35, :] = [255,0,0]                       # 25~35행 모든 열에 [255,0,0], 파랑색 할당 
img[55:65, :] = [0, 255, 0]                     # 55~65행 모든 열에 [0,255,0], 초록색 할당 
img[85:95, :] = [0,0,255]                       # 85~95행 모든 열에 [0,0,255], 빨강색 할당 
img[:, 35:45] = [255,255,0]                     # 모든행 35~45 열에 [255,255,0], 하늘색 할당 
img[:, 75:85] = [255,0,255]                     # 모든행 75~85 열에 [255,0,255], 분홍색 할당 

cv2.imshow('BGR', img)
cv2.waitKey(0)
cv2.destroyAllWindows()


image

다음 코드는 np.zeros((120,120,3), dtype=np.uint8) 로 120행, 120열 크기의 3개의 채널을 갖는 3차원 배열을 생성했습니다. 3개의 채널은 각각 B(Blue, 파랑), G(Green, 초록), R(Red, 빨강) 값을 나타내는 0~255 사이의 값을 나타냅니다. 앞 코드와 같은 방법으로 3개의 행과 2개의 열 단위로 10픽셀씩 색상을 지정해서 검은 바탕에 여러 가지 색상의 체크무늬 이미지를 생성했습니다.