Unity와 Python의 TCP 소켓 통신을 구현해보자

하나둘셋·2024년 5월 2일

저번 게시물에서 관절의 위치를 이용해 아바타의 움직임을 구현해 보았다.
이번에는 mediaPipe 모듈로 실시간 감지한 관절의 위치를 유니티에 전송해서 데이터를 잘 받아올 수 있는지 확인해 보겠다.



파이썬 TCP 소켓 통신 코드

import mediapipe as mp
import socket
import time
import cv2


mpPose = mp.solutions.pose
pose = mpPose.Pose()

# 0은 웹캡
cap = cv2.VideoCapture(0)
# 이전 프레임 처리 시간 저장 변수 초기화
pTime = 0
# 출력하고 싶은 관절들의 인덱스
desired_indices = [0, 11, 12, 13, 14, 15, 16, 23, 24, 25, 26, 27, 28]

# TCP 서버 생성
server_socket = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
server_socket.bind(('localhost', 포트번호))
server_socket.listen(1)  # 최대 1개의 연결을 기다림

print("서버가 연결을 대기 중입니다.")

while True:
    
    # 클라이언트 연결 대기
    try:
        client_socket, addr = server_socket.accept()
        print(f"{addr}에서 연결이 수락되었습니다.")

        # 문자열 대기
        start_message = client_socket.recv(5).decode()
        if start_message == 'start':
            start_message = ''


        # 실시간을 위해 무한 루프 돌려 줍니다
        while True:
            
            # 바이트 배열 초기화
            coordinates_bytes = bytearray()
            
            # 성공여부, 이미지
            success, img = cap.read()
            
            # 이미지 크기 축소
            new_width = 480  # 가로 크기
            new_height = 640  # 세로 크기
            img_small = cv2.resize(img, (new_width, new_height))
            
            #rgb로 변경 (mediapipe는 rgb 이미지를 사용)
            imgRGB = cv2.cvtColor(img_small, cv2.COLOR_BGR2RGB)
            results = pose.process(imgRGB)
            # print("시작")
            
            if results.pose_world_landmarks:
                
                for desired_idx in desired_indices:
                    landmark = results.pose_world_landmarks.landmark[desired_idx]
                    x = landmark.x
                    y = landmark.y
                    z = landmark.z

                    # 선택한 관절 지점의 정보 출력
                    print(f"{x:.5f} {y:.5f} {z:.5f}".format(x,y,z))
                    
                    # 좌표값을 바이트 배열에 추가
                    coordinates_bytes += f"{x:.5f} {y:.5f} {z:.5f}\n".format(x, y, z).encode()

                coordinates_bytes = coordinates_bytes.rstrip(b'\0')
                
                # 바이트 배열로 변환된 좌표값을 Unity 서버로 전송
                coordinates_length = len(coordinates_bytes)
                length_bytes = coordinates_length.to_bytes(4, byteorder='big') 

                # 길이 정보 전송

                client_socket.sendall(length_bytes)
                        
                # 바이트 배열로 변환된 좌표값을 Unity 서버로 전송
                client_socket.sendall(coordinates_bytes)
            
                

            cTime = time.time()
            fps = 1 / (cTime - pTime)
            pTime = cTime

            # 이미지에 프레임 속도 표시
            # cv2.putText(img, str(int(fps)), (70, 50), cv2.FONT_HERSHEY_PLAIN, 3, (255, 0, 0), 3)
            cv2.imshow("image", img)

            if cv2.waitKey(10) & 0xFF == ord('q'):  # 'q' 키를 누르면 루프 종료
                break
            
    except Exception as e:
        print(f"연결 수락 중 오류 발생: {e}")
        

# 연결 종료
client_socket.close()
server_socket.close()
cap.release()
cv2.destroyAllWindows()



유니티 소켓 통신 코드

public class FinalTest : MonoBehaviour
{
    public static Socket sock;
    string serverIP = "123.45.67.89";
    int port = 9999;
    bool socketConnect = false;
    string startPoint = "start"; 

    public Animator anim;
    StoreJointData storeData;
    MoveAvatar moveAvatar;
    
    Vector3[] realJoint = new Vector3[13];
    string[] textLine;
    string[] splitXYZ;

    private void Start()
    {
        anim = GetComponent<Animator>();
        
        storeData = new StoreJointData(anim);
        moveAvatar = new MoveAvatar();

        try
        {	
            // TCP 소켓 객체 생성
            sock = new Socket(AddressFamily.InterNetwork, SocketType.Stream, ProtocolType.Tcp);
			
            // 서버에 연결하기
            IPEndPoint serverEP = new IPEndPoint(IPAddress.Parse(serverIP), port);
            sock.Connect(serverEP);
            
            socketConnect = true;
            Debug.Log("Connect Success");
			
            // 서버에 "Start" 문자열 전송
            byte[] buff = Encoding.UTF8.GetBytes(startPoint);
            sock.Send(buff);

        }
        catch (Exception e)
        {
            Debug.Log("오류: " + e);
        }
    }

    private void Update()
    {   

        try {
            
            if (socketConnect)
            {	
                // 전송 받을 바이트 수 먼저 받아오기
                byte[] buffer = new byte[4];
                int byteCount = sock.Receive(buffer);
                Array.Reverse(buffer);
                int dataByteCount = BitConverter.ToInt32(buffer, 0);
                
                // 받으려고 하는 데이터의 수만큼 byte 배열 선언 후 데이터 저장
                byte[] receivedBuffer = new byte[dataByteCount]; 
                byteCount = sock.Receive(receivedBuffer);

                string msg = Encoding.UTF8.GetString(receivedBuffer, 0, byteCount);

                textLine = msg.Split("\n");

                for(int i = 0; i < textLine.Length - 1; i++)
                {

                    string line = textLine[i];

                    splitXYZ = line.Split(' ');

                    if (splitXYZ.All(x => x is string))
                    {
                        realJoint[i].x = float.Parse(splitXYZ[0]);
                        realJoint[i].y = float.Parse(splitXYZ[1]);
                        realJoint[i].z = float.Parse(splitXYZ[2]);
                    }
                    else
                    {
                        realJoint[i].x = 0f;
                        realJoint[i].y = 0f;
                        realJoint[i].z = 0f;
                    }

                }
            }
        }
        catch(Exception e) 
        { 
            Debug.Log("오류 : " + e);
            socketConnect = false;
            sock.Close();
        }

    }

}



통신하면서 겪은 오류 1

13개의 관절 좌표값을 올바르게 전달 받지 못했다.

-0.02801599 1.530401 0.01000143
0.1409548 1.397012 0.02304516
-0.1969829 1.397008 0.01986888
0.37999 1.397016 0.02304507
-0.4362946 1.397007 0.01986932
0.6318882 1.39702 0.02304519
-0.688216 1.397006 0.01986977
0.0739904 0.884761 0.002631317
-0.1300061 0.884761 0.002718598
0.06866307 0.4908463 0.009188599
-0.124666 0.4908559 0.007917952
0.07400247 0.1169901 0.0270884
-0.1299936 0.1169791 0.02709265

통신할 때 13개의 관절 x y z 좌표값이 위와 같은 형태로 전달 되어야 배열에 값이 잘 저장될 수 있다.



-0.02801599 1.530401 0.0100
0.1409548 1.397012 0.02304516345
-0.1969829 1.397008 0.019868883
0.37999 1.397016 0.023045073535
-0.4362946 1.397007 0.0198
0.6318882 1.39702 0.02304519
-0.6 1.397006 0.01986977
0.0739904 0.884761 0.002631317
-0.1300061 0.884761 0.002718598
0.06866307 0.4908463 

이처럼 x, y, z값 중에 z값이 없거나, 13개의 관절 위치값을 받아오지 못하는 일이 발생했다.


원인 :
mediaPipe 모듈로 관절의 위치가 매우 미세하게 측정되어 3차원 좌표값의 길이가 매 프레임마다 달라졌기 때문에 일정한 데이터의 길이로 데이터를 전송 받기 어려웠다.

해결 :
전송 받을 바이트 수를 먼저 받아온 후, 그 바이트 수만큼의 배열을 생성해 데이터를 저장했다.

  // 전송 받을 바이트 수 먼저 받아오기
  byte[] buffer = new byte[4];
  int byteCount = sock.Receive(buffer);
  Array.Reverse(buffer);
  int dataByteCount = BitConverter.ToInt32(buffer, 0);
                
  // 받으려고 하는 데이터의 수만큼 byte 배열 선언 후 데이터 저장
  byte[] receivedBuffer = new byte[dataByteCount]; 
  byteCount = sock.Receive(receivedBuffer);

그런데 이 방식은 크기가 다른 배열을 매 프레임마다 새로 생성하기 때문에 힙 메모리를 낭비할 수 있는 문제가 있다.
코드를 수정한다면, 파이썬 서버에서 관절 데이터를 보내기 전에 일정한 길이의 데이터만 보내고, 유니티에서는 배열을 하나만 생성해서 생성한 메모리를 재사용 할 수 있도록 하는 것이 좋을 듯 하다.




통신하면서 겪은 오류 2

원인 : 바이트의 순서가 바뀌어 올바른 값을 얻지 못했다.

파이썬 측에서 보내려고 하는 바이트 수를 먼저 보낸다.

# 바이트 배열로 변환된 좌표값을 Unity 서버로 전송
coordinates_length = len(coordinates_bytes)
length_bytes = coordinates_length.to_bytes(4, byteorder='big') 

# 길이 정보 전송
client_socket.sendall(length_bytes)

# 바이트 배열로 변환된 좌표값을 Unity 서버로 전송
client_socket.sendall(coordinates_bytes)           

to_bytes()로 전송하려고 하는 바이트의 길이를 빅 엔디안(big endian)으로 저장해서 보냈지만, 유니티 측에서는 리틀 엔디안(little endian)으로 받게 되어 올바른 값을 확인하기 힘들었다.

해결 :

// 전송 받을 바이트 수 먼저 받아오기
byte[] buffer = new byte[4];
int byteCount = sock.Receive(buffer);
Array.Reverse(buffer);

유니티에서 전송받은 데이터를 Array.Reverse() 로 이용해 받은 바이트를 역순으로 바꾼다. 바꾼 값을 확인해 봤을 때 파이썬에서 전송했던 값을 올바르게 받을 수 있었다.

profile
하나씩 뚝딱뚝딱

0개의 댓글