[62호]자동시스템과 원격 제어 앱을 갖춘 버스용 창문
2020 ICT 융합 프로젝트 공모전 우수상
자동시스템과 원격 제어 앱을 갖춘 버스용 창문
글 | 국민대학교 구창진, 박지호, 임성현, 최형종
1. 심사평
칩센 기술을 논하자면 모터 제어를 통한 슬라이드 창문 개폐기 정도로 요약이 가능할듯 합니다. 하지만, 특정한 목표를 가지고 있고 그것이 필요한 이유에 대한 기획의도가 명확합니다. 또한, 그 기획의 타당성 또한 실제 수요층을 대상으로 수요 조사까지 이루어진 주제입니다. 많은 사람이 기능이 단순한 것은 문제가 아니라는 것, 그리고 그 기술이나 기능이 어떻게 쓰여질지 아는 것 또한 분명 중요한 포인트라는 것을 간과하는 경우가 있는데 지원자는 그 부분에 대하여 명확하게 짚어 낸 듯 합니다. 여러 상황에 대한 시연 동영상 또한 심플하지만, 제작 의도를 명확하게 보여주었습니다. 따로 흠잡을 것이 없지만, 창문이 열리고 닫히는 반응이 좀 느린 듯 한데 뭐가 원인일까 궁금해지기는 합니다.
펌테크 기획구성부터 제작과정을 포함해 사전 설문조사까지 시행하는 꼼꼼함이 반영된 실용적인 작품이라고 생각합니다. 제출된 보고서의 내용도 명확했고, 제품완성도 면에서 우수한 작품이라고 판단됩니다.
위드로봇 버스의 창문을 공기 질에 따라 자동으로 여닫는 아이디어는 좋으나 구현한 방법의 기대효과는 떨어집니다. 좀 더 다른 방법의 구현 아이디어가 필요합니다.
2. 개요
버스 외부의 미세먼지와 우천 시 빗물 유입을 자동 센서로 막겠다는 목적으로 작품 개발을 시작하였다. 우리 팀의 작품명은 ‘자동시스템과 원격제어 앱을 갖춘 버스용 창문’이다.
우리 팀이 해결하려는 문제점은 대중 버스의 창문이 열려있고, 그 좌석이 공석일 때 창문을 닫아줄 사람이 없다는 것이다. 이와 같은 문제 때문에 우천 시 빗물과 미세먼지의 유입에 대한 불만, 경제적 비효율, 불편리함 등을 줄이려고 이 작품을 만들게 되었다.
대중 버스 이용 중에 문제점의 실태를 파악하고, 그 문제점을 ICT 기술을 통해 해결하려 했다. 현재 우리나라 미세먼지의 위험성의 급증에 따른 피해를 최소화하기 위한 기술이다. 버스 천장에 미세먼지 센서와 빗물 센서를 부착하였고, 센서에서 인식한 값을 버스 기사가 편리하고 쉽게 알 수 있도록 어플을 개발하였다. 미세먼지 농도를 이미지로 표현하여 버스 기사가 앱을 통해 원격으로 제어할 수 있다.
대중 버스 창문 개방의 문제점과 이에 대한 해결책인 원격 제어 창문에 대한 대중의 인식을 버스기사와 승객한테 직접 설문조사하여 파악했다. 문제점을 자세히 알기 위해 관련 기사를 참고하였다. 원격 제어 창문의 기대효과의 설문 조사 결과가 매우 긍정적임을 보았을 때 기술의 효율성과 긍정성이 상당히 높다고 판단하였다.
2.1 작품의 필요성
2.1.1 미세먼지
2.1.1.1 심각성
미세먼지는 인체에 매우 유해하다. 위의 기사에 따르면, 세계보건기구(WHO)가 ‘2019년 건강을 위협하는 10대 요인’의 첫 번째로 ‘대기오염과 온난화’를 꼽았는데 이 대기오염을 유발하는 미세먼지를 ‘1군 발암물질’로 분류하고 있다. 미세먼지는 암, 치매, 우울증 등 많은 병을 유발하고 미세먼지 때문에 이른 나이에 사망하는 인구가 세계적으로 연간 700만 명에 달한다고 한다. 이는 흡연으로 인한 사망자보다 많은 수이다. 이런 미세먼지를 우리는 더 이상 간과해서 안 되며, 심각성을 인지하고 대비해야 한다. 또한 미세먼지에 대한 뉴스의 보도는 약 3시간을 간격으로 발표될 만큼 상당히 민감한 문제임을 확인할 수 있다.
2.1.1.2 미세먼지에 대한 대중 버스의 취약점
창문이 열려있는 공석에서의 미세먼지 유입이 버스 내부의 미세먼지 농도를 높이는 주요 원인이다. 공석 자리의 창문 개방을 확인하기 위해 총 45번의 버스를 이용하였고(기간 :2020.02.15.~2020.03.27.), 총 24번 이러한 상황을 목격하였다. 열려있을 확률은 53.3%로 꽤 높다는 것을 확인하였다.
2.1.1.3 미세먼지에 대한 대중의 시선
2.1.1.4 미세먼지에 대한 설문조사 (대상 : 버스기사)
총 3일간 버스 기사님 43명을 직접 만나 설문조사하였습니다.
버스 창문 설문지 결과 조사 미세먼지와 빗물의 유입으로 인해서 불편한 점이 많고, 이 문제를 해결해야 할 필요성을 느끼게 되었다.
2.1.2. 고속버스
2.1.2.1. 고속버스의 특성
고속버스는 안전성, 소음 최소화, 연료 효율성을 위해 통유리를 사용한다. 통유리를 사용하지 않으면 승객이 창문을 열 수 있고, 위에 적은 다양한 문제가 발생할 것이다. 통유리를 사용하기 때문에 고속버스 내부를 청소한다거나 저속 주행 시에 내부 환기 시스템에만 의존해야 한다.
2.1.2.2. 통유리의 불이익과 해결방안
통유리가 아닌 자동 창문을 사용하게 되면 승객들의 자의적인 창문을 열고 닫음을 방지할 수 있으며, 통유리 구조가 아닌 개·폐형 창문을 사용할 수 있게 된다. 또한 저속 주행을 할 때, 차량 내부를 청소할 때 창문을 열 수 있다.
2.2. 기술 도입에 따른 기대 효과
① 편리성
자동 창문 시스템으로 인하여 공석의 열려있는 창문을 불편하다고 생각하는 누군가가 대신해서 닫아야 하거나, 열려있는 상태 그대로 미세먼지가 버스 내부로 유입되는 것을 방지할 수 있다.
② 만족감
미세먼지 방지의 기술을 탑재함으로써 버스 내 미세먼지를 저감해 승객들의 만족감을 올려줄 수 있다. 위 기술의 도입으로 인해 대중버스에 대한 신뢰성이 높아질 것이다.
③ 경제성
원격 제어 앱을 통해 창문을 한 번에 열고 닫아 냉, 난방 손실을 줄이고 에너지 효율을 높일 수 있다. 고속버스가 고속 주행할 때 창문을 한 번에 닫아 공기저항을 줄여서 연비 효율을 높일 수 있다.
3. 작품 설명
3.1. 주요 동작 및 특징
3.1.1. 앱을 통한 주요 동작 및 특징
3.1.2. 센서 값에 따른 모터의 주요 동작 및 특징
3.1.3. app 버튼에 따른 모터의 주요 동작 및 특징
ⓐ app에서 제공하는 버튼은 총 3개이며 open, close, stop이 있다. ⓑ open은 ‘a’, close는 ‘b’, stop은 ‘c’를 나타낸다. ⓒ 각 버튼을 눌렀을 시, 버튼에 해당하는 값을 블루투스를 통해 아두이노로 보내준다. ⓔ 이렇게 얻어온 값은 bluetoothh()함수를 통해 motor_run()함수로 값을 보내준다. ⓕ ‘a’가 입력되면 case4, ‘b’가 입력되면 case5, ‘c’가 입력되면 case3로 이동하게 된다. ⓖ case4와 case5에선 case1과 case2로 이동하게 된다. case 1번과 2번으로 바로 넘어가지 않은 이유는 Timer2의 초기화 작업이 필요해서이다. ⓗ case1은 창문을 여는 방향, case2는 닫는 방향, case 3는 창문의 구동을 멈춘다. |
3.2. 전체 시스템 구성
3.2.1. HW
3.2.1.1. 하드웨어 구성
· 아두이노 보드 : 총 44개의 핀과 단자들로 구성되어 있고, 각 핀과 단자들은 아두이노와 다른 보드 또는 센서들의 제어에 이용할 수 있는 장비이다.
· 구동 모터 : 헬리컬기어의 모터로써 다른 모터보다 출력이 상대적으로 높아 낮은 감속비일 때 더 높은 토크와 RPM을 낼 수 있다. 이 모터를 활용하여 창문을 닫고 여는데 실질적인 역할을 하는 구성요소이다.
· 블루투스 HC-06 모듈 : 아두이노에서 시리얼통신을 이용하여 우리가 직접 만든 앱과 데이터 값을 주고받을 수 있는 모듈이며, 약 10m 정도 거리에서 무선으로 데이터를 보내거나 받을 수 있는 요소이다.
· 모터 드라이버 (L298N) : 모터와 아두이노를 연결하는 모듈로써, 모터의 속도와 위치 제어를 할 수 있다. 또한 DC모터 2개 또는 스테핑 모터를 연결하여 제어가 가능하며, 입력전압은 여기선 5V를 이용하였다.
· 빗물 센서 : 창문에 부착하여 비가 오는지 안 오는지 판단할 수 있게 하는 센서이다. 빗방울 감지 센서 기판의 전극부분이 물과 접한 면적이 클수록 저항 값이 작아지고, 흐르는 전류량이 상대적으로 커지게 된다.
· 미세먼지 센서 : 대기 중의 미세먼지를 측정하는 센서로써, 직경 10마이크로 이하의 입자상 물질을 측정해 낸 다. 미세먼지 센서 안에는 적외선 센서가 포함되어, 적외선 수신기와 송신기가 먼지에 의해 반사되는 빛의 양을 파악해 입자를 감지하여 대기 중의 미세먼지를 측정한다.
· 랙과 피니언 : CATIA 프로그램을 통해 3D 설계를 한 후 랙과 피니언 시뮬레이션을 거쳤다. 이에 3D 프린팅을 외주 작업을 거쳐 만들었다.
· 각목, 골판지, 단프라박스, 아크릴판: 버스를 만드는 데에 쓰인 재료이다. 각목을 통해 창틀을 만들었고, 창문은 아크릴 판을 제작하여 만들었다. 골판지를 통해 차체 내부를 만들었으며 단프라 박스를 이용하여 차체의 카울을 만들었다.
3.2.1.2 하드웨어 구조
3.2.2 SW
3.2.2.1 구성 및 각 함수별 기능
① 미세먼지 센서 함수 주요 기능
② 빗물 센서 함수 주요 기능
③ 블루투스 함수 주요 기능
④ 모터 방향 및 속도제어
⑤ 모터 타이머 설정하는 함수 주요 기능
⑥ 상황에 따른 모터 제어 함수의 주요 기능
3.2.3. SW – App – Remote Control
3.2.3.1. 앱 화면
환경부의 기준에 맞게 분류하였다. 나쁨과 매우나쁨에서 자동화 시스템이 작동하며, OPEN, CLOSE, STOP으로 작동을 시킬 수 있다.
3.2.3.2. 앱 개발
① 앱 설명
① 맨 왼쪽 위에 블루투스 설정을 할 수 있는 버튼을 생성해주었다. ② 그 바로 아래에 가로로 나눠서 OPEN, CLOSE, STOP 버튼을 만들어 주었다. 다만, 수직 배치 레이아웃을 사용하여 왼쪽 사진에는 버튼이 보이지 않는다. ③ 레이블1 : 아두이노에 연결한 미세먼지, 빗물 센서의 값을 블루투스 모듈을 통하여 앱에 전송해주는 칸이다. ④ 레이블3 : 미세먼지 센서에 의한 측정값에 따라 ‘인식중’, ‘좋음’, ‘보통’, ‘나쁨’, ‘매우나쁨’을 표시해주도록 설계하였다. ⑤ 이미지 : 사용자가 쉽고 빠르게 미세먼지 농도를 알아차릴 수 있도록 레이블 3 밑에 이미지가 같이 나타나게끔 사진을 첨부하였다. ⑥ ‘보이지 않는 컴포넌트’ : ‘BluetoothClient1’ 과 ‘시계1’을 추가하였다. ‘BluetoothClient1’은 블루투스를 연결하기 위해 필요한 컴포넌트이고 ‘시계1’은 앱 내부에서 작동하는 시계로, 시간이 증가함에 따라 다음 센서 값을 받아오기 위해 필요한 컴포넌트이다. |
② 블루투스 연결
위 사진은 블루투스를 연결하기 위해 필요한 블록이다. ‘목록선택버튼1’이란 앱 디자인 사진에서 ‘블루투스 설정’ 버튼을 의미한다.
① ‘목록선택버튼1’을 클릭하기 전 : 앱 상에 ‘블루투스 설정’이라고 나타나있다.
② ‘목록선택버튼1’을 클릭한 후 : 연결 가능한 블루투스 기기들 목록이 표시된다. 아두이노의 블루투스 모듈(HC-06)을 선택하면 사용자의 기기와 이 시스템이 통신을 시작한다.
③ 블루투스 기능 블록
① “Button1 = OPEN 버튼” 이 버튼을 누르면 Bluetooth로 아두이노에 a라는 값을 보내게 된다.
② 이 값은 아두이노 소스코드의 ‘val’이라는 값에 저장된다.
③ ‘val’은 소스코드의 조건문을 타고 모터를 구동 시키게 되고 창문을 열 수 있다.
동일한 원리로 “Button2 = CLOSE(창문을 닫음)”, “Button3 = STOP(모터를 멈춤)” 기능을 한다.
④ 센서에 따른 이미지 블록
미세먼지 센서 값과 그에 따른 이미지를 보여주는 블록이다. 시계1을 항상 활성화 시켜 놓고 타이머 간격은 1초로 설정하였다. 앱이 시작하자마자 앱 내의 시계가 작동하며, 1초 간격으로 위 조건문이 실행하게 된다.
① 가장 우선순위가 높은 조건문은 ‘블루투스 연결이 되었는가’이다.
② 연결이 되었다면 다음 조건문인 ‘블루투스로 받을 데이터가 0보다 큰 가’를 확인한다. 받을 데이터가 있다면 그 데이터를 레이블1에 표시해준다. 아두이노 코드를 통해 미세먼지 센서 값과 빗물 센서 값을 블루투스로 송신하도록 설계하였다.
③ 블루투스로 송신한 값을 받기 위해 앱 자체적으로 지역 변수 ‘dust_standard’를 만들었다. 1바이트의 숫자데이터를 받는다. 이 값은 -1, 0, 1, 2, 3이 되는데 각 경우는 미세먼지 농도에 따른다.
④ 텍스트와 이미지로 각 경우에 따라서 스마트폰 화면에 함께 나타내준다.
3.3. 개발 환경
3.3.1. S.W 개발
개발 OS : Windows 10, 통합개발환경 : Arduino IDE, 개발 언어 : C언어
3.3.2. H.W 개발
설계 프로그램 : CATIA, 제작 프로그램 : 3D Printing5
3.3.3. 앱 개발
사용 OS : Android, 개발 프로그램 : MIT APP INVENTOR
4. 단계별 제작 과정
4.1. 아이디어 구상
4.2. S/W 개발 및 설계
4.3. H/W 설계 및 제작
4.4. 조립 완성 및 기술 시연
5. 기타
5.1. 회로도
5.2. 소스코드
#include <MsTimer2.h>
#include <SoftwareSerial.h>
#define BT_RXD 8
#define BT_TXD 7
SoftwareSerial bluetooth(BT_RXD, BT_TXD);
int dust_sensor = A0; // 미세먼지 핀 번호
int Raindrops_pin = A1; // 빗방울센서 핀을 A1으로 설정
int rain_value = 0; // 센서에서 입력 받은 빗물 값
int dust_value = 0; // 센서에서 입력 받은 미세먼지 값
int dustDensityug=0; // ug/m^3 값을 계산
int sensor_led = 12; // 미세먼지 센서 안에 있는 적외선 led 핀 번호
int sampling = 280;
int waiting = 40;
float stop_time = 9680; // 센서를 구동하지 않는 시간
int Dir1Pin_A = 2; // 제어신호 1핀
int Dir2Pin_A = 3; // 제어신호 2핀
int SpeedPin_A = 10; // PWM제어를 위한 핀
int Dir1Pin_B = 4; // 제어신호 1핀
int Dir2Pin_B = 5; // 제어신호 2핀
int SpeedPin_B = 11; // PWM제어를 위한 핀
int Timer = 0;
int Timer2 = 0;
static int MOTER_DIR = 0; // 모터제동 bool
static int motor_check = 0; // 모터 on/off
char dust_standard = 0;
void bluetoothh();
void rain_sensor();
void dust_sensorr();
void motor_clock1();
void motor_counterclock1();
void motor_clock2();
void motor_counterclock2();
void motor_stop();
void motor_run();
void ISR_Timer();
void Serial_print();
void setup(){
Serial.begin(9600); // 시리얼 모니터 시작, 속도는 9600
pinMode(sensor_led,OUTPUT); // 미세먼지 적외선 led를 출력으로 설정
pinMode(A1 , INPUT);
pinMode(4, OUTPUT);
pinMode(Dir1Pin_A, OUTPUT); // 제어 1번핀 출력모드 설정
pinMode(Dir2Pin_A, OUTPUT); // 제어 2번핀 출력모드 설정
pinMode(SpeedPin_A, OUTPUT); // PWM제어핀 출력모드 설정
pinMode(Dir1Pin_B, OUTPUT); // 제어 1번핀 출력모드 설정
pinMode(Dir2Pin_B, OUTPUT); // 제어 2번핀 출력모드 설정
pinMode(SpeedPin_B, OUTPUT); // PWM제어핀 출력모드 설정
MsTimer2::set(1000, ISR_Timer); //1000ms, ISR_Timer
MsTimer2::start(); // Timer start
bluetooth.begin(9600);
}
void loop(){
dust_sensorr();
rain_sensor();
motor_run();
bluetoothh();
Serial_print();
}
void bluetoothh(){
if(bluetooth.available()){
char val = bluetooth.read();
Serial.write(bluetooth.read());
if(val == ‘a’){
MOTER_DIR=4;
}
if(val == ‘b’){
MOTER_DIR=5;
}
if(val == ‘c’){
MOTER_DIR=3;
}
}
if(Serial.available()) {
bluetooth.write(Serial.read());
}
bluetooth.print(dust_standard);
bluetooth.print(“\n”);
bluetooth.print(“미세먼지: “);
bluetooth.print((dustDensityug+50));
bluetooth.print(“[ug/m3] “);
bluetooth.print(“\n빗물 센서값: “);
bluetooth.print(analogRead(A1));
}
void rain_sensor(){
rain_value = analogRead(Raindrops_pin);//빗물 값 읽어오기
delay(1000);
}
void dust_sensorr(){
digitalWrite(sensor_led, LOW); // LED 켜기
delayMicroseconds(sampling); // 샘플링해주는 시간.
dust_value = analogRead(dust_sensor); // 센서 값 읽어오기
delayMicroseconds(waiting); // 너무 많은 데이터 입력을 피해주기 위해 잠시 멈춰주는 시간.
digitalWrite(sensor_led, HIGH); // LED 끄기
delayMicroseconds(stop_time); // LED 끄고 대기
dustDensityug = (0.17 * (dust_value * (5.0 / 1024)) – 0.1) * 1000; // 미세먼지 값 계산
delay(1000);
if ((dustDensityug)<0){
dust_standard = -1; //인식중
}
else if ((dustDensityug>=0) && (dustDensityug<30)){
dust_standard = 0;
}
else if ((dustDensityug>=30) && (dustDensityug<80)){
dust_standard = 1;
}
else if ((dustDensityug>=80) && (dustDensityug<120)){
dust_standard = 2;
}
else{
dust_standard = 3;
}
}
void motor_clock1(){ //창문 닫는 방향
digitalWrite(Dir1Pin_A, HIGH); //모터가 시계 방향으로 회전
digitalWrite(Dir2Pin_A, LOW);
digitalWrite(SpeedPin_A, HIGH); //모터 속도를 최대로 설정
delay(1000);
}
void motor_counterclock1(){ //창문 여는 방향
digitalWrite(Dir1Pin_A, LOW); //모터가 반시계 방향으로 회전
digitalWrite(Dir2Pin_A, HIGH);
digitalWrite(SpeedPin_A, HIGH); //모터 속도를 최대로 설정
delay(1000);
}
void motor_clock2(){
digitalWrite(Dir1Pin_B, HIGH); //모터가 시계 방향으로 회전
digitalWrite(Dir2Pin_B, LOW);
digitalWrite(SpeedPin_B, HIGH); //모터 속도를 최대로 설정
delay(1000);
}
void motor_counterclock2(){
digitalWrite(Dir1Pin_B, LOW); //모터가 반시계 방향으로 회전
digitalWrite(Dir2Pin_B, HIGH);
digitalWrite(SpeedPin_B, HIGH); //모터 속도를 최대로 설정
delay(1000);
}
void motor_stop(){
digitalWrite(SpeedPin_A, LOW);
digitalWrite(SpeedPin_B, LOW);
}
void motor_run(){
switch (MOTER_DIR)
{
case 0:
if((rain_value >60)&&(dustDensityug < 80)&&motor_check == 1 ){
motor_stop();
}
else if(((dustDensityug > 80)|| (rain_value < 60)) && motor_check == 1)
{
motor_counterclock1();
motor_counterclock2();
}
else{
motor_clock1();
motor_clock2();
}
break;
case 1:
if(Timer2<4){
motor_clock1();
motor_clock2();
}
else{
MOTER_DIR = 0;
}
break;
case 2:
if(Timer2<4){
motor_counterclock1();
motor_counterclock2();
}
else{
MOTER_DIR = 0;
}
break;
case 3:
motor_stop();
MOTER_DIR = 0;
break;
case 4:
Timer2=0;
motor_stop();
MOTER_DIR=1;
break;
case 5:
Timer2 = 0;
motor_stop();
MOTER_DIR = 2;
break;
default:
break;
}
}
void Serial_print(){ // 시리얼 모니터에 미세먼지 값 출력
Serial.print(“Dust Density [ug/m3]: “);
Serial.print(dustDensityug);
Serial.print(“\n”);
Serial.println(analogRead(A1));
Serial.println(MOTER_DIR);
Serial.print(“Timer2 : “);
Serial.println(Timer2);
}
void ISR_Timer(){
Timer++;
Timer2++;
if(Timer > 4){
Timer = 0;
motor_check = 0;
}
motor_check = 1;
}
5.3. 제품도
5.4. 참고문헌
1. 기어 한국미스미, 기어 기술자료, file:///C:/Users/user/AppData/Local/Microsoft/Windows/INetCache/IE/8U9RBEK8/3010gijutu_kr.pdf
2. IAMAMAKER, MIT APP INVENTOR 교육자료, http://www.iamamaker.kr/ko/tutorials/%EC%95%B1-%EC%9D%B8%EB%B2%A4%ED %84%B0-%EC%8B%9C%EC%9E%91%ED%95%98%EA%B8%B0/
3. NAVER 지식 IN, 미세먼지 싫어요 검색, https://search.naver.com/search.naver?sm=top_hty&fbm=0&ie=utf8&query=%EB%AF%B8%EC%84%B8%EB%A8%BC%EC%A7%80+%EC%8B%AB%EC%96%B4%EC%9A%94
4. 조현진,“[학생 칼럼] 한반도 뒤덮는 미세먼지의 심각성”, 경기일보, 2020.03.26 http://www.kyeonggi.com/news/articleView.html?idxno=2262228
5. 최영일,“비상 문 없는 통유리, 참사 키워”, YTN, 2016.10.14 https://www.ytn.co.kr/_ln/0103_201610142027549854
6. naver포스트, 통 유리 사고 https://m.post.naver.com/viewer/postView.nhn?volumeNo=15321360&memberNo=40864363&vType=VERTICAL, 2020년
7. 환경부, 미세먼지 대처방안, http://www.me.go.kr/home/file/readDownloadFile.do?fileId=97828&fileSeq=1&openYn=Y
8. 우성은 외 2명 , 대중교통 차량의 실외 미세먼지 유입에 관한 연구, 대한건축학회 학술발표대회 논문집 , 2018년
9. 이용일 외 6명 , 대중교통수단 및 자동차의 실내공기질에 대한 노출 분석, 환경독성보건학회 추계국제학술대회[초록집] , 2013년
10. 이현우 외 7명 , 국내 외 자동차 실내공기질 관련 연구 동향 , 한국자동차공학회 춘 추계 학술대회 논문집 , 2006년
[62호]TWO EYES (Two Camera watch wet road)
2020 ICT 융합 프로젝트 공모전 우수상
TWO EYES (Two Camera watch wet road)
글 | 한국항공대학교 노윤석, 고려대학교 이준세, 가천대학교 김지연, 정진화
1. 심사평
칩센 이미 공공 서비스 영역에서 유사한 방식으로 영상을 이용한 포트홀 검출과 그에 대한 유지 보수를 적용하는 것으로 알고 있습니다. 이는 스마트시티의 여러 서비스 중에 하나로서 당연히 V2V, M2M 까지의 서비스를 염두에 두고 있는 부분입니다. 이미지 프로세싱을 통한 젖은 노면을 검출하는 것은 실질적으로는 다양한 변수에 의해서 충분한 보완이 필요하겠으나, 해당 동작을 구현하고 RC카에 카메라를 장착하여 시연을 할 수 있다는 것은 매우 인상적입니다. 향후 진보된 작품이 기대됩니다.
펌테크 작품의 아이디어와 실용성 창의성이 돋보이는 작품으로 생각됩니다. 영상처리, 서버 구축, 구동부 로봇 구성 등의 기술을 효율적으로 접목하여 기획의도에 맞게 시스템을 안정적이고 완성도 높게 구현하였고 제출된 보고서 구성 내용도 명확하고, 충실했다고 생각이 듭니다. 전체적으로 기획의도, 기술 구현도, 완성도 등에서 상당히 뛰어나고 훌륭한 작품으로 생각됩니다.
위드로봇 편광 필터를 통과한 두 장의 이미지에서 젖은 노면을 찾아내는 연구는 최근 연구소에서도 많이 진행하고 있는 주제입니다. 검출 영역의 정확도를 좀 더 신경 쓰면 좋은 결과를 얻을 수 있을 것으로 보입니다.
2. 작품 개요
삼성교통안전문화연구소의 2009년~2013년까지 5년간 비 오는 날 교통사고 조사결과에 따르면, 우천 시 교통사고 100건당 치사율은 2.28명으로 평균 교통사고 치사율의 4.3배에 달했다. 또한 비 오는 날 교통사고 발생 건수는 하루 평균 2천800여 건으로 평상시보다 10.3% 많았다. 사고 유형별로는 차량 단독사고의 발생률이 1.5배 이상 크게 높아졌다. 이는 차량간 사고가 아니기 때문에 운전자가 노면 상태에 대해 인지하고 빗길 주행 시 커브 길에서의 도로 이탈이나 수막현상으로 인한 전도 및 전복 등에 주의하여 적절한 대처를 한다면 사고율을 크게 낮출 수 있다는 것을 의미한다. 하지만 많은 운전자가 노면 상태를 인지하지 못하거나, 인지하더라도 대처를 제대로 하지 못하는 것이 현재 상황이다.
이러한 문제를 해결하기 위해 비, 결빙 등에 의한 주의해야할 노면 상태를 인식하여 운전자에게 현재 노면 상황을 인지시키고, 이에 따른 대처까지 할 수 있도록 하였다.
또한, 현재 시판되는 자율주행 시스템은 단순 영상 분석으로 도로 상황을 인지하기 때문에, 젖은 도로에 대한 인식률이 떨어진다. 그리고 역광이나 반사광에 의해 분석해야 할 이미지가 제대로 인식되지 않는 경우도 있다. 실제로 18년 3월 테슬라의 자율주행 차량이 역광으로 인해 트럭을 하늘로 잘못 인식해 사고가 발생하기도 했었다.
본 프로젝트의 편광필터를 이용한 영상 분석 시스템은 역광과 반사광에 의한 인식률 저하를 해결할 수 있고, 물리적인 반사광의 차이로 젖은 노면을 검출하기 때문에 기존의 시스템보다 더 정확한 노면 분석이 가능하게 하였다.
2.1. 비즈니스적 이점
2.1.1. 현재 시장에서의 이점
대중화 가능한 블랙박스
저렴한 가격에 다양한 기능(편광필터로 인한 반사광 보정 영상 제공, 노면 상태 알림)이 포함된 블랙박스 형태로 제작하여 대중화함으로써 블랙박스 제공자가 다양한 도로정보 데이터셋을 수집할 수 있다. 또한 도로정보가 필요한 네비게이션 회사나 보험사 등에 데이터셋을 제공 하여 추가적인 이윤 창출도 가능하다.
노면상태 검출 알고리즘
도로 결빙이 잦은 구간, 수막현상으로 인한 사고가 빈번한 구간의 도로 감시 시스템으로 본 알고리즘을 납품해 이윤을 창출할 수 있다. 고가의 센서를 연동하지 않고, 2개의 카메라를 통해 도로 노면의 상황을 감시하고 노면의 젖음을 쉽게 인지할 수 있기 때문에 기존의 가로등 또는 전광판에 소형 카메라 모듈을 부착해 구현할 수 있어 관련 공사의 번거로움이 적어 잠재적인 고객(도로교통 공사 등)을 설득할 수 있다. 잠재적인 고객은 실시간 도로 노면의 상황을 영상처리로 획득할 수 있어 ‘감시카메라’ + ‘도로 노면 모니터링 장비’의 2가지 기능을 한번에 구현할 수 있는 것이 장점이다.
2.1.2. 미래 시장에서의 이점
자율주행 시스템의 보완
소비자들은 자율주행차의 상용화 가능성에 대한 의구심을 떨쳐내지 못하고 있다. 이는 자율주행차의 안전성에 대한 소비자들의 불신 때문이다. 많은 곳에서 자율주행차 기술의 완성도를 높이기 위해 많은 힘을 쏟고 있지만 이러한 노력에도 소비자들의 불신이 유지되는 이유는 아직 존재하는 모든 상황에 자율주행차가 대응하지 못하기 때문이다. 비나 눈, 결빙 등에 의해 변화되는 특수한 노면 상태는 사람의 육안으로는 쉽게 구별 가능하더라도 자율주행차는 구별이 어려워 대처가 미숙하다. 운전자가 필요없는 5단계의 완전 자율주행 시스템을 달성하기 위해서는 사람의 개입이 전혀 없는 상태에서도 모든 상황에 대응할 수 있어야 한다. 그렇기 때문에 본 프로젝트에서 제안되는 기술을 통해 젖은 노면이라는 구체적인 상황에 대응하는 모습을 보여준다면 소비자들의 신뢰를 얻을 수 있고 이는 기업의 이익 창출에 도움이 될 것이다.
2.2. 작품 목표
2.2.1. 우천 시 교통사고 예방 효과를 높인다
노면 상태가 불량할 시 운전자에게 경고를 하여 주의하여 주행 하도록 유도할 수 있다.
2.2.2. 특수 노면 상태에 대한 라벨링된 데이터셋을 가공하여 자율주행 머신러닝과 관련 기업에 제공할 수 있다.
머신러닝 기반의 자율주행 시스템의 완성도를 높이기 위해서는 잘 정제된 데이터셋이 필수적이다. 검출된 노면 상태와 그에 대응하는 도로 이미지와 위치정보, 날씨 등을 하나의 데이터셋으로 가공하여 더욱 정교한 자율주행 시스템 개발에 도움을 줄 수 있다. 또한 도로 상태 정보를 자동차보험회사나 도로교통공단 등에 제공하여 이윤 창출이 가능하다.
2.2.3. 일반 차량에 설치하여 대중화를 목표로 한다.
머신러닝 기반의 자율주행 시스템에는 데이터셋의 질 뿐만 아니라 양도 중요하다. 적은 부품으로 다양한 기능이 구현된 블랙박스로 구성하여 제품에 대한 접근성을 높이고, 그에 따라 더 많은 다양한 차량과 상황에 대한 데이터셋 수집이 가능하다.
2.2.4. 자율주행차량의 특수 노면 상태에 대한 인식률을 높인다.
현재 시장의 자율주행 시스템은 우천이나 결빙 등의 특수 노면 상태에 대한 대처가 미숙한 상황이기 때문에, 이를 보강할 수 있다.
2.2.5. 블랙박스 영상의 정확도를 높인다.
기존의 블랙박스에도 역광 또는 반사광 보정 기능이 있지만, 소프트웨어상에서의 보정이기 때문에 한계가 있다. 편광 필터에 의한 반사광 제거를 통해 더욱 정확한 영상 녹화가 가능하다.
3. 작품 설명
3.1. 전체 동작 및 특징
3.1.1. 편광 필터 카메라를 통한 노면 상태 검출 시스템 제작
편광 필터가 부착된 웹캠을 이용하여 노면 상태를 검출한다. 본 시스템은 젖은 도로의 노면 부위를 검출하기 위해 각각 수평, 수직 편광 필터가 부착된 2개의 USB 카메라에서 이미지 정보를 습득한 뒤에, 임계값 기법 알고리즘(Threshold)를 통해 습득된 이미지의 상호 유사도를 추정한다. 웹캠의 영상을 실시간으로 분석하여 편광 필터에 의해 반사광이 필터링 된 영상과 그렇지 않은 영상을 비교하여 젖은 노면이 검출됨이 판단될 시 차량에 대한 직접적인 제어 및 알람을 사용자에게 전달한다.
3.1.2. 차량 실제 적용 방법을 고려한 외부 상황 인지
본 시스템은 본래 블랙박스 형태의 제품이지만, 개발 과정에서는 실제 차량으로 시험하는 것이 여러 가지 제약 조건이 있어 RC카 차체를 이용하여 프로토타입을 제작하였다. 실제 차량에 적용될 시 OBD단자를 통해 얻을 대기 온/습도 등의 데이터는 따로 센서를 연결하여 프로토타입에 구현하였다. 프로토 타입에서는 주의가 필요한 노면 상태가 검출될 때, 외장 되는 온/습도 센서를 통해 대기의 온습도, 적외선 센서를 통한 노면의 온도, GPS 모듈을 이용해 위도와 경도를 획득하여 관련 데이터베이스에 보고한다.
3.1.3. 서버구축 및 데이터 저장
노면 상태 검출 시스템을 통해 젖은 노면이 검출된 경우, 관련된 정보(젖은 노면의 지리적 정보, 대기 온/습도, 노면 온도)를 온라인 웹 서버에 보고해 DB에 실시간 Data set을 만든다. DB에 생성되는 Data set은 해당 노면의 상태 사진이고, 해당 사진에 위치/시간/대기상태 등의 정보가 Labeling 된 상태이다. 이를 이용해 특수 노면 상태가 자주 검출되는 위치와 대기, 날씨 등의 실시간 정보를 축적할 수 있는 데이터베이스와 서버를 구축하고 공유하여 3차 사고방지를 위해 사용한다.
3.2. 전체 시스템 구성
3.2.1. 하드웨어 아키텍쳐
TWO EYES 프로젝트는 2개의 카메라를 통해 도로 노면의 정보를 획득하고, 관련된 영상처리를 통해 도로 노면의 젖음을 인식하는 프로젝트이다. 2개의 카메라는 차량에 장착되어 도로 노면의 상태를 ‘실시간’으로 추정하고 / 만일 도로 젖음이 인지되는 경우 주변 차량에 해당 정보를 통보 (V2V)를 하는 동시에 (젖은 도로의 위치 정보_위도/경도, 대기 온도, 대기 습도, 노면 온도)의 센서 데이터 정보를 외부의 온라인서버에 연동하는 것을 특징으로 한다. 본 시스템에서 지향해야 하는 내용은 다음과 같다.
Low latency
단말기 내부에서만 센서데이터의 습득 및 처리가 이뤄지는 것이 아닌, 여러개의 연산장치 및 외부 온라인 서버와 연동되어야 하는 동시에 저지연 (low_latency) 의 기술적 목표를 성취해 ‘실시간 시스템(real time)’ 을 구현해야 한다.
Fail Safe
차량 내 설치되어 실시간 노면의 상태를 추정하는 동시에, 도로가 젖음을 인지하게 되는 경우 자동차의 속력을 낮추는 등의 ‘자동차 제어’기능을 구현하였다. 단순 모니터링 장치가 아닌, 차량의 ‘제어’부분까지 개입하게 되는 경우, System Failure로 인한 오작동은 상당히 위험하기 때문에, 해당 부분에 관련 솔루션을 제시할 수 있는 Fail Safe를 구현해야 한다.
상기의 2가지 목표를 수행하기 위해서는 특정 MCU 또는 연산 장치에 집중적(high density)이거나 의존적(high dependency)인 경우에는 외부에 위협 요인에 취약한 System이 구현되게 된다. 해당 문제를 해결하기 위해 설계 단계에서 부터, 각 MCU 및 연산 장치의 한계성능 및 안정성능을 검증한 다음, 적절히 연산을 분배해 전체 하드웨어 아키텍쳐의 구성을 진행한다.
상기의 2가지 목표를 수행하기 위해서 연산을 분할하고 서로 다른 MCU 또는 연산장치에 부여하게 되는 경우, Logic level voltage가 달라 회로적(Circuit problem)인 문제가 발생할 수 있다. 역시, 각각의 회로적인 문제도 구상단계에서 최대한 고려해 하드웨어 아키텍쳐에 반영해야 한다.
상기의 내용과 같이 자동차 내부의 구조(영상 습득 및 영상 처리)부분을 <그림 1>과 같이 나타낸다면, 단말단(자동차)과 외부의 온라인 연동은 <그림 2>과 같이 구성된다.
<그림 2> 도로의 노면 젖은 상태를 인지하게 되면 Local(단말단)에서 센서처리를 한 뒤에, Online에 해당 센서 데이터를 보고해, 데이터베이스에 노면이 젖은 위치와 그 당시의 환경 상황이 저장될 수 있도록 구성한다.
<그림 3>은 Hardware Level에서의 전체 시스템 작동 예시를 Flow chart로 나타낸 그림이다. 전술된 구현 하고자 하는 지향점을 반영하기 위해 연산량을 각 MCU 또는 연산장치에 분할한 동시에 각각의 장치들이 어떻게 연동되는지 나타낸 그림이다.
저성능 연산장치(라즈베리파이) 및 고성능 연산장치(노트북)은 영상습득 및 처리/온라인 서버 연동의 역할을 담당하고, MCU(Arduino)에서는 관련된 센서데이터의 취득을 한다. 동시에 취득된 데이터 중 유효한 정보 및 주변 자동차에서 발생한 경고 신호(블루투스)의 유무를 판단해 해당 정보를 저성능 연산장치(라즈베리파이)에 전송하는 형태로 각 연산장치들의 통신이 구성된다.
3.2.2. 소프트웨어 아키텍쳐
TWO EYES 프로젝트는 두 개의 카메라 렌즈에 편광필름을 각각 수평과 수직으로 부착하여 받아온 영상 촬영을 기반으로 한다. 판별 이후 언제나 제약 없이 가능한 서비스화를 위해 AWS EC2 인스턴스를 할당받아 우리 서비스의 관제소 역할을 담당하는 Web Server를 구축한다.
우리 소프트웨어 아키텍처는 HTTP 통신을 기반으로 하며, 이하 기술할 작업을 수행하기 위해서는 AWS 콘솔 내에서 MySQL, SMTP 등에 대한 인바운드 규칙을 적절히 설정해야 한다.
Web Server에서는 디스플레이 및 센서처리를 담당하는 라즈베리파이에서 GPS 정보 및 각종 센서 데이터를 GET 방식으로 받아와 데이터베이스에 저장한다. 그 후 서버 단에서 데이터베이스로부터 Fetch 하여 가져온 Data row 값을 적절히 가공한다.
사전에 준비한 Google Map API를 활용하여 젖은 노면 발생 위치의 위,경도를 지도에 매핑한다. 또한, GPS를 포함한 각종 센서데이터 정보를 브라우저 위에 차트 형식으로 표현하여 사용자가 원하는 방식으로 데이터를 얻을 수 있도록 한다.
우리 서비스는 추후 상업화를 대비한 AI 데이터셋 제공을 목적으로 한다. 상태 정보가 Wet으로 판별되면 해당 젖은 노면 이미지를 초당 POST 방식으로 Web Server에 보내도록 한다. 이는 Web Server 내의 디렉토리에 JPG 형식으로 저장한다. (그림4)
정식 서비스화를 가정하여 DataSet 구매 및 서비스에 대한 문의를 위한 e-mail 전송 기능을 구축했다. SMTP 프로토콜 사용을 위해 Port 번호 587을 할당하며, AWS 클라우드 서버에서 이를 구현하기 위해 해당 인스턴스를 Amazon SES sand box 밖으로 빼내는 작업이 필요하다.
3.3.개발 환경
3.3.1. 개발언어 및 Tool
3.3.2. System Element
4. 단계별 제작 과정
4.1. 웹캠 이미지 분석 알고리즘
본 시스템은 각각 수평, 수직 편광 필터가 부착된 두 개의 웹캠에서 받아온 이미지를 분석하여 노면의 상태를 확인한다. 이미지 분석에는 Python3.0 기반의 opencv와 numpy를 주로 사용하였다.
4.1.1. 수면 검출을 위한 편광 현상 연구
빛이 굴절률이 다른 두 매질 사이를 지날 때, 보통 그 경계면에서 반사가 일어난다. 하지만, 어떤 편광상태의 빛이 특정한 입사각으로 입사되면 경계면에서 반사되지 않는데, 이 특정한 입사각을 브루스터 각(Brewster Angle)이라 한다. 이 각도에서는 편광된 빛의 전기장이 입사되는 평면과 나란하기 때문에 반사되지 않는다. 이 편광상태의 빛은 p-편광(p-polarized) 상태라고 하며, 빛이 입사된 평면과 수직한 상태로 편광되어 있다면 s-편광(s-polarized) 상태라고 한다. 따라서 무편광 상태의 빛이 브루스터각으로 입사되면, 반사된 빛은 항상 s-편광 상태가 된다.
위의 이론에 따르면, 브루스터각으로 입사한 자연광의 반사광은 s-편광 상태이므로 수직편광 필터는 통과하지만 수평편광 필터는 통과하지 못한다. 따라서 각각 수평편광 필터와 수직편광 필터를 부착한 웹캠을 통해 반사면을 촬영하면 수평편광 필터가 부착된 웹캠의 반사면이 수직편광 필터가 부착된 웹캠의 반사면보다 어둡게 나타난다. 편광필터로 촬영된 이미지는 필터가 없는 상태에서 촬영된 이미지와 투사되는 빛의 양이 차이가 크므로 반사광이 없을 때 두 웹캠에 투사되는 빛의 양을 동일하게 하기 위해 양 쪽 모두 편광필터를 부착해야한다. 빛의 반사는 모든 매질에서 발생하지만 물과 얼음의 반사율은 주로 아스팔트로 이루어져 있는 도로의 반사율보다 현저하게 높으므로 수평편광 필터를 통과한 물에 젖은 구간과 결빙 구간의 이미지는 다른 구간에 비해 밝기 차이가 크게 난다. 아래는 영상 분석에 사용된 편광필터로 촬영한 도로 위의 수면 이미지이다. 수직편광 필터로 촬영하여 반사광이 그대로 통과된 왼쪽 이미지와 수평편광 필터로 인해 반사광이 통과하지 못한 오른쪽 이미지의 수면 밝기 차이가 큰 것을 확인할 수 있다.
본 프로젝트의 목표는 노면의 젖은 구간과 결빙 구간을 검출해내는 것에 있음으로, 공기에 대한 물과 얼음의 반사광을 분석해야 한다. 프레넬 방정식(Fresnel equations)에 의해 유도되는 브루스터각의 유도식은 다음과 같다.
공기에 대한 물과 얼음의 상대 굴절률은 각각 약 1.33, 1.31이므로, 위의 유도식에 의해 각각의 브루스터 각은 53.1도, 52.6도이다. 본 프로젝트에서는 웹캠의 세밀한 각도 조정은 불가능하여서 지면에서 반사되는 빛의 각도를 약 53도로 설정하여 설계하였다.
4.1.2. 이미지 전처리
Threshold Method는 OpenCV의 이미지 처리 기능 중 하나이다. 이미지를 Grayscale로 읽어오면 픽셀의 요소는 명암 한가지만 남는데, 0(검은색)부터 255(흰색)까지의 값을 갖는다. Threshold 함수는 0부터 255 사이의 설정한 값을 기준으로 이미지에서 기준값보다 큰 픽셀 값은 255로, 기준값보다 작은 픽셀 값은 0으로 이진화 시킨다. 반사광의 이미지는 다른 부분과 밝기 차이가 크므로 Threshold 과정을 거치면 두 편광 이미지의 차이가 극명하게 나타난다. [그림 5.4, 5.5]는 실제로 편광필터로 촬영한 이미지를 Threshold 과정을 거쳐 변환한 결과이다. 수평 편광 필터를 통과한 수면의 이미지만 두드러지게 어둡게 변환되어 수직 편광 필터의 이미지와 큰 차이를 보여 수면을 쉽게 구분할 수 있음을 알 수 있다.
4.1.3. 영상 구역 분할
두 개의 웹캠의 이미지가 완전히 같을 수 없으므로 이미지에서 최대한 구역을 ROI(Region Of Interesting)을 설정 하였다. 상기의 코드 내용은 후의 코드문서에 첨부한다. 그 후의 과정으로 영상 구역을 분할하였다. 수평 편광 필터에 의해 반사광이 흡수된 이미지는 상대적으로 어두운 픽셀이 많다. 그에 따라 threshold로 이진화 처리된 이미지의 검은색이나 하얀색 픽셀의 개수 차이를 분석하여 젖은 노면을 검출할 수 있다. 본 시스템에서는 pixel ratio라는 변수로 이를 표현하였다. pixel ratio는 다음과 같이 이미지의 흰색 픽셀 수의 차이를 전체 픽셀 수로 나눈 비율로 표현하였다. 실험 결과 이미지에 반사면이 있는 경우에는 pixel ratio 값이 0.7 이하로 검출되고, 그렇지 않은 경우에는 0.85 이상의 값을 보여주었다. 하지만 전체 이미지에 대해 pixel ratio를 구하면 다른 요소에 의해 픽셀 수의 변화가 생기는 경우도 있기 때문에 더 정확한 반사면 검출을 위해 이미지의 구역을 분할하여 구역별로 pixel_ratio를 분석하였다. 한 구역의 길이를 unit으로 정의하여 unit*unit 크기의 구역으로 이미지를 분할하여 각 unit마다 pixel ratio를 계산 후, numpy array의 형태로 구역별 값을 저장하였다. unit의 값을 조절하여 resolution의 정도를 조절할 수 있다.
아래는 이미지를 분할하는 함수의 소스코드다.
def seperated_image_ratio(frame1, frame2, unit):
#unit의 수만큼 이미지를 분할하여 분할된 이미지의 pixel_ratio를 array에 저장
ratio_arr = np.zeros((unit,unit), dtype = int)
U = int(frame1.shape[0]/unit)
# ratio 행렬 생성
for i in range(unit):
for j in range(unit):
img_trim1 = frame1[i*U:(i+1)*U, j*U:(j+1)*U]
img_trim2 = frame2[i*U:(i+1)*U, j*U:(j+1)*U]
ratio = int(pixel_ratio(img_trim1, img_trim2))
ratio_arr[i,j] = ratio return ratio_arr
4.1.4. 구역별 Wet Point 추출
전처리된 이미지를 통해 검출할 수 있는 반사면의 특징은 다음과 같다.
1) 반사면과 반사면이 아닌 곳의 상대적인 pixel ratio 값의 차이가 크다.
2) 반사면의 절대적인 pixel ratio 값이 평균 픽셀 값보다 작다.
1)의 특징점으로 반사면의 경계를 추출할 수 있고 2)의 특징점으로 반사면 경계 내부를 추출할 수 있다. 다음과 같이 각각의 unit에 접근해 1), 2)의 조건을 만족하는 unit의 좌표를 unit array와 같은 크기의 array 형태로 이진화된 값으로 저장하였다. 그 후 wet point를 검출하기 위해 filtering 작업을 하였다. 그 결과<그림 10>과 같이 wet point를 잘 검출한다.
아래는 젖은 부분을 인식하여 표시하는 함수의 소스코드이다.
def wetpoint_array(r_arr):
# 1. 각각의 unit의 인접한 unit의 ratio값의 차가 일정 값을 넘으면 wet list에 좌표 값 저장
# 2. unit의 ratio값이 일정 값을 넘으면 wet list에 좌표 값 저장
wet_arr = np.zeros(r_arr.shape, dtype = int)
ref1 = 25 # 인접한 unit간 ratio차이의 기준값
ref2 = 50 # unit의 ratio 기준값
for i in range(len(r_arr[0])):
for j in range(len(r_arr[1])):
R = r_arr[i,j]
# 조건 1
Rlst = []
if (1<=i<=len(r_arr[0])-2 and 1<=j<=len(r_arr[1])-2):
for m in range(3):
for n in range(3):
Rlst.append(r_arr[i-1+m,j-1+n])
for r in Rlst:
if r-R > ref1:
wet_arr[i,j] = 1
# 조건 2
if np.mean(r_arr)-R > ref2 and R!=0:
wet_arr[i,j] = 1
return wet_arr
4.2. 외부 환경 상황인지 센서 구현
자동차가 영상 습득 후, 고성능 연산 장치에서 연산한 연산 결과가 ‘젖은 노면’임을 검출한 경우, Arduino에서 실시간 외부 상황을 인지 후, 인지된 데이터는 온라인 서버에 보고되어 데이터 베이스에 각각의 센서데이터가 저장된다. Arduino에서 습득되고 온라인에 보고되는 데이터의 종류는 ‘대기 온도, 대기 습도, 노면 온도, 위도, 경도’이다. 하기의 사진은 본 팀이 구현한 자동차에 내장된 센서의 모습이다.
1 : 전방 노면 온도 인지용 적외선 온도 센서 2 : 실시간 위치 정보를 검출하기 위한 GPS 모듈
3 : 실시간 대기 온/습도 추출을 위한 DHT11 온습도 센서
4.2.1. 온습도센서, 적외선 센서를 통한 주변환경 온도검출
‘젖은 노면 상황’을 인지하게 되면 Arduino에 외기 온도 상황을 검출하라는 명령이 라즈베리파이로 부터 USB Serial을 통해 명령이 하달된다. 해당 상황에서 Arduino에서 DHT11 및 적외선 온도센서를 통해 대기 온/습도 및 노면 온도를 추출한다.
대기 온/습도 의 추출 방법
Arduino (Atmega 2560)과 DHT11온/습도 센서는 One-Wire 통신을 이용해 실시간 온/습도를 검출한다. DHT11 센서의 경우, Arduino에서 잦은 센서 정보 갱신(update)를 요구하게 되는 경우 Arduino에서의 실행시간 ( 1 loop cycle time )이 길어지는 단점이 있기 때문에, 도로 노면이 ‘젖은 상태’임을 검출한 당시에만 DHT11로부터 온/습도 정보를 갱신받도록 하였다.
노면 온도의 추출방법
DHT11 온/습도 센서로부터의 온도 및 습도의 검출이 끝난 경우에는 적외선 온도센서를 이용해 노면 표면의 온도를 검출한다. Arduino 와 SPI 통신을 하는 본 센서를 통해 노면의 온도를 검출하고, 비접촉 방식의 온도 검출이 가능한 것이 특징이다.
상기의 그림은 본 센서의 Data sheet에서 제시하는 검출 가능한 영역에 대한 내용이다. 적외선 온도 센서의 설치 높이 및 노면 지향각도에 따라 센서가 검출할 수 있는 온도의 영역 넓이가 달라진다. 그렇기 때문에 본 팀은 센서 고정 마운트를 독자 설계 해, 센서의 끝 부분에서 노면 끝 부분까지 250mm 가 될 수 있도록 설계하였다. 설계 결과 검출하고자 하는 노면을 영역을 대략 장축길이 기준 37.93mm 의 타원 형태로 검출할 수 있도록 했다.
4.2.2. GPS를 통한 위치파악
NMEA 형태로 GPS 데이터를 송출하는 GPS 모듈로부터 Serial 통신 방법을 이용해 Atmega 328p(Arduino UNO)에서 정보를 수신한다. Arduino에 수신된 데이터는 상위 프로세서(SBC_라즈베리파이 3B+)로 송신된다. 본 프로젝트에서 GPS 모듈을 이용해 추출하고자 하는 데이터는 ‘위도’ 및 ‘경도’ 정보이기 때문에 수신된 데이터에서 “GPRMC” 문자를 찾아 해당 ‘열(row)’ 에서 특정 위치에 있는 반점 (‘,’) 인근의 데이터를 추출해 실시간 자동차의 위도 및 경도 정보를 Python의 find 및 split을 이용해 추출한다.
추출된 데이터의 형태는 DMM 형태로, 일반적인 GPS 좌표 형태 (DD)방식이 아니다. 그렇기 때문에 DMM 형태의 위도 및 경도를 DD형태로 변환(conversion) 후 외부의 서버에 전송된다.
변환된 좌표의 형태이기 때문에 기존 지도 api에서 바로 사용이 가능하다.
DD = d + (min/60) + (sec/3600)
상기의 식을 이용해 DMM 기준의 식을 DD 형태로 좌표변환을 한다.
4.2.3. 습득된 센서 데이터 전송
습득된 <대기 온/습도 및 위치정보(DD 형태의 위도 및 경도), 노면 온도>의 데이터를 상위 컨트롤러 (라즈베리파이)에 데이터 손실없이 빠르게 전송해야 한다. 그렇기에 Arduino에서 상위컨트롤러로 9600 보드레이트 속도의 Serial 기법으로 센서데이터를 ‘한 줄 (1 line)’ 보내는 형태로 전송한다. 보내는 데이터의 형태는 상기의 사진과 같다.
데이터의 시작을 알리는 선행 문자 ‘A’를 Start token으로 지정해 데이터의 시작을 알리는 동시에, 반점 (‘,’)으로 구분된 다양한 데이터가 전송된다. Arduino에서 전송되는 데이터의 끝은 New line(‘\n’)으로 종결되기 때문에 센서 데이터를 수신하는 수신단에서는 개행문자가 ‘A’인 경우 센서데이터 수신을 대기하며 ‘\n’이 검출되는 경우 수신을 종결하면 센서데이터의 수신을 수행할 수 있다.
4.3. 서버 구축
4.3.1. 웹 서버구축 및 데이터 저장
TWO EYES 프로젝트는 상황에 제약 받지 않는 웹 서버 구축을 위해 AWS EC2 인스턴스를 사용한다. EC2는 가상서버를 클라우드에 빠르게 올릴 수 있게 해주며 OS를 포함한 Application 계층 전체를 관리할 수 있는 컴퓨팅자원(인스턴스)를 제공하는 서비스이다. AWS 서버에서 사용하기 위한 인스턴스로 Ubuntu Server 18.04 LTS (HVM), SSD Volume Type을 채택했다.
인스턴스 발행 후 Ubuntu 내에서 Python 기반의 웹 프레임워크 Flask를 통해 웹 서버를 구축한다. 또한 전송받은 데이터는 MySQL을 통해 이를 관리하며 pymysql 모듈로 웹 서버 내에서 데이터베이스에 접근하도록 한다.
다음은 data 테이블의 스키마를 출력한 내용이다.
Field를 내림차순으로 봤을 때 각각 위도, 경도, 대기온도, 노면온도, 습도, 날짜, 시간을 Server로부터 GET 방식으로 받아온다. 받아온 데이터는 pymysql 모듈을 통해 데이터베이스를 업데이트 한다. 또한, 데이터베이스 내의 내용은 필요 시 fetch하여 웹 서버로 가져와 가공한다.
4.3.2. 소켓통신
고성능 컴퓨팅 장치(노트북)에 비해 연산 능력이 적은 장치(라즈베리파이)에서 영상을 습득하는 동시에 영상처리를 통해 젖은 부분을 검출하는 것은 많은 지연(latency)이 발생하기 때문에, 저성능 컴퓨팅 장치에서 2개의 영상을 습득 후, 고성능 컴퓨팅 장치에서 영상처리를 진행하는 것으로 본 시스템이 구성되어 있다. 서로 다른 컴퓨팅 장치간 영상 공유는 Streaming 형태로 영상이 전송되는 것을 특징으로, 각각의 컴퓨팅 장치는 무선 온라인 망(Local WiFi)를 통해 연결되며, 연결방식은 “TCP Sokcet” 방식으로 진행된다.
TCP Socket의 경우 연결 이후 데이터의 전송되며 및 데이터의 전달 안정성 및 데이터 전송 순서를 보장하기 때문에 실시간으로 2개 종류의 영상을 Streaming 하기에 적합한 통신방식이다. 통신 속도 및 통신 트래픽의 과도한 증가를 방지하기 위해 영상 정보를 습득한 저성능 컴퓨팅 장치는 ‘흑백’ 이미지 정보를 고성능 컴퓨팅 장치 에 String 형태로 전송한다. 이때의 인코딩 형태는 일반적인 방식인 ‘utf-8’을 준수한다.
단순하게 영상 정보의 공유를 제외한, 영상처리 결과(노면의 젖음 유무) 또한 고성능 연산장치에서 저성능 연산장치로 소켓통신을 통해 통보한다. 해당 결과를 수신한 저성능 컴퓨팅 장치는 외부 서버 연동 및 센서데이터 취득을 위한 일련의 과정을 거쳐 자동차의 속도 제어 또는 서버/데이터베이스에 센서데이터 전송 등을 수행하게 된다.
저성능 컴퓨팅 장치에서 영상 정보를 습득해 Streaming 하는 Server의 역할을 수행하고, 고성능 컴퓨팅 장치에서 해당 영상 정보를 요청하는 Client 형태로 작동하며, 고성능 컴퓨팅 장치는 저성능 컴퓨팅 장치와 연결이 수행된 뒤에 다음과 같은 순서로 데이터의 전송 및 수신이 이뤄진다.
① 고성능 장치(노트북) -> 저성능 장치(라즈베리파이) / 메시지 ‘1’
○ 영상 요청 (1번 카메라 영상 요청)
② 저성능 장치(라즈베리파이) -> 고성능 장치(노트북)
○ 습득 영상 정보 송신 (1번 카메라 영상)
③ 고성능 장치(노트북) -> 저성능 장치(라즈베리파이) / 메시지 ‘2’
○ 영상 요청 (2번 카메라 영상 요청)
④ 저성능 장치(라즈베리파이) -> 고성능 장치(노트북)
○ 습득 영상 정보 송신(2번 카메라 영상)
⑤ 고성능 장치(노트북)에서 영상처리 수행 후 노면 젖은 판단
⑥ 고성능 장치(노트북) -> 저성능 장치(라즈베리파이)
○ 영상 판독 결과 통보 => 노면 젖음 : 메시지 ‘3’
○ 영상 판독 결과 통보 => 노면 마름 : 메시지 ‘4’
저성능 컴퓨팅 장치는 단순 영상 습득 및 Streaming 하는 Server 역할을 하고, 고성능 컴퓨팅 장치의 해당 영상을 습득 후 영상처리를 하는 시스템적 구조로 인해, ‘영상 습득 + 영상 처리 + 영상처리 통보’의 일련의 과정을 5Hz에 수행할 수 있다.
저성능 컴퓨팅 장치(Server)에서 고성능 컴퓨팅 장치(Client)로 영상을 전송하는 경우, 직접적으로 사진 데이터를 전송하는 것이 아닌, 전송하고자 하는 사진 데이터의 길이를 우선적으로 송신한 다음, 이미지 Raw 데이터를 전송하는 형태로 통신이 진행된다. 전송하고자 하는 데이터를 우선적으로 전송하기 때문에 수신측이 데이터를 보다 안정적으로 손실 없이 수신할 수 있다는 장점이 있다.
Socket 통신에서 데이터의 손실을 방지하고 / 데이터를 수신하는 수신측의 데이터 미수신으로 인한 무한 대기 상태(System Down)을 방지하기 위해, 본 시스템이 운용되는 Socket 통신에서 송신측은 수신측에 바로 인코딩(encoding)된 데이터를 송신하는 것이 아닌, 송신하고자 하는 데이터의 길이를 우선으로 송신한 다음, 원본데이터를 수신하는 형태로 구성되어 있다. 그렇기 때문에 수신측은, 수신될 데이터 길이를 수신한 다음, 해당 데이터 크기로 버퍼(Buffer)를 할당해 데이터를 수신하도록 구성되어 있다. 이는 System 의 안정성 증가 및 데이터 손실을 방지할 수 있는 장점이 있다.
4.3.3. 웹 서비스
향후 상업화를 가정하여 AWS EC2 인스턴스를 통한 웹 서비스를 구축하였다. 해당 서비스는 TWO EYES 프로젝트의 ‘관제소’ 역할을 담당하며, 그 역할에 맞게 브라우저 상에서 지도 상 GPS 매핑 조회, 각종 센서 데이터 현황 조회가 가능하다. 또한, 브라우저로는 드러나지 않으나 웹 서버 내에서 젖은 노면 촬영 이미지 값을 받아와 클라우드 서버 내에 저장하여 데이터셋을 적재한다. 이는 향후 AI 데이터 셋으로 활용하는 것을 목적으로 한다. 추가적으로, 실제 서비스 상용화를 가정하여 관리자에게 데이터셋 또는 서비스 문의를 위한 e-mail 샌딩 기능이 있다.
센서 처리를 담당하는 라즈베리파이로부터 젖은 노면 검출 시 GET방식으로 위,경도 값을 받아온다. 이를 웹 서버 내에서 적절한 형태로 가공하여 List로 받아온 후 Google Map을 브라우저 상에 표현한다. 이를 통해 서비스 사용자는 젖은 노면 위험 지역을 가시적으로 파악이 가능하다.
전적으로 Back-end 내에서 이루어지는 해당 서비스는 젖은 노면 검출 시 알고리즘 연산 고성능 컴퓨터로부터 클라우드 서버로 촬영 중인 이미지를 전송한다. 그 후 웹 서버는 이를 특정 디렉터리에 JPG 형식 파일로 저장한다. 우리 클라우드 서버는 촬영 이미지를 약 2초 당 한 프레임으로 받아올 수 있으며, 이는 클라우드 서버의 성능에 따라 속도가 좌우된다. 모든 데이터셋은 클라우드 서버 내에 내장되며, 브라우저에서는 가장 최근에 업로드 된 이미지의 날짜 및 시간 확인이 가능하다.
5. 연구결과
젖은 노면 검출 결과
상기의 사진은 젖은 도로 노면의 위치에 빨간 Box 가 Image overlay 가 되며 도로가 젖음을 경고하는 것을 알 수 있다. (촬영 시간 4pm)
6. 기타
6.1. 소스코드
자동차 구동 하드웨어에서 센서데이터 처리를 위한 코드이다.
LAST_PI_ARDUION_GPS 소스코드
* 자동차에 내장되는 디스플레이 장치 주변 아두이노에 업로드 되는 코드
* 라즈베리파이 (센서 데이터 요청) -> 아두이노 (센서 데아터 처리) -> 라즈베리파이
* GPS 모듈을 통해 실시간 위도 및 경도 데이터 추출
* DHT11 온습도 센서를 통해 실시간 대기 온습도 추출
* 적외선 온도센서를 통한 실시간 노면 온도 추출
* HC-06을 통해 원격의 다른 차량에게 실시간 경고 신호 송신
* 다른 자동차 -> 아두이노 -> 라즈베리파이
* 주변 자동차에서 발생하는 경고 신호 수신해 라즈베리파이에 경고 신호 수신 내용 전달
*/
#include<SPI.h>
#define OBJECT 0xA0 // 대상 온도 커맨드
#define SENSOR 0xA1 // 센서 온도 커맨드
boolean Timer1_Flag;
const int chipSelectPin = 53;
int iOBJECT, iSENSOR; // 부호 2byte 온도 저장 변수
String GPS_DATA;
String BT_DATA;
boolean BT_FLAG;
String RASP_DATA;
boolean RASP_FLAG;
boolean ALARM_FLAG,SEND_FLAG;
unsigned long ALARM_TIME;
unsigned long GPS_LAST, BT_TIME;
String dummy_lattitude = “3733.3614″; // 실내 GPS 안잡히는 경우, 임시 위도 경도
String dummy_longitude = “12702.7910″;
#include “DHT.h”
#define DHTPIN 2 //온습도 센서 디지털 2번에 신호선 연결
#define DHTTYPE DHT11
DHT dht(DHTPIN, DHTTYPE);
String lattitude;
String longitude;
int hum;
int temp;
float road_temp;
int SPI_COMMAND(unsigned char cCMD) // 적외선 온도 센서에 온도 정보 요청
{
unsigned char T_high_byte, T_low_byte;
digitalWrite(chipSelectPin , LOW);
delayMicroseconds(10);
SPI.transfer(cCMD);
delayMicroseconds(10);
T_low_byte = SPI.transfer(0×22);
delayMicroseconds(10);
T_high_byte = SPI.transfer(0×22);
delayMicroseconds(10);
digitalWrite(chipSelectPin , HIGH);
return (T_high_byte<<8 | T_low_byte); // 온도값 return */
}
void setup() {
// put your setup code here, to run once:
Serial2.begin(9600);
Serial.begin(9600);
Serial3.begin(9600);
digitalWrite(chipSelectPin , HIGH);
pinMode(chipSelectPin , OUTPUT);
SPI.setDataMode(SPI_MODE3);
SPI.setClockDivider(SPI_CLOCK_DIV16);
SPI.setBitOrder(MSBFIRST);
SPI.begin();
}
void loop()
{
if(Serial3.available()) // 다른 차량에서 발생한 외부에서 노면 미끄러움 정보를 수신한 경우
{
char data = Serial3.read();
BT_DATA += data;
BT_FLAG =true;
BT_TIME = millis();
}
else if(BT_FLAG == true && millis() – BT_TIME > 100)
{
BT_FLAG = false;
//Serial.println(BT_DATA.length());
if(BT_DATA.indexOf(‘B’) != -1)
{
Serial.println(“hello”);
ALARM_FLAG = true;
ALARM_TIME = millis();
SEND_FLAG = true;
}
BT_DATA =”";
}
if(millis() – ALARM_TIME > 3000 && ALARM_FLAG == true)
{
ALARM_FLAG = false; //경보 해제
SEND_FLAG = true;
}
//////////////////////////////////////////////////////////////////////////////////
if(Serial.available()) // 자동차 내장 라즈베리파이가 노면 미끄러움을 영상으로 인식한 경우
{
char data = Serial.read();
RASP_DATA += data;
RASP_FLAG = true;
}
else if(RASP_FLAG == true)
{
RASP_FLAG = false;
Serial3.println(RASP_DATA);
RASP_DATA =”";
}
if( RASP_DATA == “B”)
{
SEND_FLAG = true;
}
//////////////////////////////////////////////////////////////////////////////////
if(Serial2.available() && !Serial3.available() && !Serial.available())
{
char data;
data = (char)Serial2.read();
GPS_DATA += data;
GPS_LAST = millis();
}
if(millis() – GPS_LAST > 100)
{
//Serial.print(GPS_DATA);
int data_start = GPS_DATA.indexOf(“GPRMC”);
int data_end = GPS_DATA.indexOf(“GPVTG”);
if(data_start >0 && data_end >0)// 유효 데이터 확인
{
//Serial.print(data_start); Serial.print(” “); Serial.println(data_end);
GPS_DATA = GPS_DATA.substring(data_start, data_end);
//Serial.println(GPS_DATA);
int first_comma = GPS_DATA.indexOf(“,”);
int second_comma = GPS_DATA.indexOf(“,”, first_comma+1);
int third_comma = GPS_DATA.indexOf(“,”, second_comma+1);
int fourth_comma = GPS_DATA.indexOf(“,”, third_comma+1);
int fifth_comma = GPS_DATA.indexOf(“,”, fourth_comma+1);
int sixth_comma = GPS_DATA.indexOf(“,”, fifth_comma+1);
lattitude = GPS_DATA.substring(third_comma+1, fourth_comma); //위도
longitude = GPS_DATA.substring(fifth_comma+1, sixth_comma); //경도
//GPRMC,000505.021,V,,,,,0.00,0.00,060180,,,N*41
}
GPS_DATA = “”;
hum = dht.readHumidity(); //대기습도
temp = dht.readTemperature(); //대기온도
// 이 부분에 적외선 온도 센서 시작
iOBJECT = SPI_COMMAND(OBJECT); // 대상 온도 Read
road_temp = float(iOBJECT)/100,2;
// 이 부분에 적외선 온도 센서 끝
//Serial.print(“ROAD :”); Serial.println(road_temp);
// 라즈베리파이에 정보 송신
}
if(SEND_FLAG == true)
{
if(lattitude.length() <3)
lattitude = dummy_lattitude;
if(longitude.length() <3)
longitude = dummy_longitude;
SEND_FLAG = false;
Serial.print(“A ,”);
Serial.print(lattitude); Serial.print(“,”);
Serial.print(longitude); Serial.print(“,”);
Serial.print(hum); Serial.print(“,”);
Serial.print(temp); Serial.print(“,”);
Serial.print(road_temp); Serial.print(“,”);
Serial.print(ALARM_FLAG ? ‘T’ : ‘F’); Serial.print(“,”);
Serial.println();
}
}
[PYTHON]VIDEO_CLIENT
import os
import cv2
import numpy as np
import math
import time
import sys
import socket
WARN_FLAG = False
”’
1. 웹캠 이미지 리드
2. ROI 설정
3. 픽셀 값의 평균으로 필터링: 평균값보다 크면 255(white), 작으면 0(black)
4. 설정한 unit 크기로 이미지를 분할해 각각의 픽셀 수 차이 계산
5. wet으로 판단되는 unit을 box로 표시
6. box의 갯수가 일정 값 이상이면 count: while문으로 순환하기 때문에 count를 세서 지속적으로 wet일때만 젖은 도로라고 판단
7. count가 일정 값 이상이면 ‘WET ROAD DETECTED’ 출력
”’
”’
wet 판단 기준
# unit의 ratio값이 일정값 이하
# 인접한 unit의 ratio값 차이가 일정값 이상
# 위의 두 조건을 만족하면서 unit이 2*2 이상으로 밀집되어있을 때
”’
”’
설정해야 하는 변수
# Threshold 값(TH)
# image trim의 x,y 값
# 이미지를 분할할 unit의 크기: 300*300 이미지 이므로 unit은 300의 약수
# wet_point_list 함수의 ref1, ref2
”’
def im_trim(img, x, y, w, h): # 분석 가능한 이미지로 ROI 설정
(a, b) = img.shape[:2]
center = (b/3 , a/1.5)
angle90 = 90
scale = 1.0
M = cv2.getRotationMatrix2D(center, angle90, scale)
img = cv2.warpAffine(img, M, (a, b))
”’
x = 100; y = 100; #자르고 싶은 지점의 x좌표와 y좌표 지정
w = 200; h = 200; #x로부터 width, y로부터 height를 지정
”’
img_trim = img[y:y+h, x:x+w] #trim한 결과를 img_trim에 담는다
return img_trim
def grayscale(img): # 그레이스케일로 이미지 변환
return cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
def Filter(img, value = 0): # 픽셀 값 평균을 기준으로 필터링
average = img.mean()
if value == 0:
ret,img_filter = cv2.threshold(img, average, 255, cv2.THRESH_BINARY)
else:
ret,img_filter = cv2.threshold(img, average*value, 255, cv2.THRESH_BINARY)
#ret,img_filter = cv2.threshold(img, value, 255, cv2.THRESH_BINARY)
return img_filter
def count_pixel(img): # 픽셀 값이 0인 픽셀 COUNT
number = cv2.countNonZero(img)
return number
def pixel_ratio0(img1, img2): # 픽셀 값 0 비율 계산
ratio = 1-abs((count_pixel(img2)-count_pixel(img1))/img1.size)
return ratio*100
def same_bright(frame, gap): # 두 이미지의 밝기를 동일하게 조절
if gap>=0:
M = np.ones(frame.shape, dtype = “uint8″) * abs(int(gap))
transform = cv2.add(frame, M)
else:
M = np.ones(frame.shape, dtype = “uint8″) * abs(int(gap))
transform = cv2.subtract(frame, M)
return transform
def seperated_image_ratio(frame1, frame2, unit): #unit의 수만큼 이미지를 분할하여 분할된 이미지의 pixel_ratio를 array에 저장
ratio_arr = np.zeros((unit,unit), dtype = int)
U = int(frame1.shape[0]/unit)
white = (255,255,255)
black = (0,0,0)
# 격자 그리기
for x in range(unit): cv2.line(frame2, (U*x, 0), (U*x, U*unit), black, 1, 4)
for y in range(unit): cv2.line(frame2, (0, U*y), (U*unit, U*y), black, 1, 4)
# ratio 행렬 생성
for i in range(unit):
for j in range(unit):
img_trim1 = frame1[i*U:(i+1)*U, j*U:(j+1)*U]
img_trim2 = frame2[i*U:(i+1)*U, j*U:(j+1)*U]
ratio = int(pixel_ratio0(img_trim1, img_trim2))
ratio_arr[i,j] = ratio
# unit의 좌표 값과 ratio값 이미지에 표시
text = ‘(‘+str(i)+’,'+str(j)+’)’
#cv2.putText(frame2, text, (j*U+int(U*0.2), i*U+int(U*0.4)), 1, 0.5, black)
cv2.putText(frame2, str(ratio), (j*U+int(U*0.3), i*U+int(U*0.8)), 1, 0.6, black)
return ratio_arr
def wetpoint_list(r_arr): # 1. 각각의 unit의 인접한 unit의 ratio값의 차가 일정 값을 넘으면 wet list에 좌표 값 저장
# 2. unit의 ratio값이 일정 값을 넘으면 wet list에 좌표 값 저장
wet = []
ref1 = 25 # 인접한 unit간 ratio차이의 기준값
ref2 = 50 # unit의 ratio 기준값
for i in range(len(r_arr[0])):
for j in range(len(r_arr[1])):
R = r_arr[i,j]
Rlst = []
if (1<=i<=len(r_arr[0])-2 and 1<=j<=len(r_arr[1])-2):
for m in range(3):
for n in range(3):
Rlst.append(r_arr[i-1+m,j-1+n])
for r in Rlst:
if r-R > ref1:
wet.append((i,j))
if wet != []:
if wet[-1] == (i,j):
break
if wet != []:
if wet[-1] != (i,j):
if np.mean(r_arr)-R > ref2 and R!=0:
wet.append((i,j))
return wet
def wet_list_filter(arr, wet): # wet리스트의 좌표값을 r_arr와 같은 크기의 2차원 array의 좌표에 1로 표시
# 2*2구역의 값이 모두 1이면 wet_lst에 좌표값 저장
wet_arr = np.zeros(arr.shape, dtype = int)
wet_lst = []
# wet 리스트의 좌표 값을 wet_arr에 1로 표시
for n in wet:
x = n[1]
y = n[0]
wet_arr[x,y] = 1
#2*2 구역의 값이 모두 1이면 wet_lst에 좌표값 저장
for i in range(len(arr[0])):
for j in range(len(arr[1])):
if i+1<len(arr[0])-1 and j+1<len(arr[1])-1:
if wet_arr[i,j]*wet_arr[i+1,j]*wet_arr[i,j+1]*wet_arr[i+1,j+1] == 1:
wet_lst.append((i,j))
wet_lst.append((i+1,j))
wet_lst.append((i,j+1))
wet_lst.append((i+1,j+1))
wet_lst = list(set(wet_lst))
return wet_lst
def boxing_wet(frame, lst, unit): # wet list의 젖은 픽셀 좌표를 box로 화면에 표시
U = int(frame.shape[0]/unit)
thick = 2
for n in lst:
x = n[0]
y = n[1]
cv2.line(frame, (U*x, U*y), (U*x, U*(y+1)), (0,0,255), thick, 4)
cv2.line(frame, (U*x, U*y), (U*(x+1), U*y), (0,0,255), thick, 4)
cv2.line(frame, (U*(x+1), U*y), (U*(x+1), U*(y+1)), (0,0,255), thick, 4)
cv2.line(frame, (U*x, U*(y+1)), (U*(x+1), U*(y+1)), (0,0,255), thick, 4)
def nothing(x):
pass #더미 함수 생성… 트랙 바 생성시 필요하므로
def recvall(sock, Count):
buf = b”
while Count:
newbuf = sock.recv(Count)
if not newbuf: return None
buf += newbuf
Count -= len(newbuf)
return buf
def SEND_WARN():
for i in range(3):
Message = ’3′
client_socket.send(Message.encode()) ##알람 경보 활성화
print(“send”)
length = recvall(client_socket,16)
stringData = recvall(client_socket, int(length))
def SEND_SAFE():
Message = ’4′
client_socket.send(Message.encode()) ##알람 경보 활성화
length = recvall(client_socket,16)
stringData = recvall(client_socket, int(length))
def get_img_channel(channel):
message = channel
client_socket.send(message.encode()) ## 1번 이미지 전송 요청
length = recvall(client_socket,16)
stringData = recvall(client_socket, int(length))
data = np.frombuffer(stringData, dtype=’uint8′)
img = cv2.imdecode(data,1)
return img
HOST = ’192.168.0.9′
PORT = 9999
client_socket = socket.socket(socket.AF_INET,socket.SOCK_STREAM)
client_socket.connect((HOST, PORT))
cv2.namedWindow(‘Binary’) #트랙바를 붙일 윈도우를 생성
cv2.resizeWindow(‘Binary’, 400, 120)
cv2.createTrackbar(‘threshold’,'Binary’, 0, 150, nothing) #트랙바를 이름이’Binary’인 윈도우에 붙임’
cv2.setTrackbarPos(‘threshold’, ‘Binary’, 100) #초기값 100
cv2.createTrackbar(‘X’,'Binary’, 0, 100, nothing) #트랙바를 이름이’Binary’인 윈도우에 붙임’
cv2.setTrackbarPos(‘X’, ‘Binary’, 40) #초기값 40
cv2.createTrackbar(‘Y’,'Binary’, 150, 250, nothing) #트랙바를 이름이’Binary’인 윈도우에 붙임’
cv2.setTrackbarPos(‘Y’, ‘Binary’, 200) #초기값 200
count = 0
unit = 15 #pixel이 300*300 이므로 unit은 300의 약수여야함
f1 =”
f2 =”
while True:
if WARN_FLAG == True:
SEND_WARN()
WARN_FLAG = False
print(time.time())
f1 = get_img_channel(’1′) # 1번 이미지 전송 요청
f2 = get_img_channel(’2′) # 2번 이미지 전송 요청
TH = cv2.getTrackbarPos(‘threshold’,'Binary’)*0.01 # threshold 필터링 값ㅂ
x = cv2.getTrackbarPos(‘X’,'Binary’) # frame2의 x축 값 변경
y = cv2.getTrackbarPos(‘Y’,'Binary’) # frame2의 y축 값 변경
if True:
frame1_rgb = im_trim(f1, 72, 100, 300, 300)
frame2_rgb = im_trim(f2, x, y, 300, 300) # x 초기값 40, y 초기값 200
frame1 = grayscale(frame1_rgb)
frame2 = grayscale(frame2_rgb)
average1 = frame1.mean()
average2 = frame2.mean()
gap = average1 – average2
frame2_c = same_bright(frame2, gap)
average2_c = frame2_c.mean()
thresh1 = Filter(frame1, TH)
thresh2 = Filter(frame2_c, TH)
thresh3 = cv2.bitwise_xor(thresh1, thresh2) # t1, t2 이미지 겹치는 부분 0으로 변환
xor_ratio = count_pixel(thresh3) * 100/ thresh3.size
unit_arr = seperated_image_ratio(thresh1, thresh2, unit)
p_ratio = int(np.mean(unit_arr))
wet = wetpoint_list(unit_arr)
wet = wet_list_filter(unit_arr, wet)
boxing_wet(frame1_rgb, wet, unit)
if len(wet) > 3:
count += 1
else:
count = 0
sign = 0
if count > 10: ## 노면이 젖은 부분을 검출하는 부분
sign = 1
print(”,\
‘W.P ‘+str(len(wet)),\
‘R: ‘+ str(p_ratio),\
‘TH: ‘ + str(TH),\
‘GAP: ‘ + str(int(gap)),\
‘A1: ‘ + str(int(average1)),\
‘XOR: ‘ + str(int(xor_ratio)),\
‘COUNT: ‘ + str(count),\
‘WET ROAD DETECTED!!!’,\
”,\
sep = ‘ | ‘)
wet = []
WARN_FLAG = True
else:
SEND_SAFE()
print(”,\
‘W.P ‘+str(len(wet)),\
‘R: ‘+ str(p_ratio),\
‘TH: ‘ + str(TH),\
‘GAP: ‘ + str(int(gap)),\
‘A1: ‘ + str(int(average1)),\
‘XOR: ‘ + str(int(xor_ratio)),\
‘COUNT: ‘ + str(count),\
”,\
sep = ‘ | ‘)
cv2.imshow(“1″, thresh1)
cv2.imshow(“2″, thresh2)
cv2.imshow(’55′, f2)
#cv2.imshow(“3″, thresh3)
cv2.imshow(“ORIGIN1″, frame1_rgb)
#cv2.imshow(“ORIGIN2″, frame2_c)
if cv2.waitKey(1) & 0xFF == ord(‘q’):
break
# 사용자가 키보드 q 누르면 Opencv 종료
cv2.destroyAllWindows() # 리소스 반환
cv2.waitKey(0)
client_socket.close()
[PYTHON]VIDEO_HOST
# -*- coding: utf-8 -*-
# 원격 동키카 제어 및 카메라 스트리밍 코드
# 동키카는 흑백 영상을 Streaming
”’
2020.02.12
동키카 PWM / STREAMING + 외부 연산장치 연산결과에 따른 GPIO 출력
노면 상태가 젖은 경우 라즈베리파이 GPIO 21 번이 HIGH 상태가 되었다가 특정 시간이 지난 뒤에 자동으로 LOW됨
”’
import cv2
import numpy
import time
import pygame
import socket
import time
import RPi.GPIO as GPIO
from _thread import *
#라즈베리파이 GPIO
import RPi.GPIO as GPIO
GPIO_SIGNAL = 21
GPIO.setmode(GPIO.BCM)
GPIO.setup(21, GPIO.OUT)
GPIO.output(GPIO_SIGNAL,False)
import Adafruit_PCA9685
pwm = Adafruit_PCA9685.PCA9685() #pwm = Adafruit_PCA9685.PCA9685(address=0×41, busnum=2)
pwm.set_pwm_freq(60) # 서보모터 60Hz로 펄스주기를 설정.
#### 동키카 PWM 펄스 조절 부분 #########
# 이 부분의 값을 적절히 조절해서 전진/후진/정지/좌/우 조절할 것#
PWM_GO_SLOW = 395
PWM_GO_FAST = 400
PWM_GO = PWM_GO_FAST
PWM_BACK = 370
PWM_STOP = 380
PWM_LEFT = 260
PWM_RIGHT = 500
PWM_CENTER = 380
#### 동키카 PWM 펄스 조절 부분 #########
# Settings for joystick
axisUpDown = 1 # Joystick axis to read for up / down position
axisUpDownInverted = False # Set this to True if up and down appear to be swapped
axisLeftRight = 3 # 라즈베리파이에서는 3 / 컴퓨터에서는 4로 지정하면 됨
axisLeftRightInverted = False # Set this to True if left and right appear to be swapped
pygame.init()
pygame.joystick.init()
joystick = pygame.joystick.Joystick(0)
joystick.init()
HOST = ”
PORT = 9999
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1)
server_socket.bind((HOST, PORT))
server_socket.listen()
print(‘server start’)
A =b’hello’
B =b’world’
global GO
global TILT
global CONT_DATA
GO = 0
TILT = 0
tt =1
last_ch_data =0
ALARM = False
init_time = time.time()
#PCA9685 관련 펄스 초기설정 함수
def set_servo_pulse(channel, pulse):
pulse_length = 1000000 # 1,000,000 us per second
pulse_length //= 60 # 60 Hz
print(‘{0}us per period’.format(pulse_length))
pulse_length //= 4096 # 12 bits of resolution
print(‘{0}us per bit’.format(pulse_length))
pulse *= 1000
pulse //= pulse_length
pwm.set_pwm(channel, 0, pulse)
pwm.set_pwm_freq(60)
# Function to handle pygame events
def PygameHandler(events):
#조이스틱 이벤트 발생한 경우
for event in events:
if event.type == pygame.JOYAXISMOTION:
upDown = joystick.get_axis(axisUpDown)
leftRight = joystick.get_axis(axisLeftRight)
global GO
global TILT
if upDown < -0.1:
#print(“GO”)
GO = 1
elif upDown > 0.1:
#print(“BACK”)
GO = -1
else:
GO = 0
if leftRight < -0.1:
#print(“LEFT”)
TILT = 1
elif leftRight > 0.1:
#print(“RIGHT”)
TILT = -1
else:
TILT = 0
return GO, TILT
# 그레이스케일로 이미지 변환
def grayscale(img):
return cv2.cvtColor(img, cv2.COLOR_RGB2GRAY)
## time.sleep의 system 독점을 방지하기 위한 내용
def millis_python():
now_time = time.time()
now_time = (now_time – init_time)
return now_time
# 쓰레드 함수 ( 소켓 통신 개시 이후 무한 loop 문 처럼 작동하는 구문 )
def threaded(client_socket, addr):
global tt
print(‘Connected by :’, addr[0], ‘:’, addr[1])
stringData = b”
while True:
try:
data = client_socket.recv(1024)
if not data:
print(‘Disconnected by ‘ + addr[0],’:',addr[1])
pwm.set_pwm(0, 0, PWM_STOP)
break
ch_data = int(data)
if ch_data == 1:
stringData = A
#client_socket.send(str(len(A)).ljust(16).encode())
#client_socket.send(A)
last_ch_data =1
#stringData = queue1.get()
if ch_data == 2:
stringData = B
last_ch_data =2
#client_socket.send(str(len(B)).ljust(16).encode())
#client_socket.send(B)
#stringData = queue2.get()
if ch_data == 3: ### WARN SIGNAL
GPIO.output(GPIO_SIGNAL,False)
PWM_GO = PWM_GO_SLOW
print(“EEEE”)
tt = millis_python()
stringData =b’AA’
if ch_data == 4:
stringData =b’AA’
PWM_GO = PWM_GO_FAST
client_socket.send(str(len(stringData)).ljust(16).encode())
client_socket.send(stringData)
## 이 부분에 PWM 제어 신호 넣으면 됨
CONT_DATA = PygameHandler(pygame.event.get())
print(GO, TILT)
if GO == 1:
print(“FORWARD”)
pwm.set_pwm(0, 0, PWM_GO) #0번서보
elif GO == -1:
print(“BACKWARD”)
pwm.set_pwm(0, 0, PWM_BACK) #0번서보
else: # 이 부분에 전진모터 중립
pwm.set_pwm(0, 0, PWM_STOP) #0번서보
if TILT == 1:
print(“LEFT”)
pwm.set_pwm(3, 0, PWM_LEFT) #3번서보
elif TILT == -1:
print(“RIGHT”)
pwm.set_pwm(3, 0, PWM_RIGHT) #3번서보
else: # 이 부분에 조향서보모터 중립
pwm.set_pwm(3, 0, PWM_CENTER) #3번서보
## 특정 시간이 지나면 자동으로 알람을 해제하는 부분 ( 5초 이상 경과 시 OFF )
if (millis_python() – tt > 4):
PWM_GO = PWM_GO_FAST
GPIO.output(GPIO_SIGNAL,True)
ALARM = False
except ConnectionResetError as e:
print(‘Disconnected by ‘ + addr[0],’:',addr[1])
pwm.set_pwm(0, 0, PWM_STOP)
break
client_socket.close()
def webcam():
# capture1 = cv2.VideoCapture(0) # 카메라 채널 바꿔주면 됨
# capture2 = cv2.VideoCapture(2) # 카메라 채널 바꿔주면 됨
while True:
ret1, frame1 = capture1.read()
ret2, frame2 = capture2.read()
if ret1 == True:
encode_param=[int(cv2.IMWRITE_JPEG_QUALITY),50]
frame1 = grayscale(frame1)
result, imgencode = cv2.imencode(‘.jpg’, frame1, encode_param)
data1 = numpy.array(imgencode)
stringData1 = data1.tostring()
global A
A = stringData1
cv2.imshow(‘CH1′, frame1)
if ret2 == True:
encode_param=[int(cv2.IMWRITE_JPEG_QUALITY),50]
frame2 = grayscale(frame2)
result, imgencode = cv2.imencode(‘.jpg’, frame2, encode_param)
data2 = numpy.array(imgencode)
stringData2 = data2.tostring()
global B
B = stringData2
cv2.imshow(‘CH2′, frame2)
key = cv2.waitKey(1)
if key == 27:
break
GPIO.output(GPIO_SIGNAL,True)
capture1 = cv2.VideoCapture(0) # 카메라 채널 바꿔주면 됨
capture2 = cv2.VideoCapture(2) # 카메라 채널 바꿔주면 됨
start_new_thread(webcam, ())
while True:
print(‘wait’)
client_socket, addr = server_socket.accept()
#threaded(client_socket, addr)
start_new_thread(threaded, (client_socket, addr ))
server_socket.close()
GPIO.cleanup()
—————— main.py ——————-
#-*- coding: utf-8 -*-
from flask import Flask, render_template, Response, request, url_for
from flask_googlemaps import GoogleMaps
from flask_googlemaps import Map, icons
from flask import current_app as current_app
from flask_mail import Mail, Message
import dbModule
import os, datetime
import glob
app = Flask(__name__, template_folder=”templates”)
app.config['GOOGLEMAPS_KEY'] = “########”
GoogleMaps(app)
UPLOAD_FOLDER = ‘./uploads’
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
app.config['MAIL_SERVER'] = os.environ.get(‘MAIL_SERVER’, ‘smtp.gmail.com’)
app.config['MAIL_PORT'] = int(os.environ.get(‘MAIL_PORT’, ’587′))
app.config['MAIL_USERNAME'] = os.environ.get(‘MAIL_USERNAME’, ‘#######’)
app.config['MAIL_PASSWORD'] = os.environ.get(‘MAIL_PASSWORD’, ‘#######’)
app.config['MAIL_USE_TLS'] = int(os.environ.get(‘MAIL_USE_TLS’, True))
app.config['MAIL_USE_SSL'] = int(os.environ.get(‘MAIL_USE_SSL’, False))
mail = Mail(app)
db_class = dbModule.Database()
@app.route(‘/email’, methods=['POST'])
def email():
name = request.form['name']
email_address = request.form['email']
phone = request.form['phone']
message = request.form['message']
msg = Message(‘A new message from TWOEYES’, sender=email_address, recipients=['#######'])
msg.body = “You have received a new message from your website contact form.\nHere are the details:\n\nName: %s\n\nEmail: %s\n\nPhone: %s\n\nMessage: %s” % (name, email_address, phone, message)
mail.send(msg)
return ‘Sent’
@app.route(‘/data’, methods=['GET'])
def getData():
lat = request.args.get(‘req_lat’)
lng = request.args.get(‘req_lng’)
t1 = request.args.get(‘req_t1′)
t2 = request.args.get(‘req_t2′)
h = request.args.get(‘req_h’)
date = request.args.get(‘req_date’)
time = request.args.get(‘req_time’)
sql = “”"insert into data(lat, lng, t1, t2, h, date, time) values (%s, %s, %s, %s, %s, %s, %s)”"”
db_class.execute(sql, (lat, lng, t1, t2, h, date, time))
db_class.commit()
return ‘Data’
@app.route(‘/image’, methods=['POST'])
def postImage():
file = request.files['file']
if file:
#filename = secure_filename(file.filename)
filename = datetime.datetime.now().strftime(‘%y%m%d_%H%M%S’)+’.jpg’
file.save(os.path.join(app.config['UPLOAD_FOLDER'], filename))
return ‘Image’
@app.route(‘/’)
def index():
sql = “select * from data order by date”
row = db_class.executeAll(sql)
db_class.execute(“select count(*) from data”)
cnt = db_class.cursor.fetchone()
cnt = cnt.get(‘count(*)’)
gps_list=[]
for i in range(0, cnt):
row_lat = float(row[i].get(‘lat’))
row_lng = float(row[i].get(‘lng’))
gps_list.append((row_lat, row_lng))
#print(gps_list)
sndmap = Map(
style=”height: 450px; width: 1150px;”,
identifier=”sndmap”,
varname=”sndmap”,
zoom=16, #13
lat=row[0].get(‘lat’),
lng=row[0].get(‘lng’),
markers=gps_list
)
list_of_files = glob.glob(‘./uploads/*’)
latest_file = max(list_of_files, key=os.path.getctime)
return render_template(‘index.html’, sndmap=sndmap, GOOGLEMAPS_KEY=request.args.get(‘apikey’), row=row, len=len(row), latest_file=latest_file)
if __name__ == ‘__main__’:
app.run(host=’0.0.0.0′, port=5000, debug=True)
—————— dbModule.py ——————-
import pymysql
class Database():
def __init__(self):
self.db= pymysql.connect(host=’18.213.156.121′,
user=’#####’,
password=’#######’,
db=’TWOEYES’,
charset=’utf8′)
self.cursor= self.db.cursor(pymysql.cursors.DictCursor)
def execute(self, query, args={}):
self.cursor.execute(query, args)
def executeOne(self, query, args={}):
self.cursor.execute(query, args)
row= self.cursor.fetchone()
return row
def executeAll(self, query, args={}):
self.cursor.execute(query, args)
row= self.cursor.fetchall()
return row
def commit(self):
self.db.commit()
—————— contact.js ——————-
$(function() {
$(“#contactForm input,#contactForm textarea”).jqBootstrapValidation({
preventSubmit: true,
submitError: function($form, event, errors) {
// additional error messages or events
},
submitSuccess: function($form, event) {
event.preventDefault(); // prevent default submit behaviour
// get values from form
var name = $(“input#name”).val();
var email = $(“input#email”).val();
var phone = $(“input#phone”).val();
var message = $(“textarea#message”).val();
var firstName = name;
// Check the space of name for success/fail message
if (firstName.indexOf(‘ ‘) >= 0) {
firstName = name.split(‘ ‘).slice(0, -1).join(‘ ‘);
}
$this = $(“#sendMessageButton”);
$this.prop(“disabled”, true);
$.ajax({
url: “http://18.213.156.121:5000/email”,
type: “POST”,
data: {
name: name,
phone: phone,
email: email,
message: message
},
cache: false,
success: function() {
// Success message
$(‘#success’).html(“<div class=’alert alert-success’>”);
$(‘#success > .alert-success’).html(“<button type=’button’ class=’close’ data-dismiss=’alert’ aria-hidden=’true’>×”)
.append(“</button>”);
$(‘#success > .alert-success’)
.append(“<strong>Your message has been sent. </strong>”);
$(‘#success > .alert-success’)
.append(‘</div>’);
//clear all fields
$(‘#contactForm’).trigger(“reset”);
},
error: function() {
// Fail message
$(‘#success’).html(“<div class=’alert alert-danger’>”);
$(‘#success > .alert-danger’).html(“<button type=’button’ class=’close’ data-dismiss=’alert’ aria-hidden=’true’>×”)
.append(“</button>”);
$(‘#success > .alert-danger’).append($(“<strong>”).text(“Sorry ” + firstName + “, it seems that my mail server is not responding. Please try again later!”));
$(‘#success > .alert-danger’).append(‘</div>’);
//clear all fields
$(‘#contactForm’).trigger(“reset”);
},
complete: function() {
setTimeout(function() {
$this.prop(“disabled”, false);
}, 1000);
}
});
},
filter: function() {
return $(this).is(“:visible”);
},
});
$(“a[data-toggle=\"tab\"]“).click(function(e) {
e.preventDefault();
$(this).tab(“show”);
});
});
// When clicking on full hide fail/success boxes
$(‘#name’).focus(function() {
$(‘#success’).html(”);
});
[62호]국내 초소형 Serial to WiFi 컨버터 ‘sWiFi/all’ 출시
시스템베이스
국내 초소형 Serial to WiFi 컨버터 ‘sWiFi/all’ 출시
국내 유일 시리얼 통신 전문기업 시스템베이 스는 RS232/422/485를 지원하는 장비에 WiFi 무선 통신 기능을 제공하는 초소형 듀얼 밴드 무선(WiFi) 컨버터인 sWiFi/all을 출시하였다. 듀얼 밴드 로 시중에서 널리 사 용되는 2.4GHz 대역과 추가로 5GHz 대역의 WiFi 통신 기능을 제공한다. 이를 통해 사용자는 5GHz 대역을 사용함으로써 2.4GHz 대역의 혼선을 피하고, 통신의 신뢰성을 높일 수 있는 무선네트워크를 구성하여 활용할 수 있다. 유선 구간 최고 921.6Kbps의 통신 속도와 무선 구간 최고 54Mbps를 통신 속도를 지원 하고 ±15kV IEC ESD 보호 기능을 내장하여 우수한 신뢰성과 높은 성능을 동시에 제공한 다. 또한, 네트워크 (TCP/IP)를 통하여 연결된 장 치의 원격제어, 모니터링, 데이터 통신을 지원 하기 위하여 COM Port Redirector 프로그램을 제공하며, 연결된 시리얼 장치를 별도의 프로 그램 수정 없이 바로 사용 할 수 있다. 이더넷을 통하여 언제, 어디서나 시리얼 포 트를 사용할 수 있는 소형 컨버터로써 PLC 장 비, 계측 장비, 태양광 시스템, 각종 검사 장 비 등 다양한 통신 장비에 연결할 수 있고, -40~85℃의 산업용 온도 스펙을 만족하는 높은 사양의 부품으로 개발되어 다양한 현장에 서 안정적으로 사용이 가능한 특징을 갖는다. 무엇보다 sWiFi/all는 국내 제품 중 가장 작 은 초소형 컨버터이자 다양한 기능과 고성능을 제공하면서도 저렴한 가격대에 제공된다는 장 점이 있다. 디바이스마트에서는 이러한 고성능 sWiFi/all 외에도 시스템베이스의 다양한 제품 들을 만나볼 수 있다.
제품사양
· IEEE 802.11a/b/g/n (Wi-Fi) 지원
· 2.4GHz, 5GHz 듀얼 밴드 지원
· RS232/422/485 시리얼1포트, 최고 통신 속도 921.6Kbps 지원
· 산업용 동작 온도 -40~85℃ 지원
· Com Redirector, TestView™, sWiFiConfig 제공
· ±15㎸ IEC ESD 보호 기능 내장
[62호]듀얼모드가 지원되는 블루투스 5.0 ‘FB 시리즈’ 출시
[8핀 헤더 FB300BC] [22핀 헤더FB300_H]
펌테크
듀얼모드가 지원되는 블루투스 5.0 ‘FB 시리즈’ 출시
근거리 무선 통신 모듈을 연구하는 무선 솔루션 개발 전문 기업 펌테크는 블루투스 Classic & BLE가 지원되는 듀얼모드 블루투 스 5.0 ‘FB 시리즈’ 제품을 출시하였다. ‘FB 시리즈’는 총 6가지 제품으로 블루투스 모듈 4종과 인터페이스 보드 2종으로 이루 어져 있다. 모듈 4종은 8핀 헤더, 8핀 SMD, 22핀 헤더, QFN 40핀 타입이 있으며 인터 페이스 보드는 입출력 기능테스트를 쉽게 확 인할 수 있는 FB30x_H Board와 UART 통 신을 편리하게 테스트할 수 있는 FB30xBCBoard 2가지가 출시되었다. 기본적으로 FB 시리즈는 지속적인 대용량 데이터 전송에 적합한 Classic 통신 방식과 소용량 데이터 전송에 적합한 BLE 통신 방 식을 모두 지원하는 블루투스 5.0모듈이다. AT 명령어를 이용하여 환경 설정 및 동작 상 태를 쉽게 제어할 수 있으며, 각 통신 방식에 서는 Slave(Peripheral) 역할로만 동작이 가 능하다. FB 시리즈는 크게 FB300과 FB300_H 모 듈 두 가지로 크게 제품이 분리되어 출시되 었는데 하드웨어적 패키지의 차이가 있을 뿐 동일한 펌웨어가 내장되어 있어 똑같은 동 작을 하는 제품이다. FB300의 주요 입출 력 핀은 FB300_H의 총 22개 DIP타입 핀 (2.54mm 헤더)으로 빠짐없이 구현되어 있으 며, QFN 타입이므로 DIP 타입 형태의 탈착 이 가능하게 구성된 FB300_H를 사용하면 편리한 기능 테스트가 가능하다. 또한 각종기능 테스트는 디버깅이 편리한 FB300_H로 진행하고, 최종 양산시에는 FB300을 사용하 여 제품화하면 보다 좋은 효과를 볼 수 있다. FB300과 FB300_H는 모바일 악세사리, 헬스케어, 산업용, 스마트홈 등 근거리 무선 통신이 필요한 다양한 분야에 적용되어 개발 및 사용할 수 있다.
[62호]3채널 근접 및 거리 센서 모듈 신규 입고
POLOLU
3채널 근접 및 거리 센서 모듈 신규 입고
이 보드는 Texas Instruments의 OPT3101 IC를 기반으로하는 3채널 근접 및 거리 센서 모듈이다. 반사광의 세기를 사용하여 물체까지의 거 리를 추정하는 기존의 IR 센서와 달리 이 보 드는 10MHz에서 펄스 940nm 적외선을 방 출한 다음, 반사 된 신호의 위상을 측정한다. 또한, 신호의 진폭을 측정하여 물체의 밝 기, 반사, 가까운 정도를 나타낸다. 이 모듈에는 각각 약 50~60°를 커버하는 3개의 채널이 있어 센서에 넓은 시야 (FOV) 제공이 가능하고, 대상 물체에 따라 다르지 만 최대 1m 거리에서 물체를 측정 할 수 있 다. IR LED에는 각각 HDR0 및 HDR1(저조도/ 고휘도 모드)이라고도 부르는 두 가지 밝기 설정이 있는데, 저조도 모드는 주변 물체 (약 20cm 이내)에서만 잘 작동하는 반면에 고휘 도 모드는 더 긴 범위에서 작동하지만 너무 반사되거나 너무 가까운 물체는 센서가 포화 상태가되어 거리 측정에 실패 할 수 있다. 이 러한 밝기 중 하나를 사용하도록 OPT3101을 구성하거나 자동 밝기 또는 낮은 밝기 중 에서 선택하는 적응형 밝기 모드를 사용할 수 있다. 이 모듈은 모든 I2C 지원 장치와 함께 사용 할 수 있으며, 센서의 I2C 인터페이스를 통해 거리 측정이 가능하다. 2.5V~5.5V 공급에서 전원을 공급받을 수 있고 온보드 레귤레이터는 3.3V 논리 전압을 OPT3101에 공급한다. 보드에는 제공된 VIN과 동일한 로직 전압 레벨로 I2C 클록 및 데이터 라인을 전환하는 회로가 포함되어있어 보드를 3.3V 또는 5V 시스템과 간단하게 인터페이스 할 수 있다. 현재 이 모듈은 디바이스마트에서 판매중 이며, 회로도 및 샘플코드 등 더 자세한 사항 은 상세페이지를 통해 확인할 수 있다.
제품 사양