반응형

내가 대학교에 입학한 후 1학년인데 학생 연구원을 해볼 생각이 없는지 물어보셨었다.

나는 좋다고 한 후 들어갔는데

교수님이 연구를 위해 Tobii Eye Tracker를 빌려주셔서 그 기회에 이 프로젝트를 해보았다.


눈 제어해보기 전에 이것을 만들기 이전에 어떤 과정이 있었는지 말하면..

 

일단 내가 받은 아이트래커는 

이거인데 Tobii Eye Tracker 5이다. 받고난 후 어떻게 할지 검색해보다가 알게 되었다..

내가 받은건 게임용이고 연구용이 아니기에 연구용 SDK가 없다는 것을..

 

아무리 검색해도 이 기기에서는 아이트래킹 정보를 얻을 수가 없었다.

https://developer.tobii.com/product-integration/stream-engine/

 

Stream Engine - Tobii Developer Zone

The Stream Engine SDK is a low-level SDK intended for advanced users wanting to have tight control over the system resources used by Stream Engine and least amount of signal latency. To get started with the StreamEngine SDK, head over to the getting starte

developer.tobii.com

그러다가 이 것이 나와서 찾아봤는데.. 이건 일단 되는 것이다!

게임용으로 지원하는 SDK이고 이 SDK를 사용해서 아이트래킹이 지원되는 게임을 만드는 것 같다.

 

그래서 이걸 쓰기위해 보니 메일을 보내서 요청하라고 적혀있다.. 그래서 보냈더니..

한국어로 답변이 왔는데.. 아이트래커에 해킹을 시도하거나 다르게 이용을 하는 경우가 있어서 SDK를 못 준다는 말만 왔었다.. 개인적으로 사용할 것이라고 얘기해도 불가능이라고 했다.

 

그래서 그냥 포기할까 하고 한가지 생각이 들었다. 이 회사는 유명하니깐 누군가가 Github에 올려놓지 않았을까 하고..

그래서 찾아보니 진짜로 있었다. 32비트, 64비트 모두 Github에 아이트래커 프로젝트를 올리면서 같이 올려진 것들이 있다.

그래서 나는 이걸 다운받아서 시도했더니 아이트래커에 접근이 됬었던 것!

 

(근데 한가지 슬픈건.. 이 SDK가 C++과 C#만 가능한데.. 연구원에서.. 교수님과 나만 C++이 가능했던 것.. 그래서 이 연구와 관련된 프로그램을 제작하는건 모두 내가 맡아서 했었다.. 파이썬으로 실행할 수 있는 것이 github에 있던데.. 더 별로여 보여서 그냥 C++로 하기로 했다..)

 

https://developer.tobii.com/product-integration/stream-engine/tutorial_cplusplus/

 

Stream Engine Tutorial - C++ - Tobii Developer Zone

This policy applies to solutions that store or transfer Tobii eye tracking data, presence or position data, regardless of the purpose for the storing or transferring. End-users care about their data integrity and privacy. Therefore, as a vendor you must ga

developer.tobii.com

아무튼 예제 코드는 여기에 있다. C++ 프로젝트로 사용하는 방법도 나와있고..

뭐 일단 이제 VRChat과 관련된 얘기를 해보겠다!

일단 VRChat에는 OSC 라는 기능이 있다.

https://docs.vrchat.com/docs/osc-overview

 

OSC Overview

Intro to OSC OSC is a way to get different devices and applications to talk to each other. It's a favorite method of creative coders and people making weird interactive things because it's fast, networked, and very open-ended. What does this have to do wit

docs.vrchat.com

여기에 자세히 나와있는데 OSC를 이용하면 VRChat에서 아바타나 VRChat 기능과 소통하면서 여러가지 기능들을 제어할 수가 있는 것 같다.

예전에는 이걸로 번역기도 만들어보긴 했는데.. 굉장히 간단하게 할수가 있었다.

 

아무튼 저 글에서 보면 

https://docs.vrchat.com/docs/osc-eye-tracking

 

OSC Eye Tracking

VRChat now offers support for receiving eye tracking data (eyelook and eyelid) via OSC. Please note: This is an advanced feature! It is NOT plug-and-play. You must create your own program to transmit this data to VRChat using OSC. Hardware manufacturers ma

docs.vrchat.com

 

 

아이트래킹을 할 수 있는 링크가 설명이 되어있는데 대충 이거다.

/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

 

GitHub - CINPLA/oscpack: Automatically exported from code.google.com/p/oscpack

Automatically exported from code.google.com/p/oscpack - CINPLA/oscpack

github.com

 

아무튼 바로 코드!

#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 유저만 가능한 줄 알았는데 데스크탑에서도 가능하다는 것..

이게 테스트 영상!

 

아무튼 끝!

반응형
반응형

이제 선택으로 제어되도록 해야한다!!

이건 수학적 지식이 전혀 필요없어서 엄청 간단했다..!

 

자 일단 내가 구상하고 있는건 두번째 끝점을 클릭한 상태로 드래그 하면 IK로 움직이는 것이고, IK의 중간 점을 눌르면 방향이 반대로 되서 IK가 다시 계산되도록 하는 것이다..!

 

이걸 구현할려면 버튼이 클릭됬는지 안됬는지를 감지해야한다..

근데 그냥 버튼이 클릭됬는지 안됬는지를 감지하면 안된다. 이게 문제가.. 버튼을 눌렀는지 감지하는 방식이 2가지 정도가 있는데.. 마우스의 위치를 감지하면서 마우스의 위치가 내가 클릭할려는 요소 위에 있는지 감지하는 방법이 있고.. 요소 자체를 클릭했는지 뗐는지를 감지하도록 하는 방법이 있다..

근데 첫번째 방법의 경우는 마우스 아래 그 버튼이 있는지 계산하는게 알고리즘이 필요해서 구현하기는 힘들어서 넘어가고.. 그 요소를 클릭했는지 뗐는지를 감지하도록 해야한다..

 

근데 여기에서 한가지 문제가 생긴다.

끝점을 제어할 것이기 때문에 끝점을 눌르는 것까지는 문제가 안된다..

문제가 되는건 뗄때 인데.. 내가 끝점을 클릭하고 드래그 할땐 IK 계산이 되다가 마우스 클릭을 떼면 IK 계산이 풀리도록 해서 되게 하는 방식인데 요소를 클릭했는지 뗐는지 감지하는건 마우스가 요소 그 위에 있을 때만 감지가 가능하다..

 

그래서 감지를 할려면 항상 어떤 요소가 끝 점을 따라다니다가 마우스로 클릭하면 마우스를 따라오다가 뗐는지 감지하면 다시 끝점으로 돌아가도록 하면서 감지해야한다. (끝점을 따라가야하는 이유는 그 요소에만 마우스 감지 기능을 넣을 것이기 때문이다.. 그래서 끝점을 안따라다니면.. 다시 움직일 때는 그 요소의 위치에 마우스를 두고 클릭해야 움직이게 할 수 있다..)

그러기 위해선 새로운 요소를 만들어야 한다. 끝점과 똑같은걸로

 

일단 joint_2_mouse_point로 만들었는데 이것에 대해 클릭과 클릭이 떼지는걸 감지해서 다른 함수로 넘겨주는 코드를 적어준다. 아래 코드는 InitJoint 함수를 실행하면서 실행되도록 했다.

joint_2_mouse_point.PointerPressed({ this, &MainPage::JointControlJoint2PointerPressed }); // 조인트 2의 포인트를 클릭 했는지
joint_2_mouse_point.PointerReleased({ this, &MainPage::JointControlJoint2PointerReleased }); // 조인트 2의 포인트를 클릭 안했는지

그리고 항상 끝점을 따라가도록 UpdatePos를 실행할 때 마다 팔의 끝점으로 위치를 바꾸도록 했다.

void MainPage::UpdatePos() {
    // 파란색 끝 포인트의 위치 변경
    ChangePosPoint(joint_2_point, ControlJoint(), joint_2_point_xy);
    ChangePosLine(joint_2_line, joint_2_point_xy);

    // 파란색 끝 마우스 클릭 감지 포인트의 위치 변경
    ChangePosPoint(joint_2_mouse_point, ControlJoint(), joint_control_mouse_point_pos); // // 마우스 포인터를 따라가게

    // 파란색의 첫 포인트 위치 변경
    ChangePosLine(joint_2_line, joint_1_point_xy, false);

    // 빨간색의 끝 포인트 위치 변경
    ChangePosPoint(joint_1_point, ControlJoint(), joint_1_point_xy);
    ChangePosLine(joint_1_line, joint_1_point_xy);
}

 

그 후엔 그 요소가 클릭됬는지 클릭에서 뗐는지를 감지할 때 마다 어떤 함수를 실행하니 그 함수를 만들어야 한다.

일단 그 함수에서는 마우스가 클릭되었는지 안되었는지 확인을 한다. 이유는 드래그를 하고 있는지 없는지를 판단해야하기 때문이다. 그리고 마우스를 뗐을 때 UpdatePos 함수를 한번 실행하는데.. 이것의 이유는 이후에 알 수 있다. (주석에 나와있긴 하다)

 

일단 코드는!

void MainPage::JointControlJoint2PointerPressed(IInspectable const& sender, Xaml::Input::PointerRoutedEventArgs const& e) {
    is_joint_2_point_click = true; // 마우스가 클릭 되었다면
}

void MainPage::JointControlJoint2PointerReleased(IInspectable const& sender, Xaml::Input::PointerRoutedEventArgs const& e) {
    is_joint_2_point_click = false; // 마우스가 클릭 되지 않았다면

    UpdatePos(); // 마우스를 떼고 마우스를 움직여야 돌아가기 때문에.. 이걸 방지하기 위해 미리 한번 업데이트 시킴
}

이제 이렇게 한 후.. 마우스가 드래그 되고 있을 때 IK가 계산되도록 해야하기 때문에 캔버스에서 마우스의 움직임을 가져오는 함수에서 수정을 해야한다.

 

여기에서도 간단하게, 마우스가 드래그 되고 있을 때만 IK를 계산하고 업데이트 시켜야 하니 그냥 if문으로 is_joint_2_point_click이 true일 때만 되도록 했다.

void MainPage::JointControlCanvasPointerMoved(IInspectable const& sender, Xaml::Input::PointerRoutedEventArgs const& e) {
    Input::PointerPoint point = e.GetCurrentPoint(ControlJoint());
    joint_control_mouse_point_pos = { point.Position().X, point.Position().Y };

    Point_X().Text(to_hstring(joint_control_mouse_point_pos.x));
    Point_Y().Text(to_hstring(joint_control_mouse_point_pos.y));

    // 마우스 포인터가 클릭 되었을 때만 계산하고 업데이트 하도록
    if (is_joint_2_point_click) { 
        CalIKPos(joint_control_mouse_point_pos);
        UpdatePos();
    }
}

근데 이걸로 되긴 한다.. 근데 한가지 문제가 발생한다.. 마우스를 드래그 하는 것 까지는 좋은데.. 

아까 위에서 말한 것 처럼 마우스에서 클릭을 뗐을 때 다시 끝점으로 안돌아간다.. 이렇게 되면 그 요소만 클릭할 수 있어서 그 요소를 클릭해야지만 움직이게 할 수 있다.. 그래서 이걸 위해 끝점이 클릭 상태가 아니라면 끝점을 따라가고 있게 바꿨다. 그래서 코드는 이렇게!

void MainPage::UpdatePos() {
    // 파란색 끝 포인트의 위치 변경
    ChangePosPoint(joint_2_point, ControlJoint(), joint_2_point_xy);
    ChangePosLine(joint_2_line, joint_2_point_xy);

    // 파란색 끝 마우스 클릭 감지 포인트의 위치 변경
    if(!is_joint_2_point_click) // 끝점이 클릭되지 않았다면
        ChangePosPoint(joint_2_mouse_point, ControlJoint(), joint_2_point_xy); // 끝점을 따라가고 있게
    else // 끝점이 클릭되고 있다면
        ChangePosPoint(joint_2_mouse_point, ControlJoint(), joint_control_mouse_point_pos); // 마우스 포인터를 따라가게

    // 파란색의 첫 포인트 위치 변경
    ChangePosLine(joint_2_line, joint_1_point_xy, false);

    // 빨간색의 끝 포인트 위치 변경
    ChangePosPoint(joint_1_point, ControlJoint(), joint_1_point_xy);
    ChangePosLine(joint_1_line, joint_1_point_xy);
}

if else로 간단하게 해결할 수 있었다. if에서 처음을 false로 해서 한 이유는 일단 형식상으론 마우스의 끝점이 클릭되고 있는건 특별 경우라서.. 그렇게 했다. 

 

암튼 이걸로 클릭하면 아래처럼 위치가 바뀐다..! (마우스 따라올 때 흰색 점도 같이 따라오는건.. 확인하기 위해 한 것이고 나중엔 투명하게 바꿀 것이다.)

아주 잘 작동된다..!

 

이제 구현할 부분은 중간 점을 눌르면 반대의 각도로 바뀌는 것이다.. 어쨋든 내가 한 방식으로 한다면..

일단 아까처럼 InitJoint 함수에 2가지 클릭하는 것과 떼는 것 2가지를 추가한다.

joint_1_point.PointerPressed({ this, &MainPage::JointControlJoint1PointerPressed }); // 조인트 1의 포인트를 클릭 했는지
joint_1_point.PointerReleased({ this, &MainPage::JointControlJoint1PointerReleased }); // 조인트 1의 포인트를 클릭 안했는지

이런식으로 추가한 후에는 각 함수를 만드는데

어떤 식으로 마우스로 클릭했는지 감지하는 것이 문제이다..

방법이라면 그 자리에서 마우스를 떼는 것을 감지했을 때 바뀌도록 하는 방법이 있지만.. 이것의 문제는 다른 곳에서 마우스를 클릭하고 그곳에서 떼면 이것도 감지가 된다..

그리고 마우스를 그곳에서 클릭했을 때 감지되는건.. 또 너무 빠르다.. 왜냐면 잘 못 눌렀을 때 눌른 상태로 드래그 하면 선택이 안되는걸 원해서..

 

암튼 그래서 내가 원하는건 버튼에서 마우스를 클릭하고 그 버튼에서 마우스를 떼었을 때를 감지해서 일을 처리하도록 하는 것이다.

 

이걸 구현하는건 간단하다. 눌렀을 때 A변수를 true로 바꿔주고 떼는 것을 감지하는면 A변수가 true인지 확인하고 true이라면 함수를 실행하고 A변수를 false로 바꿔주면 된다..!

 

그렇기에 바로 구현!

void MainPage::JointControlJoint1PointerPressed(IInspectable const& sender, Xaml::Input::PointerRoutedEventArgs const& e) {
    is_joint_1_point_click = true; // 마우스가 클릭 되었다면
}

void MainPage::JointControlJoint1PointerReleased(IInspectable const& sender, Xaml::Input::PointerRoutedEventArgs const& e) {
    // 중간점을 클릭했을 경우 중간의 각도를 반대로 바꿔서 계산하게 해야함으로 그 버튼만 클릭하고 떼었는지 확인할려고 if문 함
    // 확인이 되면 마지막 포인트로 다시 IK를 계산하고 위치를 업데이트 시키고 다시 클릭을 뗀걸로 인식하도록 바꾼거
    if (is_joint_1_point_click) {
        joint_angle_reverse = !joint_angle_reverse; // 각도를 바꾸면 그 뒤로도 고정되서 계산되게 해야함으로 !로 true false가 서로 토글로 바뀌도록
        CalIKPos(joint_2_point_xy); 
        UpdatePos();
        is_joint_1_point_click = false; // 마우스가 클릭 되지 않았다면
    }
}

근데 여기에서 joint_angle_reverse 라는 것이 보인다. 이것의 경우 현재 중간 점의 각도가 반대로 계산할지 정상으로 계산할지 하는 변수로 했는데.. 

반대로 계산하는건 IK를 계산하는 부분에서 acos 앞에 -만 붙여서 계산하는 것이기 때문에 이후에도 계속 고정으로 계산되게 하기 위해서 필요한 변수이다. 물론 IK를 계산하는 함수에 집어넣는 방식으로 할 수도 있지만.. 내가 생각하는 형식상 맞지도 않고 굳이 또 넣으면 함수를 수정해야하기 때문이다..

 

그래서 저 변수로 각도가 조정되게 하고, 여기에서 처럼 토글을 구현할 경우에는 bool 값에는 !를 붙이면 된다. 

이유는 !는 반대로 만들어주는건데 !true -> false이고 !false -> true로 되기 때문에 앞에 !로 붙이는 것이다.

 

그 후에 CalIKPos와 UpdatePos를 붙여주는 이유는 CalIKPos와 UpdatePos가 구동되는 조건은 마우스가 움직일 때만이다. 근데 이건 클릭된 것을 감지하면 바로 바뀌어야 하는데.. 안되기 때문에 저기에도 추가해서 바뀌도록 하는 것이다.

 

암튼 이렇게 바꾼 후에는 CalIKPos에서도 IK 계산을 바꿔야 하는데 어차피 joint_angle_reverse 변수로 알 수 있기 때문에 이걸로 판단할 수 있도록 했다!

theta2 = acos((pow(mouse_x, 2) + pow(mouse_y, 2) - pow(a, 2) - pow(b, 2)) / (2.0 * a * b));

if (joint_angle_reverse) // 중간 점의 각도를 반대로 돌리는 것
    theta2 = -theta2;

이런식으로

원래 생각은 if와 else로 theta2 구하는 공식을 acos와 -acos 2개로 만들려고 했다. 

근데 그냥 생각해보니 어차피 -만 붙이는거라서 구한 값에 -를 붙일지 말지로 쉽게 구현할 수 있어서 그걸로 바꿨다..!

 

근데.. 해보니깐 잘되긴 한다.. 근데 한가지 문제가 생겼다.. 중간 점을 클릭하면 바뀌긴 하는데.. theta2 값을 확인하니 바뀔 때마다 약간의 오차가 발생한다.. 이 경우는 수학적이라서.. 어차피 계산에는 큰 영향을 안줄 것 같아서 놔두긴 했는데.. theta2값이 완전이 .000000으로 떨어지는 경우가 있다. 그곳에서 중간점을 눌르거나 하면.. 그냥 값이 계산이 안되고 고장나버린다.. 그래서 이유를 못 찾고 있다가..

옆에 있는 나보다 수학 잘하는 친구(이 문제도 그 친구가 테스트로 가지고 돌려보다가 발견한거..)가 무슨 이유인지 찾아볼려다가 내가 삼각함수에서 계산이 안되면 이런 문제가 일어난다고 하고 atan2 부분에서 문제가 일어나는 것 같다고 하니깐.. atan2에서 분모가 0이 될 수 없는데.. 0이 되버리면.. 무한의 값.. 뭐라면서 알려줬다.. 그래서 바로 나도 생각났다.. 이전에 a + b가 c보다 같거나 작을 때 계산한다고 했었는데.. 이걸 하기전에 본건.. 같거나 일 경우가 없었긴 했다.. 그래서 원래는 c <= a + b가 조건이고 이게 참일 때마다 IK를 계산하도록 했는데 이걸 c < a + b로 바꾸니.. 성공적으로 해결이 되었다..!  

 

그렇게 되어서 CalIKPos의 전체코드는 이렇다..!

void MainPage::CalIKPos(float2 point) {
    float a = joint_1_line_size; // 빨간색 라인의 길이
    float b = joint_2_line_size; // 파란색 라인의 길이
    float c = distance(center_point_xy, point); // 첫점과 끝점을 이은 길이

    // https://youtu.be/IKOGwoJ2HLk
    // x2, y2는 끝점 x1, y1은 중간점 x0, y0은 센터점

    // center_point_xy의 x와 y로 빼는 이유는 IK를 구현하는 곳이 250, 250인 중앙이여서 이걸 0, 0으로 옮기기 위해
    float mouse_x = point.x - center_point_xy.x;
    float mouse_y = point.y - center_point_xy.y;

    float theta1 = 0, theta2 = 0;
    float x2 = 0, y2 = 0;
    // a와 b의 최대 길이는 a + b가 됨, 그리고 c는 빗변이 되고 a + b를 넘으면 안됨.. 넘으면 삼각함수로 계산이 불가함, 그렇기에 미리 맞는지 확인하는 것
    if (c < a + b) { // 원래는 c <= a + b로 했는데.. 이렇게 할 경우 tan으로 구할 때 각도가 둘다 90도일 경우 터짐.. 그래서 그걸 막기 위해 완전히 같은 것도 막음
        // IK를 구현하기 위한 각도 계산
        theta2 = acos((pow(mouse_x, 2) + pow(mouse_y, 2) - pow(a, 2) - pow(b, 2)) / (2.0 * a * b));

        if (joint_angle_reverse) // 중간 점의 각도를 반대로 돌리는 것
            theta2 = -theta2;

        theta1 = atan2(mouse_y, mouse_x) - atan2(b * sin(theta2), a + b * cos(theta2));

        // 위에서 theta1구할 때 theta1 + B에서 B를 뺐을 때 그 B 값을 theta1으로 사용해서 밑변과 높이를 구할 수 있긴 하지만.. 
        // 굳이 필요 없는 계산을 해야하기도 하고.. 굳이 할 필요가 없기 때문에 그냥 마우스 포인트로 되게 함
        x2 = mouse_x + center_point_xy.x;
        y2 = mouse_y + center_point_xy.y;
    }
    else {
        // 마우스 포인트와 선을 이어서 삼각형을 만들고 theta1의 각도를 얻어서 
        // 마우스 포인트와 선을 이은거에서 중간점은 구하는 방식이 같기 때문에 넘기고, 끝점을 구하는게 목적이기 때문에
        // 끝점을 구하기 위해 쫙 폈을 때 a + b이기 때문에 그걸로 밑변과 높이를 구하는거
        theta1 = atan2(mouse_y, mouse_x);

        x2 = (a + b) * cos(theta1) + center_point_xy.x;
        y2 = (a + b) * sin(theta1) + center_point_xy.y;
    }

    // 중간 점의 x와 y를 cos, sin으로 구하는 것
    float x1 = a * cos(theta1) + center_point_xy.x; // x, y를 더한 이유, 위에서 x, y에 빼줬었으니 다시 더하는 것
    float y1 = a * sin(theta1) + center_point_xy.y;

    joint_1_point_xy = { x1, y1 };
    joint_2_point_xy = { x2, y2 };

    char debug[512];
    sprintf_s(debug, "theta1 = %f, theta2 = %f", theta1 * 180.0 / M_PI, theta2 * 180.0 / M_PI);
    debugText2().Text(to_hstring(debug));
}

 

그래서 이렇게 해서 IK 시뮬레이션 부분에선 각도 제한하는거 빼곤 전부 완성시켰다..!

그래서 결과물은 이렇다..!

 

암튼 끄읕..!

내 옆에 있던 친구는.. 내가 만드는거 자랑할 때마다.. 긍정적으로 봐주는 친구인데..! 매번 테스트할 때마다 짜잔하면 신기한건지.. 몇분을 움직여보기도 한다.. (오류를 찾아낼려고 하는 것일 수도 있지만..) 암튼.. IK를 고민할 때도 계속 도와줄려고 했었기도 했다..! 내년엔.. 다른 대학교로 간다는데.. 좀 아쉽긴 하다..

반응형
반응형

이전 글에서 말했 듯이 계산 범위를 넘어갔을 때 else로 처리한다고 했던 것에 대한 내용이다.

 

내가 생각해본 이론 설명

자세하게 말한다면 IK를 구현할려면 a, b, c가 필요하다.

a가 첫번째 팔 길이, b가 두번째 팔 길이이고 c가 a와 b의 삼각형에서 빗변이다.

사진으로 보면 이렇다.

근데.. 여기에서 마우스 포인터의 x, y와 센터점까지 길이가 c가 될 수 있는데 a와 b가 정해져 있으므로 c가 a + b를 벗어나면 삼각함수에서 계산이 전혀 되지가 않는다. 아마 코딩하면 nan(ind)인가 이걸로 값이 뜰 것이다..

 

그래서 이 경우 내가 생각한 해결 방법으론 c가 a + b를 넘어갔을 때 그냥 a와 b를 쫙 펼치고 삼각함수로 하든 어떻게든 해서 마우스쪽의 방향으로 IK를 돌려버리는 것을 생각했다..

 

그래서 일단 다시 샤프를 잡고 쫙 펴졌을 때를 그린 후에 삼각형을 찾아봤다.

그러더니.. 한가지 나왔다..

일단 빨간색 점의 경우 마우스 포인터의 위치이다. 

그래서 마우스 포인터의 위치로 직각삼각형을 그릴 수 있다..! 이걸 이용하면 밑변과 높이인 x, y를 구할 수 있다는 것..!

어쨋든 이전 글에서 θ1을 구할 때

float theta1 = atan2(mouse_y, mouse_x) - atan2(b * sin(theta2), a + b * cos(theta2));

 이런 공식을 썼다. 근데 이 공식이 θ1 + B에서 B인 atan2(b * sin(theta2), a + b * cos(theta2)) 이걸 그냥 뺀거다. 

그렇기 때문에 θ1 + B가 필요한 각도이다..!

 

그래서 그냥 theta1 = atan2(mouse_y, mouse_x)를 가져다 썼다.

그럼 이걸로 θ1을 구했기 때문에 이걸로 밑변과 높이를 구할 수 있게 된다.

 

근데 여기에서 필요한건 마우스 포인터의 위치가 아니라 a의 끝점과 b의 끝점의 x, y 좌표가 필요하다.

그래서 이 x, y 좌표를 구하기 위해 또 삼각형을 찾아보면

이런식으로 직각삼각형을 또 그릴 수 있게 된다. (일단 θ1 + B는 설명하기 쉽게 θ1을 바꿨다)

그런데 이전에서 θ1을 구했기 때문에 a 부분의 삼각형과 b부분의 삼각형의 밑변과 높이를 구할 수 있게 된다..!

 

계속 말하고 있지만..

밑변을 구하는건 빗변 * cos(θ1)이고 높이는 빗변 * sin(θ1)이기 때문에 각 삼각형의 밑변 높이를 구한다면??

a의 밑변과 높이를 구할 경우 a * cos(θ1), a * sin(θ1)으로 구할 수 있고 b의 밑변과 높이를 구할 경우 어차피 쫙 펴져있는 경우에만 구하는거라서 (a + b) * cos(θ1), (a + b) * sin(θ1)으로 구할 수 있게 된다.

 

어쨋든 밑변은 x 높이는 y로 되기 때문에 이걸 C++로 구현해보면

float theta1 = atan2(mouse_y, mouse_x);

float x2 = (a + b) * cos(theta1);
float y2 = (a + b) * sin(theta1);

이렇게 된다. x2, y2로 한 이유는 두번째 팔의 끝점을 구하는 거기 때문에.. 첫번째 팔의 끝점은 x1, y1..

 

이론 적용

암튼 여기에서도 계산했던 곳이 센터가 0, 0일 경우일 때 구한 것이기 때문에 각 x2, y2에 중앙으로 갈 수 있게 값을 더 해준다!

float mouse_x = point.x - center_point_xy.x;
float mouse_y = point.y - center_point_xy.y;

float theta1 = atan2(mouse_y, mouse_x);

float x2 = (a + b) * cos(theta1) + center_point_xy.x;
float y2 = (a + b) * sin(theta1) + center_point_xy.y;

그럼 이렇게 되면 각 필요한 x, y좌표가 구해진다..!

 

이론 적용 및 코드 수정

근데 이제 여기에서 이전 글에서 한 코드에 그대로 넣으면.. 코드에서 중복 코드가 많아진다.. 그래서 그 중복 코드는 없애고 고친결과..! 이렇게 완성되었다!

void MainPage::CalIKPos(float2 point) {
    float a = joint_1_line_size; // 빨간색 라인의 길이
    float b = joint_2_line_size; // 파란색 라인의 길이
    float c = distance(center_point_xy, point); // 첫점과 끝점을 이은 길이

    // https://youtu.be/IKOGwoJ2HLk
    // x2, y2는 끝점 x1, y1은 중간점 x0, y0은 센터점

    // center_point_xy의 x와 y로 빼는 이유는 IK를 구현하는 곳이 250, 250인 중앙이여서 이걸 0, 0으로 옮기기 위해
    float mouse_x = point.x - center_point_xy.x;
    float mouse_y = point.y - center_point_xy.y;

    float theta1 = 0, theta2 = 0;
    float x2 = 0, y2 = 0;
    // a와 b의 최대 길이는 a + b가 됨, 그리고 c는 빗변이 되고 a + b를 넘으면 안됨.. 넘으면 삼각함수로 계산이 불가함, 그렇기에 미리 맞는지 확인하는 것
    if (c <= a + b) {
        // IK를 구현하기 위한 각도 계산
        theta2 = -acos((pow(mouse_x, 2) + pow(mouse_y, 2) - pow(a, 2) - pow(b, 2)) / (2.0 * a * b));
        theta1 = atan2(mouse_y, mouse_x) - atan2(b * sin(theta2), a + b * cos(theta2));

        // 위에서 theta1구할 때 theta1 + B에서 B를 뺐을 때 그 B 값을 theta1으로 사용해서 밑변과 높이를 구할 수 있긴 하지만.. 
        // 굳이 필요 없는 계산을 해야하기도 하고.. 굳이 할 필요가 없기 때문에 그냥 마우스 포인트로 되게 함
        x2 = mouse_x + center_point_xy.x;
        y2 = mouse_y + center_point_xy.y;
    }
    else {
        // 마우스 포인트와 선을 이어서 삼각형을 만들고 theta1의 각도를 얻어서 
        // 마우스 포인트와 선을 이은거에서 중간점은 구하는 방식이 같기 때문에 넘기고, 끝점을 구하는게 목적이기 때문에
        // 끝점을 구하기 위해 쫙 폈을 때 a + b이기 때문에 그걸로 밑변과 높이를 구하는거
        theta1 = atan2(mouse_y, mouse_x);

        x2 = (a + b) * cos(theta1) + center_point_xy.x;
        y2 = (a + b) * sin(theta1) + center_point_xy.y;
    }

    // 중간 점의 x와 y를 cos, sin으로 구하는 것
    float x1 = a * cos(theta1) + center_point_xy.x; // x, y를 더한 이유, 위에서 x, y에 빼줬었으니 다시 더하는 것
    float y1 = a * sin(theta1) + center_point_xy.y;

    joint_1_point_xy = { x1, y1 };
    joint_2_point_xy = { x2, y2 };

    char debug[512];
    sprintf_s(debug, "theta1 = %.2f, theta2 = %.2f", theta1 * 180.0 / M_PI, theta2 * 180.0 / M_PI);
    debugText2().Text(to_hstring(debug));
}

저기 주석에서도 말하는 것 처럼 c <= a + b가 성립이 될 때 그냥 마우스 포인터로 하는 이유는 θ1 + B를 구해서 할 순 있긴 하지만.. 이 경우 새로운 변수도 만들어야 하고.. 굳이 계산을 해서 필요없는 계산을 하게 하기 싫어서가 이유이기도 하다..

 

결과

그래서 이걸로 된 결과물은 이렇다!!

엄청 잘 된다!!!

 

2일 동안의 공부는 잘한 것 같다!

이거는.. 이전 글 쓰고.. 바로 계산해서 1시간 안에 찾고 고친걸 적고 있는건데.. 물론 간단한 식인건 맞지만.. 빠른 속도로 1시간 안에 이걸 구하고 바로 적용한게.. 2일동안 허무한 공부를 한 보람이 있는 것 같다..! 만약 그 공부가 없었다면 이걸 1시간 안에 해내지 못했을 것이다.. 왜냐면.. 공부를 하기전에는.. 라인을 그려서 유클리드 거리로.. 어떻게든 해서 두번째 x, y를 구해볼까 생각했었는데.. 지금 생각해보니.. 만약 그렇게 했다면 각도를 알 수 없어서 두번째 x, y를 전혀 구할 수 없었을 것이다.. 뭐.. 다른 방법은 있겠지만.. 내가 생각한 방법도 맞는 것 같다..! 

 

계속 하면서 삼각형은 진짜.. 대단한 도형이라고 생각된다.. 삼각형으로 내가 필요한 대부분의 것을 구현하거나 얻을 수 있다는 것이.. 진짜 너무나도 신기하다..

 

암튼 까먹지 않을려고 구하고 적용하자마자 쓰다보니.. 새벽 1시 25분.. 바로 자야겠다..

 

암튼 끝!!

반응형
반응형

이전글에서는 아쉽게도.. Inverse Kinematics를 구현하기에.. 실패했다..

반은 되고 반은 안되고.. 암튼 그래서.. 유튜브를 찾아봤는데.. 

잘 정리된 영상이 있었고 식도 모두 있었다..!

 

식 찾기

https://youtu.be/IKOGwoJ2HLk

여기에서 식을 찾았는데 

이 식이다.

또한 팔을 반대로 접을 경우에는 q2에 cos 앞에 -만 붙이면 중간의 팔 각도가 반대로 바뀌어서 계산을 시킬 수가 있다.

이걸 이용하면 내 로봇에는 움직일 수 있는 각도가 정해져있는데.. 이 각도가 q2라면 q2가 벗어났을 때 cos값 앞에 -만 붙여서 목표 지점에 도달 할 수 있도록도 할 수 있을 것이다.. 물론.. 어렵겠지만.. 

 

IK를 코드로 구현

아무튼 이 공식으로 하기로 했다..!

근데 이 공식을 안해본건 아니였다..

이전 글에서 했던대로 너무 되지가 않아서 저 식으로 해봤는데.. 아쉽게도 작동을 이상하게 했다.. 근데 부모님에게 물어보다가 이 식을 말했더니.. 내가 했던게 tan -1이 역탄젠트여서 atan으로 했는데.. 부모님이 말하기론.. atan2로 해야한다고 한다.. 무슨 분모가 0이 될 수가 없는데 atan2는 뭐 어떻게 한다 해서.. 그걸로 했었는데 됬었다..!!

 

일단 그 복잡한 식은 전부 버리고.. 단 2개의 식만이 필요해서.. 좀 놀라웠는데.. C++로 변경하면

float theta2 = -acos((pow(x2, 2) + pow(y2, 2) - pow(a, 2) - pow(b, 2)) / (2.0 * a * b));
float theta1 = atan2(y2, x2) - atan2(b * sin(theta2), a + b * cos(theta2));

이렇게 구할 수 있다. 여기에서 q2는 theta2이고 q1은 theta1이다.

굉장히 간단한 식인데.. 그런 놀라운 계산을 할 수 있다는게.. 엄청나다.. 

 

암튼 이걸 이용해서 결국에는 중간점의 x, y 좌표를 구해야하기 때문에 θ1의 값으로 밑변과 높이를 구해야한다..!

이전 글과 마찬가지로 첫번째 팔의 길이가 l이면 l * cos(θ1)으로 밑변을 구할 수 있고 l * sin(θ1)으로 높이를 구할 수 있다..! 그리고 밑변은 x가 되고 높이는 y가 된다..! 

그래서 그걸로 C++에서 구한다면 이렇게!!

float x1 = a * cos(theta1);
float y1 = a * sin(theta1);

 

근데 여기에서 한가지 문제가 발생한다. 이전 글에서도 말했 듯이.. IK를 계산해야하는 위치는 캔버스의 크기인 500 * 500의 중심인 250, 250에서 계산을 해야한다. 근데 이 상태로 계산하면 삼각함수에서 오류가 나서 값을 못 구하게 될 것이다..

그래서 이 식의 계산했던 곳은 센터 점이 0, 0이기 때문에 250, 250에서 0, 0으로 이동시켜야 한다.

근데.. 사실 여기에서 또 문제가 발생하긴 한다.. x값은 왼쪽에서 오른쪽으로 커지니깐.. 이건 상관없는데.. y값은 원래라면 아래쪽에서 위로 커져야 하지만.. 이것의 첫 0, 0이 왼쪽 상단이기 때문에.. y 값을 반전 시켜서 계산 시켜야 하긴 한다..

근데 그러면 계산할 때 식도 어느정도 길어질 수도 있고 하고.. 그냥 y값만 반대로 된거기 때문에.. 정상적으로 작동하는데 팔의 각도가 반대로 되어있다.. θ2에서 acos 앞에 -를 붙이면 같아지긴 한다.. 그래서 그냥 반대로 되고 정상적으로 작동하긴 해서 그냥 이대로 하기로 했다.

 

어쨋든 두번째 문제는 무시하고 첫번째 문제를 해결할려면 중간 점의 x, y좌표를 각각 빼주고 나중에 더해줘야 한다. C++ 코드로 구현한다면 이렇게 된다.

float x2 = point.x - center_point_xy.x;
float y2 = point.y - center_point_xy.y;
            
float theta2 = -acos((pow(x2, 2) + pow(y2, 2) - pow(a, 2) - pow(b, 2)) / (2.0 * a * b));
float theta1 = atan2(y2, x2) - atan2(b * sin(theta2), a + b * cos(theta2));
            
float x1 = a * cos(theta1) + center_point_xy.x;
float y1 = a * sin(theta1) + center_point_xy.y;

여기에서 center_point_xy는 말그대로 중심점의 x, y다 각 값은 그냥 250, 250 이게 있는거다. 그래서 250, 250으로 뺐다가 나중에 더하는 방식으로 되고 이렇게 하는 이유가 캔버스의 크기를 바꾸면 자동으로 되게 하기 위해서이다...!

 

복잡한 코드를 바꾸기!

그리고 이전 글에서 나온 것 처럼 원래 하던 방식은..

UpdatePos에서 각도를 구하고 x, y좌표를 구해서 직접 업데이트 하는 방식인데.. 이 경우 함수화를 해서 나중에 재사용할 때 조금 문제가 생길 수 있다.. 필요한거.. 다시 만들어야 하고.. 그래서 CalIKPos라는 함수를 만들었는데.. 여기에서 각 포인트의 x, y좌표를 구해서 넣어놓고 각 포인트의 x, y좌표는 전역 변수이기 때문에 UpdatePos에서 그 점으로 라인이나 점을 이동시키는 코드로 변경했다.

 

그래서 모든 코드를 아래처럼 고쳤다.

void MainPage::InitJoint() {
    double point_width = 20;
    double point_height = 20;
    double line_thickness = 10;

    // 센터 점 위치 초기화
    center_point_xy = { (float)(ControlJoint().Width() / 2), (float)(ControlJoint().Height() / 2) }; // 기본적으로 Xaml에서 나온 값이 double이므로 float로 변환
    // 초기 상태 IK 계산
    CalIKPos({ center_point_xy.x, center_point_xy.y - joint_1_line_size - joint_2_line_size });

    // 컨트롤러 중심의 고정 센터 포인트를 표시 (그냥 참고용임)
    DrawPoint(center_point, ControlJoint(), point_width, point_height, center_point_xy);
    // 첫번째 조인트 선
    DrawLine(joint_1_line, ControlJoint(), line_thickness, center_point_xy, joint_1_point_xy, Xaml::Media::SolidColorBrush(Colors::Red()));
    // 컨트롤러 조인트 1 끝의 점
    DrawPoint(joint_1_point, ControlJoint(), point_width, point_height, joint_1_point_xy);
    // 두번째 조인트 선
    DrawLine(joint_2_line, ControlJoint(), line_thickness, joint_1_point_xy, joint_2_point_xy, Xaml::Media::SolidColorBrush(Colors::Blue()));
    // 컨트롤러 조인트 2 끝의 점
    DrawPoint(joint_2_point, ControlJoint(), point_width, point_height, joint_2_point_xy);

    joint_2_point.PointerPressed({ this, &MainPage::JointControlJoint2PointerPressed }); // 조인트 2의 포인트를 클릭 했는지
    joint_2_point.PointerReleased({ this, &MainPage::JointControlJoint2PointerReleased }); // 조인트 2의 포인트를 클릭 안했는지
    ControlJoint().PointerMoved({ this, &MainPage::JointControlCanvasPointerMoved }); // 캔버스의 마우스 포인터 위치를 얻기 위한거
}

void MainPage::CalIKPos(float2 point) {
    float a = joint_1_line_size; // 빨간색 라인의 길이
    float b = joint_2_line_size; // 파란색 라인의 길이

    float c = distance(center_point_xy, point); // 첫점과 끝점을 이은 길이

    // a와 b의 최대 길이는 a + b가 됨, 그리고 c는 빗변이 되고 a + b를 넘으면 안됨.. 넘으면 삼각함수로 계산이 불가함, 그렇기에 미리 맞는지 확인하는 것
    if (c <= a + b) {
        // x2, y2는 끝점 x1, y1은 중간점 x0, y0은 센터점

        // center_point_xy의 x와 y로 빼는 이유는 IK를 구현하는 곳이 250, 250인 중앙이여서 이걸 0, 0으로 옮기기 위해
        float x2 = point.x - center_point_xy.x;
        float y2 = point.y - center_point_xy.y;

        // IK를 구현하기 위한 각도 계산
        float theta2 = -acos((pow(x2, 2) + pow(y2, 2) - pow(a, 2) - pow(b, 2)) / (2.0 * a * b));
        float theta1 = atan2(y2, x2) - atan2(b * sin(theta2), a + b * cos(theta2));

        // 중간 점의 x와 y를 cos, sin으로 구하는 것
        float x1 = a * cos(theta1) + center_point_xy.x; // x, y를 더한 이유, 위에서 x, y에 빼줬었으니 다시 더하는 것
        float y1 = a * sin(theta1) + center_point_xy.y;

        joint_1_point_xy = { x1, y1 };
        joint_2_point_xy = point;

        char debug[512];
        sprintf_s(debug, "theta1 = %.2f, theta2 = %.2f", theta1 * 180.0 / M_PI, theta2 * 180.0 / M_PI);
        debugText2().Text(to_hstring(debug));
    }
    else { // 여기에선 마우스가 a + b보다 넘게 벗어났으므로 마우스와 센터점을 연결해서 삼각형을 그리고 그걸로 각 지점의 위치를 구해야함

    }
}

void MainPage::UpdatePos() {
    // 파란색 끝 포인트의 위치 변경
    ChangePosPoint(joint_2_point, ControlJoint(), joint_2_point_xy);
    ChangePosLine(joint_2_line, joint_2_point_xy);

    // 파란색의 첫 포인트 위치 변경
    ChangePosLine(joint_2_line, joint_1_point_xy, false);

    // 빨간색의 끝 포인트 위치 변경
    ChangePosPoint(joint_1_point, ControlJoint(), joint_1_point_xy);
    ChangePosLine(joint_1_line, joint_1_point_xy);
}

void MainPage::JointControlCanvasPointerMoved(IInspectable const& sender, Xaml::Input::PointerRoutedEventArgs const& e) {
    Input::PointerPoint point = e.GetCurrentPoint(ControlJoint());
    float2 point_pos(point.Position().X, point.Position().Y);

    Point_X().Text(to_hstring(point_pos.x));
    Point_Y().Text(to_hstring(point_pos.y));

    CalIKPos(point_pos);
    UpdatePos();

    char debug[512];
    sprintf_s(debug, "Center X: %.2f, Y: %.2f || Joint1 X: %.2f, Y: %.2f || Joint2 X: %.2f, Y: %.2f", center_point_xy.x, center_point_xy.y, joint_1_point_xy.x, joint_1_point_xy.y, joint_2_point_xy.x, joint_2_point_xy.y);
    debugText1().Text(to_hstring(debug));
}

나머지 ChangePosPoint이나 ChangePosLine이나 DrawLine, DrawPoint에 대한 함수는 이전 글에 있다!

 

암튼 debug는 내가 실시간으로 값을 확인할려고 적어둔 것이고

저기 중간에서 else 에서 비워둔 곳에.. 주석으로 쓰여있긴 하지만..

c 값이 a + b 보다 초과할 경우 삼각함수로 계산이 안되어서.. 이 경우엔 현재 계획중이지만 마우스와 센터점을 연결해서 삼각형을 그리거나 라인을 그려서 그 사이에 점들이 일정하게 들어가도록 할 예정이다.

 

결과

암튼 이렇게 해서 제작된 결과물은 이렇게 된다..!

 

이제 저기 안에서 제작할 부분은.. 끝점을 클릭한 상태로 드래그 할 때만 IK가 계산되도록 하는 것이고 중간 점을 클릭하면 θ2구할 때 acos앞에 - 붙여서 반대로 되게 해서 하는 것과 각도 제한시키는 것과 c값이 벗어날 경우 일자로 뻣어서 마우스 쪽으로 따라오도록 만들 것이다..! 

 

한가지 아쉬운건 IK를 구현하는 다른 방법이 생각나긴 했는데.. 이걸 한다고 해도 제대로 되지 않을 것 같고 그냥 이대로 하는게 가장 나을 것 같아서.. 이건 완성한 뒤에..! 할 수 있으면 해보는걸로 하기로 했다..!

 

암튼 이번건 여기에서 끄읕!!

반응형
반응형

이전 글에 이어서 연결됩니다..!

 

이제 프로그램을 제작할려는 이유 중 하나인 제일 어려운 IK(Inverse Kinematics)를 구현해야 된다..!

근데.. 이 것의 경우 수학이기 때문에.. 굉장히 어렵다.. 왜냐면.. 내가 수학을 거의 포기해서 고등학교에서도 수학이 10 ~ 30점 대였으니.. 거기에 지금은 고3이니깐.. (특성화고라서 3학년은 일반 교과목이 없다.)

 

근데.. 이번 기회로 수학을 좀 알아야 됬었다..

일단 수학으로 넘어가기 전에.. 사전 준비를 해야한다.. 그래픽으로 표시를 해야하기 때문에 일단 먼저 라인을 그려줘야 한다.

이전 글에서 일단 그리드를 그렸는데.. 로봇 팔을 그릴 때는 나중에 값을 쉽게 바꿔서 해야하기 때문에 Xaml에는 그리지 않고 C++로 전부 그리게 할 것이다.

 

IK를 적용하기 전 팔 그리기

그래서 일단 그려야 하는데..

한가지 문제는.. 여기에 있는 코드는 아직 확실하지가 않다.. 왜냐면 지금도 글을 쓰는데 IK 문제를 해결하고 있기 때문이다.. 일단 대부분 테스트 코드라서.. 확신하기는 어렵다..

// 필요한 전역 변수 
float joint_1_line_size = 100;
float joint_2_line_size = 100;

float2 center_point_xy = float2(250, 250); // 중앙 센터 포인트
float2 joint_1_point_xy = float2(250, 250 - joint_1_line_size); // 첫번째 조인트
float2 joint_2_point_xy = float2(250, 250 - joint_1_line_size - joint_2_line_size); // 두번째 조인트

Xaml::Shapes::Ellipse center_point = Xaml::Shapes::Ellipse(); // 센터에 표시용 점
Xaml::Shapes::Ellipse joint_1_point = Xaml::Shapes::Ellipse(); // 첫번째 조인트 끝 점 위치 제어
Xaml::Shapes::Ellipse joint_2_point = Xaml::Shapes::Ellipse(); // 두번째 조인트 끝 점 위치 제어
Xaml::Shapes::Line joint_1_line = Xaml::Shapes::Line();
Xaml::Shapes::Line joint_2_line = Xaml::Shapes::Line();
Xaml::Shapes::Line joint_3_line = Xaml::Shapes::Line(); // 마지막 집게 부분 회전시킬 수 있게 하는거

일단 이런식으로 지정했다. 내가 프로그램에 알려줄껀 로봇 팔의 각 길이이고, 최종적으로 움직여야 할 끝점의 좌표를 알려주고 나머지는 다 자동으로 계산해야하기 때문에 저렇게 변수를 만들었다.

일단 임시로 100, 100으로 되고 나중에는 로봇 팔의 길이에 따라서 각각 비율에 맞춰 500 * 500 안에 넣을 생각이다.

 

그 아래는 초기에 그냥 표시하기 용으로 할려고 각각의 점을 미리 위치를 지정한 것이다.

그리고 그 아래는 그냥 GUI에 표시하기 위한 제어할 수 있도록 하는 점이나 선들이다.

 

그리고 C++로 기본 위치를 표시해야한다. 나의 경우엔 아래 처럼 코드를 짰다.

void MainPage::DrawPoint(Xaml::Shapes::Ellipse& point, Xaml::Controls::Canvas& canvas, double point_width, double point_height, float2 pos, Xaml::Media::Brush color) {
    // 중심점 크기나 색 설정
    point.Width(point_width);
    point.Height(point_height);
    point.Fill(color);

    canvas.Children().Append(point);

    // 중심점 위치 설정
    Xaml::Controls::Canvas::SetLeft(point, pos.x - (point_width / 2));
    Xaml::Controls::Canvas::SetTop(point, pos.y - (point_height / 2));

    Xaml::Controls::Canvas::SetZIndex(point, 1);
}

void MainPage::DrawLine(Xaml::Shapes::Line& line, Xaml::Controls::Canvas& canvas, double thickness, float2 pos_1, float2 pos_2, Xaml::Media::Brush color) {
    // 위치나 색 설정
    line.X1(pos_1.x);
    line.Y1(pos_1.y);
    line.X2(pos_2.x);
    line.Y2(pos_2.y);

    line.StrokeThickness(thickness);
    line.Stroke(color);

    canvas.Children().Append(line);

    Xaml::Controls::Canvas::SetZIndex(line, 0);
}

void MainPage::InitJoint() {
    double canvas_width = ControlJoint().ActualWidth();
    double canvas_height = ControlJoint().ActualHeight();

    double point_width = 20;
    double point_height = 20;
    double line_thickness = 10;

    // 컨트롤러 중심의 고정 센터 포인트를 표시 (그냥 참고용임)
    DrawPoint(center_point, ControlJoint(), point_width, point_height, center_point_xy);

    // 첫번째 조인트 선
    DrawLine(joint_1_line, ControlJoint(), line_thickness, center_point_xy, joint_1_point_xy, Xaml::Media::SolidColorBrush(Colors::Red()));

    // 컨트롤러 조인트 1 끝의 점
    DrawPoint(joint_1_point, ControlJoint(), point_width, point_height, joint_1_point_xy);

    // 두번째 조인트 선
    DrawLine(joint_2_line, ControlJoint(), line_thickness, joint_1_point_xy, joint_2_point_xy, Xaml::Media::SolidColorBrush(Colors::Blue()));

    // 컨트롤러 조인트 2 끝의 점
    DrawPoint(joint_2_point, ControlJoint(), point_width, point_height, joint_2_point_xy);

    joint_2_point.PointerPressed({ this, &MainPage::JointControlJoint2PointerPressed }); // 조인트 2의 포인트를 클릭 했는지
    joint_2_point.PointerReleased({ this, &MainPage::JointControlJoint2PointerReleased }); // 조인트 2의 포인트를 클릭 안했는지
    ControlJoint().PointerMoved({ this, &MainPage::JointControlCanvasPointerMoved }); // 캔버스의 마우스 포인터 위치를 얻기 위한거
}

한가지 아쉬운점은.. 라인의 위치를 지정하는건 쉬운데.. Ellipse의 위치를 지정할 땐.. 캔버스 기준으로 지정해야하는게 조금 아쉬웠다.. 다른 방법도 있긴 했지만.. 저게 가장 쉽고 기본적인 방법인 것 같다.. 그리고 이전글에서 마우스 정보를 함수로 넘기는 것이 저 아래쪽에 추가를 했다. (초기 팔 위치가 잡혀야지 마우스 포인트 위치를 얻어서 제어를 할 수 있기 때문에.. 그리고 맨 끝점의 마우스를 클릭하는 것과 떼는 것도 넘겨야 한다. 지금은 구현을 안했지만.. 나중에 각 점을 클릭해서 어떤 작업을 수행하도록 해야하기 때문이다.

 

그렇게 해서 위 코드의 결과물은 이렇다..!

 

짜증나는 수학..

그러면 이제 준비를 완료 했으므로 수학으로 넘어가야 한다..

처음엔 내가.. 이걸 이해하기 위해 3일을 썼다.. 그래서 나만의 방식을 찾았고 그걸 구현했지만.. 결과로는 실패다..

반은 되는데 반은 안되었기 때문에..

그래서 만약 이 글을 참고한다면.. Y값이 양수나 음수 한쪽만 필요하다면 이 글의 방법을 참고하면 되지만.. 그게 아니라면 그냥 이후 글을 참고하던지.. 유튜브 영상을 보면 된다..

 

일단 내가 최대한 이해해서 정리한건 아래 사진이다.

여기에서 구해야할 값은 C로 표시되어있는 부분의 점의 좌표이다. b와 a가 만나는 지점의 좌표

 

일단 여기에서 고정되어있거나 기본적으로 계산 없이 알 수 있는 값은

코드의 결과물 사진 처럼 빨간색은 첫번째 팔(a)이고 파란색이 두번째 팔(b)과 초록색 선으로 표시된 x와 y이다.. 

각 a와 b는 팔의 길이를 가지고 있고.. x와 y는 파란색의 끝점의 좌표이다.

 

일단 여기에서 필요 없는 변수는 C와 θ2 이다.. 지금 내 방법으론 필요없다는거지.. 이후 글에서 나올 식에는 필요하다..

 

암튼 x, y좌표를 알고 있다면 c의 값을 쉽게 구할 수 있다. 센터 점에서 x와 y 값의 거리를 구하면 되기 때문이다.. 이때! 유클리드 거리 공식을 이용하면 된다..! 근데 WinRT에서 float2에 쪽에 기본적으로 distance 함수가 있어서 이걸 이용했다. distance 함수도 유클리드 거리 공식을 이용해서 float 값으로 나온다..!

 

그렇게 해서 c를 구하게 되고

위 사진처럼 대충 칠해놓은 것을 보면 삼각형인 것을 알 수가 있다..!

 

여기에서 초록색 삼각형에서 오른쪽 상단에 I 로 되어있는 값을 구해야한다..!

I 값의 공식은 물론 직각 삼각형이기 때문에 역코사인으로 I 값을 찾을 수 있다. 하지만 x, y, c 값을 이미 알고 있고 이 뒤에 계산해야할 것은 3개의 변의 길이로 구하는 것이기 때문에 함수화를 할려고 3개 변으로 각도를 구하는 방식으로 했다.

 

https://www.mathsisfun.com/algebra/trig-solving-sss-triangles.html

 

Solving SSS Triangles

Solving SSS Triangles "SSS" means "Side, Side, Side" "SSS" is when we know three sides of the triangle, and want to find the missing angles. To solve an SSS triangle: We use the "angle" version of the Law of Cosines: cos(C) = a2 + b2 − c2 2ab cos(A) = b2

www.mathsisfun.com

3개의 변으로 구할 땐 위 링크에서 처럼 SSS 방식을 이용해서 풀면 된다..! (분명 저거 중학교 때 열심히 외웠던 것 같은데..고1 수학 선행 때문에..)

 

그렇게 하면 I 값이 나오게 되고 아래 X 선과 위의 초록색 X선은 평행하기 때문에 왼쪽 아래에 B + θ1의 값은 I와 동일하다는 것이 된다..! 내가 이걸 기억하고 있어서 다른 영상에서는 안나오는 방법이지만.. 찾아내서 좋았는데.. 결국 이것 때문에.. 결과물이 이상해지긴 했다..

 

암튼 이제 I값과 동일하기 때문에 θ1을 구할려면 I 값에서 B 값을 빼면 θ1 값이 나오는 것이다..!

그리고 B 값도 아까 I 값을 구한 것 처럼 주황색 삼각형의 세 변을 알고 있고 내부 각을 알고 싶기 때문에 위 사이트에 나온 걸로 구하면.. B 값도 쉽게 구할 수 있다..!

 

그렇게 구하면 B를 알게 되고 I - B를 해서 θ1을 알게 된다..

 

그 후에

보라색으로 대충 칠한 부분을 보면 이것도 삼각형인걸 알 수 있다..!

그리고 여기에서는 알고 싶은건 a와 b가 만나는 점의 좌표를 구하고 싶은 것이기 때문에 θ1의 값이 필요했던 것이다.

암튼 그걸로 직각삼각형에서는 θ1 부분의 각도를 알면 밑변과 높이를 구할 수 있는데

밑변은 a * cos(θ1) 로 구할 수 있고 높이는 a * sin(θ1)으로 구할 수 있다.

그래서 밑변은 구할려는 값의 x가 되고 높이는 y값이 됨으로.. 이걸로 구할 수가 있다..!

 

그래서 이 모든 과정을 코드로 짜보았다..!

// SSS로 각을 구하는 것, 입력 값은 △ 이런 삼각형에서 오른쪽변, 왼쪽변, 밑변 순서로 입력하고 필요한 각도의 번호를 입력하는 것, a, b, c를 입력하고 b c사이의 각도를 구하고 싶다면 1
// 입력값 a, b, c / 구하고 싶은 각 bc: 1, ca : 2, ab : 3 으로
float MainPage::CalSSSRadian(float a, float b, float c, int angleNum) {
    float angle = 0;

    switch (angleNum) {
        case 1: // A
            angle = (pow(b, 2) + pow(c, 2) - pow(a, 2)) / (2.0 * b * c);
            break;
        case 2: // B
            angle = (pow(c, 2) + pow(a, 2) - pow(b, 2)) / (2.0 * c * a);
            break;
        case 3: // C
            angle = (pow(a, 2) + pow(b, 2) - pow(c, 2)) / (2.0 * a * b);
            break;
    }

    return acos(angle);
}

void MainPage::UpdatePos() {
    float x = joint_2_point_xy.x;
    float y = joint_2_point_xy.y;

    ChangePosPoint(joint_2_point, ControlJoint(), float2(x, y));
    ChangePosLine(joint_2_line, float2(x, y));

    // 각 변수 이름은 사진에 있는 이름을 참고
    // 그래픽을 표시할 땐 theta 2의 값을 구할 필요가 없음, 다만 나중에 로봇을 제어할 땐 필요함, C도 구할 필요는 없을 듯
    float a = joint_1_line_size; // 빨간색 라인의 길이
    float b = joint_2_line_size; // 파란색 라인의 길이
    float c = distance(center_point_xy, joint_2_point_xy); // 가상 라인의 길이, 유클리드 거리 사용, 첫번째와 마지막의 점의 길이를 구하는 것

    if (c <= joint_1_line_size + joint_2_line_size) { // 임시로 하는 것, c의 구하는 방식 때문에 제한없이 joint_2_point가 따라오고 그것과 센터 포인트의 길이를 구하면서 200을 넘어가게 되서.. 일단 임시로
        float C = CalSSSRadian(a, b, c, 3); // C의 각도를 구하는 것
        float B = CalSSSRadian(a, b, c, 2); // B의 각도를 구하는 것

        float I = CalSSSRadian(x - 250, y - 250, c, 2); // 250을 빼주는 이유는 계산했던 기준의 센터점이 0, 0 이였기 때문에 그쪽으로 옮기기 위해

        float theta1 = I - B;

        float j1x = a * cos(theta1) + 250; // 250을 더한 이유, 위에서 x, y에 빼줬었으니 다시 더하는 것
        float j1y = a * sin(theta1) + 250;

        ChangePosLine(joint_2_line, float2(j1x, j1y), false);
        ChangePosPoint(joint_1_point, ControlJoint(), float2(j1x, j1y));
        ChangePosLine(joint_1_line, float2(j1x, j1y));

        char debug1[512];
        sprintf_s(debug1, "a = %.2f, b = %.2f, c = %.2f || C = %.2f, B = %.2f || I = %.2f, theta1 = %.2f", distance(center_point_xy, joint_1_point_xy), distance(joint_1_point_xy, joint_2_point_xy), c, C, B, I, theta1);
        debugText1().Text(to_hstring(debug1));

        char debug2[512];
        sprintf_s(debug2, "BX = %.2f, BY = %.2f || CX = %.2f, CY = %.2f || AX = %.2f, AY = %.2f", center_point_xy.x, center_point_xy.y, j1x, j1y, x, y);
        debugText2().Text(to_hstring(debug2));
    }
}

여기에서 UpdatePos 함수는 마우스의 위치 값을 가져오는 JointControlCanvasPointerMoved 함수에다가 넣었다. 

 

현재는 저기에서 모든 것을 구현하지만.. 나중에는 먼저 값을 계산해서 각 좌표 변수에다가 값을 넣고 UpdatePos를 실행해서 업데이트 할 예정이다.

 

여기에서 CalSSSRadian은 각도를 구하기 위한 함수이다. 여기에서 Radian은.. SSS로 계산을 하면.. Radian 값으로 나와서이다. 물론 이걸 각도로 다시 바꿀려면 radian * 180.0 / M_PI 이런식으로 하면 되긴 하는데.. 이후에 cos와 sin으로 밑변와 높이를 구할 때 각도를 넣는게 아닌 radian 값을 넣어서 구해야하기 때문이다.

 

그리고 if (c <= joint_1_line_size + joint_2_line_size) 이 if문이 필요한 이유는 c가 첫점과 끝점의 길이인데.. 각 팔의 길이가 100이고 더하면 200이다. 그래서 첫점과 끝점의 길이가 최대 200으로 밖에 될 수 밖에 없다.. 만약 여기에서 200 이상인걸 넣으면 cos나 sin 값이 계산이 안된다. 그렇기 때문에 미리 제한을 두는 것이다.

 

또한 아래에 debug라고 되어있는건 그냥 내가 실시간으로 각 값을 확인할려고 해둔 것이다.

그리고 250을 빼고 더하고 하는 부분이 있는데 이유는 내가 계산한 좌표는 첫점이 0, 0일 경우를 두고 한건데.. 지금의 경우 캔버스의 가로 세로가 500이기 때문에 가운데 좌표가 250, 250이다.. 그래서 이것 때문에 250, 250을 뺀 후에 계산을 하고 나중에 250, 250을 더하는 것이다. 물론 나중에 이것도 250, 250도 자동으로 구해서 자동으로 넣어지도록 할 예정이다. 암튼 이걸로 만든 프로그램을 실행해보았다..!!

 

결과

영상을 보면 알겠지만.. 망했다.. 중앙보다 아래에서 마우스를 움직일 경우에는 정상적으로 작동하지만.. 위에선 비정상적으로 작동하게 된다.. 물론 이 경우에 if문을 사용하면 처리가 가능하긴 하다. 각 4분면 마다 x, y 값을 경우데 따라서 반전 시키면 되긴 한다. 근데.. 굉장히 어렵기도 하고.. y값이 250 보다 작을 때는 다른 계산을 해서 해결하는 방법도 가능할 것이다.. 근데 해보긴 했다.. 근데 안된다.. 만약 이 방법이 되었다 해도 아마 쓰진 않았을 것 같다. 분명히 이건 나중에 로봇을 제어할 때 각도를 알아야 하기 때문이다.. 근데 4분면에서 필요에 따라 반전을 시키면 각도 값을 정확하게 알기는 어려울 것이다.. 그래서 이 방법은 버렸다..

 

위에서 말한대로 하면.. 되긴 한다.. 근데 이것도.. 저렇게 되는거라면.. 문제가 있는 것이기 때문에.. 마찬가지로 버리기로 했다..

 

실패지만.. 실패는 아닌 듯..

사실 여기까지 구하기 위해.. 거의 잊어버린 수학을 다시 꺼내기 위해.. 삼각함수에 대해서 다시 공부해야하기도 했고.. 학교 수학 선생님이나.. 부모님이나.. IK와 로봇을 잘 아시는 분에게 물어보았다.. 결국에는 이 방법은 내가 찾았긴 했지만.. 이 것을 찾기 위해 도와주신 분들에게 뭔가.. 죄송한 마음이 들었다.. 물론 실패도 있어보는 것이.. 좋다고 할 순 있지만.. 그래도 도와주신 분들이 있는데.. 이렇게 실패를 해버리니.. 아쉬웠다..

 

어쨋든 이 방법은 실패를 했지만.. 그동안 내가 수학 수업을 받으면서.. 지루한 식 외우기나 문제집에서 내준 예시 문제를 푸는 것이 아닌.. 내가 직접 문제 상황을 해결하기 위해 수학을 쓸려는 예시를 보게 되어서.. 예전까지만 해도.. 코딩이나 로봇에는 + - * / 만 알면 되는줄 알았는데.. 그것이 아니란 것을 이제야 알 수 있었다.. 그래서 나는 이 과정을 통해서 엄청난 큰 도움을 얻기도 해서 실패라고 할 순 없는 것 같긴 하다..!

 

암튼 그래서 이번 파트는 여기까지 쓰지만.. 다음 파트에선 진짜로 IK를 구현한 것을 쓸 것이다..! 왜냐면.. 유튜브 영상에서 식을 가져왔기 때문.. 암튼.. 여기서 끝..! 내.. 3일..!! 어디갔어..!

반응형
반응형

 

 

지식 없이 완전 처음부터 하니.. 틀린정보가 많습니다..

 

https://cheongpark.tistory.com/54

 

SCARA 로봇 제작하기 Pt. 6 (제작 - 끝)

이제.. 다 조립했으니.. 보드에 선을 연결해야하는데.. 제일 헷갈린 부분이 여기다.. 최대한 찾아서 끼우긴 했지만.. 계속 잘 못 끼웠었었다.. 그러다가 결국엔 끼웠지만.. 저 사진에 보이는 것 처

cheongpark.tistory.com

여기에서 로봇을 모두 제작했었다.

그런데 한가지 문제가 있다.. 로봇을 제어할 프로그램이 필요한데.. 예제에서 제공하는 프로그램은 도무지 알 수도 없고.. 여러 버그도 있고 잘 되지 않는 문제들이 있다.. 

그래서 그냥 처음부터 프로그램을 제작하는 것을 목표로 잡았다..

 

구상

일단 GUI 프로그램으로 만들 예정인데..

나에게는 웹페이지로 GUI를 구현하는 방법, 프로그램으로 GUI를 구현하는 방법 2가지가 있었는데..

난 여기에서 프로그램으로 GUI를 만드는걸로 선택했다.

이유는 프로그램으로 GUI를 만들어 본적이.. 예전에 OpenCV로 비슷하게나마 만들어본게 끝이고.. 제대로 만들어본 적은 없고.. C++를 연습하기 위해서이다.

일단.. C++로 프로그램을 제작한다면.. MFC나.. WinRT 이런게 있는데.. 나는 나중을 생각해서 크로스 플랫폼이 지원되는 WinRT로 정했다.

 

그래서 Visual Studio에 WinRT 프로그램을 제작하기 위한 설치 파일을 설치한 후 프로젝트를 만들었다.

 

그 후 일단 구상을 했는데

대충 이런 느낌으로 제작하는 것이다. 메인 그리드 부분에선 Inverse Kinematics로 로봇의 암을 제어하고 오른쪽 설정 창에선 집게를 펼지 말지 이런거를 세팅하고, 왼쪽에선 높이로 로봇의 Z축을 제어하는 곳으로 정했다.

또한 아래쪽의 검은색 저부분은 각각의 세팅 값이다. 설정 창에서 저장버튼을 눌르면 아래에 스택이 쌓이고 각 스택을 눌르면 그 때 저장했던 값들이 다시 나오게 되는 프로그램을 제작하기로 했다.

 

현재도 제작중이기 때문에.. 저기에서 어떤 점이 바뀔지는 모르겠지만.. 이 상태로 하기로 정했다.

 

GUI Xaml 제작 및 프로그래밍

일단 여기에서 제일 중요한 부분은 Inverse Kinematics 부분이다. 물론 각도로 일일이 다 지정을 할 순 있겠지만.. 그렇게 하면.. GUI 프로그램을 만드는 의미가 사라진다..

그래서 일단 저기를 구현해야하는데.. 구현하기 전에 사전에 세팅을 했다.

 

그리드 그리기

먼저 그리드를 표시하는 것!

일단 Xaml로 저것이 표시될 구간을 정한다!

현재 나의 경우엔.. 이걸 처음하다보니.. 아는게 없었다.. 그래서 어느정도 전문가가 보기엔 불편한 것들이 많겠지만.. 일단 이대로 진행하기로 했다. 아래 코드 구조에서 Canvas가 2개가 있는데.. 하나는 그리드를 표시하는 용이고 하나는 Inverse Kinematics로 팔 부분을 표시하는 것이다.

<Border
    Width="506"
    Height="506"
    BorderBrush="#f8f8f2"
    BorderThickness="3"
    CornerRadius="15,15,15,15"
    RelativePanel.AlignVerticalCenterWithPanel="True"
    Margin="200, 0, 0, 0">
    <!-- 그리드 위에 컨트롤를 넣는 느낌으로 할려 했지만 그리드가 나중에 렌더링 되어서 Canvas.ZIndex로 직접 바꿈 -->
    <Canvas
        x:Name="DrawGrid"
        Width="500"
        Height="500"
        Canvas.ZIndex="0"
        Background="Transparent"> 
        <Canvas
            x:Name="ControlJoint"
            Width="500"
            Height="500"
            Canvas.ZIndex="1"
            Background="Transparent"> <!-- 배경을 투명하게라도 주는 이유는 안하면 ControlJoint로 마우스 위치를 얻을려고 하면 Shape 부분만 얻을 수 있어서.. 전부 얻기 위해서 하는거 -->
        </Canvas>
    </Canvas>
</Border>

주석에 달린 것 처럼.. ZIndex를 넣은 이유가.. 그리드 배경 위에 팔 부분이 나오면 좋겠지만.. 렌더링 되는 것이.. 그리드가 먼저 되다보니.. ZIndex로 직접 바꾼거다.. 그리고 움직일 부분에서 마우스로 제어할 것인데.. 거기에서 마우스로 제어하는 경우 그리드에선 전부 추적이 가능하지만.. ControlJoint에선 팔 부분만 마우스의 좌표를 알 수 있다.. 그래서 그냥 배경을 추가하는 방식으로 진행했다.

 

이제 저렇게 하면 프로그램 안에 500 * 500 의 사각 박스가 나오게 된다.

이 사각 박스 안에 격자를 그려야하는데 이걸 위해 함수를 만들었다.

일단 line의 경우 좌표에서 필요한 속성은 X1, Y1, X2, Y2가 있다.

그래서 내가 생각한 방법으론 500을 내가 필요한 만큼의 라인 갯수로 나눠서 각 지점에 직접 배치하는 방식으로 했다.

 

그렇게 해서 C++ 코드를 작성했다.

void MainPage::DrawLine(Xaml::Shapes::Line& line, Xaml::Controls::Canvas& canvas, double thickness, float2 pos_1, float2 pos_2, Xaml::Media::Brush color) {
    // 위치나 색 설정
    line.X1(pos_1.x);
    line.Y1(pos_1.y);
    line.X2(pos_2.x);
    line.Y2(pos_2.y);

    line.StrokeThickness(thickness);
    line.Stroke(color);

    canvas.Children().Append(line);

    Xaml::Controls::Canvas::SetZIndex(line, 0);
}

void MainPage::DrawGridLines(int grid_x_count, int grid_y_count) {
    double grid_width = DrawGrid().ActualWidth();
    double grid_height = DrawGrid().ActualHeight();
    double line_thickness = 1;

    // 가로 줄 세로로 여러개 그리는거
    for (double i = grid_height / grid_y_count; i < grid_height; i += grid_height / grid_y_count) {
        Xaml::Shapes::Line line = Xaml::Shapes::Line();
        DrawLine(line, DrawGrid(), line_thickness, float2(0, i), float2(grid_width, i));
    }

    // 세로 줄 가로로 여러개 그리는거
    for (double i = grid_width / grid_x_count; i < grid_width; i += grid_width / grid_x_count) {
        Xaml::Shapes::Line line = Xaml::Shapes::Line();
        DrawLine(line, DrawGrid(), line_thickness, float2(i, 0), float2(i, grid_height));
    }
}

이 코드로 DrawGridLines를 처음에 프로그램을 시작할 때 실행해서 각 x축으로 몇개 y축으로 몇개 넣을지 선택해서 하면 

 

결과적으로 이렇게 표시가 된다..!

라인을 각각 10개씩 넣어서 했다.

 

마우스 위치 알아내고 표시하기

그리고 이후에 팔을 제어하기 위해 마우스를 사용해야해서 마우스 포인터의 위치를 알아야 한다..!

근데 일단 보통은 콘솔창에서 확인을 하는데.. 콘솔창은 어떻게 띄우는지도 모르겠고.. 그냥 GUI 화면에서 확인하는게 빠를 것 같아서 GUI 화면에서 표시되도록 했다..!

 

일단 Xaml 코드에선

<StackPanel
    BorderBrush="White"
    BorderThickness="2" RelativePanel.AlignBottomWithPanel="True" RelativePanel.AlignLeftWithPanel="True">
    <StackPanel
        Orientation="Horizontal">
        <TextBlock
            Width="50"
            TextAlignment="Center"
            Text="X"
            FontSize="35"/>
        <TextBlock
            x:Name="Point_X"
            Width="100"
            TextAlignment="Right"
            Text="0"
            FontSize="35"/>
    </StackPanel>
    <StackPanel
        Orientation="Horizontal">
        <TextBlock
            Width="50"
            TextAlignment="Center"
            Text="Y"
            FontSize="35"/>
        <TextBlock
            x:Name="Point_Y"
            Width="100"
            TextAlignment="Right"
            Text="0"
            FontSize="35"/>
    </StackPanel>
</StackPanel>

이런식으로 했다. X인지 Y인지 알 수 있게 하고 그 바로 옆에 숫자를 표시하도록 했다.

그래서 이걸로 해서 실행하면 저런식으로 나온다.

 

그리고 각 X, Y 좌표를 얻어야 한다.

이 경우엔 좀 간단한데.. 보통은 Xaml 코드에서 X, Y 값을 가져올 함수를 지정할 순 있지만.. 나는 나중에 함수 이름을 쉽게 변경하기 위해 모두 C++에서 진행했다.

일단 X, Y 좌표를 가져올 캔버스는 ControlJoint라는 것이기 때문에 여기의 PointerMoved로 접근해서 함수로 넘겨주는 방식으로 했다.

void MainPage::Init() {
	ControlJoint().PointerMoved({ this, &MainPage::JointControlCanvasPointerMoved });
}

void MainPage::JointControlCanvasPointerMoved(IInspectable const& sender, Xaml::Input::PointerRoutedEventArgs const& e) {
    Input::PointerPoint point = e.GetCurrentPoint(ControlJoint());
    float2 point_pos(point.Position().X, point.Position().Y);

    Point_X().Text(to_hstring(point_pos.x));
    Point_Y().Text(to_hstring(point_pos.y));
}

이런식으로 하면 마우스 포인터의 값을 가져와서 아까 만들었던 Text로 집어넣게 된다.

Init 함수는 그냥 임의로 만든거고.. 프로그램이 시작할 때 실행되도록 하면 된다. 그렇게 해서 마우스 값을 넘겨주고 거기에서 X, Y 값을 뽑아서 Text를 지정하는 방식이다.

 

결과물!!

그래서 이것으로 일단 프로젝트 준비는 끝났다..!!

이걸로 지금까지 한 것의 결과물을 보면 아래 처럼된다..!

그렇게 해서 일단 Pt. 1은 끝났다..! 

이 모든 것이.. 3일 정도 걸려서 알아내고 만든 결과다..! 물론 GPT의 도움도 받긴 했지만.. 암튼.. 다음에선 IK를 구현하는 것이다..!

반응형
반응형

일단 바로 제작된 결과!

음 얼굴을 찾아서 뭔가 하는 프로젝트를 하는데 문제가 생겼다. 얼굴 이미지를 벡터로 변환하는 과정에서 원본 이미지와 얼굴 이미지를 보여줄려 하는데 여기에서 원본 이미지를 보여줄 때 보여주는 곳이 정사각형이다..

근데 OpenCV에서 이미지를 바로 넣을려니깐 이미지를 리사이즈 해서 바로 넣는 것은 좀 불편하고.. 이미지를 정사각형에 맞춰 그냥 리사이즈 하면 이미지가 뭉개지고..

 

그래서 생각한건 흰 배경 중심에 이미지를 넣는 것인데

이걸 해결하기 위해 1시간 반이 걸렸다..

대충 생각을 해본건 

  • 원본 이미지 보여주는 곳과 같은 크기의 배경을 만들어 중앙에 이미지를 배치
  • 원본 이미지의 가장 긴 변의 길이 대로 정사각형 배경을 만들어 중앙에 이미지를 배치

이런거고 중앙에다 배치할땐

  • 비율에 맞춰 원본 이미지를 축소해서 정사각형 배경의 반에 원본 이미지의 짧은 변의 반을 빼서 위치 구하기
  • 정사각형 배경의 반에 원본 이미지의 짧은 변의 반을 빼서 위치 구하기

등을 생각했는데.. 난 여러가지 방법을 생각해보고 여러가지 방법을 해보다가 헷갈리기도 하고 수정하기도 하며 결국엔

원본 이미지의 가장 긴 변의 길이 대로 정사각형 배경을 만들어 중앙에 이미지를 배치할 때 정사각형 배경의 반에 원본 이미지의 짧은 변의 반을 빼서 위치를 구해서 바로 넣는 방법을 하기로 결정했다..

 

그래서 이걸 대충 이미지로 표시하면!

 

이런식으로 

일단 정사각형이면 그냥 패스

가로가 더 길 경우 가로 길이에 대한 정사각형을 만들고 그게 검은색 사각형!

거기에 빨간색이 원본 이미지꺼..

그래서 파란색 동그라미 부분의 위치를 구하는건데

 

검은색 사각형에서 반을 나누고 거기에서 빨간색 사각형의 반을 빼면 저 위치가 나올 것 같다 라는 생각이 들어서 한 것!

파란색은 반대로 생각하면 되는거!

 

대충 식은

(Square / 2) - (OriginalImg / 2)

뭐 어쨌든 이런 방법으로 코드를 구현한 결과!

 

if (I_celeb_img.nc() == I_celeb_img.nr())
    GUICon::putWebcamView(I_celeb_img, preSetImage);
else {
    int squareLen = (I_celeb_img.nc() < I_celeb_img.nr() ? I_celeb_img.nr() : I_celeb_img.nc());

    cv::Mat originalViewImage(squareLen, squareLen, CV_8UC3);
    originalViewImage = cv::Scalar(0xFF, 0xFF, 0xFF);

    //배경 이미지 중앙에 사진 넣기
    //사진의 세로가 가로보다 짧을 경우 squareLen의 길이에서 반을 나누고 이미지 세로의 반 만큼 빼서 좌표 구하기						
    if (I_celeb_img.nr() < I_celeb_img.nc()) { //가로가 더 클 경우 같을 경우 비교 안하는건 위에서 이미 해서
        int ypos = (squareLen / 2) - (I_celeb_img.nr() / 2); //세로가 짧을 경우 정사각형의 한 변의 길에서 이미지 세로 반을 빼서 위치 구하는 것
        CPputImage(I_celeb_img, originalViewImage, cv::Rect(0, ypos, I_celeb_img.nc(), I_celeb_img.nr()));
    }
    else {
        int xpos = (squareLen / 2) - (I_celeb_img.nc() / 2); //위와 반대
        CPputImage(I_celeb_img, originalViewImage, cv::Rect(xpos, 0, I_celeb_img.nc(), I_celeb_img.nr()));
    }
    cv::cvtColor(originalViewImage, originalViewImage, cv::COLOR_RGB2BGR);
    GUICon::putWebcamView(originalViewImage, preSetImage);
}

코드는 이렇다!

여기에서 CPputImage는 그냥 OpenCV 이미지의 특정 위치에 이미지를 넣는 함수고

GUICon::putWebCamView는 원본 이미지 표시하는 쪽에 이미지 넣는 함수다.. 암튼 그럼!

 

그럼 끝!

반응형
반응형

아마 글꼴을 찾는데 문제가 있을 수 있습니다 "맑은 고딕"은 정상적으로 작동되지만 컴퓨터마다 다를 수 있을 것 같습니다. 될 수도 있고 안 될 수도 있다는 점.. 안된다면.. 폰트 쪽 코드를 수정하면 될지도..?

 

일단 음.. 이걸 성공하게 된건.. 한국어나 영어로 찾아볼 땐 아무리 찾아도 안나왔는데.. 일본에는 전문가들이 많으니깐 있지 않을까해서 일본어로 검색해보니.. 역시 있었다. 그래서 해결하게 됨

 

바로 가져갈 분은 맨 아래쪽에 있어요.

 

https://jitaku.work/it/category/image-processing/opencv/write_japanese/

 

C++のOpenCVで日本語をputTextする

Top 作成日: 2020.06.29 C++のOpenCVで日本語をputTextする Pythonの場合比較的容易にできる、日本語を画面描画する方法。 C++はそういったことができないので非常に困っていた。 で、ネットからパク

jitaku.work

일단 나는 여기 것을 사용했는데. 여기에 있는 코드를 바로 쓰면 당연히 안될꺼다.

HFONT hFont = ::CreateFontA(
        fontSize, 0, 0, 0, FW_DONTCARE, FALSE, FALSE, FALSE,
        SHIFTJIS_CHARSET, OUT_DEFAULT_PRECIS,
        CLIP_DEFAULT_PRECIS, DEFAULT_QUALITY,
        VARIABLE_PITCH | FF_ROMAN, fontname);

코드를 보면 이런데.. 저기 아래 SHIFTJIS_CHARSET 저것때문에 안된다. 아마 인코딩 문제 같은데 뭐 저거를 다른 대부분 언어도 되게 할려면 DEFAULT_CHARSET으로 하면 아마 될 것 같다. 당연 한국어도 됨!

 

일단 저걸로 하면 되고

추가적으로 수정한 것

일단 내 프로젝트에선 좀 중요하기 때문에 얘를 그냥 출력하면 안티에일리어싱이 결국엔 안된다.

위의 코드에서 안티에일리어싱을 키면 되는데 킨다 하더라도 코드 때문에 결국은 안된다.

 

일단 이 코드의 방식을 보면 먼저 MFC로 어디선가?에서 적을 OpenCV Mat에서의 가로 길이만큼의 텍스트 이미지를 생성한 후 가져와서 비트맵을 Mat 방식대로 바꿔서 적용하는 방식이다. (아마 맞을꺼임)

 

근데 저기 코드를 대충 보면

if (_tmp[0] == 0 && _tmp[1] == 0 && _tmp[2] == 0) {
                    _img[0] = (unsigned char)color.val[0];
                    _img[1] = (unsigned char)color.val[1];
                    _img[2] = (unsigned char)color.val[2];
                }

이런 코드가 있다. 그리고 위쪽에 보면 back_color로 0x99가 정의 되어 있는데.. 이걸 보면 대충 알 수 있다. 일단 MFC에서 텍스트 이미지를 생성하면 텍스트 밖은 기본적으로 흰색(255, 255, 255), 텍스트는 검은색(0, 0, 0)이다.이다. 또한 0x99는 153으로 RGB 153 153 153 뭐 이래보면 회색이다. 최대가 255이니.. 그래서 최종적으로 저 코드는 텍스트외의 배경 (153, 153, 153)과 (255, 255, 255) 부분을 제외한 검은색 부분(텍스트 부분)을 가져와 사전에 정의한 RGB색대로 다시 설정하는 방식이다.

 

이 때문에 이 코드에선 아무리 안티에일리어싱을 한들.. 안티 에일리어싱이 픽셀 사이사이에 텍스트보다 연한 색을 찍는건데 이 것들이 다 제외되면서 결국엔 안티 에일리어싱이 안찍힌다..

 

그래서 내가 생각한 방식은 안티 에일리어싱의 경우 연한 색도 처리해야하기 때문에 배경색이 바뀔때마다 당연히 글자 색도 변경될테니 배경색을 직접 지정하고 텍스트 색도 직접 지정해서 나중에 배경색만 빼는 것이다. 그러면 결국엔 적용되는건 텍스트의 진한 픽셀부터 연한 픽셀까지이다. 그래서 이 방법으로 코딩한건 뭐 아래에 넣을꺼고

 

또한 내가 지정한 것 중에 다른 것들도 있지만 ori가 있다. 내가 당시 했을때 오리엔테이션인가 뭔가 하는걸로 줄여서 적은 것 같은데.. 잘 못 지정한 것 같다.

어쨌든 저 코드를 보면

int posX = (ori == 0 ? org.x : (ori == 1 ? org.x - (size.cx / 2) : org.x - size.cx)); //기준이 왼쪽이면 그대로, 중간이면 기준점 - 넓이 / 2, 오른쪽이면 기준점 - 넓이

대충 이렇게 적어놨다. 조건연산자로 이리저리 해놨는데 주석문을 보면 대충 알 수 있다. 0이면 왼쪽 기준 1이면 중간 기준 2이면 오른쪽 기준

말 그대로다. 사전에 정의한 글자 위치대로 0이면 텍스트의 맨 왼쪽 기준으로 텍스트가 생성되고 1이면 텍스트의 중간으로 생성 2이면 텍스트의 오른쪽 끝으로 생성된다.

 

그리고 세로 축도 할려했는데.. 음..! 이 때 당시엔 필요가 없었어서.. 하실 분들은 직접 하시길! 내가 기억하기론 세로는 맨 위쪽 기준으로 되는걸로 알고 있다.

 

코드를 보면 뭐 배경색 칠하는 것도 있고 텍스트 색 지정하는 것도 있고 그렇다.. 어쨌든 그런거임!

 

이제 코드를 보여줌!

코드

void _putText(cv::Mat& img, const cv::String& text, const cv::Point& org, const int ori, const char* fontname, const int fontWeight, const double fontScale, const bool anti, const RGBScale textcolor, const RGBScale bkcolor) {
	int fontSize = (int)(10 * fontScale);
	int width = img.cols;
	int height = fontSize * 3 / 2;

	HDC hdc = CreateCompatibleDC(NULL);

	HBRUSH hBrush = CreateSolidBrush(bkcolor.rgb);

	RECT rect;
	rect.left = rect.top = 0;
	rect.right = width;
	rect.bottom = height;

	BITMAPINFOHEADER header;
	ZeroMemory(&header, sizeof(BITMAPINFOHEADER));
	header.biSize = sizeof(BITMAPINFOHEADER);
	header.biWidth = width;
	header.biHeight = height;
	header.biPlanes = 1;
	header.biBitCount = 24;
	BITMAPINFO bitmapInfo;
	bitmapInfo.bmiHeader = header;
	HBITMAP hbmp = CreateDIBSection(NULL, (LPBITMAPINFO)&bitmapInfo, DIB_RGB_COLORS, NULL, NULL, 0);
	SelectObject(hdc, hbmp);

	FillRect(hdc, &rect, hBrush);

	BITMAP  bitmap;
	GetObject(hbmp, sizeof(BITMAP), &bitmap);

	HFONT hFont = CreateFontA(
		fontSize,
		0,
		0,
		0,
		fontWeight,
		FALSE,
		FALSE,
		FALSE,
		DEFAULT_CHARSET,
		OUT_DEFAULT_PRECIS, //SHIFTJIS_CHARSET -> DEFAULT_CHARSET으로 변경
		CLIP_DEFAULT_PRECIS,
		(anti ? ANTIALIASED_QUALITY : DEFAULT_QUALITY), //안티를 허용하면 해주고 아니면 안해주고
		VARIABLE_PITCH | FF_ROMAN,
		fontname);
	SelectObject(hdc, hFont);
	SetTextColor(hdc, textcolor.rgb);
	SetBkColor(hdc, bkcolor.rgb);

	//넓이 높이 구하기
	SIZE size;
	GetTextExtentPoint32A(hdc, text.c_str(), (int)text.length(), &size);

	TextOutA(hdc, 0, height / 3 * 1, text.c_str(), (int)text.length());
	int posX = (ori == 0 ? org.x : (ori == 1 ? org.x - (size.cx / 2) : org.x - size.cx)); //기준이 왼쪽이면 그대로, 중간이면 기준점 - 넓이 / 2, 오른쪽이면 기준점 - 넓이
	int posY = org.y - (size.cy / 2 + 5);

	unsigned char* _tmp;
	unsigned char* _img;
	for (int y = 0; y < bitmap.bmHeight; y++) {
		if (posY + y >= 0 && posY + y < img.rows) {
			_img = img.data + (int)(3 * posX + (posY + y) * (((bitmap.bmBitsPixel / 8) * img.cols) & ~3));
			_tmp = (unsigned char*)(bitmap.bmBits) + (int)((bitmap.bmHeight - y - 1) * (((bitmap.bmBitsPixel / 8) * bitmap.bmWidth) & ~3));
			for (int x = 0; x < bitmap.bmWidth; x++) {
				if (x + posX >= img.cols) {
					break;
				}

				if (_tmp[0] != bkcolor.b || _tmp[1] != bkcolor.g || _tmp[2] != bkcolor.r) { //순서를 bgr를 써서 bgr로 한거, rgb 순서이면 그걸로 해야함
					_img[0] = (unsigned char)_tmp[0];
					_img[1] = (unsigned char)_tmp[1];
					_img[2] = (unsigned char)_tmp[2];
				}
				_img += 3;
				_tmp += 3;
			}
		}
	}

	DeleteObject(hFont);
	DeleteObject(hbmp);
	DeleteDC(hdc);
}

주석문 보고 뭐 대충 하면 될 것 같다. 난 OPENCV의 기본 MAT의 설정인 BGR때문에 아래가 BGR 기준이고 RGB면 순서를 바꿔서 작성해야 할 것이다.

 

cv::Mat& img, const cv::String& text, const cv::Point& org, const int ori, const char* fontname, const int fontWeight, const double fontScale, const bool anti, const RGBScale textcolor, const RGBScale bkcolor

파라미터를 대충 설명하면

  • cv::Mat& img -> 텍스트를 적용할 이미지
  • cv::String& text -> 출력할 텍스트
  • cv::Point& org -> OpenCV 이미지에서 텍스트를 지정할 위치
  • int ori -> 텍스트 위치를 지정할 때 왼쪽 중앙 오른쪽으로 어느 위치로 정렬해서 출력할지
  • char* fontname -> 폰트 이름 (예: 맑은 코딕)
  • int fontWeight -> 글자의 굵기 (예: FW_BOLD 이런거 하면 됨.. 그냥 숫자로 해도 되고)
  • double fontScale -> 글자 크기 (예: 이건 좀 보정을 안했는데 그냥 무조건 큰 숫자 적으면 진짜 커지므로 기본 생각했던 것보다 반 작게 줄여서 적으면 아마 맞을꺼다)
  • bool anti -> 안티에일리어싱을 할꺼냐 말꺼냐
  • RGBScale textcolor -> 텍스트의 컬러 지정 (예: RGBScale(255, 0, 0))
  • RGBScale bkcolor -> 배경색 지정 (예: RGBScale(255, 255, 255))

어쨌든 이정도다.. 뭐 쓰다보니 파라미터가 너무 많아지긴 했지만.. 뭐 내가 쓰는건데 뭐 어때!

_putText(outImg, "나", cvPoint(50, 50), 1, "맑은 고딕", FW_BOLD, 6, true, RGBScale(0, 0, 0), RGBScale(255, 255, 255));

사용 예시는 위와 같다.

 

어쨌든 그렇다. 이거 글 쓰는 것도 힘들 구 생각하는 것도 힘들었는데..

나중에 자기 글에다 쓰고 싶다면... 적어도 링크라도.. 부탁.. (이거 그럼 만든게 2022년 11월 쯤에 만들었으니.. 고등학교 2학년 때 만든거군.. 아 여기 학교 C/C++과목 전혀 없음)

 

뭐 어쨌든 끄읕!

 

얼마나 사용해줄진 모르겠지만.. C++은 겁나 복잡해!!

 

(파이썬은 짧고 쉽던데.. 방식은 이거와 비슷)

 

2023-03-01 추가

코드에서 RGBScale에 대한 구조체 내용이 없어서 다시 정리하고 구조체에 대한 것도 새로 적습니다.

struct RGBScale {
    int r = 0;
    int g = 0;
    int b = 0;
    int rgb = 0;

    RGBScale(int r, int g, int b) {
        this->r = r;
        this->g = g;
        this->b = b;

        this->rgb = RGB(r, g, b);
    }
};

RGBScale(0xFF, 0xFF, 0xFF)로 적으면 되는데 0xFF는 그냥 16진수 이기 때문에 0부터 255 숫자중 원하는 색깔에 맞춰 적으시면 됩니다! 그리고.. 기본적으로 cv::Mat 생성할 때 RGB가 아닌 BGR로 생성하기 때문에 아마 RGB 이미지를 넣어서 적용시키면 색깔이 다르게 나올꺼기 때문에.. 인풋엔 BGR로 넣으시면 됩니다. 수정할려면 아래 적혀있는 코드에서 비트맵을 OpenCV로 바꿔주는 곳에서 b, g, r 적혀있는 것을 r, g, b로 변경하시면 될겁니다. RGBScale를 따로 만든 이유는 그냥 r, g, b도 추출하고 rgb도 출력하고 하기 위함!

 

아래는 다시 정리해본 코드!

void CPputText(cv::Mat& O_image, cv::String text, cv::Point org, int ori, const char* fontName, int fontWeight, double fontScale, RGBScale textColor, RGBScale bkColor) {
	int fontSize = (int)(10 * fontScale);
	int width = O_image.cols;
	int height = fontSize * 3 / 2;

	HDC hdc = CreateCompatibleDC(NULL); //텍스트 이미지를 만들어두는 곳 같은거

	HBRUSH hBrush = CreateSolidBrush(bkColor.rgb); //채우는 방식인데 bkColor로 단색으로 채우는거

	//텍스트 이미지 크기 정하는거
	RECT rect;
	rect.left = rect.top = 0;
	rect.right = width;
	rect.bottom = height;

	//비트맵의 구조를 사전에 정의하는 것 크기나 색
	BITMAPINFOHEADER header;
	ZeroMemory(&header, sizeof(BITMAPINFOHEADER));
	header.biSize = sizeof(BITMAPINFOHEADER);
	header.biWidth = width;
	header.biHeight = height;
	header.biPlanes = 1;
	header.biBitCount = 24;
	BITMAPINFO bitmapInfo;
	bitmapInfo.bmiHeader = header;
	HBITMAP hbmp = CreateDIBSection(NULL, (LPBITMAPINFO)&bitmapInfo, DIB_RGB_COLORS, NULL, NULL, 0);
	SelectObject(hdc, hbmp); //hdc에 적용? 하는 거

	FillRect(hdc, &rect, hBrush); //지정한 크기만큼 완전하게 채우는거 (다 채움)

	BITMAP bitmap;
	GetObject(hbmp, sizeof(BITMAP), &bitmap);

	//텍스트 이미지 만들 때 사용할 수 있는 폰트를 생성? 하는 그런거
	HFONT hFont = CreateFontA(
		fontSize,
		0,
		0,
		0,
		fontWeight,
		FALSE,
		FALSE,
		FALSE,
		DEFAULT_CHARSET, //한국어나 일본어나 해주게 하는거 (아마)
		OUT_DEFAULT_PRECIS,
		CLIP_DEFAULT_PRECIS,
		ANTIALIASED_QUALITY, //안티 에일리어싱을 켜주는거
		VARIABLE_PITCH | FF_ROMAN,
		fontName);
	SelectObject(hdc, hFont);
	SetTextColor(hdc, textColor.rgb);
	SetBkColor(hdc, bkColor.rgb);

	//계산을 위해 미리 텍스트의 사이즈 구하는거
	SIZE size;
	GetTextExtentPoint32A(hdc, text.c_str(), (int)text.length(), &size);

	TextOutA(hdc, 0, height / 3 * 1, text.c_str(), (int)text.length()); //이미지에 텍스트 적는거
	int posX = (ori == 0 ? org.x : (ori == 1 ? org.x - (size.cx / 2) : org.x - size.cx)); //기준 정하는거 0은 텍스트의 왼쪽 1은 텍스트의 중간 2는 텍스트의 오른쪽
	int posY = org.y - (size.cy / 2 + 5);

	//비트맵 사진을 OpenCV이미지에 삽입해주는거
	unsigned char* _tmp;
	unsigned char* _img;
	for (int y = 0; y < bitmap.bmHeight; y++) {
		if (posY + y >= 0 && posY + y < O_image.rows) {
			_img = O_image.data + (int)(3 * posX + (posY + y) * (((bitmap.bmBitsPixel / 8) * O_image.cols) & ~3));
			_tmp = (unsigned char*)(bitmap.bmBits) + (int)((bitmap.bmHeight - y - 1) * (((bitmap.bmBitsPixel / 8) * bitmap.bmWidth) & ~3));
			for (int x = 0; x < bitmap.bmWidth; x++) {
				if (x + posX >= O_image.cols)
					break;

				if (_tmp[0] != bkcolor.b || _tmp[1] != bkcolor.g || _tmp[2] != bkcolor.r) { //텍스트 이미지의 배경 컬러는 없애기 위한 것, bgr 순서로 하는 이유는 Mat 이미지를 처음에 만들 때 BGR 순이여서
					_img[0] = (unsigned char)_tmp[0]; //B
					_img[1] = (unsigned char)_tmp[1]; //G
					_img[2] = (unsigned char)_tmp[2]; //R
				}
				_img += 3;
				_tmp += 3;
			}
		}
	}

	//메모리에서 삭제해주는거 이거 안하면 메모리 계속 사용함
	DeleteObject(hBrush);
	DeleteObject(hFont);
	DeleteObject(hbmp);
	DeleteObject(hdc);
}
cv::Mat inputMat(500, 500, CV_8UC3);
CPputText(inputMat, "안녕!", cvPoint(inputMat.cols / 2, inputMat.rows), 1, "맑은 고딕", FW_BOLD, 6, RGBScale(0xFF, 0xFF, 0xFF), RGBScale(0, 0, 0));

아아마 이렇게 하면 500 * 500 으로 이미지를 생성하고 그 중간에 텍스트를 넣는거니 완전 중간은 아니고.. 세로가 좀 아래로 쳐져있긴 하지만.. 저렇게 하면 잘 작동은 합니다!

반응형

+ Recent posts