내가 대학교에 입학한 후 1학년인데 학생 연구원을 해볼 생각이 없는지 물어보셨었다.
나는 좋다고 한 후 들어갔는데
교수님이 연구를 위해 Tobii Eye Tracker를 빌려주셔서 그 기회에 이 프로젝트를 해보았다.
눈 제어해보기 전에 이것을 만들기 이전에 어떤 과정이 있었는지 말하면..
일단 내가 받은 아이트래커는
이거인데 Tobii Eye Tracker 5이다. 받고난 후 어떻게 할지 검색해보다가 알게 되었다..
내가 받은건 게임용이고 연구용이 아니기에 연구용 SDK가 없다는 것을..
아무리 검색해도 이 기기에서는 아이트래킹 정보를 얻을 수가 없었다.
https://developer.tobii.com/product-integration/stream-engine/
그러다가 이 것이 나와서 찾아봤는데.. 이건 일단 되는 것이다!
게임용으로 지원하는 SDK이고 이 SDK를 사용해서 아이트래킹이 지원되는 게임을 만드는 것 같다.
그래서 이걸 쓰기위해 보니 메일을 보내서 요청하라고 적혀있다.. 그래서 보냈더니..
한국어로 답변이 왔는데.. 아이트래커에 해킹을 시도하거나 다르게 이용을 하는 경우가 있어서 SDK를 못 준다는 말만 왔었다.. 개인적으로 사용할 것이라고 얘기해도 불가능이라고 했다.
그래서 그냥 포기할까 하고 한가지 생각이 들었다. 이 회사는 유명하니깐 누군가가 Github에 올려놓지 않았을까 하고..
그래서 찾아보니 진짜로 있었다. 32비트, 64비트 모두 Github에 아이트래커 프로젝트를 올리면서 같이 올려진 것들이 있다.
그래서 나는 이걸 다운받아서 시도했더니 아이트래커에 접근이 됬었던 것!
(근데 한가지 슬픈건.. 이 SDK가 C++과 C#만 가능한데.. 연구원에서.. 교수님과 나만 C++이 가능했던 것.. 그래서 이 연구와 관련된 프로그램을 제작하는건 모두 내가 맡아서 했었다.. 파이썬으로 실행할 수 있는 것이 github에 있던데.. 더 별로여 보여서 그냥 C++로 하기로 했다..)
https://developer.tobii.com/product-integration/stream-engine/tutorial_cplusplus/
아무튼 예제 코드는 여기에 있다. C++ 프로젝트로 사용하는 방법도 나와있고..
뭐 일단 이제 VRChat과 관련된 얘기를 해보겠다!
일단 VRChat에는 OSC 라는 기능이 있다.
https://docs.vrchat.com/docs/osc-overview
여기에 자세히 나와있는데 OSC를 이용하면 VRChat에서 아바타나 VRChat 기능과 소통하면서 여러가지 기능들을 제어할 수가 있는 것 같다.
예전에는 이걸로 번역기도 만들어보긴 했는데.. 굉장히 간단하게 할수가 있었다.
아무튼 저 글에서 보면
https://docs.vrchat.com/docs/osc-eye-tracking
아이트래킹을 할 수 있는 링크가 설명이 되어있는데 대충 이거다.
/tracking/eye/EyesClosedAmount
/tracking/eye/CenterPitchYaw
/tracking/eye/CenterPitchYawDist
/tracking/eye/CenterVec
/tracking/eye/CenterVecFull
/tracking/eye/LeftRightPitchYaw
/tracking/eye/LeftRightVec
(참고로 여기로 9000 포트와 함께 데이터를 보내면 OSC가 작동한다.)
일단 Stream Engine을 살펴보니 잘은 모르겠지만 내가 보고 있는 시선의 위치만 가져올 수 있는 것 같다. 뭐.. 게임 용이라 그런 것 같은데.. 아니면 내가 제대로 안 찾아본 것일 수도 있고..
아무튼 내가 보고 있는 위치만 가져올 수 있으니깐.. 사용할 수 있는 OSC는 "/tracking/eye/CenterPitchYaw" 이거다. 이거는 문서에 잘 설명 되어있다.
그리고 눈을 감았는지에 대한건 "/tracking/eye/EyesClosedAmount" 이걸 사용하면 될 것 같다.
아무튼 이걸 알았으니 이제 예제 코드를 실행해볼껀데, 그냥 실행하면 이런 오류가 난다. 아마도 내꺼가 Github에서 가져와서 버전이 낮은 것 때문에 그런 것 같은데..
// Connect to the first tracker found
tobii_device_t* device = NULL;
result = tobii_device_create(api, url, TOBII_FIELD_OF_USE_STORE_OR_TRANSFER_FALSE, &device);
assert(result == TOBII_ERROR_NO_ERROR);
여기 코드에서 TOBII_FIELD_OF_USE_STORE_OR_TRANSFER_FALSE 이게 오류가 난다.
그냥 해결 방법은 그냥 저거 빼고
사진에 나온거 2개 중에 아무거나 넣으면 된다.
그리고
이런 오류 나면.. 뭐 다르게 해결하는 방법이 있지만 빠르게 테스트 하기 위해 맨 상단에 이걸 넣는다.
#define _CRT_SECURE_NO_WARNINGS
아무튼 이러면 오류가 사라진다.
이제 실행해보면 Tobii Eye Tracker이 정상적으로 연결 되어있는 경우 작동하는데 해보면 내가 보고 있는 시선이 x, y로 나올꺼다. 그런데 내가 눈을 감거나 감지할 수 없는 영역을 쳐다보면 아무것도 출력이 안되는데.. 이걸 알 수 있다는 것은 눈을 깜박이는 것을 알 수 있다는 것이다! 확실히 알기 위해 코드를 수정했다!
void gaze_point_callback(tobii_gaze_point_t const* gaze_point, void* /* user_data */)
{
// Check that the data is valid before using it
if (gaze_point->validity == TOBII_VALIDITY_VALID)
printf("Gaze point: %f, %f\n",
gaze_point->position_xy[0],
gaze_point->position_xy[1]);
else // 추가
printf("Gaze Error\n"); // 추가
}
// 추가 라고 되어있는 것이 내가 추가한건데 그냥 인식 안되면 출력하라는거다..
이렇게 하면 정상적으로 눈을 깜으면 Gaze Error이 표시되고 감지 영역에서 눈을 움직이면 쳐다보는 곳이 잘 출력된다..!
그리고 한가지 알아둘 것이 있는데 출력되고 있는 것을 보면
Gaze point: 0.695525, 0.411705
Gaze point: 0.634280, 0.405759
Gaze point: 0.541244, 0.393794
Gaze point: 0.465725, 0.383487
Gaze point: 0.423483, 0.384978
Gaze point: 0.402698, 0.381798
Gaze point: 0.368750, 0.379669
Gaze point: 1.126799, 0.609565
Gaze point: 1.110942, 0.619803
Gaze point: 1.121417, 0.617664
Gaze point: 1.146164, 0.605248
Gaze point: 1.153657, 0.606785
Gaze point: 1.154399, 0.599591
Gaze point: 1.155324, 0.597877
이런게 있는데.. 이 숫자에 대한 것이다.
이 숫자는 그냥 x와 y의 시선 값인데 0부터 1 사이 값들은 모니터 안에 있는 것이고
1 이상들은 모니터 밖을 쳐다보고 있다는 것이다.
또한 모니터가 1920 * 1080 이면 각각에 x와 y 값을 곱해보면 내가 보고 있던 픽셀을 알수도 있다는 것이다.
이번에는 VRChat OSC에 정보를 보내는 방법에 대한 얘기인데
Python이라면 그냥 python-osc 이거 사용해서 로컬 9000 포트로 보내면 끝난다.
근데 C++이기에 OSC 라이브러리를 사용해야한다.
일단 난 그냥 간단하게 이걸 사용했다.
https://github.com/CINPLA/oscpack
아무튼 바로 코드!
#include "tobii/tobii.h"
#include "tobii/tobii_streams.h"
#include <iostream>
#include <stdio.h>
#include <assert.h>
#include <string.h>
#include <conio.h>
#include <Windows.h>
#include <typeinfo>
#include "osc/OscOutboundPacketStream.h"
#include "ip/UdpSocket.h"
#include <opencv2/opencv.hpp>
#pragma comment(lib, "Ws2_32.lib")
#define CanvasW 1920 / 4
#define CanvasH 1080 / 4
const char* ip = "127.0.0.1";
const int port = 9000;
float last_sent_value = -1.0f; // 이전에 보낸 값을 저장할 변수
bool test_mode = true; // 테스트 모드 설정
cv::Point2f current_point(0.0f, 0.0f); // 현재 점의 위치
cv::Point2f target_point(0.0f, 0.0f); // 목표 점의 위치
const float gaze_x_min = -25.0f;
const float gaze_x_max = 25.0f;
const float gaze_y_min = -15.0f;
const float gaze_y_max = 20.0f;
cv::Mat canvas(CanvasH, CanvasW, CV_8UC3, cv::Scalar(255, 255, 255)); // canvas 선언
void send_message(UdpTransmitSocket& transmitSocket, const char* address, float value) {
if (value == last_sent_value) return; // 이전에 보낸 값과 같다면 보내지 않음
char buffer[1024];
osc::OutboundPacketStream p(buffer, 1024);
p << osc::BeginMessage(address) << value << osc::EndMessage;
transmitSocket.Send(p.Data(), p.Size());
last_sent_value = value; // 보낸 값을 저장
}
void send_gaze_message(UdpTransmitSocket& transmitSocket, const char* address, float x, float y) {
char buffer[1024];
osc::OutboundPacketStream p(buffer, 1024);
p << osc::BeginMessage(address) << x << y << osc::EndMessage;
transmitSocket.Send(p.Data(), p.Size());
}
void gaze_point_callback(tobii_gaze_point_t const* gaze_point, void* user_data)
{
UdpTransmitSocket* transmitSocket = static_cast<UdpTransmitSocket*>(user_data);
if (gaze_point->validity == TOBII_VALIDITY_VALID) {
printf("Gaze point: %f, %f\n",
gaze_point->position_xy[0],
gaze_point->position_xy[1]);
float x = gaze_point->position_xy[0] * (gaze_x_max - gaze_x_min) + gaze_x_min;
float y = gaze_point->position_xy[1] * (gaze_y_max - gaze_y_min) + gaze_y_min;
send_gaze_message(*transmitSocket, "/tracking/eye/CenterPitchYaw", y, x);
send_message(*transmitSocket, "/tracking/eye/EyesClosedAmount", 0.0f);
if (test_mode) {
target_point = cv::Point2f(gaze_point->position_xy[0] * CanvasW, gaze_point->position_xy[1] * CanvasH);
current_point = target_point;
cv::circle(canvas, current_point, 3, cv::Scalar(255, 0, 0), -1); // 파란색으로 변경
}
}
else {
std::cout << "Invalid" << std::endl;
send_message(*transmitSocket, "/tracking/eye/EyesClosedAmount", 1.0f);
if (test_mode) {
current_point = target_point;
cv::circle(canvas, current_point, 3, cv::Scalar(0, 0, 255), -1); // 빨간색으로 변경
}
}
}
void url_receiver(char const* url, void* user_data)
{
char* buffer = (char*)user_data;
if (*buffer != '\0') return; // only keep first value
if (strlen(url) < 256)
strcpy_s(buffer, 256, url); // strcpy_s로 변경
}
int main()
{
// Create API
tobii_api_t* api = NULL;
tobii_error_t result = tobii_api_create(&api, NULL, NULL);
assert(result == TOBII_ERROR_NO_ERROR);
// Enumerate devices to find connected eye trackers, keep the first
char url[256] = { 0 };
result = tobii_enumerate_local_device_urls(api, url_receiver, url);
assert(result == TOBII_ERROR_NO_ERROR);
if (*url == '\0')
{
printf("Error: No device found\n");
//return 1;
}
// Connect to the first tracker found
tobii_device_t* device = NULL;
result = tobii_device_create(api, url, TOBII_FIELD_OF_USE_ANALYTICAL, &device);
std::cout << typeid(result).name() << std::endl;
assert(result == TOBII_ERROR_NO_ERROR);
// Subscribe to gaze data
UdpTransmitSocket transmitSocket(IpEndpointName(ip, port));
result = tobii_gaze_point_subscribe(device, gaze_point_callback, &transmitSocket);
assert(result == TOBII_ERROR_NO_ERROR);
// Initialize socket and server address
while (true) {
// Optionally block this thread until data is available. Especially useful if running in a separate thread.
result = tobii_wait_for_callbacks(1, &device);
assert(result == TOBII_ERROR_NO_ERROR || result == TOBII_ERROR_TIMED_OUT);
// Process callbacks on this thread if data is available
result = tobii_device_process_callbacks(device);
assert(result == TOBII_ERROR_NO_ERROR);
if (test_mode) {
cv::imshow("Gaze Point", canvas);
cv::waitKey(1);
canvas.setTo(cv::Scalar(255, 255, 255));
}
if (_kbhit()) {
int ch = _getch();
if (ch == 27) // ESC Key
break;
}
}
// Cleanup
result = tobii_gaze_point_unsubscribe(device);
assert(result == TOBII_ERROR_NO_ERROR);
result = tobii_device_destroy(device);
assert(result == TOBII_ERROR_NO_ERROR);
result = tobii_api_destroy(api);
assert(result == TOBII_ERROR_NO_ERROR);
return 0;
}
일단 이건 내가 그냥 간단하게 테스트만 해볼려고 만든 코드이다.
그래서 코드가 더러운 것도 있고 굳이 필요 없는 기능이 있기도 하다.
뭐 그래도 대충 이 코드를 실행하면 정상적으로 눈 제어가 가능한 아바타에서는 작동한다!!
그리고 코드에 보면 캔버스 설정한게 있는데 그냥 그건 OpenCV로 어디를 보고 있는지 표시하기 위해 놔둔 것이다. test_mode 끄면 나오지 않는다.
const float gaze_x_min = -25.0f;
const float gaze_x_max = 25.0f;
const float gaze_y_min = -15.0f;
const float gaze_y_max = 20.0f;
그리고 이런 코드가 있는데.. 이 코드는 VRChat OSC로 데이터를 보낼 때 데이터 범위이다.
각 아바타 마다 데이터의 범위가 정해져있는 것 같다. 일단 내가 사용하는 아바타는 일일이 값을 보내서 테스트 해본 결과 저정도 인 것 같다.
아무튼 이렇게 하면 눈 제어와 눈 깜박이는 것을 모두 아이트래커로 가능하다! 심지어 놀라운건 나는 아이트래킹은 VR 유저만 가능한 줄 알았는데 데스크탑에서도 가능하다는 것..
이게 테스트 영상!
아무튼 끝!
'개발 > C++' 카테고리의 다른 글
C/C++ OpenCV로 배경 중앙에 이미지 넣어보기 (0) | 2023.02.10 |
---|---|
C++ OpenCV 영어 말고 한국어 출력하기 +안티에일리어싱 (WINDOWS만) (2) | 2022.12.17 |
C++ 그래픽으로 푸리에 급수의 뭔가를 그려보기 (0) | 2022.09.14 |
프로그래머스 (PROGRAMMERS) 1단계 스킬 체크 통과 (0) | 2022.08.21 |
YOLO V5를 C++로 돌려보았다! (1) | 2022.07.29 |