2020년 4월 30일 목요일

고정밀 LiDAR 스캔 정합 품질 확인하기

이 글은 고정밀 LiDAR 스캔 정합 결과의 품질을 확인하는 과정을 정리한 것이다.

스캔 대상은 한국건설기술연구원 실내 지하 1층부터 지상 5층이다. LiDAR 스캔 장비는 건설 인프라에서 사용하는 고정밀 장비로 Trimble TX8이다. 연구원 장비를 사용해 스캔 및 측량 작업이 수행되었으며, 수행 인원은 3명이었다. 스캔 수행은 담원에서 수고해 주셨다. 이 글에서는 3개 층에 대해 간단한 스캔 정합 품질 검토를 진행하였다. 스캔 용량이 큰 관계로 검토 시간은 2시간 30분 정도 걸렸다.

정합 방식은 Realworks를 이용한 자동 정합 방식을 사용하였다. 자동 정합이므로, 부족한 부분이 있으면 스캔 장면을 추가하기 쉽다. 각 층별 점군 갯수는 2~3억 포인트이다. 파일 용량은 각 층당 약 3GB이다. 파일 포맷은 E57이다.

층당 10에서 15장의 스캔을 수행하였다. 정밀도는 30m에서 11mm 밀도로 설정되었다. 한 스캔 당 4분 소요된다. 그러므로, 순수 스캔 시간에만 4~6시간이 소요된다. 스캔 작업 이동, 측량 및 확인 시간 등을 포함하면, 하루 안에 스캔을 모두 하기에는 어려운 상황이다.
Trimble TX8 Level 2 스캔 파라메터 설정

정확도 검토를 위해 스캔 보다 정확도가 높은 토탈스테이션을 사용해 주요 지점을 측량하였다. 측량 지점은 통계적 처리가 가능한 수준으로 계획해 수행하였다. 측량은 무타겟으로 수행되었고, 타겟은 체커보드를 미리 준비해 부착한 후 스캔 시 장면에 캡쳐되도록 하였다. 이를 통해, 스캔과 측량 결과의 정확도 비교가 가능하다.
스캔 점군 정확도 검증을 위한 토탈 스테이션

실제로는 사람이 없는 주말에 시설물 관리부서의 협조를 받아 하루에 스캔을 수행하였다. 시간 제약으로 인해 각 층별 스캔 장면 개수가 서로 차이가 있어 보인다.

파티션이 많은 환경이고 복도 교차로 부분에서 스캔하고, 각 파티션 안에서는 보안 등의 이유로 스캔하지 않았다. 마찬가지로, 보직자 및 공용 공간이 아닌 곳은 스캔하지 않기로 하였다.

정합 결과 품질 확인
대부분 정합은 큰 문제 없었으나, 부분적으로는 문제가 있는 듯 하다.

쉐도우(shadow) 문제
다음은 2층에서 스캔된 모습이다.

아래 그림에서 객체 트리 구조를 살펴보면, 정합된 스캔 장면 구조가 포함되어 있지 않다. 차후 재정합할 때 원본 파일이 필요하거나 E57파일에 스캔 장면 구조를 포함시켜야 한다. 용량이 커서 각 층별 파일을 로딩하는 데 약 7분 걸린다(MSI INTEL I7. GTX 1070). 이런 로딩 속도를 고려해, 작업 시 개별 스캔 원본 파일도 필요해 보인다.

정합 결과에 이슈가 있는 부분을 살펴보면 다음과 같다.

아래 그림의 하부 쪽 부분은 거의 점군이 없는 공간이 있다.

계단 입구인데, 계단 스캔 시 정합에 문제가 있을 수 있다. 계단부는 보이는 입구와 계단 바닥에 대해 스캔할 필요가 있다.

복도 교차로 부분에 스캔이 덜 되었다. 다른 층들도 유사한 부분이 있다. 

다음은 지하 식당층이다. 전체 스캔 장면은 10개로 보인다.

아래 그림 우측 강당 부분에 스캔이 부족하다. 한점만 스캔되었다.

식당 부이다. 큰 공간만 스캔되었고, 라운트 테이블 뒤쪽은 스캔되지 않았다. 최소 2곳은 스캔되어야 할 것으로 보인다. 쉐도우 현상이 보인다.

노이즈 이슈
아래 부분은 정합이 이상하다. 상부에 동떨어진 점군이 보인다. 유리창을 통해 스캔된 것인지 확인이 필요하다.

아래 그림은 중앙에 찌그러져 보이는 점군이 보인다. 잘못 정합된 노이즈로 보인다. 

다음 그림과 같이 노이즈가 발생된 위치를 확인해 보았다. 해당 위치는 소화전 패널 옆 부분으로 금속재질 부착물로 보인다. 스캔 시 반사재질이 영향을 준 것으로 보인다.

아래 그림에서 사람이 지나가면서 발생한 노이즈가 보인다.

아래는 4층이다. Recap을 사용해 확인해 보았다. Recap으로 파일(약 2억 포인트) 로딩은 20분 정도 걸린다. 로딩된 후에 속도는 빠르다. Recap은 LoD 데이터 파일을 뷰 거리에 따라 미리 생성해 놓고 렌더링하여 CloudCompare보다 관찰하는 속도가 체감상 3-5배 빠르다(다만, CloudCompare나 Realworks에 비해 분석 기능이 떨어진다).

4층은 파티션이 많아, 쉐도우가 많다. 이로 인해, 바닥이 잘 안보인다. 다른 층과 마찬가지로 계단부는 정합 시 이슈가 될 수 있다.

기타 사항
점군 품질은 좋은 편이다. 점군 밀도는 균일한 편이고, 노이즈로 크게 튀는 부분은 없다. 다만, 반사 재질이 있을 경우, 노이즈가 발생하는 경우가 있다. 심한 부분은 노이즈 필터링이 필요하다.

스캔 정합 결과에 대한 몇몇 이슈 부분을 살펴보았다. 
 
차후, 스캔 품질을 좀 더 개선하여, Scan To BIM, 딥러닝 시멘틱 세그먼테이션 등 기술 개발에 활용할 계획이다.

참고 - 실내 딥러닝 모델 테스트
쉐도우가 없는 균일 점균 모델로 학습된 실내 포인트 클라우드 시멘틱 세그먼테이션 모델에 본 점군을 입력해 보았다. 결과는 다음과 같다. 벽체는 제대로 세그먼테이션 되지만, 바닥 등은 제대로 분류되지 않는다. 참고로, 점군 밀도는 원본에서 0.02m 간격으로 resample하였고 입력 데이터 크기는 86MB이다.

2020년 4월 20일 월요일

개방형 플랫폼 보안을 위한 OAuth기반 인증 개발 방법

이 글은 OAuth 기반 웹 서비스 인증 기능 개발 방법을 간단히 다룬다. 최근 건설 분야에서 개방형 플랫폼, 디지털 트윈 플랫폼 등의 요구가 많아지고 있다. 개방형 플랫폼을 개발하기 위해서는 API가 필수적이다. 하지만, 누구나 플랫폼에 접근해 필요한 정보를 얻거나 수정한다면 보안상 큰 문제가 된다. 보안 및 인증 문제를 해결하기 위한 방법이 오픈소스 기반 OAuth를 이용하는 것이다. 이 글은 다음 내용을 포함한다.
  • 구글, 페이스 북 등을 포함한 로그인 기능
  • API 접근 인증을 위한 토큰 획득 및 로그인 기능

이 글은 nodejs의 React, Strapi를 이용해 인증을 구현한다. API 사용 인증이나, 다른 인증 제공자(Facebook, GitHub, Google ...)를 이용한 로그인 방법을 다룬다.

프로젝트 개발
우선 Strapi API를 작성한다.

$ npm install strapi@alpha -g
$ strapi new my-app
$ cd my-app && strapi start
$ npm install create-react-app -g
$ create-react-app good-old-react-authentication-flow

이 결과 다음과 같은 폴더가 만들어진다.
/src
└─── containers // React components associated with a Route
 |    └─── App // The entry point of the application
 |    └─── AuthPage // Component handling all the auth views
 |    └─── ConnectPage // Handles the auth with a custom provider
 |    └─── HomePage // Can be accessed only if the user is logged in
 |    └─── NotFoundPage // 404 Component
 |    └─── PrivateRoute // HoC
 |
└─── components // Dummy components
 |
└─── utils
     └─── auth
     └─── request // Request helper using fetch

인증 뷰를 구현하려면 먼저 사용자가 특정 URL에 액세스 할 수 있는지 확인하는 HoC( Higher Order Component)를 만들어야 한다. 이를 위해, auth.js를 사용한다.

import React from 'react';  
import { Redirect, Route } from 'react-router-dom';

// Utils
import auth from '../../utils/auth';

const PrivateRoute = ({ component: Component, ...rest }) => (  
  <Route {...rest} render={props => (
    auth.getToken() !== null ? (
      <Component {...props} />
    ) : (
      <Redirect to={{
        pathname: 'auth/login',
        state: { from: props.location }
        }}
      />
    ):
  )} />
);

export default PrivateRoute;  

라우팅을 만들어 본다.
import React, { Component } from 'react';  
import { BrowserRouter as Router, Switch, Route } from 'react-router-dom';

// Components
import AuthPage from '../../containers/AuthPage';  
import ConnectPage from '../../containers/ConnectPage';  
import HomePage from '../../containers/HomePage';  
import NotFoundPage from '../../containers/NotFoundPage';

// This component ios HoC that prevents the user from accessing a route if he's not logged in
import PrivateRoute from '../../containers/PrivateRoute';

// Design
import './styles.css';

class App extends Component {  
  render() {
    return (
      <Router>
        <div className="App">
          <Switch>
            {/* A user can't go to the HomePage if is not authenticated */}
            <PrivateRoute path="/" component={HomePage} exact />
            <Route path="/auth/:authType/:id?" component={AuthPage} />
            <Route exact path="/connect/:provider" component={ConnectPage} />
            <Route path="" component={NotFoundPage} />
          </Switch>
        </div>
      </Router>
    );
  }
}

export default App;  

이제 인증 뷰를 만든다. 먼저 forms.json 인증보기 양식을 만든다. 이 구조는 다음 JSON과 같다.

{
  "views": {    
    "login": [
      {
        "customBootstrapClass": "col-md-12",
        "label": "Username",
        "name": "identifier",
        "type": "text",
        "placeholder": "johndoe@gmail.com"
      },
      {
        "customBootstrapClass": "col-md-12",
        "label": "Password",
        "name": "password",
        "type": "password"
      },
      {
        "customBootstrapClass": "col-md-6",
        "label": "Remember me",
        "name": "rememberMe",
        "type": "checkbox"
      }
    ]
  },
  "data": {
    "login": {
      "identifier": "",
      "password": "",
      "rememberMe": false
    }
  }
}

auth/login, auth/register는 다음 같은 수명주기를 사용한다.

componentDidMount() {  
  // Generate the form with a function to avoid code duplication
  // in other lifecycles
  this.generateForm(this.props);
}
componentWillReceiveProps(nextProps) {  
  // Since we use the same container for all the auth views we need to update
  // the UI on location change
  if (nextProps.location.match.params.authType !== this.props.location.match.params.authType) {
    this.generateForm(nextProps);
  }
}

양식을 만들려면 forms.json파일에서 검색한 데이터를 매핑하면 된다.
handleChange = ({ target }) => this.setState({ value: { ...this.state.value, [target.name]: target.value } });

render() {  
  const inputs = get(forms, ['views', this.props.match.params.authType, []);

  return (
    <div>
      <form onSubmit={this.handleSubmit}>
        {inputs.map((input, key) => (
          <Input
            autoFocus={key === 0}
            key={input.name}
            name={input.name}
            onChange={this.handleChange}
            type={input.type}
            value={get(this.state.value, [input.name], '')}
          />
        ))}
        <Button type="submit" />
      </form>
    </div>
  );
}

이제 사용자 인증에 필요한 모든 보기를 작성해야 한다.

API 호출을 수행하려면 request 헬퍼를 사용한다.

handleSubmit = (e) => {  
  e.preventDefault();
  const body = this.state.value;
  const requestURL = 'http://localhost:1337/auth/local';

  request(requestURL, { method: 'POST', body: this.state.value})
    .then((response) => {
      auth.setToken(response.jwt, body.rememberMe);
      auth.setUserInfo(response.user, body.rememberMe);
      this.redirectUser();
    }).catch((err) => {
      console.log(err);
    });
}

redirectUser = () => {  
  this.props.history.push('/');
}

API에서 응답을 받으면 필요한 정보를 localStorageor 에 저장하고 사용자를 HomePage로 리디렉션한다.

인증 공급자 사용
사용자가 Facebook, GitHub 또는 Google을 선택하더라도 Strapi를 사용하여 사용자를 인증하는 것은 쉽다. 이 예에서는 Facebook을 사용하는 방법을 보여준다.

흐름은 다음과 같다.

  1. 사용자는 Facebook으로 로그인을 클릭한다
  2. 앱을 승인 할 수 있도록 다른 페이지로 리디렉션한다.
  3. 승인되면 Facebook은 URL의 코드를 사용하여 사용자를 앱으로 리디렉션한다.

componentDidMount에서 ConnectPage컨테이너 응답에 따라 API를 호출하고 사용자를 리디렉션한다.

componentDidMount() {  
  const { match: {params: { provider }}, location: { search } } = this.props;
  const requestURL = `http://localhost:1337/auth/${provider}/callback${search}`;

 request(requestURL, { method: 'GET' })
   .then((response) => {
      auth.setToken(response.jwt, true);
      auth.setUserInfo(response.user, true);
      this.redirectUser('/');
   }).catch(err => {
      console.log(err.response.payload)
      this.redirectUser('/auth/login');
   });
}

redirectUser = (path) => {  
  this.props.history.push(path);
}

SocialLink 컴포넌트 구현은 다음과 같다.

/**
*
* SocialLink
*
*/

import React from 'react';  
import PropTypes from 'prop-types';

import Button from '../../components/Button'

function SocialLink({ provider }) {  
  return (
    <a href={`http://localhost:1337/connect/${provider}`} className="link">
      <Button type="button" social={provider}>
        <i className={`fab fa-${provider}`} />
        {provider}
      </Button>
    </a>
  );
}

SocialLink.propTypes = {  
  provider: PropTypes.string.isRequired,
};

export default SocialLink;  

이제 AuthPage를 추가한다.
render() {  
  const providers = ['facebook', 'github', 'google', 'twitter']; // To remove a provider from the list just delete it from this array...

  return (
     <div>
       {providers.map(provider => <SocialLink provider={provider} key={provider} />)}
       {/* Some other code */}
     </div>
  );
}

레퍼런스


오픈소스 Grafana 기반 공간정보지도 데쉬보드 개발 방법

이 글은 오픈소스 Grafana 기반 공간정보지도 데쉬보드 개발 방법에 대한 간단한 소개이다. 최근 IoT 등 센서 데이터, 박데이터 분석 결과를 효과적으로 보여주는 데쉬보드의 활용도가 높아지고 있다. 예를 들어, 텍스트 정보를 시각적으로 보여주려면 데쉬보드를 이용해야 한다.

머리말
그라파나는 유명한 오픈소스 기반 데쉬보드이며, 쉽게 다양한 패널을 플러그인할 수 있다. 다음은 그라파나를 이용한 다양한 형태의 패널 플러그인을 보여준다.

동작 개념 및 기능 소개
그라파나는 다양한 시계열 데이터가 저장된 데이터베이스를 지원한다. 각 데이터소스는 제공하는 쿼리 편집기를 통해 보여지는 데이터 뷰를 정의할 수 있다. 다중의 데이터소스에서 하나의 단일 데쉬보드로 연결할 수도 있다. 즉, 아래와 같이 데이터 연결이 유지되어 데쉬보드로 모니터링하는 방식이다.

데쉬보드 - 질의 - 데이터소스

데이터베이스 종류는 시계열 DB인 influxDB를 포함한 대부분을 지원한다(참고). 그라파나를 설치하면, 이미 만들어진 Grafana (랜덤 walk data), Mixed (다중 데이터 소스), Dashboard (유사 데쉬보드로 부터 결과를 사용하는 모드) 를 선택할 수 있다. 

질의는 데이터소스에 따라 적절한 데이터 질의 방식을 지정해야 한다. 

데쉬보드 패널은 다양한 스타일을 가질 수 있다. 패널은 드래그&드롭으로 정렬할 수 있다. 그래프는 Time series, state timeline, status history, bar chart, histogram, heatmap, pie chart, bar gauge, table, logs, node graph, dashboard list, text panel 등을 지원한다(참고). 

그라파나는 다양한 데이터소스 플러그인을 지원한다. 상세 내용은 여길 참고한다.
그라파나 plugin 검색 화면 

그라파나를 설치하면 CLI (Command Line Interface)를 제공한다. 상세 명령은 설치 후 다음을 입력한다(참고). 
grafana-cli -h

디자인된 데시보드는 viewer, editor, organization administrator 계정으로 구분되 접근할 수 있다(참고).

그라파나 설정 파일은 /etc/grafana/grafana.in 에 있다(참고). 

이외, 그라파나는 데쉬보드 시간 범위, 단축키 등을 지원한다(참고).

설치 방법
Grafana 설치는 순서는 다음과 같다. 참고로, 우분투 기준으로 진행한다. 상세 내용은 여기를 참고한다.

1. 몇몇 디펜던시와 설치키를 다운로드 함
sudo apt-get install -y apt-transport-https
sudo apt-get install -y software-properties-common wget
sudo wget -q -O /usr/share/keyrings/grafana.key https://apt.grafana.com/gpg.key

2. 설치 프로그램(stable version) 리파짓토리 추가
echo "deb [signed-by=/usr/share/keyrings/grafana.key] https://apt.grafana.com stable main" | sudo tee -a /etc/apt/sources.list.d/grafana.list

3. 그라파나 설치
sudo apt-get update
sudo apt-get install grafana
sudo apt-get install grafana-enterprise

그라파나 설치 모습

그라파나 서버 시작
정상 설치되면, 그라파나 서버를 실행할 수 있다. 
sudo systemctl daemon-reload
sudo systemctl start grafana-server
sudo systemctl status grafana-server

만약, 부팅시 자동 시작하게 만들려면, 아래와 같이 입력한다.
sudo systemctl enable grafana-server.service


클라이언트에서 서버 접속
이제 서버가 실행되었으니, 클라이언트에서 접속해 본다. 

웹브라우저를 띄우고 주소를 입력한다.

로그인 화면

유저네임, 암호에 admin 을 입력해 로그인한다.
로그인 후 모습

데쉬보드 생성
이제 데쉬보드를 생성해 보자. 상세 내용은 여기를 참고한다. 

왼쪽 메뉴바 > Dashboards > Library panels > New dashboard 선택한다.

Dashboard > Add an empty panel 선택한다.

이 New dashboard / Edit Panel 화면에서 Query 탭을 선택한다.

Data source 콤보 박스에서 -- Grafana -- 데이터 소스 선택자(data source selector)를 선택한다.

화면 오른쪽 저장 버튼을 클릭해, 데쉬보드 디자인을 저장한다. 이제 데이터 소스에서 데이터를 읽어 차트로 보여주는 데쉬보드를 확인할 수 있다.

이제 오른쪽 위 패널 추가 버튼을 클릭해 현재 데쉬보드에 다양한 그래프를 추가할 수 있다.

다음은 geomap을 포함한 추가된 그래프들이다. 각 그래프에 적절한 데이터 소스를 쿼리를 이용해 설정하면 이에 맞게 그래프가 가시화된다. 

생성된 데시보드들은 다음과 같이 폴더 아래에 저장되어 있어, 재활용할 수 있다. 

데이터 소스 지정
그라파나는 수많은 데이터소스 플러그인을 제공한다. 

MongoDB 플러그인은 디폴트로 제공하지 않기 때문에, 필요하다면, 여기를 참고해 설치하도록 한다. 플러그인 설치 후 서버를 시작한다. missing signature 에러로 플러그인 안보이면, 설정 파일의 unsigned plugin section을 수정하거나, 환경변수에 unsign 플러그인 아이디를 설정하고, 그라파나를 재실행한다.
sudo gedit grafana.ini 
GF_PLUGINS_ALLOW_LOADING_UNSIGNED_PLUGINS=<your_plugin_id>
sudo systemctl restart grafana-server
npm run server

MySQL과 같은 플러그인은 디폴트로 제공한다. 이 사례에서는 다음과 같이 MySQL에 건물 관련 정보가 저장되어 있다고 가정한다.
MySQL

그라파나에서 MySQL 데이터소스를 검색해, 접속 주소, 아이디, 암호를 입력하고, 저장 및 테스트를 한다.
데이터 소스 추가 화면
데이터 소스 접속 정보 설정 및 테스트 화면

이전처럼 데쉬보드를 추가하고, 데이터소스를 선택한 후, 표현할 SQL 쿼리문을 적절히 정의한다. 출력될 컬럼들, 색상 스키마, 단위 등을 적절히 선택한다.
쿼리문 정의 및 SQL 수정 화면
생성된 그라파나 데쉬보드 (빌딩 정보 데쉬보드 예시)

공간 지리 정보 출력
공간 지리 정보를 그라파나 데쉬보드에 출력하려면, MySQL과 유사하게, PostGIS 서버 등을 설치, 실행한 후, 데이터를 업로드해야 한다. 이후, 앞의 과정과 유사하게 적절한 데이터소스를 추가하고, 데쉬보드를 생성한다.

Geomap은 데이터베이스에서 저장된 Long, Lat를 디스플레이할 수 있다. 쿼리에 위경도가 저장된 컬럼을 질의하고, 레이어를 추가한 후, 레이어에 표시될 좌표의 위경도가 저장된 컬럼을 지정한다. 그럼 다음과 같이 맵에 해당 좌표가 마크로 표시된다.

만약, 지도를 대체하고 싶다면, geojson파일을 생성하고, 다음 폴더에 파일을 복사해 넣는다. 그리고, static map으로 이 파일을 지정하면 된다. 단, 기존 geojson과 동일한 좌표체계와 구조를 가져야 기존 맵과 동일한 좌표계에 피쳐들이 디스플레이된다. 

그라파나를 다시 로딩하면, geomap 의 레이어에서 다음과 같이 해당 파일을 선택할 수 있게 된다. 선택하면, 해당 geojson이 맵에 출력된다.

데쉬보드 생성 결과는 다음과 같다.

이외에, 3차원 구글 어스 형식 데이터소스인 세슘 등 다양한 플러그인을 다음과 같이 설치할 수 있다.
grafana-cli plugins install satellogic-3d-globe-panel
sudo systemctl restart grafana-server


좀 더 상세한 내용은 레퍼런스를 살펴보길 바란다.

레퍼런스