이전 글 [Deskew for ocr] Rotation correction 에서 regression으로 회전된 문서를 바로잡는 방식을 시도해봤다.

 

이전 글에서 쓴 모델의 문제는 크게 2가지

1. 회전되지 않는 문서를 회전된 문서로 오인식해 오히려 이상하게 회전시키는 문제가 발생한다.

2. 정확도가 굉장히 떨어진다.

 

이를 해결하기 위해 새로운 방식을 조사, 적용 했고 결과가 좋아서 공유한다.

 

이번엔 회전된 문서를 바로잡는 문제를 분류 문제로 정의 하고 해결해 본다. 이 방식은 ocropus3 를 참고 했다.

 

코드는 아래 링크의 devStream_fft branch 에 있다.

link: https://github.com/pajamacoders/ocrDeskew/tree/devStream_fft

문제 정의

문서가 얼마나 회전되어있는 지를 분류 문제로 정의 하고 풀기 위해서 각 회전의 정도에 class를 부여 해야 한다.

 

나는 0.5도 단위를 하나의 클래스로 정의했다.

예를 들자면 아래와 같은 방식이다. 문서가 회전된 각도를 degree 라고 표현 했을 때 회전의 정도(degree) 가 -1 도에서 -0.5 도 이내이면 class 0에 배정 하는 방식이다.

range -1< degree<-0.5 -0.5<= degree <0 0<= degree <0.5
class 0 1 2

이러한 방식으로 -89~89도 사이에서 회전된 문서의 rotation correction 문제는

356 클래스를 가지는 분류 문제로 정의 할 수 있다.

 

개발 환경

개발 환경은 ngc repo에서 아래 이미지를 다운 받았다.

docker image : nvcr.io/nvidia/pytorch:22.01-py3

train metric tracking: mlflow

 

전처리

전처리 과정은 이전 글 [Deskew for ocr] Rotation correction 의 전처리와 거의 유사하지만 rotation의 정도에 class를 대응 하는 부분만 차이가 있다.

 

이전 글에서 바뀐 GT 생성 부분인 RandomRotation class 는 아래와 같다.

 

class RandomRotation(object):
    def __init__(self, ratio, degree, buckets=None):
        self.variant = eval(degree) if isinstance(degree, str) else degree
        self.ratio = eval(ratio) if isinstance(ratio, str) else ratio
        self.buckets = eval(buckets) if isinstance(buckets, str) else buckets

    def __call__(self, inp):
        if  np.random.rand()<self.ratio:
            deg = np.random.uniform(-self.variant, self.variant-0.1)
            img = inp['img']
            h,w= img.shape
            matrix = cv2.getRotationMatrix2D((w/2, h/2), deg, 1)
            dst = cv2.warpAffine(img, matrix, (w, h),borderValue=0)
            inp['img'] = dst
        else:
            deg = 0

        if self.buckets:
            rad = np.deg2rad(deg)
            range_rad = np.deg2rad(self.variant)
            bucket = int(self.buckets * (rad+range_rad) / (2*range_rad))
            inp['rot_id'] = bucket # 이 값이 문서가 회전된 정도를 class에 할당 한 값 즉 target class 값이다.

        inp['degree'] = deg
        return inp

 

모델

이번 모델의 특이점은 중간에 fft를 사용 하는 layer 가 들어간다는 것이다.

 

모델은 아래와 같다.

class DeskewNetV4(nn.Module):
    def __init__(self, buckets, last_fc_in_ch, pretrained=None):
        super(DeskewNetV4, self).__init__()
        buckets = eval(buckets) if isinstance(buckets, str) else buckets
        assert isinstance(buckets, int), 'buckets must be type int'
        k=5
        self.block1 = nn.Sequential(
                ConvBnRelu(1,8,k,padding=k//2),
                nn.MaxPool2d(2,2), #256x256
                ConvBnRelu(8,16,k,padding=k//2),
                nn.MaxPool2d(2,2), #128x128
            )
        
        self.block2 = ConvBnRelu(16,8,k,padding=k//2)
        self.fc = nn.Sequential(nn.Linear(131072,last_fc_in_ch, bias=False),
            nn.BatchNorm1d(last_fc_in_ch),
            nn.ReLU(True),
            nn.Linear(last_fc_in_ch, buckets, bias=False))

        self.__init_weight()
        if pretrained:
            self.load_weight(pretrained)

    def forward(self, x):
        out = self.block1(x)
        out = torch.fft.fft2(out)
        out = out.real**2+out.imag**2 
        out = torch.log(1.0+out)
        out = self.block2(out)
        bs,c,h,w = out.shape
        out = out.reshape(bs,-1)
        out = self.fc(out)

        return out

 

결과

모델의 last_fc_in_ch의 값으로 128을 사용 할 경우 아래와 같은 결과를 얻었다.

학습은 총 800에폭을 돌렸는데 굳이 이럴 필요까진 없었다.

optimizer로 adam을 사용 했고 lr_schedule은 cosineannealing을 lr range  1e-3 ~1e-6으로 사용 했다.

 

ce_loss:0.1241

precision:0.9466

recall:0.9426

f1_score:0.9427

 

아래는 train, validation 시의 f1 score의 값을 나타낸 그래프 이다.

200epoch 쯤 되면 성능 향상은 거의 없는 것을 볼 수 있다.

아래 그림은 임의의 숫자를 적은 문서로 테스트 한 결과 이다.

왼쪽이 입력으로 들어간 회전된 문서이고 오른쪽인 inference로 회전을 바로잡은 결과이다.

regression 모델 보다는 전체적인 결과가 훨씬 좋다.

 

- 끝 -

배경

이번 포스팅에서는 ocr 성능을 높이기 위해 간단한 전처리를 통해 회전된 문서를 올바르게 돌려 주는 문제를 풀어보고자 한다.

 

OCR 은 optical character recognition의 약자로 hand writing, 인쇄된 문서 등을 카메라로 찍거나 스캔했을때 그 문서 내의 글자를 인식해 문서를 전산화 할때 자주 사용되는 기술이다.

 

이때 ocr의 성능을 떨어뜨리는 문제 중 하나는 입력으로 들어오는 문서가 회전되어있는 경우 이다.

문서가 회전된 상태로 스캔되면 각 문자 자체는 인식이 할수 있지만 문자를 단어로 머지(merge)하는 과정이나 숫자의 연속인 여권번호, 운전면허 번호, 주민등록 번호 등 긴 문자의 경우 하나의 시퀀스로 머지 되지 않아 잘못된 패턴으로 인식 되는 경우가 종종 발생해  최종 인식률이 떨어질 수 있다. 

 

이런 문제를 해결하기 위해 인식 네트워크 자체가 문서 얼라인먼트 기능을 가지도록 설계할 수도 있고, hand craft feature extraction을 기가 막히게 설계 하고 이를 이용한 homography 계산 알고리즘을 설계하는 방식도 있겠으나!!!

여기서는 간단한 네트워크 설계하고 학습해 rotation correction하는 방식을 시도해 본다.

 

* 이 포스팅에 사용된 문서는 공공문서 양식 중 하나이고 이런 DB는 'aihub -> 음성/자연어->공공 행정문서 OCR'에서 아주 손쉽게 구할수 있다.

 

이 포스트에서 개발한 코드는 이 링크에 있다.

code :https://github.com/pajamacoders/ocrDeskew

목표

Fig 1. 왼쪽은 회전된 문서, 오른쪽은 회전 없이 정상적으로 스캔된 문서

이 포스팅의 목적을 명확히 나타내는 그림이 Fig 1. 이다. Fig 1 의 왼쪽은 회전된 문서를 보여준다. 우리의 목표는 이렇게 회전된 문서를 오른 쪽과 같이 회전이 없는 상태로 만드는 것이다.

 

문제 정의

나는 이 포스팅의 목표인 rotation correction 문제를 image orientation prediction 문제로 정의 했다.

 

사고의 흐름은 입력 이미지로 부터 이미지의 회전 정도(orientation)를 구하면 그 회전의 크기만큼 반대 방향으로 회전을 시켜 줌으로서 이미지를 정상적으로 만들수 있기 때문이다.

 

목표 추정 범위는 -30~30degree 이내의 회전으로 정했다.

회전의 크기를 일정 step으로 양자화 해서 각 구간에 class를 부과해 classification으로 해결 할 수도 있을것 같지만

회전 크기를 degree로 바로 추정하는 regression 문제로 정의 하고 풀고자 한다.

 

개발 환경

개발 환경은 ngc repo에서 아래 이미지를 다운 받았다.

docker image : nvcr.io/nvidia/pytorch:22.01-py3

train metric tracking: mlflow

 

전처리

전처리는 과정에서는 크게 아래 4가지를 수행했다.

  1. Color conversion(BGR 2 GRAY)
  2. Resize
  3. Rotation (이 과정은 target GT를 생성하는 과정이다.)
  4. Normalization

이때 resize시에 특정한 문제가 발생했고 이를 해결하기 위해 약간의 꼼수(=테크닉)가 사용되어 그 부분에 대해 잠시 설명 하고자 한다.

 

입력 이미지가 대부분 A4등의 문서를 스캔한 것이기 때문에 풀고자하는 문제에 비해 불필요 하게 크다고 생각했고 입력 이미지의 크기를 512x512로 정의 했다.

하지만 입력 중 어떤 이미지들은 3508x2480(width x height)의 크기를 지녔고 이걸 (512x512)로 리사이즈 하니 다음과 같은 이미지가 생성되었다.

Fig 2. 회전된 이미지

예제의 이미지는 그나마 좀 나은 편인데 심한 것들은 의미있는 글자나 선등이 sampling이 거의 안되어 노이즈 처럼 점만 뿌려져 있는 것처럼 보이기도 한다.

문제는 이러한 텍스트 문서의 경우 배경에 비해 정보를 담고 있는 글자나 테이블을 이루는 선등의 면적이 매우 작아 리사즈 할때 텍스트나 선 부분에서 샘플링되는 양이 매우 작기 때문이다.

 

위에 서 말했듯이 나는 원인이 정보를 포함하는 텍스트나 선이 차지하는 면적이 작기 때문으로 보았기 때문에 리사이즈 하기 전에 이 정보를 포함하는 부분의 면적을 키우기로 했다.

 

가장 먼저 떠오른 것은 dilate 연산이다.

dilate을 적용하기 위해 이미지의 색을 반전 시켜 준다. 색 반전의 이유는 입력 이미지는 흰색의 배경을 가지는데 기본적으로 dilate연산은 intensity가 큰 부분은 확장하는 효과를 가지기 때문에 내가 면적을 키우고자 하는 텍스트와 선 등 정보를 가진 pixel이 큰intensity 가지고 background가 낮은 intensity를 가지게 하기 위해서 이다.

아래 코드와 같이 이미지를 읽고 gray scale로 색변환 한 후 background와 foreground의 색상을 반전 시켰다.

    img = cv2.imread(self.img_pathes[index])
    # transform
    img = cv2.cvtColor(img, cv2.COLOR_BGR2GRAY)

    if self.inverse_color:
        img = 255-img # inverse image to apply dilate
    res_dict = {'img':img, 'imgpath':self.img_pathes[index]}

다음으로 적용한 테크닉은 3508 x 2480 을 곧바로 512x512로 리사이즈 하는게 아니라

dilate ->1/2 resize -> 1/2 resize-> resize to 512x512 로 리사이즈 스탭을 진행해 orientation 추출에 필요한 정보를 최대한 많이 보존 하게 하는 것이다.

이 부분의 코드는 다음과 같다.

class Resize(object):
    def __init__(self, scale=4):
        assert (scale!=0) and (scale&(scale-1))==0, 'scale must be power of 2'
        self.iter = np.log2(scale).astype(int)
    
    def __call__(self, inp):
        img = inp['img']
        h,w = img.shape
        inp['org_height']=h
        inp['org_width']=w
        k = cv2.getStructuringElement(cv2.MORPH_RECT, (3,3))
        img = cv2.dilate(img, k)
        for i in range(self.iter):
            h, w = h//2, w//2
            img=cv2.resize(img, (w, h), interpolation=cv2.INTER_CUBIC)
            h, w = img.shape
        inp['img']=cv2.resize(img, (512, 512), interpolation=cv2.INTER_CUBIC)
        return inp

이렇게 만들어진 이미지는 아래 Fig 3과 같다.  Fig 2와 텍스트와 선분의 정보 손실 정도를 비교해 보자.

리사이즈 방식에 따라 최종 목적인 regression 성능이 얼마나 달라지는지 ablation study를 진행해 보지는 않았지만 정보의 손실의 최소화 하고자 하는 측면에서는 이게 맞지 않나 싶다.

Fig 3. resize로 인한 글자 정보손실을 최소화한 이미지

또 다른 방식으로는 텍스트의 뭉개짐을 무시하고 높은 intensity를 가지는 부분을 최대화 하고자 한다면 아래와 같이 입력을 변환 하는 방식도 가능 할 것이다.

Fig 4.

Fig 4에서는 글자를 알아 보기는 힘들지만 이미지의 orientation을 결정하는데에 foreground 정보의 양이 중요할 경우 유용 할 것이다. Fig 4는 위 Resize 클래스 에서 cv2.dilate() 함수를 for 문 안에 cv2.resize함수 호출 바로 앞으로 옮긴 경우 만들어 지는 이미지 이다.

 

GT 생성

위 이미지 전처리 과정에서 Rotation 과정은 아래와 같은 이유에서 추가되었다.

 

이미지가 회전된 정도를 GT로 만들어 놓은 경우는 거의 없기 때문에 정상적인 이미지를 opencv를 이용해 랜덤으로 -30~30degree 사이 각도로 회전시켜 회전된 정도를 GT로 사용 한다.

 

아래코드는 resize된 이미지를 임의의 각도로 회전하고 회전한 각도를 target GT로 저장하는 코드이다.

 

class RandomRotation(object):
    def __init__(self,degree):
        self.variant = eval(degree) if isinstance(degree, str) else degree

    def __call__(self, inp):
        deg = np.random.uniform(-self.variant, self.variant)
        img = inp['img']
        h,w= img.shape
        matrix = cv2.getRotationMatrix2D((w/2, h/2), deg, 1)
        dst = cv2.warpAffine(img, matrix, (w, h),borderValue=0)
        inp['img'] = dst
        inp['degree'] = deg
        return inp

 

모델 설계

입력의 회전된 각도를 추정하는 아주 가벼운 네트워크를 아래와 같이 구성했다.

import torch
import torch.nn as nn


class ConvBnRelu(nn.Module):
    def __init__(self, in_channel, out_channel, kernel_size, stride=1, padding=0, dilation=1,
                 groups=1):
        super(ConvBnRelu, self).__init__()
        self.conv_bn_relu = nn.Sequential(
            nn.Conv2d(in_channel, out_channel, kernel_size, stride, padding, dilation, groups,
                      False),
            nn.BatchNorm2d(out_channel),
            nn.ReLU(True))

    def forward(self, x):
        return self.conv_bn_relu(x)

class DeskewNet(nn.Module):
    def __init__(self, pretrained=None):
        super(DeskewNet, self).__init__()
        k=3
        self.backbone = nn.Sequential(
            ConvBnRelu(1,8,k,padding=k//2),
            nn.MaxPool2d(2,2), #256x256
            ConvBnRelu(8,16,k,padding=k//2),
            nn.MaxPool2d(2,2), #128x128
            ConvBnRelu(16,32,k,padding=k//2),
            nn.MaxPool2d(2,2), #64x64
            ConvBnRelu(32,64,k,padding=k//2),
            nn.MaxPool2d(2,2), #32x32
            ConvBnRelu(64,64,k,padding=k//2),
            nn.MaxPool2d(2,2), #16x16
        )
        self.avgpool = nn.AvgPool2d((16,16))
        self.fc = nn.Sequential(nn.Linear(64,64),nn.Linear(64,1))
        self.__init_weight()
        if pretrained:
            self.load_weight(pretrained)

    def forward(self, x):
        out = self.backbone(x)
        out = self.avgpool(out)
        out = self.fc(out.squeeze())
        return out

    def __init_weight(self):
        for layer in self.modules():
            if isinstance(layer, nn.Conv2d):
                nn.init.kaiming_normal_(layer.weight, nonlinearity='relu')
            else: 
                pass

 

학습

학습 환경은 아래와 같이 구성했다.

optimizer: Adam

lr_scheduler: CosineAnnealingLR -> 초기 lr =0.001, eta_min 1e-6, T_Max=300

loss: MSELoss

batch: 128

아래는 학습 코드 메인 문이다. train, valid 함수 구현은 뻔하니 생략한다.

if __name__ == "__main__":
    args = parse_args()
    with open(args.config, 'r') as f:
        cfg = json.load(f)
        cfg['config_file']=args.config
        if args.run_name:
            cfg['mllogger_cfg']['run_name']=args.run_name
    
    tr = build_transformer(cfg['transform_cfg'])
    train_loader, valid_loader = build_dataloader(**cfg['dataset_cfg'], augment_fn=tr)

    logger.info('create model')
    model = build_model(**cfg['model_cfg'])#torch.hub.load('pytorch/vision:v0.10.0', cfg['model_cfg']['type'])
    model.cuda()
    logger.info('create loss function')
    fn_loss = torch.nn.MSELoss()

    logger.info('create optimizer')
    opt=torch.optim.Adam(model.parameters(), **cfg['optimizer_cfg']['args'])
    lr_scheduler = torch.optim.lr_scheduler.CosineAnnealingLR(opt,**cfg['lr_scheduler_cfg']['args'])

    max_epoch = cfg['train_cfg']['max_epoch']
    valid_ecpoh = cfg['train_cfg']['validation_every_n_epoch']
    logger.info(f'max_epoch :{max_epoch}')
    logger.info('set mlflow tracking')
    mltracker = MLLogger(cfg, logger)
    for step in range(max_epoch):
        train(model, train_loader, fn_loss, opt, mltracker, step)
        if (step+1)%valid_ecpoh==0:
            valid(model, valid_loader, fn_loss,  mltracker, step)
        lr_scheduler.step()

 

결과

아래는 train, validation loss를 mlflow tracking 으로 추적한 결과 이다. loss가 초반에 급격히 떨어지고 수렴은 하지만

validation이 중간 중간 불안정 하게 튀는 모습을 볼 수 있다.  이건 해결을 좀 해야겠다.

Fig5. loss graph

아래 Fig 6.의 왼쪽은 회전왼 입력 이미지이고 오른쪽 그림은 orientation을 추정해 correction 한 이미지 이다. 정확하진 않지만 생태는 많이 나아 졌다. 좀더 정확하게 correction 되도록 data검증 및 네트우크 수정을 해봐야겠다.

 

끝.

jetson tx2에서 작업을 하려다 기본 용량이 32GB 밖에 안되어 용량이 부족하다 생각해 

docker image를 외부 저장 장치에 저장하기 위한 방법을 알아 보고 이를 정리 한다. 

 

이 포스팅의 목적:

docker image의 기본 저장 위치를 외부 저장 장치인 sd카드로 영구적으로 바꾸는 방법을 알아 본다. 

 

환경:

docker version: Docker version 20.10.2, build 20.10.2-0ubuntu1~18.04.2

os: ubuntu 18.04 

 

step 1. 도커 기본 저장 디렉토리 확인

일반적으로 도커의 기본 이미지 저장 디렉토리는  /var/lib/docker 이다. 

아래 명령으로 확인할 수 있다.

docker info

위 명령입력시 아래와 같은 화면을 볼수 있는데 노란색으로 표시한 Docker Root Dir가 우리가 image등을 pull 하면 기본적으로 저장되는 위치이다. 

우리의 목표는 위에 명시 했듯 이 위치를 저장공간이 풍부한 외부 디바이스로 재 지정하는 것이다. 

step 2. 도커 서비스 정지지

 

목적을 위해 가장 먼저 해야 할 일은 도커 서비스를 정지 하는 것이다. 아래 명령으로 도커 서비스를 정지하고 정지된 상태를 확인할 수 있다. 

바로 밑의 그림은 systemctl status docker의 결과 화면 이다. 3번째 라인에서 Active: inactive 라고 확인 할 수 있다. 

sudo systemctl stop docker
sudo systemctl status docker

step 3. 외부 저장장치 마운트 위치 확인

다음으로 할 일은 외부 저장 장치를 마운트 하는 것이다. 나는 이 외부 저장 장치를 영구적으로 docker의 root dir로 사용 할 것이므로 

이 장치가 항상 고정된 위치에 마운트 되게 설정할 것이다. 

우선 마운트 할 저장 장치의 이름을 확인 하기 위해 아래 명령을 입력한다.

df -h

위 명령의 결과 화면은 아래 그림과 같다. 노란색으로 표시한게 외부 저장장치 이고 녹색으로 표시한게 현재 마운팅된 위치이다. 

나는 기존에 작업을 해놔서 /mnt/ant/antvolume으로 마운트 되었는데 마운팅 되는 위는 각자 알아서 만들면 된다. 

단 마운트 할 위치를 반드시 기억하기 바란다.

마운트 할 위치 만드는 법:

더보기

참고로 마운트 할 위치를 나는 다음 명령어로 미리 만들어 두었다. 

sudo mkdir -p /mnt/ant/antvolume

 

step 4. 마운트 위치 고정을 위한 /etc/fstab 파일 변경

(이 파일에 마운트할 devicde 파일을 잘 못 기입하면 부팅시 이를 찾지 못해 문제가 발생할 수 있으니 조심하자. 일반 적으로 uuid를 사용하지만 나는 그냥 device 파일 경로로 사용했다.)

 

UUID 확인 방법

더보기

sudo blkid

아래 결과 화면의 흰색이 내 sd카드의 uuid를 확인 할수 있는 부분이다.

sudo vim /etc/fstab

위 명령을 입력해 파일을 열고 아래 그림의 노란 부분과 같이 파일 시스템과 마운팅 위치를 입력한다. type을 주의 해서 설정해야 한다. 

내 경우 위 step 3에서 확인한 디바이스 파일(파일 시스템)과 마운트 위치를 이용했다.

5. 도커 기본 저장 경로 변경

도커의 기본 저장 경로를  바꾸기 위해 '/etc/docker/daemon.json' 파일을 변경해야 한다.

간단히 'data-root'를 항목을 추가 하면 된다. 옛 버전에서는 'graph'라는 항목을 추가 했어야 하는데 현 버전에서는 'data-root'로 바뀌었다.

입력 방식은 아래와 같다.

"data-root": "/path/to/newdir"

아래는 입력을 마친 내 파일이다. 노란 부분이 추가한 부분이다.

6. 바뀐 설정 사용

 6.1 재부팅 없이 사용 하기

바꾼 도커 root dir을 사용 하기 위해 일단 파일시스템을 정해진 위치에 마운트 시키고 도커를 재시작한다. 

mount -a 를 하는 이유는 step 3에서 확인 한 파일시스템의 마운트 경로와 step 4, step 5 에서 지정한 파일 시스템 위치가 다를 경우를 위한 것이다. 나와 같이 이미 마운트 되어있는 위치를 그대로 사용한 경우 mount -a 는 필요 없다.

sudo mount -a
sudo systemctl restart docker

6.2 재부팅 하기

재부팅 하면 알아서 fstab 파일에 있는 파일시스템이 마운팅 되므로 추가 명령이 필요 없다.

 

결과:

step 1처럼 아래 명령을 입력해 root dir이 변경 되었는지 확인해 보자.  아래 그림의 노란색 부분을 보면 step 1과는 다르게 내가 원하는 위치로 변경된 것을 확인 할 수 있다. 

 

docker info

이번 포스팅은 deepstream 6.0을 이용한 PYTHON 예제를 실행 할 수 있는 환경을 docker에서 구성하는 과정을 담는다.

분명 하라는 instruction에서 하라는 대로 했지만 실패 해서 이 문서 저문서를 참고해 환경 구성에 성공했다.

 

deepstream 이 뭔지가 궁금한 분들은 아래 페이지를 읽어 보도록 하자. 내가 설명 하는 것 보다는 nvidia의 설명을 그대로 읽는게 더 도움이 될것 같다.

deepstream: https://developer.nvidia.com/deepstream-sdk

 

NVIDIA DeepStream SDK

Build and deploy AI-powered Intelligent Video Analytics apps and services. DeepStream offers a multi-platform scalable framework to deploy on the edge or connect to any cloud.

developer.nvidia.com

 

실패의 경험으로 부터 찾은 성공 방법이 누군가에게 도움이 되길 바란다.

 

목표:

deepstream 6.0 docker 이미지에 python 예제를 실행 할수 있도록 deepstream-python-apps을 바인딩

deepstream 6.0부터는 python binding이 기본 제공이 안된다고 한다.

그래서 python api 를 바인딩하고 예제를 돌려 봄으로서 정상적으로 binding 된것을 확인하는게 목표이다.

환경

나는 jetson nano에 도커를 이용해 환경을 구성했다.

 

host 환경

hardware: jetson nano

jetpack: 4.6(r32.6.1)

cudnn:8.2.1

cuda: 10.2

 

위 호스트 환경에서 deepstream-l4t docker이미지를 다운 받아 python 예제 실행을 위해 python binding을 진행했다.

 

docker version: 20.10.2, build 20.10.2-0ubuntu1~18.0.2

base image: nvcr.io/nvidia/deepstream-l4t:6.0-samples

아래 명령으로 받을 수있다. 

docker pull nvcr.io/nvidia/deepstream-l4t:6.0-samples

 

성공적으로 python 바인딩을 빌드 하기 위해 나는 호스트와 컨테이너(docker container)내부에서 필요 라이브러리나 패키지를 설치 했다.

 

따라서 호스트 사이드에서한 작업과 컨테이너 내부에서 한 작업을 각 섹션에서 설명한다. 

 

Host side

호스트에서 가장 먼저 한일은 nvidia-l4t-gstreamer를 설치 하는 것이었다.

(사실 이과정이 정확히 필요한지는 모르겠다.  아래 실패 사례의 기록에서 보면 gst-python 빌드 과정에서 gst/gst.h를 찾을수 없다는 에러가 발생해 그걸 해결하는 과정에서 이걸 설치 하게되었다. 혹 이게 필요 없는 과정이라면 댓글로 알려주시면 참 고맙겠습니다.)

우선 vi나 vim으로 아래 파일을 열고

 vi /etc/apt/sources.list.d/nvidia-l4t-apt-source.list

아래 두 문장을 파일에 쓰고 저장한다. 

이때 두번째 줄에 'jetson/t210'이라고 적은 부분이 있는데  't210' 은 jetson nano를 지칭하는 platform 구분자이다. tx2나 xavier는 다른 플렛폼 구분자를 써줘야 한다.

deb https://repo.download.nvidia.com/jetson/common r32.6 main
deb https://repo.download.nvidia.com/jetson/t210 r32.6 main

그리고 나서 아래 명령 실행한다.

apt update

실행 시 아래와 같은 에러를 마주 치는 경우가 있다.

Fig 1. NO_PUBKEY Error

Err:1 https://repo.download.nvidia.com/jetson/common r32.6 InRelease
  The following signatures couldn't be verified because the public key is not available: NO_PUBKEY 0D296FFB880FB004
Err:2 https://repo.download.nvidia.com/jetson/t210 r32.6 InRelease
  The following signatures couldn't be verified because the public key is not available: NO_PUBKEY 0D296FFB880FB004
Hit:4 http://ports.ubuntu.com/ubuntu-ports bionic-updates InRelease
Hit:5 http://ports.ubuntu.com/ubuntu-ports bionic-backports InRelease
Hit:6 http://ports.ubuntu.com/ubuntu-ports bionic-security InRelease
Reading package lists... Done                      
W: GPG error: https://repo.download.nvidia.com/jetson/common r32.6 InRelease: The following signatures couldn't be verified because the public key is not available: NO_PUBKEY 0D296FFB880FB004
E: The repository 'https://repo.download.nvidia.com/jetson/common r32.6 InRelease' is not signed.
N: Updating from such a repository can't be done securely, and is therefore disabled by default.
N: See apt-secure(8) manpage for repository creation and user configuration details.
W: GPG error: https://repo.download.nvidia.com/jetson/t210 r32.6 InRelease: The following signatures couldn't be verified because the public key is not available: NO_PUBKEY 0D296FFB880FB004
E: The repository 'https://repo.download.nvidia.com/jetson/t210 r32.6 InRelease' is not signed.
N: Updating from such a repository can't be done securely, and is therefore disabled by default.
N: See apt-secure(8) manpage for repository creation and user configuration details.

위 에러를 해결 하기 위해 나는 아래와 같은 명령을 입력했다.  repo에 접근 하기 위한 public key를 받아 오라는 명령이다.

 apt-key adv --fetch-key https://repo.download.nvidia.com/jetson/jetson-ota-public.asc

위 명령으로 에러를 해결하고 난후 'apt update' 명령은 아래 처럼 에러 없이 수행된다.

Hit:1 http://ports.ubuntu.com/ubuntu-ports bionic InRelease                                                 
Get:2 https://repo.download.nvidia.com/jetson/common r32.6 InRelease [2555 B]                               
Hit:3 http://ports.ubuntu.com/ubuntu-ports bionic-updates InRelease                
Get:4 https://repo.download.nvidia.com/jetson/t210 r32.6 InRelease [2547 B]
Hit:5 http://ports.ubuntu.com/ubuntu-ports bionic-backports InRelease              
Hit:6 http://ports.ubuntu.com/ubuntu-ports bionic-security InRelease    
Get:7 https://repo.download.nvidia.com/jetson/common r32.6/main arm64 Packages [17.4 kB]
Get:8 https://repo.download.nvidia.com/jetson/t210 r32.6/main arm64 Packages [9493 B]

업데이트가 끝나면 목표로 처음 목표 였던 nvidia-l4t-gstreamer를 설치 한다.

sudo apt install --reinstall nvidia-l4t-gstreamer

 

 

Inside docker

도커를 실행 하기 위해 내가 사용한 명령 및 옵션은 아래와 같다.

docker

 

컨테이너 내부에서도 가장 먼저 한일은 gstreamer관련 툴, 라이브러리, 패키지 등을 설치하는 것이다.

host side에서 nvidia-l4t-gstreamer를 설치 한 것과 같은 이유로 gst-python을 빌드 하는 과정에서

gst/gst.h 와 관련된 에러가 나오는데 이 문제를 해결하기 위한 방법이다.

 

검색을 한참 해보고 nvidia측에서 한 답변과 인터넷의 똑똑한 분들이 추천한 방법들을 많이 시도해 봤지만

내 경우에 그 제안들은 해결방법이 아니었기 때문에 아래 문서를 보고 gstreamer와 관련된 걸 전부다 설치 했다.

 

최소한의 설치로 이 문제를 해결하면 edge device의 특성상 disk space를 아낄 수 있겠지만 시간 관계상 급한데로 전부 설치 했다.

 

GStreamer-1.0 Installation and Setup

 

generic-no-api_r2

Your browser has DOM storage disabled. Make sure DOM storage is enabled and try again.

docs.nvidia.com

 

위와 같은 이유로 나는 아래 명령을 컨테이너 내부에서 실행해 gstreamer와 관련된 패키지들을 설치 했다.

apt-get update
apt-get install gstreamer1.0-tools gstreamer1.0-alsa \
  gstreamer1.0-plugins-base gstreamer1.0-plugins-good \
  gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly \
  gstreamer1.0-libav
apt-get install libgstreamer1.0-dev \
  libgstreamer-plugins-base1.0-dev \
  libgstreamer-plugins-good1.0-dev \
  libgstreamer-plugins-bad1.0-dev

그리고 아래 패키지들도 추가로 설치 했다. (나는 이걸 컨테이너 내부에서 설치했는데 nvidia 측에서는 이건 host에 설치해야 하는 거고 docker 옵션 중 --runtime nvidia 를 사용 하면 아래 명령으로 설치한 것들이 container에 자동으로 마운트 되는 구조라고 한다. )

apt install libssl1.0.0 libgstreamer1.0-0 libgstrtspserver-1.0-0 libjansson4=2.11-1

그리고 나서 아래 link의 안내를 따라 진행했다.

link:https://github.com/NVIDIA-AI-IOT/deepstream_python_apps/tree/master/bindings

 

GitHub - NVIDIA-AI-IOT/deepstream_python_apps: DeepStream SDK Python bindings and sample applications

DeepStream SDK Python bindings and sample applications - GitHub - NVIDIA-AI-IOT/deepstream_python_apps: DeepStream SDK Python bindings and sample applications

github.com

apt install -y git python-dev python3 python3-pip python3.6-dev python3.8-dev cmake g++ build-essential \
    libglib2.0-dev libglib2.0-dev-bin python-gi-dev libtool m4 autoconf automake
    
cd /opt/nvidia/deepstream/deepstream-6.0/sources
git clone https://github.com/NVIDIA-AI-IOT/deepstream_python_apps.git
cd deepstream_python_apps
git submodule update --init

Installing Gst-python

cd 3rdparty/gst-python/
./autogen.sh
make
make install

 Building the bindings

cd deepstream_python_apps/bindings
mkdir build
cd build
cmake ..
make

이러고 나니 이렇게 에러 없이 빌드를 성공할 수 있었다.

 

Fig 2. build bindings 성공 화면

 

예제 실행

위 설명에서 build bindings 까지 성공하고 나면 아래 그림 처럼 build 폴더내에 'pyds.so' 파일이 생성된다.

Fig 3. pyds.so 생성된 모습

생성된 pyds.so 파일은  /opt/nvidia/deepstream/deepstream-6.0/lib 에 복사 해줘야 한다.

아래 명령에서 나는 상대 경로를 사용했다. pyds.so 파일이 최종적으로 위치 해야 하는 곳에 복사만 해주면 된다.

cp pyds.so ../../../../lib/
# 또는
# cp pyds.so /opt/nvidia/deepstream/deepstream-6.0/lib

만약 pyds.so 파일을 저 위치에 복사 하지 않으면 아래와 같은 에러 메시지를 보게 될 것이다.

내가 실제로 봤다...  저거 복사하라는 말은 instruction에서 못봤는데...

 

Traceback (most recent call last):
File “deepstream_test_1_usb.py”, line 29, in
import pyds
ModuleNotFoundError: No module named ‘pyds’

 

pyds.so 복사 하고 난 후 python 예제가 실행 되는 지 확인 하기 위해 아래 파일을 실행했다.

example path : /opt/nvidia/deepstream/deepstream-6.0/sources/deepstream_python_apps/apps/deepstream-test1-usbcam

cd /opt/nvidia/deepstream/deepstream-6.0/sources/deepstream_python_apps/apps/deepstream-test1-usbcam
python3 deepstream_test_1_usb.py /dev/video0

Fig 4. 예제 실행 후 warning이 뜨는 화면

예제를 실행 하고 나면 gstreamer warning이 마구 발생하며 꼭 실행이 안될 것 처럼 log가 나온다.

하지만 실행은 된다.

아래는 실행 화면 인다. 아이패드로 유재석 씨 사진을 띄워 카메라 앞에 두니 사람으로 인식하는 모습을 볼 수 있다.

Fig 5. 예제 실행 후 사람 인식 모습

이렇게 deepstream 6.0 python예제를 실행 시키는 방법을 알아 봤다.

이제 예제 코드를 분석해 sdk 사용법을 파악 한 후 본 프로젝트 개발을 시작해야겠다.

 

 

아래는 첫 시도에서 실패 했던 과정에 대한 기록이다.

더보기

down load deepstream 6.0-l4t sample container:

docker pull nvcr.io/nvidia/deepstream-l4t:6.0-samples

앞으로의 내용은 거의 아래 link를 따라 한것 인데 나는 이걸 따라 해서 실패 했다. 그래서 이 문서 저문서를 참고해 성공한 방법이 위 포스팅 내용이었다.

 

GitHub - NVIDIA-AI-IOT/deepstream_python_apps: DeepStream SDK Python bindings and sample applications

DeepStream SDK Python bindings and sample applications - GitHub - NVIDIA-AI-IOT/deepstream_python_apps: DeepStream SDK Python bindings and sample applications

github.com

python binding installation

apt-get install python-gi-dev
export GST_LIBS="-lgstreamer-1.0 -lgobject-2.0 -lglib-2.0"
export GST_CFLAGS="-pthread -I/usr/include/gstreamer-1.0 -I/usr/include/glib-2.0 -I/usr/lib/x86_64-linux-gnu/glib-2.0/include"

apt-get install python3-dev libpython3-dev
apt-get install libtool m4 automake 
apt install libgstreamer1.0-dev

git clone https://github.com/GStreamer/gst-python.git
cd gst-python
git checkout 1a8f48a
./autogen.sh PYTHON=python3
./configure PYTHON=python3
make
make install

위 명령을 실행 시키다 보면 아래와 같은 에러가 발생한다.

libtoolize:   error: One of these is required:
libtoolize:                 gm4 gnum4 m4
libtoolize:   error: Please install GNU M4, or 'export M4=/path/to/gnu/m4'.

그래서 아래 패키지를 설치해줬다.

apt-get install libtool m4 automake

 

설치후 다시 gst-python 을 빌드하면 다시 아래와 같은 error가 발생한다.

정확히는 './autogen.sh PYTHON=python3' 명령 실행 후 발생한다.

checking for headers required to compile python extensions... not found
configure: error: could not find Python headers
  configure failed

그래서 아래 명령을 통해 필요한 패키지들을 설치

apt-get install python3-dev libpython3-dev

그리고 나면 './autogen.sh PYTHON=python3' 명령은 무사히 넘어 갈수 있다.

 

그러나 './configure PYTHON=python3' 실행시 문제의 아래 error가 발생한다.

make[3]: Entering directory '/opt/nvidia/gst-python/gi/overrides'
  CC       _gi_gst_la-gstmodule.lo
gstmodule.c:31:10: fatal error: gst/gst.h: No such file or directory
 #include <gst/gst.h>
          ^~~~~~~~~~~
compilation terminated.

위 에러에 대한 해결 방법을 검색 하면 아래 페이지를 볼 수 있다. 하지만 시는데로 해도 문제가 해결되지 않아 나는 위에서 열거한 그 많은 gstreamer 관련 패키지를 모두 설치 한것이다....

link:https://forums.developer.nvidia.com/t/no-such-file-or-directory-include-gst-gst-h/156423/6

 

No such file or directory #include <gst/gst.h>

cc -c -o deepstream_app.o -DPLATFORM_TEGRA -I./ -I…/…/apps-common/includes -I…/…/…/includes -DDS_VERSION_MINOR=0 -DDS_VERSION_MAJOR=5 pkg-config --cflags gstreamer-1.0 gstreamer-video-1.0 x11 json-glib-1.0 deepstream_app.c Package gstreamer-video

forums.developer.nvidia.com

apt-get install libgstreamer1.0-dev libjson-glib-dev

여기 까지가 실패했던 첫번째 시도의 내용이다.

그리고 container를 종료 하고 인터넷을 열심히 검색해 이문서 저문서의 내용을 종합해 python bindings 빌드에 성공한 내용이 이번 포스팅의 내용이다.

 

결론 부터 말하면 이 시도는 실패에 대한 기록을 담고 있다.

목적은 fastMot를 jetson nano에서 실행해 응용 프로그램에서 사용 하는 것이었는데 jetson nano의 performance를 잘못 파악했고 내 실수 인지 버그인지 inference 수행 시 메모리 부족 에러가 발생하고 정상 실행되지 않는다.

 

차후 이건 다시 시도 해보고 성공하면 성공 기록을 정리해 남겨야겠다.

 

결과

(이 포스팅 따라하시면 같은 사유로 inference 시 저와 같은 문제가 발생 할 수 있음을 분명히 미리 말씀드립니다. )

실행 할 수있게 각종 dependencies들을 설치 하고 빌드는 성공했으나 inference 수행히 메모리 에러 발생하며 실행 종료됨

 

목표:

아래 리포지토리의 모델 inference가 가능한 환경을 도커로 세팅해 jetson nano에서 사용 할수 있게 배포

target:https://github.com/GeekAlexis/FastMOT

 

GitHub - GeekAlexis/FastMOT: High-performance multiple object tracking based on YOLO, Deep SORT, and KLT 🚀

High-performance multiple object tracking based on YOLO, Deep SORT, and KLT 🚀 - GitHub - GeekAlexis/FastMOT: High-performance multiple object tracking based on YOLO, Deep SORT, and KLT 🚀

github.com

환경

 

host 환경

hardware: jetson nano

jetpack: 4.6(r32.6.1)

cudnn:8.2.1

cuda: 10.2

 

위 호스트 환경에서 deepstream-l4t docker이미지를 다운 받아 python 예제 실행을 위해 python binding을 진행했다.

 

docker version: 20.10.2, build 20.10.2-0ubuntu1~18.0.2

base image: nvcr.io/nvidia/l4t-ml:r32.5.0-py3

docker pull nvcr.io/nvidia/l4t-ml:r32.5.0-py3

base image에는 다음과 같은 패키지가 설치되어있다.

더보기
  • TensorFlow 1.15
  • PyTorch v1.7.0
  • torchvision v0.8.0
  • torchaudio v0.7.0
  • onnx 1.8.0
  • CuPy 8.0.0
  • numpy 1.19.4
  • numba 0.52.0
  • OpenCV 4.1.1
  • pandas 1.1.5
  • scipy 1.5.4
  • scikit-learn 0.23.2
  • JupyterLab 2.2.9

디펜던시 설치

1. cupy 9.2 설치

installed version:8.0.0b4

required version: 9.2

fastMot 를 실행 하는데 필요한 cupy로 버전 업데이트 진행

pip3 install --upgrade cython
pip3 install cupy==9.2

2. numba 버전 변경

installed version: '0.52.0'

required version: '0.48'

컨테이너에 기본 설치 된 numba 버전이 필요 버전보다 상위 버전이므로 다운 그레이드

아래 첫번째 명령에서 'apt install llvm-**X** ' 이걸 실행했는데 이러면 현 os와 호환가능한 등록된 모든 버전이 설치된다.

버전을 명시해서 설치하기 바란다...

# llvmlite requires below libraries
apt install llvm-**X** 

#set LLVM_CONFIG path
export LLVM_CONFIG=/usr/bin/llvm-config-7 

#install llvmlite
pip3 install llvmlite==0.31.0
pip3 install numba==0.48

 

Inference 시도

 

[ WARN:0] global /home/nvidia/host/build_opencv/nv_opencv/modules/videoio/src/cap_gstreamer.cpp (1757) handleMessage OpenCV | GStreamer warning: Embedded video playback halted; module v4l2src0 reported: Internal data stream error.
[ WARN:0] global /home/nvidia/host/build_opencv/nv_opencv/modules/videoio/src/cap_gstreamer.cpp (886) open OpenCV | GStreamer warning: unable to start pipeline
[ WARN:0] global /home/nvidia/host/build_opencv/nv_opencv/modules/videoio/src/cap_gstreamer.cpp (480) isPipelinePlaying OpenCV | GStreamer warning: GStreamer: pipeline have not been created
Traceback (most recent call last):
  File "app.py", line 120, in <module>
    main()
  File "app.py", line 66, in main
    stream = fastmot.VideoIO(config.resize_to, args.input_uri, args.output_uri, **vars(config.stream_cfg))
  File "/home/FastMOT/fastmot/videoio.py", line 84, in __init__
    raise RuntimeError('Unable to read video stream')
RuntimeError: Unable to read video stream

디펜던시를 필요 버전으로 설치 하고 inference를 실행 하면 위와 같은 에러가 발생했다.

혹시나 카메라 문제인가 하고  코드에서 카메라 입력을 받아 오는 부분만 따로 때서 아래와 같이 테스트 코드를 만들어 돌려 봤다.

 

import cv2

cam = cv2.VideoCapture("v4l2src device=/dev/video0 ! video/x-raw, width=1920, height=1080, format=YUY2, framerate=5/1 ! videoscale ! video/x-raw, width=1280, height=720 ! videoconvert ! appsink sync=false")

for i in range(2000):
    ret, img = cam.read()
    cv2.imshow('test', img)
    if cv2.waitKey(1) == 27:
        break
~

이것도 에러가 발생한다.

[ WARN:0] global /home/nvidia/host/build_opencv/nv_opencv/modules/videoio/src/cap_gstreamer.cpp (711) open OpenCV | GStreamer warning: Error opening bin: no element "4l2src"
[ WARN:0] global /home/nvidia/host/build_opencv/nv_opencv/modules/videoio/src/cap_gstreamer.cpp (480) isPipelinePlaying OpenCV | GStreamer warning: GStreamer: pipeline have not been created
Traceback (most recent call last):
  File "../show.py", line 10, in <module>
    cv2.imshow('test', img)
cv2.error: OpenCV(4.1.1) /home/nvidia/host/build_opencv/nv_opencv/modules/highgui/src/window.cpp:352: error: (-215:Assertion failed) size.width>0 && size.height>0 in function 'imshow'

첫 번째 에러 솔루션

웬지 카메라 입력 받아 오는 부분이 문제인거 같아  아래 처럼 카메라 스펙을 확인해 봤다.

$v4l2-ctl -d /dev/video0 --list-formats-ext

	Index       : 1
	Type        : Video Capture
	Pixel Format: 'YUYV'
	Name        : YUYV 4:2:2
		Size: Discrete 1280x720
			Interval: Discrete 0.100s (10.000 fps)
			Interval: Discrete 0.200s (5.000 fps)
		Size: Discrete 640x480
			Interval: Discrete 0.033s (30.000 fps)
			Interval: Discrete 0.040s (25.000 fps)
			Interval: Discrete 0.050s (20.000 fps)
			Interval: Discrete 0.067s (15.000 fps)
			Interval: Discrete 0.100s (10.000 fps)
			Interval: Discrete 0.200s (5.000 fps)
		Size: Discrete 640x360
			Interval: Discrete 0.033s (30.000 fps)
			Interval: Discrete 0.040s (25.000 fps)
			Interval: Discrete 0.050s (20.000 fps)
			Interval: Discrete 0.067s (15.000 fps)
			Interval: Discrete 0.100s (10.000 fps)
			Interval: Discrete 0.200s (5.000 fps)

코드에서 요구하는 건 1920x1080 resolution인데 내 카메라는 그 사이즈를 지원하지 않는다.

그래서 아래 처럼 resolution을 1280x720, framerate 5/1 로 바꿧더니 에러가 해결 됐다.

cam = cv2.VideoCapture("v4l2src device=/dev/video0 ! video/x-raw, width=1280, height=720, format=YUY2, framerate=5/1 ! videoscale ! video/x-raw, width=1280, height=720 ! videoconvert ! appsink sync=false")

 

그래서 cfg/mot.json 파일에서 input stream resolution을 변경했다.

아래는 변경된 mot.json (configuration file)이다.

 

 

이후 아래와 같이 래퍼런스를 돌렸다.

python3 app.py --input-uri /dev/video0 --mot

아래와 같은 에러가 발생했다.

하지만 이건 문제가 아니다. chrome을 켜놓고 있었는데 tab을 전부 다 닫으니 이 문제는 안생긴다.

크롬을 닫고 실행 하면 에러 및 워닝 메시지가 출력되고

4분 쯤 모델 로딩 하는 시간이 걸리지만 뭔가 실행되고 있는 것같은 메시지가 아래 화면 처럼 나온다.

하지만 결과가 시각화 되지 않는다.

시각 화를 위해 아래와 같이 option을 추가 했다.

python3 app.py --input-uri /dev/video0 --mot --show

그랬더니 아래 화면 처럼 이라는 에러가 발생한다.

원인 파악 하고 고칠 시간이 없어 여기 까지만 확인하고 아직 고쳐서 돌려 보진 못했다.

 

LLVM ERROR: Unable to allocate memory for common symbols!

이전에 만든 도커 이미지에서 usbcamera로 영상을 받아 처리하는 프로그램을 만든뒤 실행 파일로 배포 하기 위해 

pyinstaller를 이용하기로 했다. 

 

최종 목표는 '[Jetson nano] docker를 이용한 개발 환경 세팅'에서 만든 컨테이너에서 실행 파일을 만들어 

개발에 필요한 pakage들이 없는 환경에서 프로그램이 실행되도록 만드는 것이다. 

 

실제 배포할 프로젝트의 코드는 좀 크니 여기선 간단히 opencv만 있는 단일 파일을 기준으로 실행파일을 만들어 본다. 

 

아래는 연습용으로 빌드할 스크립트이다. 

import cv2

cam = cv2.VideoCapture(0)
cam.set(3, 640)
cam.set(4, 480)

for i in range(2000):
    ret, img = cam.read()
    cv2.imshow('test', img)
    if cv2.waitKey(1) == 27:
        break

 

빌드

위의 스크립트를 main_cam.py로 저장한 후 아래 명령으로 빌드를 시도 했다.

pyinstaller -w -F main_cam.py

만들어진 파일을 실행 시켜 보니 아래와 같은 opencv 에러가 발생했다. 이런 이슈가 생길거 같아 미리 opencv만 있는 작은 

연습 스크립트로 빌드를 테스트 해본것이다. 

에러의 내용은 cv2를 반복적으로 로드 하는 현상이 발생한다는 것이다. 

Fig 1. Error 캡쳐

디버깅

사실 위 문제가 왜 발생하는지 알아내는데 2일이나 시간을 허비했지만 스스로 원인은 파악하지 못했고

nvidia에 l4t container에서 opencv 응용 프로그램을 pyinstaller로 빌드하려고 하니 위 에러가 발생했다고 

문의 하니 이것은 잘 알려진 버전 문제라고 응답했다. (잘 알려진 문제 치곤 검색해도 해결법이 잘 안나온다..)

 

원인은 pyinstaller 버전과 프로그램에서 사용하는 opencv 버전이 호환 가능하지 않다는 것이다. 

 

솔루션

처음 빌드 하는데 사용한 내 환경은

pyinstaller version: 4.8

opencv 버전은 :4.1.1 (이전 포스트에선 3.2.0을 설치 했는데 문제가 좀 있어서 업그레이드 했다.)

 

아래 링크는 이 문제를 해결하는 방법이 기록된 영문 페이지 이다. 

link:https://elinux.org/Jetson/L4T/TRT_Customized_Example#PyInstaller_with_OpenCV

 

Jetson/L4T/TRT Customized Example - eLinux.org

This page collects information to deploy customized models with TensorRT and some common questions for Jetson. TensorRT Python OpenCV with ONNX model Below is an example to deploy TensorRT from an ONNX model with OpenCV images. Verified environment: import

elinux.org

 

nvidia 측에 따르면 두가지 해결 방법이 있다.

 

 

1. opencv 버전을 업그레이드.  

아래 명령을 이용하라고 한다. (하지만 이건 좀 애매하다 나는 4.5.* 버전의 opencv도 설치해서 이 문제를 해결하려고 시도했지만 같은 문제가 지속적으로 발생해 포기했다.)

 pip3 install opencv-python

2. pyinstaller 와 pyinstaller-hooks-contrib 버전 다운 그레이드

아래 명령을 이용하면 된다. 

pip3 install pyinstaller==4.2
pip3 install pyinstaller-hooks-contrib==2021.2
pyinstaller --onefile --paths="/usr/lib/python3.6/dist-packages/cv2/python-3.6" target.py

나는 두번째 옵션인 pyinstaller 버전 다운 그레이드를 선택했다.

실행 결과 아래와 같이 위 스크립트를 onefile로 빌드해 성공했다. 

Fig 2.실행 성공 화면

 

* pyinstaller 버전마다 자동으로 package import 를 문제 없이 할수 있는 package 버전이 다른 것으로 보인다. 

나도 pyinstaller를 자주 사용하진 않아 이 문제를 정확히 파악하진 못했다. 

다만 혹시 비슷한 문제를 격고 있는 분들은 pyinstaller 와 프로젝트에서 사용하는 package 버전들 간의 호환성 문제는 아닌지 고려해 보고 호환 되는 버전의 pyinstaller를 사용 하면 도움이 될것 같다. 

 

이번엔 jetson nano의 docker container 에 opencv-python 4.5.1 버전을 설치해 보고 설치 과정 중 내가 격은 에러 사항과
해결 방법을 기록한다.

 

참고로 nvidia에서 제공하는 l4t-ml docker image에는 opencv가 이미 설치되어있다. pytorch, tensorflow도 설치 되어있다. 

빌드하느라 고생하기 싫은 분들은 저런 도커 이미지를 사용하는게 훠얼씬 시간절약 및 정신 건강에 좋다.

 

 
설치 과정은 아래 link되어있는 페이지에 소개된 내용을 따라 했다. 이런 좋은 가이드를 올려주신 개발자 분에게 감사드리고 싶다.
참조 페이지: link

 

로그인 또는 가입하여 보기

Facebook에서 게시물, 사진 등을 확인하세요.

ko-kr.facebook.com


유일한 다른 점은 나는 host에 직접 설치 한게 아니라 이전 포스팅([jetson nano] docker를 이용한 개발 환경 세팅) 에서 다운 받은 'l4t-pytorch:r32.6.1-pth1.8-py3 ' docker image 에 opencv 4.5.1 을 설치 했다는 것이다.

패키지 업데이트 및 설치

apt update a
pt install -y python3-pip python3-dev python3-numpy
apt install -y libcanberra-gtk* libgtk2.0-dev 
apt install -y libtbb2 libtbb-dev libavresample-dev libvorbis-dev libxine2-dev 
apt install -y curl

나의 경우 위의 명령을 실행하면 아래와 같이 libopencv와 관련된 log가 무수히 많이 발생했다.

간단한 opencv 동작 테스트 때는 문제가 되지 않았다. 

/sbin/ldconfig.real: File '/usr/loib/aarch64-linux-gnu/libopencv_imgproc.so is emptry, not checked'

 

 

math, video, image format 관련 library 설치

apt install -y libxvidcore-dev libx264-dev libgtk-3-dev 
apt install -y libjpeg-dev libpng-dev libtiff-dev 
apt install -y libmp3lame-dev libtheora-dev libfaac-dev libopencore-amrnb-dev 
apt install -y libopencore-amrwb-dev libopenblas-dev libatlas-base-dev 
apt install -y libblas-dev liblapack-dev libeigen3-dev libgflags-dev 
apt install -y protobuf-compiler libprotobuf-dev libgoogle-glog-dev 
apt install -y libavcodec-dev libavformat-dev gfortran libhdf5-dev 
apt install -y libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev 
apt install -y libv4l-dev v4l-utils qv4l2 v4l2ucp libdc1394-22-dev

 

opencv 다운로드 및 압축해제

curl -L https://github.com/opencv/opencv/archive/4.5.1.zip -o opencv-4.5.1.zip curl -L https://github.com/opencv/opencv_contrib/archive/4.5.1.zip -o opencv_contrib-4.5.1.zip unzip opencv-4.5.1.zip unzip opencv_contrib-4.5.1.zip cd opencv-4.5.1/ mkdir build cd build cmake -D WITH_CUDA=ON \ -D ENABLE_PRECOMPILED_HEADERS=OFF \ -D OPENCV_EXTRA_MODULES_PATH=../../opencv_contrib-4.5.1/modules \ -D WITH_GSTREAMER=ON \ -D WITH_LIBV4L=ON \ -D BUILD_opencv_python2=ON \ -D BUILD_opencv_python3=ON \ -D BUILD_TESTS=OFF \ -D BUILD_PERF_TESTS=OFF \ -D BUILD_EXAMPLES=OFF \ -D CMAKE_BUILD_TYPE=RELEASE \ -D CMAKE_INSTALL_PREFIX=/usr/local \ -D OPENCV_EXTRA_MODULES_PATH=../../opencv_contrib-4.5.1/modules \ -D EIGEN_INCLUDE_PATH=/usr/include/eigen3 \ -D CUDA_ARCH_BIN="5.3" \ -D CUDA_ARCH_PTX="" \ -D WITH_CUDNN=ON \ -D WITH_CUBLAS=ON \ -D ENABLE_FAST_MATH=ON \ -D CUDA_FAST_MATH=ON \ -D OPENCV_DNN_CUDA=ON \ -D ENABLE_NEON=ON \ -D WITH_QT=OFF \ -D WITH_OPENMP=ON \ -D WITH_OPENGL=ON \ -D BUILD_TIFF=ON \ -D WITH_FFMPEG=ON \ -D WITH_TBB=ON \ -D BUILD_TBB=ON \ -D WITH_EIGEN=ON \ -D WITH_V4L=ON \ -D OPENCV_ENABLE_NONFREE=ON \ -D INSTALL_C_EXAMPLES=ON \ -D INSTALL_PYTHON_EXAMPLES=ON \ -D BUILD_NEW_PYTHON_SUPPORT=ON \ -D BUILD_opencv_python3=TRUE \ -D OPENCV_GENERATE_PKGCONFIG=ON \ -D BUILD_EXAMPLES=OFF .. make -j4 sudo make install


여기 까지 하고 나면 cv2==4.5.1 버전의 설치는 완료 된다. 에러도 없이 아주 잘 설치 된다.
cv2==3.2.0 버전에서 잘동작 하던 카메라 영상을 받아오는 스크립트를 실행 시키면 아래와 같은 에러가 발생한다.

No protocol specified nvbuf_utils: Could not get EGL display connection
(Argus) Error FileOperationFailed: Connecting to nvargus-daemon failed: No such file or directory (in src/rpc/socket/client/SocketClientDispatch.cpp, function openSocketConnection(), line 205) 
(Argus) Error FileOperationFailed: Cannot create camera provider (in src/rpc/socket/client/SocketClientDispatch.cpp, function createCameraProvider(), line 106) 
[ WARN:0] global /home/download/opencv-4.5.1/modules/videoio/src/cap_gstreamer.cpp (961) open OpenCV | GStreamer warning: Cannot query video position: status=0, value=-1, duration=-1 
No protocol specified Unable to init server: Could not connect: Connection refused Traceback (most recent call last): 
File "show.py", line 10, in <module> cv2.imshow('img', img)
cv2.error: OpenCV(4.5.1) 
/home/download/opencv-4.5.1/modules/highgui/src/window_gtk.cpp:624: 
error: (-2:Unspecified error) 
Can't initialize GTK backend in function 'cvInitSystem'

카메라가 문제인가 하고 gst-launch-1.0으로 영상 출력을 확인했더니 xvimagesink 가 없어 패키지를 설치했다.

gstreamer1.0-x 를 설치 하면 된다.

$ gst-launch-1.0 v4l2src device=/dev/video0 ! videoconvert ! xvimagesink 
error occcur: no element xvimagesink 
$ apt install gstreamer1.0-x

xvimagesink설치 후 다시 gst-launch-1.0으로 카메라 영상 출력을 시도해보았다.

역시 안된다. 

이때 든생각이 docker에서 display 넘겼었나...? 

컨테이너를 종료하고 

도커를 다시 실행했다. 이번엔 아래 처럼 xhost + 를 입력해 

모든 호스트에 대해 그래픽 요청을 허용 하도록 하고 (접근제어 안한다는 뜻)

DISPLAY도 잊지 않고 넘겼다.

xhost +
docker run -it --rm --runtime nvidia --network host --priviledged -e DISPLAY=$DISPLAY
~~~ nvcr.io/nvidia/l4t-ml:r32.5.0-py3

그리고 다시 gst-launch-1.0으로 카메라를 테스트 하고 영상 출력이 정상인것을 확인후 opencv 테스트 프로그램을 실행했다.

잘된다. 

gst-launch-1.0 v4l2src device=/dev/video0 ! videoconvert ! xvimagesink 
#run show.py again 
python3 show.py


#complete

 

이전 포스팅에서 젯슨 나노 조립을 완료 했고 이번에 할일은 usb 웹캠을 통해 영상을 입력 받고 pytorch를 설치 하는 것이다.
 
내 최종 목표는 만든 응용 프로그램을 도커라이즈 해서 자동 배포 하는 것 이므로
웹캠 영상을 도커 컨태이너 내부에서 받아 사용해야 하고 pytorch 도 컨테이너 내부에 설치 해야 한다.
 
위의 문제를 가장 간단히 해결할 수 있는 방법은 바로 nvidia 에서 제공하는 L4T-pytorch를 사용 하는 것이다.
적어도 내 목적을 이루는 대에는 가장 손쉬운 방법이며 더 복잡한 것을 해야 하는 사람들에게 좋은 출발점이 될것 이다.

 

sudo 없이 도커사용

가장 먼저 한일은 sudo 명령 없이 도커를 사용하기 위해 docker group에 user를 포함시키는 일이다.
nvidia에서 제공하는 jetson nano os 를 설치 하고 나면 기본적으로 docker는 설치 되어있지만
user를 자동으로 docker그룹에 포함시지는 않는다.
 
그래서 아래 명령을 이용해 사용자를 docker group에 할당한다.

 

 sudo usermod -aG docker $USER

그리고 로그아웃 후 재 로그인 해주면 docker 명령을 sudo 명령 없이 사용할 수 있다.
 
다음으로 swap memory를 4GB 늘리는 작업이다.(일단 4GB로 했는데 opencv 4.5 설치시 6GB이상으로 늘려야 한다고 한다...)
 아래의 swap 메모리 설정은 nvidia에서 제공하는 tutorial을 참고 해 작성했다.
 
link : nvidia jetson nano tutorial

Jetson AI Courses and Certification

Jetson AI Courses and Certifications NVIDIA’s Deep Learning Institute (DLI) delivers practical, hands-on training and certification in AI at the edge for developers, educators, students, and lifelong learners. This is a great way to get the critical AI s

developer.nvidia.com

우선 swap 메모리를 4GB 할당하는 파일을 만든다. (혹 swap 메모리가 뭔지 모르는 독자는 os 와 관련된 내용을 아주 약간이나마 공부해 두면 이런거 이해하기가 편하다...)

sudo systemctl disable nvzramconfig
sudo fallocate -l 4G /mnt/4GB.swap
sudo chmod 600 /mnt/4GB.swap
sudo mkswap /mnt/4GB.swap

그리고 만들어진 swap 설정을 boot시 로드하기 위해 /etc/fstab에 적용한다.

sudo vim /etc/fstab

vim으로 연 /etc/fstab 파일의 맨 아래 줄에 다음을 추가 한 후 저장한다.

/mnt/4GB.swap            swap                     swap           defaults                                     0 0

바뀐 설정을 로드 하고 (그냥 재부팅 해도 된다.) swap 용량을 확인하기 위해 아래 명령을 이용한다.

free -m

아래와 같이 swap size가 바뀐걸 확인할 수 있다.

swap 메모리 용량 확인

적절한 docker image 다운 받기

다음으로 할 일은 자신의 목적에 맞는 적절한 도커 이미지를 다운 받는 일이다.
나는 torch1.8, torchvision, opencv가 기본적으로 필요하다. docker file을 만들어 필요한걸 일일이 설치 할 수도 있지만
시간 절약과 효율성을 위해서 nvidia에서 제공하는 L4T image 를 사용하기로 했다.
아래 링크를 타고 들어가면 jetson nano에서 사용가능한 pytorch, torchvision등을 포함해 생성된 이미지를 다운받을 수 있다.
 
link:https://catalog.ngc.nvidia.com/orgs/nvidia/containers/l4t-pytorch

NVIDIA L4T PyTorch | NVIDIA NGC

PyTorch is a GPU accelerated tensor computational framework with a Python front end. This container contains PyTorch and torchvision pre-installed in a Python 3.6 environment to get up & running quickly with PyTorch on Jetson.

catalog.ngc.nvidia.com

내가 다운 받은 버전은 아래 버전이다. 내 목적에 부합해서 받은 것 뿐 각자 자기의 목적에 맞는 버전을 받으면 된다.
 

  • l4t-pytorch:r32.6.1-pth1.8-py3
    • PyTorch v1.8.0
    • torchvision v0.9.0
    • torchaudio v0.8.0

아래 명령을 통해 이미지를 다운 받을 수 있다.

docker pull nvcr.io/nvidia/l4t-pytorch:r32.6.1-pth1.8-py3

 

이미지를 다운 받고 컨테이너를 실행해 잘 동작하는지 확인해 보자.
container 실행 명령은 다음과 같다. 

docker run -it --rm --runtime nvidia --network host --privileged -e DISPLAY=$DISPLAY -v /tmp/.X11-unix:/tmp/.X11-unix -v /dev:/dev nvcr.io/nvidia/l4t-pytorch:r32.6.1-pth1.8-py3

 
 

docker conatiner 에서 usb cam 영상 읽기

다운 받은 컨테이너에서 cv2.VideoCapture()를 이용해 usb영상을 읽으려 했으나 아래와 같은 문제가 발생했다.

AttributeError: module 'cv2' has no attribute 'VideoCapture'

그래서 이게 뭔일인가 하고 cv2.__path__ 로 모듈 경로를 확인해 해당 폴더를 확인해 보니 거기엔 아무 것도 없었다.
왜 없는 건지 모르겠지만 깨끗하게 단하나의 파일도 없었다. 그래서  아래 명령을 통해 opencv 를 설치 했다.
 

apt update 
apt-get install python3-opencv

상기 명시된 컨테이너에서 위 명령으로 설치한 opencv 버전은 '3.2.0'이다.
 
그리고 미리 만든 show.py란 파일을 실행시켜 카메라 동작을 확인했다.
아래는 show.py의 내용이다.

import cv2

if __name__=='__main__':
	cam =cv2.VideoCapture(0)
    if cam.isOpened():
        cam.set(cv2.CAP_PROP_FRAME_WIDTH, 640) #영상 width 설정
        cam.set(cv2.CAP_PROP_FRAME_HEIGHT, 480) #영상 height 설정
        while True:
        	ret, img =cam.read() #영상 읽기
            cv2.imshow('img', img)
            if cv2.waitKey(1) == 27 : #esc 누르면 종료
            	break

아래 사진은 위 스크립트 실행시 나타나는 usb camera 영상이다.
영상은 잘 나오지만 맘에 안드는 것은 터미널에 지속적으로 뜨는 'currupt JPEG data:~~' 라는 warning 들이다.
다음 검색을 좀 해보니 opencv설치 시 함께 설치 되는 필요 libary와 관련된 버그 라고 하는데
다음엔 최신 버전인 opencv 4.5 설치 해보고 이문제 가 사라지는지 확인해봐야겠다. (opencv4.5 는 필요해서 설치하는 것이다... 이 문제를 해결할 기 위해 설치하는건 아니다..)

 

container 내에서 usb camera 입력 확인 영상

 

+ Recent posts