본문 바로가기

시각장애인용 길 안내 스마트 신발 - [2017 무한 상상 발명 한마당 제작 과정 온라인 공유]

반응형


잡동사니 세상 - 무한 상상 발명 한마당 2017

  

링크 주소 : http://sangsangmaker.kr/i2fair2017/

친구들과 2017 무한 상상 발명 한마당에 참가했다.

그래서 이번 포스트로 2017 무한상상 발명 한마당 제작 과정을 공유 하려고 한다. 

 우리가 만든 작품은 '시각장애인용 길 안내 스마트 신발' 이다. 스마트폰 어플리케이션을 키고 음성인식 버튼을 누르고, 도착지 주소를 말한다. 그리고, GPS 정보 보내기 버튼을 누르면 길 안내가 시작된다. 계속 걷다가 우회전 해야하는 사거리에 도착했을 때, 오른쪽 신발에 진동이 울린다. 이 작품은 이러한 방식을 통해서 시각장애인들에게 길을 안내한다.

제작 동기

 이 작품을 생각하게 된 계기는 TED강연 이었다. 데니스 홍의 TED강연 중, 시각장애인이 직접 운전하는 자동차이 있었다. 이 강연을 보기전에는 그저 시각장애인들을 위한 자율 주행자동차를 개발한 것이지, 직접 운전할 것이라고는 생각하지 못했다. 우리는 주제 그대로 시각장애인들이 주위 상황을 파악하고 직접 판단하여 운전할 수 있는 자동차였다는 것에 충격을 받았고, 시각장애인들이 그 자동차를 운전하고 나서 행복에 겨워 울었다는 점에 또 충격을 받았다. 시각 장애인들에게 자동차를 운전하는 것은 불가능에 가까운 일이었다. 하지만, 그들은 불가능하다고 생각되던 일들을 자신의 힘으로 해내었다는 것에 매우 행복해했다. 우리는 그것을 보고 그들에게 불가능하다고 생각되는 일들을 할 수 있게 발명품을 만들어 그들을 행복하게 만들어 주고 싶어졌다. 그러던 중 경북 시각 장애인 연합회에서 작성한 '시각 장애인의 문제 및 욕구 파악을 위한 면접 조사 결과 보고서' 를 보게 되었다. 이 보고서에 따르면 시각장애인들은 시각을 잃게 되면서 이동능력을 상실하게 되었고, 대부분이 집에서 하루를 보낸다는 것을 알 수 있다. 그래서 우리는 상실한 이동능력을 되찾아 주어 그들을 행복하게 만들어 주겠다고 다짐했다.


[데니스 홍 박사의 TED 강연 - 시각장애인이 직접 운전하는 자동차]


제작 과정 9/4 (월) - 아이디어 생각하기

아이디어 : 시각장애인들의 이동능력 되찾아 주기 

- 시각장애인에게 길 안내하기


1. 길 안내 정보를 전달할 방법 찾기

1). 감각

 사람들에게는 정보를 인식할 수 있는 5가지의 감각 시각,청각,촉각,미각,후각이 있다. 그중에서 미각과 후각은 구현하기 힘들기 때문에 제외했다. 그리고 시각장애인들은 시각으로 정보를 인식 할  수 없기 때문에 시각도 제외했다. 그러면 최종적으로 청각과 촉각이 남는다. 한국 도로 교통 공단에서 작성한 '보행중 음향기기 사용이 교통 안전에 미치는 영향 연구' 에 따르면, 보행중 음향 기기 사용은 사고날 확률을 높이는데 기여한다는 것을 확인 할 수 있다. 이를 통해 청각으로 정보를 인식하는 것은 보행중에 위험하다는 것을 확인하였다. 그러므로 남은 감각인 촉각을 이용한 길 안내 정보 전달 방법을 선택했다.

2). 촉각

 촉각으로 정보를 전달 할 수 있는 방법은 꼬집기, 누르기, 진동 등 여러가지 자극이 있다. 꼬집기는 사용자에게 통증을 줄 수 있으므로 제외한다. 누르기는 작품이 작용될 위치에 따라서 정보를 인식할 수도 없을 수도 있으므로 제외했다. 그리고 남은 자극 중에서 우리는 진동을 선택하였다. 진동은 모터하나만으로도 쉽게 구현이 가능하고, 진동은 붙어있는 대상에게 전달이 가능하기 때문에 한 곳에서의 진동이 퍼져나가 그 주변 부위에서도 감지 할 수 있어서 정보 인식을 더 잘 할 수 있게 해준다. 고로 우리는 진동을 통해 정보를 전달 하는 방식을결정했다.


2. 길 안내 정보를 전달할 위치 

1). 신체 부위

 진동으로 정보를 전달하기 때문에 왼쪽 오른쪽 구분이 확실하게 되어있는 신체부위는 팔(손)과 다리(발)이다. 손 같은 경우는 여러가지 일들을 많이 하기 때문에, 손은 제외하면, 발이 남는다. 그래서 우리는 발에 정보를 전달 할 것이다. 


2). 신발

 주로 발에 장착하는 물건들은 양말과 신발이다. 양말은 하루에도 여러번 다른 양말을 바꿔 신을 수 있기 때문에 정보를 전달할 임베디드 시스템을 장치하기에 적합하지 않은 구조를 가지고 있다. 그래서 우리는 비교적으로 하루에 매일 밖에 나갈 때 신는 신발에 이 임베디드 시스템을 장착하여 정보를 전달 할 것이다.

정리 : 신발에 장착된 임베디드 시스템을 통해 진동으로 길 안내 정보를 전달할 것이다.


3. 길 안내 정보를 수집할 방법

 SK PLANET에서 tmap api,sdk를 opensource로 제공하고 있다. 또한 Google에서도 Google Map api,sdk를 opensource로 제공하고 있다. 그 중에서 SK PLANET에서 제공하는 tmap은 각 지점에서의 Turntype(회전방향)정보를 제공할 뿐만 아니라한국에 최적화된 지도를 갖고 있어 우리가 생각하는 정보와 일치하기 때문에 tmap을 선택하였다.

tmap opensource : https://developers.skplanetx.com/


4. 수집한 정보를 임베디드 시스템에 전달 할 방법

 tmap은 웹을 통해서 정보를 받기 때문에 임베디드 시스템에 정보를 전달할 기기는 웹에 접속이 가능해야한다. 그래서 우리는 그 기기로 스마트폰을 선택하였다. 스마트폰은 GPS와 Bluetooh,가 내장되어 있기 때문에 사용자의 위치를 쉽게 파악할 수 있을 뿐만 아니라 bluetooth를 통해서 임베디드 시스템으로 정보를 전달 할 수 있다. 그러므로 우리는 수집한 정보를 스마트폰을 통해서 bluetooth 통신으로 정보를 전달 할 것이다. 


5. 이 작품의 기대효과

 시각장애인들이 이 작품을 사용하게 된다면, 첫째로 하루를 집에서 종일 보내는 시각장애인의 수가 줄어들 것이다. 그들은 이 작품을 통해 길 안내를 받아서 혼자서 직장으로 출근을 할 수도 있고, 공부를 하기 위해서 학원이나 복지관으로 갈 수 있다. 두번째, 사회복지사의 일이 줄어들어서 더 많은 장애인들에게 복지를 해줄 수 있다. 사회 복지사나 봉사활동자의 일들 중 한가지는 시각장애인이나 신체가 불편한 장애인들 같이 이동능력이 상실된 사람들의 거동을 도와 산책을 하거나 복지관으로 이동하여 교육을 받도록 도와준다. 하지만 시각장애인들이 이 작품으로 혼자서 이동할 수 있게 된다면 사회복지사들과 봉사활동자들은 그들의 거동을 도울 필요가 없어져 일이 줄어들고, 다른 일들에 시간을 더 투자할 수 있게 된다. 우리는 이 작품을 통해서 시각장애인들이 행복해하고, 사회복지사들의 일들이 줄어들어 더 많은 복지가 일어날 것이고, 시각장애인들의 우울증에도 좋은 영향을 미칠 것이라고 기대한다. 


제작 과정 9/5(화) - 자료 조사

1. 김서준 - 어플리케이션 제작 툴 및 방법 조사

2. 남정현 - tmap api,sdk 사용 방법 및 사용 툴 조사

3. 박광렬 - 임베디드 시스템으로 사용할 CPU 선택, 부품 조사 및 구매


제작 과정 9/6(수) - 아이디어 구체화


1. 원래 계획 

 스마트폰으로 tmap sdk를 실행하여 turnType과 점의 좌표를 불러오고, 스마트폰 내장의 GPS를 통해서 위치정보를 읽어와 비교하고, 회전해야하는 위치에 도착했을 때, bluetooth로 임베디드 시스템에 정보를 전달할 것이다.

- 우리 중에서 안드로이드 스튜디오나 eclipse android를 사용할 수 있는 사람이 없었다. 

- 위와 같은 이유로 tmap sdk를 사용할 수 없다.

- tmap api는 사용할 수 있다.

- 우리 모두 각각 라즈베리파이로 서버를 운영하고 있고, 경험이 있다.

- 서버를 이용하면 tmap api로 정보를 수집하고 전달 할 수 있다.

- 스마트폰은 인터넷에 연결이 가능하기 때문에 서버와 통신할 수 있다.

계획 수정 : 스마트폰으로 tmap sdk를 실행하지 않고, 서버를 만들어 서버에서 tmap api를 실행한다.


2. 수정된 계획

 스마트폰으로 출발지 도착지 정보를 서버로 전송하면 서버에서 tmap api를 실행하여 turnType과 점의 좌표를 불러오고, 스마트폰으로 이 정보를 전달한다. 다음 스마트폰 내장의 GPS를 통해서 위치 정보를 읽어와 서버에서 받은 정보와 비교하고, 회전해야하는 위치에 도착해을 때, bluetooth로 임베디드 시스템에 정보를 전달 할 것 이다.


3. 각 부분별 실행 방법

- 서버

 1). 스마트폰으로 부터 현재위치와 도착지 정보를 받는다.

 2). tmap api에 출발지 도착지 정보를 보내어 경로 정보를 요청한다.

 3). tmap api로 부터 받은 경로정보를 파싱하여 lat,lon,turntype..,. 등의 문자열로 나타낸다.

 4). http 통신으로 스마트폰으로 문자열을 전송한다.


- 스마트폰 어플리케이션

 1). 음성인식으로 도착지 정보를 받는다.

 2). GPS측정으로 현재위치 정보를 받는다.

 3). web을 통해서 http 통신으로 서버에 출발지, 도착지 정보를 보낸다.

 4). 서버로 부터 받은 문자열을 split을 통해 배열에 저장한다.

 5). 움직이면서 바뀌는 위치 정보를 GPS측정으로 확인하고 이를 배열에 저장된 좌표와 비교한다.

 6). 현재 위치가 회전해야하는 위치인 경우, bluetooth통신으로 turntype 정보를 임베디드 시스템에 전송한다.


- 임베디드 시스템

 1). bluetooth 통신으로 turntype정보를 전송받는다.

 2). 받은 정보가 왼쪽이면 왼쪽 신발을, 오른쪽이면 오른쪽 신발을 진동시킨다.


제작 과정 9/7~10 - 서버 제작(남정현)

 서버로는 라즈베리파이를 사용하였다. go 프로그래밍을 통해서 작성되었다. 

package main import ( "bytes" "encoding/json" "errors" "fmt" "io" "log" "net/http" "net/url" "regexp" "strconv" ) // T-Map appKey const API_KEY = "" // app key는 보안 정보이므로 밝히지 않겠다. // 위-경도 사용 const COORD_TYPE = "WGS84GEO" const GEOCODING_API = "https://apis.skplanetx.com/tmap/geo/fullAddrGeo" // API parameters var GEOCODING_PARAMS = []string{ "version", "addressFlag", "fullAddr", "format", } const ROUTE_API = "https://apis.skplanetx.com/tmap/routes/pedestrian?version=1" // API version is 1, fixed // API payloads(POST) var ROUTE_PAYLOADS = []string{ "startX", "startY", "endX", "endY", "startName", "endName", } // App과의 통신 데이터 파싱용 포맷 var FMT_ADDR_REQUEST = regexp.MustCompile(`startAddress : (\d+\.\d+) , (\d+\.\d+) endAddress : ([\p{L}\w\s]+)`) func geocoding_ok(s string) bool { if s == "" || s == "0" { return false } return true } // Geocoding API에 GET 리퀘스트를 주어진 파라미터로 보낸 후 응답을 raw bytes로 리턴 func geocoding_api_raw(param_list ...string) (resp *http.Response, err error) { var build_url *url.URL if build_url, err = url.Parse(GEOCODING_API); err != nil { return } params := url.Values{} params.Add("appKey", API_KEY) for i, v := range param_list { params.Add(GEOCODING_PARAMS[i], v) } // force coordinate type to COORD_TYPE if _, ok := params["coordType"]; !ok { params.Add("coordType", COORD_TYPE) } build_url.RawQuery = params.Encode() resp, err = http.Get(build_url.String()) return } // geocoding_api_raw에서 받은 raw bytes를 JSON 파싱해 가능한 주소 리턴 // 우선순위: 구주소 < 신주소 , 실제 위치 < 건물 위치 ({Lat,Lon}Entr) func geocoding(addr string) (x, y float64, err error) { var resp *http.Response resp, err = geocoding_api_raw("1", "F00", addr, "json") var b bytes.Buffer io.Copy(&b, resp.Body) log.Print("Geocoding response: ", string(b.Bytes())) dec := json.NewDecoder(&b) type RespParam struct { Error json.RawMessage `json:"error"` CoordInfo struct { CoordType string `json:"coordType"` Coord []struct { Lat string `json:"lat,omitempty"` // Y Lon string `json:"lon,omitempty"` // X NewLat string `json:"newLat,omitempty"` // Y NewLon string `json:"newLon,omitempty"` // X LatEntr string `json:"latEntr,omitempty"` // Y LonEntr string `json:"lonEntr,omitempty"` // X NewLatEntr string `json:"newLatEntr,omitempty"` // Y NewLonEntr string `json:"newLonEntr,omitempty"` // X } `json:"coordinate"` } `json:"coordinateInfo"` } t := RespParam{} if err = dec.Decode(&t); err != nil { return } if len(t.Error) > 0 { err = errors.New(string(t.Error)) return } if t.CoordInfo.CoordType != "" && t.CoordInfo.CoordType != COORD_TYPE { err = fmt.Errorf("Invalid coordinate type: %d", t.CoordInfo.CoordType) return } candidate := make([][2]string, 0) for _, c := range t.CoordInfo.Coord { candidate = append(candidate, [2]string{c.NewLonEntr, c.NewLatEntr}, [2]string{c.NewLon, c.NewLat}, [2]string{c.LonEntr, c.LatEntr}, [2]string{c.Lon, c.Lat}, ) } for _, c := range candidate { if geocoding_ok(c[0]) && geocoding_ok(c[1]) { if x, err = strconv.ParseFloat(c[0], 64); err != nil { continue } if y, err = strconv.ParseFloat(c[1], 64); err != nil { continue } break } } if x == 0 && y == 0 { err = errors.New("Lat/Lon parse failed") } return } // Route API에 주어진 payload를 보내고 응답을 raw bytes로 리턴 func route_api_raw(param_list ...string) (resp *http.Response, err error) { fmt.Println(param_list) var build_url *url.URL if build_url, err = url.Parse(ROUTE_API); err != nil { return } build_url.RawQuery = (url.Values)(map[string][]string{"appKey": []string{API_KEY}}).Encode() params := url.Values{} for i, v := range param_list { params.Add(ROUTE_PAYLOADS[i], v) } params.Add("reqCoordType", COORD_TYPE) params.Add("resCoordType", COORD_TYPE) fmt.Println("Builded ROUTE API url:", build_url.String()) fmt.Println("ROUTE API params:", params) resp, err = http.PostForm(build_url.String(), params) return } // Route API의 Feature중 Point, 또는 Line 데이터 type routeEntry struct { Point [2]float64 PointTurn int LinePoints [][2]float64 } // route_api_raw에서 받아온 raw bytes를 파싱해 routeEntry 배열로 리턴 func route(start_x, start_y, end_x, end_y float64, start, end string) (re []routeEntry, err error) { var resp *http.Response resp, err = route_api_raw( strconv.FormatFloat(start_x, 'f', -1, 64), strconv.FormatFloat(start_y, 'f', -1, 64), strconv.FormatFloat(end_x, 'f', -1, 64), strconv.FormatFloat(end_y, 'f', -1, 64), start, end, ) if err != nil { return } if resp.StatusCode != 200 { err = fmt.Errorf("Route API returned error code: %d", resp.StatusCode) return } var b bytes.Buffer io.Copy(&b, resp.Body) type FeatureCollection struct { Features []struct { Geometry struct { Type string `json:"type"` Name string `json:"name"` Desc string `json:"description"` Coord json.RawMessage `json:"coordinates"` } `json:"geometry"` Properties struct { TurnType int `json:"turnType"` } `json:"properties"` } `json:"features"` } fc := FeatureCollection{} dec := json.NewDecoder(&b) if err = dec.Decode(&fc); err != nil { return } re = make([]routeEntry, len(fc.Features)) for i, f := range fc.Features { geo := f.Geometry if geo.Type == "Point" { var coord [2]float64 if err = json.Unmarshal(geo.Coord, &coord); err != nil { return } re[i] = routeEntry{Point: coord, PointTurn: f.Properties.TurnType} } else if geo.Type == "LineString" { var coord [][2]float64 if err = json.Unmarshal(geo.Coord, &coord); err != nil { return } re[i] = routeEntry{LinePoints: coord} } } return } func main() { // Test code /* start_x, start_y, err := geocoding("서울시 마포구 신수동 459") if err != nil { log.Fatal("Geocoding API error 1: ", err) } // end_x, end_y, err := geocoding("서울시", "중랑구", "신내동", "487") end_x, end_y, err := geocoding("서울시 마포구 신수동 460") fmt.Println(start_x, start_y, end_x, end_y) if err != nil { log.Fatal("Geocoding API error 2: ", err) } var re []routeEntry if re, err = route(start_x, start_y, end_x, end_y, "김서준", "남정현"); err != nil { log.Fatal("Route API error: ", err) } fmt.Println(re) */ log.Print("HTTP Server start") http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { var b bytes.Buffer io.Copy(&b, r.Body) reqstr := string(b.Bytes()) log.Print("Request: ", reqstr) match := FMT_ADDR_REQUEST.FindAllStringSubmatch(reqstr, -1) startX, err := strconv.ParseFloat(match[0][1], 64) if err != nil { log.Fatal("Request Parsing(startX) error: ", err) } startY, err := strconv.ParseFloat(match[0][2], 64) if err != nil { log.Fatal("Request Parsing(startY) error: ", err) } endX, endY, err := geocoding(match[0][3]) if err != nil { log.Fatal("Geocoding API error: ", err) } log.Print(startX, startY, endX, endY) re, err := route(startX, startY, endX, endY, "시작 지점", "끝 지점") if err != nil { log.Fatal("Route API error: ", err) } var retstr string for _, r := range re { if r.LinePoints != nil { continue } retstr += strconv.FormatFloat(r.Point[0], 'f', -1, 64) + "," + strconv.FormatFloat(r.Point[1], 'f', -1, 64) + "," + strconv.FormatInt(int64(r.PointTurn), 10) + "," } fmt.Println(retstr) w.Write([]byte(retstr)) }) http.ListenAndServe(":11234", nil) }


- 서버 시연영상


제작 과정 9/7~10 - 어플리케이션 제작(김서준) & 임베디드 시스템 제작(박광렬)

1. 임베디드 시스템과 블루투스 통신 확인 테스트

- 앱 실행 화면


- 앱 프로그램

1). listpicker 를 통해서 bluetooth연결할 수 있는 목록을 불러온다.

2). 목록중 임베디드 시스템에 해당하는 블루투스 항목을 선택한다.

3). Button LEFT를 누르면 12가, Button RIGHT를 누르면 13이 블루투스 통신으로 전송된다.



-시연 영상


2. PROTO TYPE

- 앱 실행 화면


- 음성 인식 수행

1). 음성 인식 버튼을 누른다.

2). voiceRecognizer.getText 메서드가 실행되어 음성인식을 수행한다.


- 음성 인식 확인 과정

1). 음성 인식이 수행되었을때 , 음성 인식한 문자열을 화면에 표시한다.

2). 문자열을 endAddress 변수에 저장한다.

3). TextToSpeech.Speak 메서드가 실행되어 음성인식했던 문자열을 재생하여 음성인식이 제대로 되었는지 확인한다.


- 출발지 도착지 서버에 전송

1). SEND GPS 버튼을 누른다.

2). GPS로 부터 현재 위치의 경도와 위도 정보를 읽어온다.

3). web.PostText 메서드를 통해 endAddress와 startAddress를 서버에 전송한다.

- 서버에서 경로정보 전송받기

1). HTTP request가 들어왔을때 response 문자열을 data 변수에 저장한다.

2). array 배열에 data를 ','로 split하여 경로정보를 저장한다.

3). 화면에 정보를 받았다는 표시로 received 를 출력한다.


- Haversine fomular계산법


- 현재 위치와 경로좌표와의 비교

1). FIND 버튼을 누른다.

2). 현재 위치의 경도와 위도 정보를 읽어온다.

3). 회전해야하는 위치의 경.위도와 현재 위치의 경.위도를 가지고 Haversine fomula 계산법으로 두 위치 사이의 거리를 구한다.

4). 만약 두 위치 사이의 거리가 3m 이내인 경우 블루투스 통신으로 turntype정보를 보낸다.


제작 과정 9/12 (수) - 임베디드 시스템 장착

- 신발에 임베디드 시스템 장착 한 모습


- 신발을 착용한 모습


제작 과정 9/13 (목) - 

서버,어플리케이션,임베디드 시스템 작동 실험


-시연 영상

1분 31초 쯤에 정지를 누르면 RIGHT라는 LABEL이 나타났다가 사라짐을 확인할 수 있다. ( 버튼을 너무 빨리 반복해서 눌러서 잠깐 나왔다가 사라졌다.)



반응형