2021년 7월 13일 화요일

Ubuntu 20.04, DOCKER, CUDA 11.0 기반 NVIDIA-DOCKER, 텐서플로우, 파이토치 설치 및 사용기

이 글은 우분투 20.04, 도커, CUDA 11.0 기반 NVIDIA-DOCKER, Tensorflow, Pytorch 설치 및 사용 방법을 간단히 설명한다. NVIDIA-DOCKER를 이용하면, 딥러닝 프레임웍 설치 시 많은 문제를 일으키는 패키지 의존 에러 없이, 미리 설치된 딥러닝 관련 패키지를 이용해 편리하게 딥러닝 모델을 개발할 수 있다. 또한, 아마존, 구글 같이 비싼 기계학습 딥러닝용 클라우드를 사용하지 않고도, 직접 여러사람이 네트워크로 접속할 수 있는 서버를 개발할 수 있다.

NVIDIA DOCKER 아키텍처 구조

여기서는 엔비디아 CUDA및 도커는 알고 있다는 전제하에 진행한다. 관련 내용은 다음 링크를 참고한다.

NVIDIA-DOCKER 설치

설치는 다음 링크를 참고한다.

다 설치한 후 다음 명령을 입력해 정상 출력되면 성공한 것이다.
sudo docker run --rm --gpus all nvidia/cuda:11.0-base nvidia-smi

NVIDIA 도커 이용하기
다음과 같이 NVIDIA 이미지 내 명령을 실행한다.
sudo nvidia-docker run --rm hello-world

apt-cache madison nvidia-docker2 nvidia-container-runtime

sudo docker run -it nvidia/cuda:11.0-base

도커 텐서플로우 설치 및 실행
이제 다음 명령으로 도커 텐서플로우 이미지를 설치하고, 실행한다. 
sudo docker run -it --rm --gpus all tensorflow/tensorflow:latest-gpu python -c "import tensorflow as tf; tf.config.list_physical_devices('GPU')"

텐서플로우 이미지 내 우분투를 실행해 본다. 
sudo docker run -it tensorflow/tensorflow:latest-gpu
exit

텐서플로우 소스코드 예제를 다음과 같이 실행해 본다. 화면처럼 출력되면 성공한 것이다.
cd ~
git clone https://github.com/tensorflow/benchmarks
sudo docker run --gpus all -it --rm -v /home/ktw/benchmarks:/benchmarks tensorflow/tensorflow:latest-gpu python benchmarks/scripts/tf_cnn_benchmarks/tf_cnn_benchmarks.py --num_gpus=1 --model resnet50 --batch_size 64

파치토치 도커 이미지 설치 및 실행
다음과 같이 파이토치 도커 이미지를 설치하고, 실행한다. 참고로, NVIDIA DRIVER가 서로 호환되지 않은 도커 이미지라면 드라이버 호환 에러가 발생한다. 이 경우, 여기 링크에 드라이버 버전을 확인하고, 해당 도커를 설치해 실행하기를 바란다.
sudo docker pull nvcr.io/nvidia/pytorch:21.02-py3
sudo docker run --gpus all -it -p 8888:8888 nvcr.io/nvidia/pytorch:21.02-py3

아래와 같이 파이썬 코드를 입력해 실행해 본다.
python
import torch
print(torch.cuda.device_count())
print(torch.cuda.current_device())
print(torch.cuda.get_device_name(torch.cuda.current_device()))
exit()

쥬피터 노트북 실행
다음과 같이 쥬피터 노트북을 도커에서 실행해 본다.
jupyter notebook

크롬을 실행하고, http://localhost:8888/ 주소를 입력한다.

노트북에 파이썬 모듈을 하나 생성하고 다음 코드를 입력해 실행한다.
Input the below code
import torch import torch.nn as nn import torch.nn.functional as F class Net(nn.Module): def __init__(self): super(Net, self).__init__() # 1 input image channel, 6 output channels, 3x3 square convolution # kernel self.conv1 = nn.Conv2d(1, 6, 3) self.conv2 = nn.Conv2d(6, 16, 3) # an affine operation: y = Wx + b self.fc1 = nn.Linear(16 * 6 * 6, 120) # 6*6 from image dimension self.fc2 = nn.Linear(120, 84) self.fc3 = nn.Linear(84, 10) def forward(self, x): # Max pooling over a (2, 2) window x = F.max_pool2d(F.relu(self.conv1(x)), (2, 2)) # If the size is a square you can only specify a single number x = F.max_pool2d(F.relu(self.conv2(x)), 2) x = x.view(-1, self.num_flat_features(x)) x = F.relu(self.fc1(x)) x = F.relu(self.fc2(x)) x = self.fc3(x) return x def num_flat_features(self, x): size = x.size()[1:] # all dimensions except the batch dimension num_features = 1 for s in size: num_features *= s return num_features net = Net() print(net)

아래와 같이 출력되면 딥러닝 모델이 정상 실행된 것이다.

이제, 복잡한 딥러닝 패키지를 설치하고 설정할 필요없어, 도커 기반으로 딥러닝 모델을 손쉽게 개발할 수 있다.

참고

2021년 7월 6일 화요일

3차원 포인트 클라우드와 언리얼 연결

이 글은 3차원 포인트 클라우드와 언리얼 연결 방법을 간단히 정리한다. 관련 활용 사례는 다음을 참고한다.


플러그인 활성화
언리얼을 실행한다. 
메뉴에서 편집 > 플러그인을 선택한다.

"LiDAR 포인트 클라우드 지원"을 검색하고, 활성화됨 박스를 체크한다.

LAS파일 포인트 클라우드 임포트
새 프로젝트를 생성한다. 임포트할 LAS 포인트 클라우드 파일을 선택한 뒤 콘텐츠 브라우저에 드래그한다. 

콘텐츠 브라우저에서 포인트 클라우드를 뷰포트 안으로 드래그한다. 콘텐츠 브라우저에서 포인트 클라우드를 우클릭하고 편집한다. 콜리전을 클릭하고 드롭다운 메뉴에서 콜리전 빌드를 선택한다. 플레이를 클릭한다.

포인트 클라우드 편집
콘텐츠 브라우저에서 포인트 클라우드 를 우클릭하고 편집을 선택한다. 포인트를 선택하고 나면 선택한 포인트를 숨기기, 삭제 또는 추출 할 수 있다.

레퍼런스

IoT 연결을 위한 언리얼기반 인터넷 서버 데이터 실시간 획득 방법

이 글은 IoT 연결을 위한 언리얼기반 인터넷 서버 데이터 실시간 획득 방법을 간략히 설명한다. 이 글에서 사용하는 방법을 이용해 IoT와 언리얼을 연결해, 물리 세계와 가상 세계를 연결하는 간단한 메타버스, 디지털 트윈 앱을 개발할 수 있다.

물리 세계 - 센서 - 아두이노 - 시리얼포트 - 인터넷 서버 - 언리얼 - 가상 디지털 세계

언리얼 네트워크 통신 방법은 다양하다. 제일 편한 방법은 누군가 만들어놓은 플러그인을 사용하는 것이다. 그럼 코딩없이 블루프린트에서 데이터를 교환할 수 있다. 직접 개발하려면 C++을 사용해야 한다. 이 경우, 라이센스 비용 없이 기능을 구현할 수 있고, 실무적으로는 이런 방식이 복잡한 문제인 경우 유연성이 높아 자주 사용한다.

IoT 장치와 연결하기 위해서는 다양한 데이터소스와 연결하는 프로토콜을 구현해야 한다. 데이터소스는 Serial port, Socket, Websocket, HTTPsocket, MQTT, TCP/IP, UDP 외에 MySQL, MongoDB와 같은 데이터베이스가 될 수도 있다. C++로 구현할 경우, 언리얼 개발사에서 제공하는 예제가 그리 많지 않다. 그래서, 소스코드를 확인하고, 개발 문서를 체크해야 한다.

여기서는 대표적인 방법을 설명한다.

SocketIOClient 방법
이는 기본 제공하는 SocketIOClient 컴포넌트를 블루프린트에서 개발하는 방법이다. 개발 방법을 이해하기 쉽게, IoT에서 데이터를 받아 그 값을 인터넷으로 방송하는 new wind서버를 만든다. new wind 토픽의 값은 아두이노 센서에서 얻은 데이터로 한다. 개발 순서는 다음과 같다. 

1. 아두이노를 연결하고 펌웨어 설치(파일>예제>Firmata>StandardFirmataPlus 선택 후 업로드)(참고)
2. Socket이란 이름의 블루프린터를 액터를 파생받아 생성
3. 블루프린트를 다음과 같이 코딩함 
4. 서버 개발을 위한 폴더 생성 후 node 패키지 설치
npm install --save express
npm install johnny-five --save
npm install serialport
npm i socket.io
npm i http
npm i util

5. 서버를 server.js이름으로 코딩
해당 패키지의 메뉴얼(Johnney-five manual)을 참고해, 아두이노에서 센서 데이터를 가져와 변수로 저장한 후, 코드에서 io.emit() 에 해당하는 부분에 해당 변수값을 넣어준다. 

var app = require('express')();
var http = require('http').Server(app);
var io = require('socket.io')(http);
var util = require('util');
var five = require('johnny-five'); // nw.require('nwjs-j5-fix').fix();
var SerialPort = require("serialport");

var clients = [];

io.on('connection', function(socket){
 clients.push(socket.id);
 var clientConnectedMsg = 'User connected ' + util.inspect(socket.id) + ', total: ' + clients.length;
 console.log(clientConnectedMsg);
socket.on('disconnect', function(){
  clients.pop(socket.id);
  var clientDisconnectedMsg = 'User disconnected ' + util.inspect(socket.id) + ', total: ' + clients.length;
  console.log(clientDisconnectedMsg);
 })
});

var sensorValue = 0;
var board = new five.Board();

board.on('ready', function () {
  var sensor = new five.Sensor("A0");
  // Scale the sensor's data from 0-1023 to 0-10 and log changes
  sensor.on("change", function() {
    sensorValue = this.scaleTo(0, 1000);
    // console.log(sensorValue);
  });
});

http.listen(3000, function(){
  console.log('listening on *:3000');
});

function getRandomInRange(min, max) {
  return Math.random() * (max - min) + min;
}
function sendWind() {
 console.log('Wind sent to user: ' + sensorValue);
 io.emit('new wind', sensorValue); // getRandomInRange(0, 360));
}
setInterval(sendWind, 3000);

6. 서버 실행
node server.js

7. 언리얼 레벨에 개발된 블루프린트 socket 액터를 드래그&드롭함
8. 언리얼 플레이

다음은 개발 결과이다. 서버로 부터 데이터를 네트워크로 잘 받아오고 있는 것을 확인할 수 있다.


FSocket 방법
C++을 이용해 다음 소켓 클래스를 직접 사용해 서버와 데이터를 주고 받는다.

FSocket* Socket = ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM)->CreateSocket(NAME_Stream, TEXT("default"), false);

FIPv4Address ip(127, 0, 0, 1);
TSharedRef<FInternetAddr> addr = ISocketSubsystem::Get(PLATFORM_SOCKETSUBSYSTEM)->CreateInternetAddr();
addr->SetIp(ip.Value);
addr->SetPort(65500);

bool connected = Socket->Connect(*addr);

레퍼런스

2021년 7월 4일 일요일

간단한 실시간 센서 데이터 가시화를 위한 아두이노와 언리얼 연결 방법

이 글은 아두이노와 언리얼(Unreal) 게임엔진을 연결하는 간단한 방법을 설명한다. 이 글은 실시간으로 시리얼 통신에서 얻은 센서값을 언리얼의 액터 메쉬의 머터리얼(재질) 색상에 직접 설정하는 방법을 포함한다. 센서와 언리얼 연결 방법을 보여주기 위해, 별도 상용 언리얼 플러그인을 사용하지 않고, C++에서 통신 모듈을 간단히 개발해 사용한다. 이 글에서 설명하는 같은 방식으로 텍스처, 광원, 메쉬 위치 등을 센서 값에 따라 실시간으로 변경할 수 있다.

이 글의 개발 결과는 다음과 같다. 화면에서 각 큐브는 물리적 센서에서 얻은 데이터값을 색상으로 표시한다. 이를 잘 이용하면, 언리얼을 이용해 IoT와 연결된 디지털트윈(Digital Twin)이나 메타버스(Metaverse)를 쉽게 개발할 수 있다.

개발 환경

언리얼과 아두이노 IDE를 각각 설치한다.

이 글은 언리얼에 대한 기본 개념은 설명하지 않는다. 이 내용은 여기를 참고한다. 아두이노에 대한 내용은 여기를 참고한다.

개발 순서

간단한 구현을 위해, 센서는 아두이노에서 얻은 값을 사용하고, RS232 시리얼 통신을 이용한다. 

센서 - 아두이노 보드 - RS232 시리얼 통신 - 언리얼 - 데이터 가시화

주요 개발 순서는 다음과 같다. 

1. 아두이노 IDE에서 센서 데이터 획득하는 회로와 프로그램 작성. 아두이노 A0핀에 광센서 등 사용해 시그널을 A0에 입력. LED는 9번핀에 연결.

주요 코드는 다음과 같음.
const int analogInPin = A0;  // Analog input pin that the potentiometer is attached to
const int analogOutPin = 9; // Analog output pin that the LED is attached to

int sensorValue = 0;        // value read from the pot
int outputValue = 0;        // value output to the PWM (analog out)

void setup() {
  Serial.begin(9600);
}

void loop() {
  // read the analog in value:
  sensorValue = analogRead(analogInPin);
  
  // map it to the range of the analog out:
  outputValue = map(sensorValue, 0, 500, 0, 255);
  analogWrite(analogOutPin, outputValue);

  // print the results to the Serial Monitor:
  Serial.write(outputValue);
  delay(50);
}

2. 언리얼 실행. 빈 프로젝트를 C++ 형식으로 생성
3. 블루프린트 작성. 기본 클래스는 Actor에서 파생받음. 이름은 ColoredCube로 설정. 블루프린트에서 Cube 메쉬 객체 설정

4. 액터의 메쉬 객체에 적용할 재질 작성 및 설정. 재질 이름은 Color로 설정하고 다음과 같이 재질 작성
5. ColoredCube에 Color 재질을 설정
6. Visual Studio에서 Serial Port로 부터 센서 데이터를 얻는 모듈을 작성. 헤더 파일은 다음과 같음. 소스 코드는 여기를 참고.
#pragma once

#include "CoreMinimal.h"
#include "CoreTypes.h"

/**
 * 
 */
class IOT6_API SerialPort
{
public:
SerialPort();
~SerialPort();

bool open(const TCHAR* sPort, int nBaud = 9600);
void close();

int write(TArray<uint8>& Buffer);
int read(TArray<uint8>& Buffer);

private:
void* m_PortHandle;
};

7. Visual Studio에서 액터 소스 파일 편집. Serial Port에서 얻은 센서 데이터로 액터 메쉬의 머터리얼을 얻어 생상을 설정함. 주요 코드는 다음과 같음.
#include "ColoredCube.h"
#include "Materials/MaterialInstanceDynamic.h"
#include "Components/StaticMeshComponent.h"
#include "Materials/MaterialInterface.h"

#pragma optimize("", off)

class SerialPortInstance
{
public:
SerialPortInstance();
~SerialPortInstance();

SerialPort _port;
};

SerialPortInstance::SerialPortInstance()
{
_port.open(_T("COM3"));  // 이 부분은 각자 시리얼 통신 포트 이름으로 변경해야 함
}

SerialPortInstance::~SerialPortInstance()
{
_port.close();
}


SerialPortInstance _serial;

// Sets default values
AColoredCube::AColoredCube()
{
  // Set this actor to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
PrimaryActorTick.bCanEverTick = true;
}

// Called when the game starts or when spawned
void AColoredCube::BeginPlay() // 최초 액터 생성 시 실행
{
Super::BeginPlay();

auto Cube = FindComponentByClass<UStaticMeshComponent>();
auto Material = Cube->GetMaterial(0);

_DynamicMaterial = UMaterialInstanceDynamic::Create(Material, NULL);
Cube->SetMaterial(0, _DynamicMaterial);

_randomColor = FMath::Rand() % 64;
}

// Called every frame
void AColoredCube::Tick(float DeltaTime)  // 프레임 렌더링 전에 호출됨
{
Super::Tick(DeltaTime);

TArray<uint8> Buffer;
Buffer.Add(0);
int len = _serial._port.read(Buffer);
if (len)
{
FString string = FString::Printf(TEXT("Data = %d"), Buffer[0]);
GEngine->AddOnScreenDebugMessage(-1, 15.0f, FColor::Yellow, string);

float blend = ((float)Buffer[0] + _randomColor) / (255.0);
// float blend = 0.5f + FMath::Cos(GetWorld()->TimeSeconds) / 2;
_DynamicMaterial->SetScalarParameterValue(TEXT("Blend"), blend);
}
}

#pragma optimize("", on)

구현 결과
이 결과는 다음과 같다. 센서 값에 따라 큐브 색상에 실시간으로 변화하는 것을 확인할 수 있다.

실제 구현 코드는 아래와 같다.
마무리
이 방법을 응용하면 IoT와 같은 객체에서 얻은 센서값을 언리얼과 연결할 수 있다. 이렇게 연결하여, 물리세계를 게임엔진 기반 3차원 가상세계로 맵핑하여 가시화하거나 그 반대로 액추에이터를 제어하는 앱을 개발할 수 있다. 

레퍼런스