이전에 퀘스트2 virtual desktop인가 immersed인가 사용했었는데

거기서는 핸드 컨트롤러로 했었지만

 

컴퓨터에서는 손 자세 추정을 통해서 만들어 보려고 한다.

그런데 mediapipe의 손 랜드마크 추정을 쓰자니

그냥 파이썬처럼 쉽게 할수가 없다.

 

바젤 없이는 빌드는 물론 실행 조차 못한다하고

 

언리얼에선 파이썬 못쓰나 찾아봤지만

게임 실행중에는 안되고 에디터 상에서만 사용가능해보인다.

 

언리얼 미디어파이프 플러그인도 있긴한데 구버전에서 동작해서 잘 안되다보니

아쉬운데로 손 추정에 사용하는 모델을 cv::dnn 으로 추론해서

직접 사용해보려고한다.

 

아래의 코드는 블라즈팜 오닉스 추론 예제인데

일단 파이썬으로 손 검출부터 해도 실제 손 전체를 못감싸준다.

 

여기서 사용한 블라즈팜이 128 x 128 입력으로받는 옛날 모델인데

미디어 파이프 모델들은 tflite로 올라와있지만 최근건 tflite2onnx로 변환이 불가

 

 

https://colab.research.google.com/drive/1dgKi8btAKu2ihB7XOJbNwBcs7246B79_?usp=sharing#scrollTo=dm-e2eb_X7Q7

 

BlazePalm ONNX Inference Test

Colaboratory notebook

colab.research.google.com

 

 

 

 

 

 

전에는 못찾았는데

찾다보니 미디어파이프 tflite 모델을 파이토치 파일로 변환한걸 찾음

 

https://github.com/vidursatija/BlazePalm/blob/master/ML/ConverterPalmDetector.ipynb

 

 

https://github.com/zmurez/MediaPipePyTorch

 

GitHub - zmurez/MediaPipePyTorch: Port of MediaPipe tflite models to PyTorch

Port of MediaPipe tflite models to PyTorch. Contribute to zmurez/MediaPipePyTorch development by creating an account on GitHub.

github.com

 

 

 

 

 

다행이 이 링크에서 제공하는 데모에서는 미디어파이프 없이 blaze 핸드, 얼굴, 팜 등 코드가 동작한다.

여길 참고해서 c++에서 동작하게 고쳐주면 될듯한데

 

쉽진안지만 잘 모르는 pbtxt 범벅된 미디어파이프 코드보단 그래도 알아볼만하다.

팜 디텍터 결과보면 손바닥 영역만 찾아내는게 맞앗었나보다.



 

그러면 블라즈 핸드의 입력으로 들어가는 이미지가 어떻게 되어있나 싶었는데

회전을 풀어낸 1 x 3 x 256 x 256 이미지였다.

 

 

블라즈 팜은 회전이 고려되지 않은 사각형이 나오는데

어떻게 손 방향대로 얼마만큼 회전된걸 알았길래 손을 위를 향하도록 되돌릴수 있었을까.

 

 

 

 

디텍션2roi 함수에서

xy 센터와 스케일, 세타(방향) 정보를 반환하도록 되어있다.

 

 

 

디텍션 2roi 보기전에 드로 디텍션을 봤다.

디텍션즈에 알고보니 뭔진 모를 키포인트 좌표도 같이 있었는데

손 내에 파란점들로

다시보니 손바닥 점들의 좌표를 나타내는 것으로 보인다.

아마 이 손바닥 키포인트들을 이용해서 방향을 찾은듯

 

 

 

 

앞서 블라즈팜에서도 키포인트를 구해서 드로잉했던것처럼

디텍션2roi에선 이 블라즈팜 키포인트로 방향-세타 를 구해내도록 되어있다.

    def detection2roi(self, detection):
        """ Convert detections from detector to an oriented bounding box.

        Adapted from:
        # mediapipe/modules/face_landmark/face_detection_front_detection_to_roi.pbtxt

        The center and size of the box is calculated from the center 
        of the detected box. Rotation is calcualted from the vector
        between kp1 and kp2 relative to theta0. The box is scaled
        and shifted by dscale and dy.

        """
        if self.detection2roi_method == 'box':
            # compute box center and scale
            # use mediapipe/calculators/util/detections_to_rects_calculator.cc
            xc = (detection[:,1] + detection[:,3]) / 2
            yc = (detection[:,0] + detection[:,2]) / 2
            scale = (detection[:,3] - detection[:,1]) # assumes square boxes

        elif self.detection2roi_method == 'alignment':
            # compute box center and scale
            # use mediapipe/calculators/util/alignment_points_to_rects_calculator.cc
            xc = detection[:,4+2*self.kp1]
            yc = detection[:,4+2*self.kp1+1]
            x1 = detection[:,4+2*self.kp2]
            y1 = detection[:,4+2*self.kp2+1]
            scale = ((xc-x1)**2 + (yc-y1)**2).sqrt() * 2
        else:
            raise NotImplementedError(
                "detection2roi_method [%s] not supported"%self.detection2roi_method)

        yc += self.dy * scale
        scale *= self.dscale

        # compute box rotation
        x0 = detection[:,4+2*self.kp1]
        y0 = detection[:,4+2*self.kp1+1]
        x1 = detection[:,4+2*self.kp2]
        y1 = detection[:,4+2*self.kp2+1]
        #theta = np.arctan2(y0-y1, x0-x1) - self.theta0
        theta = torch.atan2(y0-y1, x0-x1) - self.theta0
        return xc, yc, scale, theta

 

 

 

중심점과 세타는 그런데

스케일 계산에서 dscale이 뭔가 싶었는데

모델별로 기본 스케일 값이 있었다. 

 

 

 

 

 

대충 궁금했던 내용은 여기까지보고

구현 흐름을 정리해보자.

 

일단 아래 그림은 블라즈 핸드 동작 흐름 정리된 그림인데

 

 

 

 

 

다시 코드보면서 정리해보자.

 

가장 먼저 이미지 가져온 후 리사이즈 패드 수행한다.

(중간에 다른 모델 코드는 제외)

if len(sys.argv) > 1:
    capture = cv2.VideoCapture(sys.argv[1])
    mirror_img = False
else:
    capture = cv2.VideoCapture(0)
    mirror_img = True

if capture.isOpened():
    hasFrame, frame = capture.read()
    frame_ct = 0
else:
    hasFrame = False

while hasFrame:
    frame_ct +=1

    if mirror_img:
        frame = np.ascontiguousarray(frame[:,::-1,::-1])
    else:
        frame = np.ascontiguousarray(frame[:,:,::-1])

    img1, img2, scale, pad = resize_pad(frame)

 

 

 

 

이 리사이즈 패드는 

블라즈 모델들은 128 x 128, 256 x 256 크기 이미지를 사용해서

리사이징 + 패딩 처리 한뒤, 두 크기 이미지와 스케일과 패드 스케일을 반환한다.

def resize_pad(img):
    """ resize and pad images to be input to the detectors

    The face and palm detector networks take 256x256 and 128x128 images
    as input. As such the input image is padded and resized to fit the
    size while maintaing the aspect ratio.

    Returns:
        img1: 256x256
        img2: 128x128
        scale: scale factor between original image and 256x256 image
        pad: pixels of padding in the original image
    """

    size0 = img.shape
    if size0[0]>=size0[1]:
        h1 = 256
        w1 = 256 * size0[1] // size0[0]
        padh = 0
        padw = 256 - w1
        scale = size0[1] / w1
    else:
        h1 = 256 * size0[0] // size0[1]
        w1 = 256
        padh = 256 - h1
        padw = 0
        scale = size0[0] / h1
    padh1 = padh//2
    padh2 = padh//2 + padh%2
    padw1 = padw//2
    padw2 = padw//2 + padw%2
    img1 = cv2.resize(img, (w1,h1))
    img1 = np.pad(img1, ((padh1, padh2), (padw1, padw2), (0,0)))
    pad = (int(padh1 * scale), int(padw1 * scale))
    img2 = cv2.resize(img1, (128,128))
    return img1, img2, scale, pad

 

 

 

리사이즈 패드해서 얻은 이미지로 

예측해서 정규화된 팜 디택션 결과들을 가져옴

    img1, img2, scale, pad = resize_pad(frame)

    normalized_palm_detections = palm_detector.predict_on_image(img1)

 

 

 

 

프레딕트 이미지를 보면

프레딕트 온 배치로 넘어가지는데

    def predict_on_image(self, img):
        """Makes a prediction on a single image.

        Arguments:
            img: a NumPy array of shape (H, W, 3) or a PyTorch tensor of
                 shape (3, H, W). The image's height and width should be 
                 128 pixels.

        Returns:
            A tensor with face detections.
        """
        if isinstance(img, np.ndarray):
            img = torch.from_numpy(img).permute((2, 0, 1))

        return self.predict_on_batch(img.unsqueeze(0))[0]

 

 

 

 

 

중간에 전처리 -> 모델 추론 -> 후처리 -> 디텍션 획득 -> 비최대 억제 -> 필터링된 디텍션 반환

의 흐름으로 수행된다.

 

주석 내용을보니

디텍션은 0, 17 형태를 갖고있고

ymin, xmin, ymax, xmax, 키포인트xy 6개, 신뢰도 스코어 로 구성된다고 한다.

    def predict_on_batch(self, x):
        """Makes a prediction on a batch of images.

        Arguments:
            x: a NumPy array of shape (b, H, W, 3) or a PyTorch tensor of
               shape (b, 3, H, W). The height and width should be 128 pixels.

        Returns:
            A list containing a tensor of face detections for each image in 
            the batch. If no faces are found for an image, returns a tensor
            of shape (0, 17).

        Each face detection is a PyTorch tensor consisting of 17 numbers:
            - ymin, xmin, ymax, xmax
            - x,y-coordinates for the 6 keypoints
            - confidence score
        """
        if isinstance(x, np.ndarray):
            x = torch.from_numpy(x).permute((0, 3, 1, 2))

        assert x.shape[1] == 3
        assert x.shape[2] == self.y_scale
        assert x.shape[3] == self.x_scale

        # 1. Preprocess the images into tensors:
        x = x.to(self._device())
        x = self._preprocess(x)

        # 2. Run the neural network:
        with torch.no_grad():
            out = self.__call__(x)

        # 3. Postprocess the raw predictions:
        detections = self._tensors_to_detections(out[0], out[1], self.anchors)

        # 4. Non-maximum suppression to remove overlapping detections:
        filtered_detections = []
        for i in range(len(detections)):
            faces = self._weighted_non_max_suppression(detections[i])
            faces = torch.stack(faces) if len(faces) > 0 else torch.zeros((0, self.num_coords+1))
            filtered_detections.append(faces)

        return filtered_detections

 

 

 

 

하지만 이렇게 얻은 팜 디텍션 결과는 정규화된 값으로

이전에 리사이즈 패드에서 얻은 스케일과 패드값으로 반정규화 시켜준다.

    normalized_palm_detections = palm_detector.predict_on_image(img1)

    palm_detections = denormalize_detections(normalized_palm_detections, scale, pad)

 

 

스케일과 패드를 이용해서 반정규화 코드는 이런식

def denormalize_detections(detections, scale, pad):
    """ maps detection coordinates from [0,1] to image coordinates

    The face and palm detector networks take 256x256 and 128x128 images
    as input. As such the input image is padded and resized to fit the
    size while maintaing the aspect ratio. This function maps the
    normalized coordinates back to the original image coordinates.

    Inputs:
        detections: nxm tensor. n is the number of detections.
            m is 4+2*k where the first 4 valuse are the bounding
            box coordinates and k is the number of additional
            keypoints output by the detector.
        scale: scalar that was used to resize the image
        pad: padding in the x and y dimensions

    """
    detections[:, 0] = detections[:, 0] * scale * 256 - pad[0]
    detections[:, 1] = detections[:, 1] * scale * 256 - pad[1]
    detections[:, 2] = detections[:, 2] * scale * 256 - pad[0]
    detections[:, 3] = detections[:, 3] * scale * 256 - pad[1]

    detections[:, 4::2] = detections[:, 4::2] * scale * 256 - pad[1]
    detections[:, 5::2] = detections[:, 5::2] * scale * 256 - pad[0]
    return detections

 

 

 

 

 

반정규화 이후에는 디텍션을 roi로 바꾸는데, (아까본거) xy 센터점과 스케일, 방향을 얻어내고

extract_roi로 원본 이미지로부터 img, affine2, box2를 가져오는데

 

    palm_detections = denormalize_detections(normalized_palm_detections, scale, pad)



    xc, yc, scale, theta = palm_detector.detection2roi(palm_detections.cpu())
    img, affine2, box2 = hand_regressor.extract_roi(frame, xc, yc, theta, scale)

 

 

 

roi 추출 코드를 보면

img는 정규화된 N x C x W x H 형태의 탠서이고

affines는 기울어진손 -> 정방향 손 어파인 변환을 위한, 역 어파인 변환 행렬

points는 어파인 변환에 사용하던 점들로 보인다.

 

    def extract_roi(self, frame, xc, yc, theta, scale):

        # take points on unit square and transform them according to the roi
        points = torch.tensor([[-1, -1, 1, 1],
                            [-1, 1, -1, 1]], device=scale.device).view(1,2,4)
        points = points * scale.view(-1,1,1)/2
        theta = theta.view(-1, 1, 1)
        R = torch.cat((
            torch.cat((torch.cos(theta), -torch.sin(theta)), 2),
            torch.cat((torch.sin(theta), torch.cos(theta)), 2),
            ), 1)
        center = torch.cat((xc.view(-1,1,1), yc.view(-1,1,1)), 1)
        points = R @ points + center

        # use the points to compute the affine transform that maps 
        # these points back to the output square
        res = self.resolution
        points1 = np.array([[0, 0, res-1],
                            [0, res-1, 0]], dtype=np.float32).T
        affines = []
        imgs = []
        for i in range(points.shape[0]):
            pts = points[i, :, :3].cpu().numpy().T
            M = cv2.getAffineTransform(pts, points1)
            img = cv2.warpAffine(frame, M, (res,res))#, borderValue=127.5)
            img = torch.tensor(img, device=scale.device)
            imgs.append(img)
            affine = cv2.invertAffineTransform(M).astype('float32')
            affine = torch.tensor(affine, device=scale.device)
            affines.append(affine)
        if imgs:
            imgs = torch.stack(imgs).permute(0,3,1,2).float() / 255.#/ 127.5 - 1.0
            affines = torch.stack(affines)
        else:
            imgs = torch.zeros((0, 3, res, res), device=scale.device)
            affines = torch.zeros((0, 2, 3), device=scale.device)

        return imgs, affines, points

 

 

 

이렇게 구한 텐서(블롭) img를 

hand_regressor(랜드마크 추정기)에 넣어서 플래그, 핸드, 정규화된 랜드마크들을 받아내느데 

 

    flags2, handed2, normalized_landmarks2 = hand_regressor(img.to(gpu))
    landmarks2 = hand_regressor.denormalize_landmarks(normalized_landmarks2.cpu(), affine2)

 

 

핸드 랜드마크 추론 코드

    def forward(self, x):
        if x.shape[0] == 0:
            return torch.zeros((0,)), torch.zeros((0,)), torch.zeros((0, 21, 3))

        x = F.pad(x, (0, 1, 0, 1), "constant", 0)

        x = self.backbone1(x)
        y = self.backbone2(x)
        z = self.backbone3(y)
        w = self.backbone4(z)

        z = z + F.interpolate(w, scale_factor=2, mode='bilinear')
        z = self.blaze5(z)

        y = y + F.interpolate(z, scale_factor=2, mode='bilinear')
        y = self.blaze6(y)
        y = self.conv7(y)

        x = x + F.interpolate(y, scale_factor=2, mode='bilinear')

        x = self.backbone8(x)

        hand_flag = self.hand_flag(x).view(-1).sigmoid()
        handed = self.handed(x).view(-1).sigmoid()
        landmarks = self.landmarks(x).view(-1, 21, 3) / 256

        return hand_flag, handed, landmarks

 

 

 

마지막으로 랜드마크 반정규화를보면

랜드마크를 어파인 역변환 행렬과 곱하는 내용

여기서 나오는 래졸루션은 블라즈 핸드에 입력으로 사용하는 256

 

    def denormalize_landmarks(self, landmarks, affines):
        landmarks[:,:,:2] *= self.resolution
        for i in range(len(landmarks)):
            landmark, affine = landmarks[i], affines[i]
            landmark = (affine[:,:2] @ landmark[:,:2].T + affine[:,2:]).T
            landmarks[i,:,:2] = landmark
        return landmarks

 

class BlazeHandLandmark(BlazeLandmark):
    """The hand landmark model from MediaPipe.
    
    """
    def __init__(self):
        super(BlazeHandLandmark, self).__init__()

        # size of ROIs used for input
        self.resolution = 256

 

 

일단 여기까지 파이썬 코드 살펴봤으면

c++로 만들어볼수 있을듯

GetDC로 디바이스 컨텍스트 가져와서

주모니터는 가져와지는데 다른 보조모니터는 어떻게 가져오나 해매다가 좋은 참고자료 찾음

 

https://stackoverflow.com/questions/53329673/c-getdc-all-monitors

 

C++ GetDC All Monitors

Basically, I'm making something that imitates a screen melting effect, but I can only get it working on my primary monitor. I've looked up as much as I could and there was only one forum on GetDC f...

stackoverflow.com

 

위 링크에 따르면 GetDC(0) 하면 전체 모니터 DC를 가져온다고 하는데

BitBlt에서 좌표 설정해야 된다고 하더라

 

 

중앙에 위치한 3번 모니터의 시작점은 1920, 0이므로 

 

 

 

 

 

 

BitBlt에서 x1 자리 매개변수 값을 1920으로 고치면 3번 보조모니터의 화면이 나온다.

cv::Mat ADesktopGameModeBase::GetScreenToCVMat()
{
	HDC hScreenDC = GetDC(NULL);
	HDC hMemoryDC = CreateCompatibleDC(hScreenDC);
	int screenWidth = GetDeviceCaps(hScreenDC, HORZRES);
	int screenHeight = GetDeviceCaps(hScreenDC, VERTRES);

	HBITMAP hBitmap = CreateCompatibleBitmap(hScreenDC, screenWidth, screenHeight);
	HBITMAP hOldBitmap = (HBITMAP)SelectObject(hMemoryDC, hBitmap);
	BitBlt(hMemoryDC, 0, 0, screenWidth, screenHeight, hScreenDC, 1920, 0, SRCCOPY);
	SelectObject(hMemoryDC, hOldBitmap);

	cv::Mat matImage(screenHeight, screenWidth, CV_8UC4);
	GetBitmapBits(hBitmap, matImage.total() * matImage.elemSize(), matImage.data);

	DeleteDC(hScreenDC);
	DeleteDC(hMemoryDC);

	DeleteObject(hBitmap);
	DeleteObject(hOldBitmap);


	return matImage;
}

 

 

중앙 보조모니터(3번)

 

 

이제 세 모니터를 띄우는게 가능할것같다.

일단 스크린부터 3개로 늘리자

 

 

배치후 BP_Screen1, 2, 3으로 이름 변경

 

 

 

BP 데스크탑 게임모드베이스에서 기존 코드는 잠깐때고

한번 디스플레이 이름 출력해보면

 

 

순서는 섞였지만 나오긴 나온다.

이 디스플레이 이름 확인해서 넣어주면될듯

 

 

일단 1, 3, 4화면을 utexture2d로 변환하는 코드 만들어보면

 

 

 

 

 

 

 

 

 

기존 코드는 가능한 나두고

모니터 가로세로길이

이미지텍스처스크린1,2,3

cvmat 이미지스크린1,2,3

함수로 ScreensToCVMats, CVMatsToTextures

 

UCLASS()
class HANDDESKTOP_API ADesktopGameModeBase : public AGameModeBase
{
	GENERATED_BODY()

protected:
	// Called when the game starts or when spawned
	virtual void BeginPlay() override;

public:
	cv::VideoCapture capture;
	cv::Mat image;

	UFUNCTION(BlueprintCallable)
	void ReadFrame();

	UPROPERTY(EditAnywhere, BlueprintReadWrite)
	UTexture2D* imageTexture;
	void MatToTexture2D(const cv::Mat InMat);

	cv::Mat GetScreenToCVMat();




	int monitorWidth = 1920;
	int monitorHeight = 1080;
	UPROPERTY(EditAnywhere, BlueprintReadWrite)
	UTexture2D* imageTextureScreen1;
	UPROPERTY(EditAnywhere, BlueprintReadWrite)
	UTexture2D* imageTextureScreen2;
	UPROPERTY(EditAnywhere, BlueprintReadWrite)
	UTexture2D* imageTextureScreen3;
	cv::Mat imageScreen1;
	cv::Mat imageScreen2;
	cv::Mat imageScreen3;

	void ScreensToCVMats();
	void CVMatsToTextures();
};

 

 

비긴 플레이에 초기화 코드 추가

#include "DesktopGameModeBase.h"


void ADesktopGameModeBase::BeginPlay()
{
	Super::BeginPlay();

	capture = cv::VideoCapture(0);

	if (!capture.isOpened())
	{
		UE_LOG(LogTemp, Log, TEXT("Open Webcam failed"));
		return;
	}
	else
	{
		UE_LOG(LogTemp, Log, TEXT("Open Webcam Success"));
	}
	imageTexture = UTexture2D::CreateTransient(monitorWidth, monitorHeight, PF_B8G8R8A8);



	imageScreen1 = cv::Mat(monitorHeight, monitorWidth, CV_8UC4);
	imageScreen2 = cv::Mat(monitorHeight, monitorWidth, CV_8UC4);
	imageScreen3 = cv::Mat(monitorHeight, monitorWidth, CV_8UC4);
	imageTextureScreen1 = UTexture2D::CreateTransient(monitorWidth, monitorHeight, PF_B8G8R8A8);
	imageTextureScreen2 = UTexture2D::CreateTransient(monitorWidth, monitorHeight, PF_B8G8R8A8);
	imageTextureScreen3 = UTexture2D::CreateTransient(monitorWidth, monitorHeight, PF_B8G8R8A8);

}

 

 

리드 프레임 수정

void ADesktopGameModeBase::ReadFrame()
{
	/*
	if (!capture.isOpened())
	{
		return;
	}
	capture.read(image);

	cv::Mat desktopImage = GetScreenToCVMat();
	MatToTexture2D(desktopImage);
	*/

	ScreensToCVMats();
	CVMatsToTextures();

}

 

모니터 1, 3, 4를 cv::Mat imageScreen1, 2, 3에 저장하는 코드

void ADesktopGameModeBase::ScreensToCVMats()
{
	HDC hScreenDC = GetDC(NULL);
	HDC hMemoryDC = CreateCompatibleDC(hScreenDC);
	int screenWidth = GetDeviceCaps(hScreenDC, HORZRES);
	int screenHeight = GetDeviceCaps(hScreenDC, VERTRES);

	HBITMAP hBitmap = CreateCompatibleBitmap(hScreenDC, screenWidth, screenHeight);
	HBITMAP hOldBitmap = (HBITMAP)SelectObject(hMemoryDC, hBitmap);

	//screen 1
	BitBlt(hMemoryDC, 0, 0, screenWidth, screenHeight, hScreenDC, 0, 0, SRCCOPY);
	GetBitmapBits(hBitmap, imageScreen1.total() * imageScreen1.elemSize(), imageScreen1.data);

	//screen 2
	BitBlt(hMemoryDC, 0, 0, screenWidth, screenHeight, hScreenDC, 1920, 0, SRCCOPY);
	GetBitmapBits(hBitmap, imageScreen2.total() * imageScreen2.elemSize(), imageScreen2.data);

	//screen 3
	BitBlt(hMemoryDC, 0, 0, screenWidth, screenHeight, hScreenDC, 3840, 0, SRCCOPY);
	GetBitmapBits(hBitmap, imageScreen3.total() * imageScreen3.elemSize(), imageScreen3.data);
	SelectObject(hMemoryDC, hOldBitmap);


	DeleteDC(hScreenDC);
	DeleteDC(hMemoryDC);

	DeleteObject(hBitmap);
	DeleteObject(hOldBitmap);

}

 

 

 

imageScreen1,2,3을 imageTextureScreen1,2,3에 저장하는 코드

이런 식으로 만들 필요는 없는데

이미지텍스처 스크린을 언리얼 프로퍼티로 설정해서 BP에서 사용하려다가 좀 이상하게 됨

void ADesktopGameModeBase::CVMatsToTextures()
{
	for (int i = 0; i < 3; i++)
	{
		if (i == 0)
		{
			FTexture2DMipMap& Mip = imageTextureScreen1->GetPlatformData()->Mips[0];
			void* Data = Mip.BulkData.Lock(LOCK_READ_WRITE);//lock the texture data
			FMemory::Memcpy(Data, imageScreen1.data, imageScreen1.total() * imageScreen1.elemSize());//copy the data
			Mip.BulkData.Unlock();

			imageTextureScreen1->PostEditChange();
			imageTextureScreen1->UpdateResource();
		}
		else if (i == 1)
		{
			FTexture2DMipMap& Mip = imageTextureScreen2->GetPlatformData()->Mips[0];
			void* Data = Mip.BulkData.Lock(LOCK_READ_WRITE);//lock the texture data
			FMemory::Memcpy(Data, imageScreen2.data, imageScreen2.total() * imageScreen2.elemSize());//copy the data
			Mip.BulkData.Unlock();

			imageTextureScreen2->PostEditChange();
			imageTextureScreen2->UpdateResource();
		}
		else if (i == 2)
		{
			FTexture2DMipMap& Mip = imageTextureScreen3->GetPlatformData()->Mips[0];
			void* Data = Mip.BulkData.Lock(LOCK_READ_WRITE);//lock the texture data
			FMemory::Memcpy(Data, imageScreen3.data, imageScreen3.total() * imageScreen3.elemSize());//copy the data
			Mip.BulkData.Unlock();

			imageTextureScreen3->PostEditChange();
			imageTextureScreen3->UpdateResource();
		}


	}


}

 

 

 

데스크탑 게임모드베이스 블루프린트로 들어와

Screen1,2,3(BP_MainWidget) StrScreen1,2,3(포 이치 루프에서 이름으로 찾아내기 위함) 추가

StrScreen1,2,3의 디폴트 값으로 BP_Screen1, BP_Screen2 , BP_Screen3 으로 설정

 

 

 

BP_Screen 모든 액터 가져와 포 이치 루프

아까 정의한 StrScreen1,2,3으로 액터 확인, BP MainWidget 생성후 Screen 1,2,3에 등록

Screen 1, 2, 3(BP_MainWidget)을 루프로 가져온 Screen(BP_Screen)에 등록

 

 

 

 

이벤트 틱에서 각 Screen 변수의 imageWidget과 c++에서 설정한 imageTextureScreen(UProperty)로
SetBrushFromTexture로 텍스쳐 드로잉

 

 

 

전체 스크린에 대해 수행한 결과

 

 

 

 

정리하면 이런형태

 

 

 

 

 

일단 가상 모니터 추가하는 방법 찾아봤는데

 

아래 링크에서

amyuni 소개해줘서 보고 따라해봄

 

https://quasarzone.com/bbs/qf_sw/views/50718

 

모니터 없이 디스플레이 확장 방법 없을까요?

윈도우7쓸대는 디스플레이 확장 메뉴에서 모니터 없어도 감지눌러서 추가하는게 가능했는데지금 윈도우10 최신버전…

quasarzone.com

 

 

 

설명 글대로

 

 

 

압축 풀고 

cmd 관리자 권한으로 deviceinstaller64.exe install usbmmidd.inf usbmmidd로 드라이버 설치후

deviceinstaller64 enableidd 1을 여러번 하면 가상 모니터가 여러번 추가된다

deviceinstaller64 enableidd 0을 하면 가상모니터가 사라지고

 

 

 

 

 

 

듀얼 모니터(1, 2)를 사용 중인데

가상 모니터(3, 4)를 이렇게 배치시켰다.

 

 

1, 3, 4 모니터 3개를 인게임에 띄우고, 보조보니터 2에는 언리얼로 실행시키기 위함

 

 

가상 모니터는 준비되었고, 언리얼 엔진 실행하고 커서로 다른걸 누르면 화면이 급 느려지는데

찾아보니 에디터 프리퍼런스에서 Use less cpu when in background option이 체크된걸 해제해주면 된다더라

 

 

https://forums.unrealengine.com/t/keeping-editor-running-as-normal-when-not-in-focus/367988

 

Keeping editor running as normal when not in focus

Hey guys, how do you keep a game running as normal when the editor is not in focus? I want to test my multiplayer with two editors open (I know you can have two instances on one editor but I need to run two editors for authentication purposes) but one edit

forums.unrealengine.com

 

 

체크 해재해주면 실행중 다른걸 눌러도 속도가 느려지지않는다.

 

 

 

그리고 계속 텍스처 스트리밍 풀 예산 초과 경고가 뜨는데

 

 

아래 링크대로 해결되지 않는다.

 

https://3dperson1.tistory.com/44

 

언리얼 텍스처 스트리밍 풀이 예산을 초과했습니다 해결 방법

안녕하세요 오랜만입니다. 요즘 언리얼만 공부를 하고 있다보니, 미드저니나 챗지피티 등등에 대해 예전만큼 시간을 쏟지 못하고 있습니다. 거두절미하고 언리얼 텍스쳐 스트리밍 풀이 예산을

3dperson1.tistory.com

 

 

 

 

 

 

 

 

작업 관리자로 보니 메모리가 가득 찼다가 non streaming mips가 해제되면서 내려갔다, 다시 올라갓다를 반복하는듯하다.

생성한 utexture2d들이 제때 제거안되서 그런듯.

 

 

 

 

 

 

 

 

생각해보니 HBITMAP 을 cvMat으로 만드는 코드에서

디바이스 콘텍스트와 비트맵을 해제안시켰었다. 수정

 

cv::Mat ADesktopGameModeBase::GetScreenToCVMat()
{
	HDC hScreenDC = GetDC(NULL);
	HDC hMemoryDC = CreateCompatibleDC(hScreenDC);
	int screenWidth = GetDeviceCaps(hScreenDC, HORZRES);
	int screenHeight = GetDeviceCaps(hScreenDC, VERTRES);

	HBITMAP hBitmap = CreateCompatibleBitmap(hScreenDC, screenWidth, screenHeight);
	HBITMAP hOldBitmap = (HBITMAP)SelectObject(hMemoryDC, hBitmap);
	BitBlt(hMemoryDC, 0, 0, screenWidth, screenHeight, hScreenDC, 0, 0, SRCCOPY);
	SelectObject(hMemoryDC, hOldBitmap);

	cv::Mat matImage(screenHeight, screenWidth, CV_8UC4);
	GetBitmapBits(hBitmap, matImage.total() * matImage.elemSize(), matImage.data);

	DeleteDC(hScreenDC);
	DeleteDC(hMemoryDC);

	DeleteObject(hBitmap);
	DeleteObject(hOldBitmap);


	return matImage;
}

 

 

 

 

DC와 비트맵을 cv::Mat 생성 후 해제시켜주니 액세스 위반으로 꺼지는건 해결됬는데

여전히 Non Streaming Pool이 가득차다 풀렸다 반복하는건 여전하다.

 

 

 

 

이미지 텍스처를 제때 해재안해준게 문젠거같아서 

전역 변수로 뺀뒤 중간에 해재하도록 수정

 

void ADesktopGameModeBase::ReadFrame()
{
	/*
	if (!capture.isOpened())
	{
		return;
	}
	capture.read(image);
	*/

	cv::Mat desktopImage = GetScreenToCVMat();
	MatToTexture2D(desktopImage);
}


void ADesktopGameModeBase::MatToTexture2D(const cv::Mat InMat)
{
	imageTexture->ReleaseResource();
	//create new texture, set its values
	imageTexture = UTexture2D::CreateTransient(InMat.cols, InMat.rows, PF_B8G8R8A8);
	if (InMat.type() == CV_8UC3)//example for pre-conversion of Mat
	{
		cv::Mat bgraImage;
		//if the Mat is in BGR space, convert it to BGRA. There is no three channel texture in UE (at least with eight bit)
		cv::cvtColor(InMat, bgraImage, cv::COLOR_BGR2BGRA);

		//Texture->SRGB = 0;//set to 0 if Mat is not in srgb (which is likely when coming from a webcam)
		//other settings of the texture can also be changed here
		//Texture->UpdateResource();

		//actually copy the data to the new texture
		FTexture2DMipMap& Mip = imageTexture->GetPlatformData()->Mips[0];
		void* Data = Mip.BulkData.Lock(LOCK_READ_WRITE);//lock the texture data
		FMemory::Memcpy(Data, bgraImage.data, bgraImage.total() * bgraImage.elemSize());//copy the data
		Mip.BulkData.Unlock();
		imageTexture->PostEditChange();
		imageTexture->UpdateResource();
	}
	else if (InMat.type() == CV_8UC4)
	{
		//actually copy the data to the new texture
		FTexture2DMipMap& Mip = imageTexture->GetPlatformData()->Mips[0];
		void* Data = Mip.BulkData.Lock(LOCK_READ_WRITE);//lock the texture data
		FMemory::Memcpy(Data, InMat.data, InMat.total() * InMat.elemSize());//copy the data
		Mip.BulkData.Unlock();
		imageTexture->PostEditChange();
		imageTexture->UpdateResource();
	}
	//if the texture hasnt the right pixel format, abort.
	imageTexture->PostEditChange();
	imageTexture->UpdateResource();
}

 

 

 

public:
	cv::VideoCapture capture;
	cv::Mat image;

	UFUNCTION(BlueprintCallable)
	void ReadFrame();

	UPROPERTY(EditAnywhere, BlueprintReadWrite)
	UTexture2D* imageTexture;
	void MatToTexture2D(const cv::Mat InMat);


	cv::Mat GetScreenToCVMat();
};

 

이랬더니 release Resouce한후에 CreateTransient해서 그런지 또 액세스 위반 크래시 예외발생

 

 

 

 

 

beginPlay에서 imageTexture를 한번 초기화해주고 

mat to texture2d에서 cvmat -> texture 변환 후 imaget texture에 복사하기만 했더니

스트리밍 풀 문제는 해결됬는데

 

void ADesktopGameModeBase::BeginPlay()
{
	Super::BeginPlay();

	capture = cv::VideoCapture(0);

	if (!capture.isOpened())
	{
		UE_LOG(LogTemp, Log, TEXT("Open Webcam failed"));
		return;
	}
	else
	{
		UE_LOG(LogTemp, Log, TEXT("Open Webcam Success"));
	}
	imageTexture = UTexture2D::CreateTransient(1080, 1920, PF_B8G8R8A8);
}


void ADesktopGameModeBase::ReadFrame()
{
	/*
	if (!capture.isOpened())
	{
		return;
	}
	capture.read(image);
	*/

	cv::Mat desktopImage = GetScreenToCVMat();
	MatToTexture2D(desktopImage);
}


void ADesktopGameModeBase::MatToTexture2D(const cv::Mat InMat)
{
	if (InMat.type() == CV_8UC3)//example for pre-conversion of Mat
	{
		cv::Mat bgraImage;
		//if the Mat is in BGR space, convert it to BGRA. There is no three channel texture in UE (at least with eight bit)
		cv::cvtColor(InMat, bgraImage, cv::COLOR_BGR2BGRA);

		//Texture->SRGB = 0;//set to 0 if Mat is not in srgb (which is likely when coming from a webcam)
		//other settings of the texture can also be changed here
		//Texture->UpdateResource();

		//actually copy the data to the new texture
		FTexture2DMipMap& Mip = imageTexture->GetPlatformData()->Mips[0];
		void* Data = Mip.BulkData.Lock(LOCK_READ_WRITE);//lock the texture data
		FMemory::Memcpy(Data, bgraImage.data, bgraImage.total() * bgraImage.elemSize());//copy the data
		Mip.BulkData.Unlock();
		imageTexture->PostEditChange();
		imageTexture->UpdateResource();
	}
	else if (InMat.type() == CV_8UC4)
	{
		//actually copy the data to the new texture
		FTexture2DMipMap& Mip = imageTexture->GetPlatformData()->Mips[0];
		void* Data = Mip.BulkData.Lock(LOCK_READ_WRITE);//lock the texture data
		FMemory::Memcpy(Data, InMat.data, InMat.total() * InMat.elemSize());//copy the data
		Mip.BulkData.Unlock();
		imageTexture->PostEditChange();
		imageTexture->UpdateResource();
	}
	//if the texture hasnt the right pixel format, abort.
	imageTexture->PostEditChange();
	imageTexture->UpdateResource();
}

 

 

 

nonstreamingpool 가득안차는건 좋은데 

imagetexture에 복붙되는게 쌓여서 그런가 겹친듯한 화면이나온다.

 

 

 

인줄 알았는데 

 

createTransient에서 row와 col을 모르고 반대로 적었더라.

원래 순서대로 1920 x 1080 해주면 

void ADesktopGameModeBase::BeginPlay()
{
	Super::BeginPlay();

	capture = cv::VideoCapture(0);

	if (!capture.isOpened())
	{
		UE_LOG(LogTemp, Log, TEXT("Open Webcam failed"));
		return;
	}
	else
	{
		UE_LOG(LogTemp, Log, TEXT("Open Webcam Success"));
	}
	imageTexture = UTexture2D::CreateTransient(1920, 1080, PF_B8G8R8A8);
}

 

 

 

액세스 위반도 없고, 스트리밍 풀 문제도 해결

 

이전 글에서는 opencv, 웹캠스트리밍 하는 법 대충 정리했는데 

원래 하려던건 언리얼 상에 가상 데스크톱 같이 만들고 싶었다.

 

듀얼 모니터 사용중에

주모니터 화면을 언리얼 화면상에 띄어보려고 시도하는데

windows.h 인클루드 시켰다가 동작안되서 맨붕왔지만

 

 

다행이 아래 링크 참조해서 문제 해결

 

 

 

https://liiyuulab.tistory.com/29

 

UE4 에서 windows.h 포함 헤더 include 시 주의점

나름 평화롭던? 나날을 보내며 UE4 게임 빌드 중 갑작스럽게 error C4003: not enough arguments for function-like macro invocation 'min' 라는 에러를 보게 되었다. 전에는 이런 에러가 한번도 난 적이 없어서 당황.

liiyuulab.tistory.com

 

 

 

 

게임모드해더에 

 

윈도우 헤더파일 인클루드하고,

GetScreenToCVMat 함수추가

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once


#include "Windows/AllowWindowsPlatformTypes.h"
#include <Windows.h>
#include "Windows/HideWindowsPlatformTypes.h"

#include "PreOpenCVHeaders.h"
#include <opencv2/opencv.hpp>
#include "PostOpenCVHeaders.h"

#include "CoreMinimal.h"
#include "GameFramework/GameModeBase.h"
#include "DesktopGameModeBase.generated.h"

/**
 * 
 */
UCLASS()
class HANDDESKTOP_API ADesktopGameModeBase : public AGameModeBase
{
	GENERATED_BODY()

protected:
	// Called when the game starts or when spawned
	virtual void BeginPlay() override;

public:
	cv::VideoCapture capture;
	cv::Mat image;

	UFUNCTION(BlueprintCallable)
	void ReadFrame();

	UPROPERTY(EditAnywhere, BlueprintReadWrite)
	UTexture2D* imageTexture;
	UTexture2D* MatToTexture2D(const cv::Mat InMat);


	cv::Mat GetScreenToCVMat();
};

 

 

 

 

 

기존 리드프레임에선 웹캠 읽을건 아니니 주석처리하고

desktopImage 가져와서 텍스처로 변환

void ADesktopGameModeBase::ReadFrame()
{
	/*
	if (!capture.isOpened())
	{
		return;
	}
	capture.read(image);
	*/

	cv::Mat desktopImage = GetScreenToCVMat();
	imageTexture = MatToTexture2D(desktopImage);
}

 

 

 

윈도우 디바이스 컨텍스트 -> 비트맵 핸들 -> CV:Mat 변환 코드

cv::Mat ADesktopGameModeBase::GetScreenToCVMat()
{
	HDC hScreenDC = GetDC(NULL);
	HDC hMemoryDC = CreateCompatibleDC(hScreenDC);
	int screenWidth = GetDeviceCaps(hScreenDC, HORZRES);
	int screenHeight = GetDeviceCaps(hScreenDC, VERTRES);

	HBITMAP hBitmap = CreateCompatibleBitmap(hScreenDC, screenWidth, screenHeight);
	HBITMAP hOldBitmap = (HBITMAP)SelectObject(hMemoryDC, hBitmap);
	BitBlt(hMemoryDC, 0, 0, screenWidth, screenHeight, hScreenDC, 0, 0, SRCCOPY);
	SelectObject(hMemoryDC, hOldBitmap);

	cv::Mat matImage(screenHeight, screenWidth, CV_8UC4);
	GetBitmapBits(hBitmap, matImage.total() * matImage.elemSize(), matImage.data);

	return matImage;
}

 

여기서 주의할건

위 코드에서 만든 cv::Mat의 타입은 CV_8UC4 

 

기존 텍스처 변환 코드에서는 3채널에 대해서만 존재하므로 

4채널에 대해서 내용 추가

 

3채널의 경우 bgra 4채널로 변환하던거라

4채널 변환없이 그대로 사용하는 내용

UTexture2D* ADesktopGameModeBase::MatToTexture2D(const cv::Mat InMat)
{
	//create new texture, set its values
	UTexture2D* Texture = UTexture2D::CreateTransient(InMat.cols, InMat.rows, PF_B8G8R8A8);

	if (InMat.type() == CV_8UC3)//example for pre-conversion of Mat
	{
		cv::Mat bgraImage;
		//if the Mat is in BGR space, convert it to BGRA. There is no three channel texture in UE (at least with eight bit)
		cv::cvtColor(InMat, bgraImage, cv::COLOR_BGR2BGRA);

		//Texture->SRGB = 0;//set to 0 if Mat is not in srgb (which is likely when coming from a webcam)
		//other settings of the texture can also be changed here
		//Texture->UpdateResource();

		//actually copy the data to the new texture
		FTexture2DMipMap& Mip = Texture->GetPlatformData()->Mips[0];
		void* Data = Mip.BulkData.Lock(LOCK_READ_WRITE);//lock the texture data
		FMemory::Memcpy(Data, bgraImage.data, bgraImage.total() * bgraImage.elemSize());//copy the data
		Mip.BulkData.Unlock();
		Texture->PostEditChange();
		Texture->UpdateResource();
		return Texture;
	}
	else if (InMat.type() == CV_8UC4)
	{
		//actually copy the data to the new texture
		FTexture2DMipMap& Mip = Texture->GetPlatformData()->Mips[0];
		void* Data = Mip.BulkData.Lock(LOCK_READ_WRITE);//lock the texture data
		FMemory::Memcpy(Data, InMat.data, InMat.total() * InMat.elemSize());//copy the data
		Mip.BulkData.Unlock();
		Texture->PostEditChange();
		Texture->UpdateResource();
		return Texture;
	}
	//if the texture hasnt the right pixel format, abort.
	Texture->PostEditChange();
	Texture->UpdateResource();
	return Texture;
}

 

 

언리얼 엔진을 보조모니터에 놓고 실행시키면 주모니터 영역만 나온다.

 

잘나오긴 한데 기존 스크린을 640 x 480으로 해서 그런지 

실제 1920 x 1080 이미지와 맞지 않게 글자가 잘려나온다.

 

 

 

 

스크린(위젯 컴포넌트) 드로 사이즈를 FHD로 고치고

메인 위젯의 이미지 위젯도 FHD로 사이즈 수정

 

 

 

아까보다 화면이 커짐

 

 

 

 

 

 

화면 커지니 글자도 잘나온다.

 

 

 

 

한달 전쯤인가 언리얼에서 opencv사용하겠다고 한참 해맸는데 정리차 다시 남김

어디 자료 참고했는지는 ppt에 남겨놓긴 했는데 여기다 다시 정리하긴 귀찬아서 돌아가는것까지만 바로간다

 

새프로젝트 만들고

 

 

 

 

빈레벨 만들고 플러그인 ㄱㄱ

 

 

 

 

 

 

 

opencv 검색한뒤 체크, yes

재시작 하라면 재시작하고

 

 

 

 

 

대충 새로만든 map을 디폴트로 설정하고

 

 

 

 

C++ Class로

게임모드베이스 desktopgamemodebase

메인위젯 mainwidget

액터 screen

어떻게 만들지는 나중에 보고 일단 이렇게 생성

 

메인위젯은 화면 그리기 용도

스크린은 위젯컴포넌트 추가할 액터

 

 

 

c++ 클래스 다만들고 BP도 생성

 

 

 

 

일단 게임모드베이스에서 웹캠읽어오도록 열고

opencv 인클루드 해보자

 

플러그인 추가했는데도 못찾는다하면

 

리프레시 한번 시켜주자

 

 

 

 

그런데 아직도 안된다.

 

 

 

 

 

빌드cs에opencvhelper, opencv 추가하는걸 잊음 

추가하고 VS끈다음에 다시 리프레시 VS프로젝트하자

 

 

 

그래도 여전히 DesktopGameModeBase에서 문제가 생겼는데

 

해더를 이렇게 안하고

#include "PreOpenCVHeaders.h"
#include <opencv2/imgproc.hpp>
#include <opencv2/highgui/highgui.hpp>
#include <opencv2/opencv.hpp>
#include "PostOpenCVHeaders.h"

 

평소 opencv 쓸때처럼 opencv.hpp만 넣었기 때문

#include <opencv2/opencv.hpp>

 

 

imgproc나 highgui는 필요없는데

 

PreOpencCVHeaders.h와 PostOpenCVHeaders.h가 꼭 있어야 제대로 빌드된다.

ref : https://forums.unrealengine.com/t/using-unreal-engine-5-0-3-or-5-1-0-built-in-opencv-plugin/744951/2

 

이걸 추가하는 이유는 기억 잘 안나는데 

opencv에서 쓰는거랑 unreal에서 opencv 사용할땐 동작 하는게 다르다고한 일본, 중국인 글을 봤었는데

위 링크 말곤 어디엇는지 몰겟다.

 

 

 

아무튼 mat.hpp도 찾아와 들어와짐

 

 

 

 

웹캠 스트리밍할수 있도록

헤더는 이렇게 작성

 

capture, image는 그렇고

블루프린트로 everytick 마다 read frame 호출하도록 UFUNCTION

유저 위젯에 띄우기 위해서 cv::Mat을 UTexture2*D로 바꾸는 함수추가

타 위젯에 쓸수있도록 imageTexutre를 UPROPERTY 등록

 

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once
#include "PreOpenCVHeaders.h"
#include <opencv2/opencv.hpp>
#include "PostOpenCVHeaders.h"


#include "CoreMinimal.h"
#include "GameFramework/GameModeBase.h"
#include "DesktopGameModeBase.generated.h"

/**
 * 
 */
UCLASS()
class HANDDESKTOP_API ADesktopGameModeBase : public AGameModeBase
{
	GENERATED_BODY()

protected:
	// Called when the game starts or when spawned
	virtual void BeginPlay() override;

public:
	cv::VideoCapture capture;
	cv::Mat image;

	UFUNCTION(BlueprintCallable)
	void ReadFrame();

	UPROPERTY(EditAnywhere, BlueprintReadWrite)
	UTexture2D* imageTexture;
	UTexture2D* MatToTexture2D(const cv::Mat InMat);
};

 

 

 

 

 

 

구현 내용은 이런식

MatToTexture2D도 어디서 구글링하다 찾은건데 어디서 했던건지 모르겟네

 

// Fill out your copyright notice in the Description page of Project Settings.


#include "DesktopGameModeBase.h"


void ADesktopGameModeBase::BeginPlay()
{
	Super::BeginPlay();
	capture = cv::VideoCapture(0);

	if (!capture.isOpened())
	{
		UE_LOG(LogTemp, Log, TEXT("Open Webcam failed"));
		return;
	}
	else
	{
		UE_LOG(LogTemp, Log, TEXT("Open Webcam Success"));
	}
}


void ADesktopGameModeBase::ReadFrame()
{
	if (!capture.isOpened())
	{
		return;
	}
	capture.read(image);

	imageTexture = MatToTexture2D(image);
}


UTexture2D* ADesktopGameModeBase::MatToTexture2D(const cv::Mat InMat)
{
	//create new texture, set its values
	UTexture2D* Texture = UTexture2D::CreateTransient(InMat.cols, InMat.rows, PF_B8G8R8A8);

	if (InMat.type() == CV_8UC3)//example for pre-conversion of Mat
	{
		cv::Mat bgraImage;
		//if the Mat is in BGR space, convert it to BGRA. There is no three channel texture in UE (at least with eight bit)
		cv::cvtColor(InMat, bgraImage, cv::COLOR_BGR2BGRA);

		//Texture->SRGB = 0;//set to 0 if Mat is not in srgb (which is likely when coming from a webcam)
		//other settings of the texture can also be changed here
		//Texture->UpdateResource();

		//actually copy the data to the new texture
		FTexture2DMipMap& Mip = Texture->GetPlatformData()->Mips[0];
		void* Data = Mip.BulkData.Lock(LOCK_READ_WRITE);//lock the texture data
		FMemory::Memcpy(Data, bgraImage.data, bgraImage.total() * bgraImage.elemSize());//copy the data
		Mip.BulkData.Unlock();
		Texture->PostEditChange();
		Texture->UpdateResource();
		return Texture;
	}
	//if the texture hasnt the right pixel format, abort.
	Texture->PostEditChange();
	Texture->UpdateResource();
	return Texture;
}

 

 

 

 

빌드 잘되는걸 확인하면 이제 화면을 띄울준비

(핫 리로드 안되면 수동으로 컴파일)

 

 

 

일단 BP_MainWidget에 들어와서

캔버스 판낼에 이미지 넣고 이름 imageWidget, isVariable 설정, 컴파일

 

 

이번엔 BP_Screen으로 들어와

Add로 Widget Component 추가

위젯 컴포넌트 이름을 Screen으로 설정하고 

 

드로 사이즈는 640 x 480

위젯 클레스는 방금 그린 BP_MainWidget

 

 

맵에 BP_Screen 추가하고 플레이어 스타트 앞에 배치

 

 

 

프로젝트 세팅스에서 GameMode를 아까 만든 BP_DesktopGameModeBase로 설정

 

 

 

 

실행해보면 아까 게임보드베이스에 작성했던 Open Webcam Success가 로그로 나온걸 확인 가능

 

 

 

 

 

 

이제 웹캠 영상을 화면에 띄울 준비

 

데스크탑게임모드베이스 블루프린트에서

이벤트 틱에 리드 프레임 연결

 

 

 

 

 

 

리드프레임에선 이미지 가져와서 텍스쳐로 변환함

 

 

 

 

MainScreen 변수 추가하고

Get All Actors of Class로 뷰포트에 있는 BP_Screen들 가져오기

첫번째(인덱스0) BP_Screen 액터로 Set Widget 준비

Create (BP Main Widget ) widget을 생성 후 MainScreen에 등록

이 MainScreen을 BP_Screen  액터의 Set Widget에 등록

 

 

 

 

메인 스크린으로부터 이미지 위젯 가져오기

이미지위젯 -> SetBrushFrom Texture 호출, 리드 프레임과 연결

블루프린트 리드라이트 설정한 imageTexture를 등록

 

 

 

 

 

실행결과

내가 왜 파이썬 로보틱스를 다루려 했더라

전에 못했던 스캔 매칭이나 SLAM 구현을

이젠 진짜 해볼수 있을 만큼 되었다 생각이 들었지만

정작 하면서 영 의욕이 생기질 않더라

 

지금은 이전에 로보틱스를 공부했을 때 만큼

절박함도 잃었고, 영 힘이 나지 않는다.

 

한동안 다른거 하다

나중에 다시 해보고 싶을 때가 오겠지

 

 

-ㅁ너ㅐㅔ채ㅔㄷㅂ퍼ㅐㅔㅐㅔㅓㄷ패ㅔㅍㄷ재ㅔㅏㅜㅇ류라ㅡㄷㅍ자ㅡ

 

 

이번에 정리하고자 하는 내용은 확장 칼만필터

 

확장 칼만 필터 EKF

확장 칼만 필터는 기존의 칼만필터의 상태 전이와 측정이 선형 함수 형태로 이루어져 있었는데

대부분 비선형적인 동작을 하는 현실 문제들에는 부적합함

 

칼만 필터에서 선형함수를 사용하여 다음 상태, 관측치를 표현한 것 대신

비선형 함수 g와 h를 사용해서 계산하는데

 

 

잠깐 가우시안 분포가 선형 변환을 하면 어떻게 되나 보자

(칼만 필터에서는 다음 상태를 구할때 하던 그거)

선형 변환때는 문제 없이 가우시안 분포를 따르는 다음 상태 p(y)를 구할수 있다.

 

 

하지만 문제는 비선형 변환을 할때이다.

가우시안 분포 p(x)를 비선형 함수 g(x)에 넣었더니

지금까지 계속 사용했던 가우시안 분포의 형태가 나오지 않아서 사용할수 없다.

* 파란 가우시안 분포는 p(y)의 평균과 분산을 계산해 만든 가우시안. 

 

 

 

대신 비선형 함수의 해당 지점을 1차 테일러 전개, 선형 근사를 하여 p(x)를 대입시키면

(비선형 함수의 1차 근사식으로 가우시안 분포를 선형 변환)

파란 가우시안 분포에 가까운 (근사화 오차를 가진, 빨간) 가우시안 분포가 나온다.

 

위에선 비선형 함수를 선형 근사하여 어떻게 가우시안 분포를 적용시켰는지 봤는데

 

비선형 함수를 이제 1차 테일러 전개, 선형화하면 아래와 같이 표현 

여기서 G_t는 비선형 함수를 상태와 제어에 대해 편미분하여 구한 자코비안 행렬

비선형 함수에대한 선형 근사식으로

상태 전이 확률을 정리하면 다음과 같음

관측 비선형 함수도 선형 근사를 통해 표현할수 있으며, 관측 확률도 가우시안으로 다음과 같음

 

 

 

 

확장 칼만 필터 알고리즘

기존의 칼만 필터랑 거의 비슷한데 

상태 전이행렬 A, 제어 행렬 B, 관측 행렬 C대신

비선형 함수 f, u와 자코비안 G가 들어간 형태

 

구현 코드랑 같이 봐야 이게 뭔가 이해되겠지만

아직 그전에 짚고 넘어갈게 좀남아서 바로 구현하질 못하겟네 ㅜㅜ

 

재ㅓㄹㅂ제ㅐ러뱆ㅇ재ㅏㅇㅈ배ㅔ

 

가우시안 필터

지금까지 앞에서 정규분포로 자꾸 센서 데이터나 상태를 표현했는데

지난 글에서 작성한 믿음도도 가우시안 분포로 표현해서 다루었었다.

베이즈 필터를 가우시안 분포를 이용해서 정리한게 가우시안 필터라고 할수 있음

칼만 필터는 노이즈를 가우시안 노이즈로 구현한 가우시안 필터를 구현한 것중 하나

 

매개변수 표현(파라미터화)

가우시안 분포 정규 분포는 보통 평균과 분산 두 수치로 표현하는데 이를 모멘트 파라미터라고 하고,

정보 행렬과 정보 벡터로 표현하는 방법을 캐노니컬 파라미터화라고 함.

정보 필터 쓸때가 아니면 보통 모멘트 표현법을 주로 사용함

 

다변량 가우시안분포는 다음 형태로 나타내는데

평균 mu와 공분산 sigma로 모멘트를 이용하여 표현함

 

 

칼만 필터 개요

이전 파이썬 필터 글에서 봤다시피 칼만 필터는 상태를 모멘트 매개변수화하여 표현했었음 

칼만 필터는 아래의 조건들을 따르면 믿음도, 사후확률을 가우시안으로 표현가능함

 

예측 과정에 사용되는 상태 전이 확률과 관측 확률이 가우시안 노이즈를 가지고(시스템/관측 행렬 + 노이즈 했던거),

* 상태 전이 함수는 시스템 행렬 A와 제어 행렬 B, 가우시안 노이즈로 구성된 선형 함수

* 관측 함수는 관측 행렬 C와 가우시안 노이즈로 구성되었음

초기 믿음도, 초기 추정치(칼만 필터할때 맨처음에 초기 평균, 공분산 준거)가 정규 분포를 따름

 

 

 

칼만 필터와 베이즈 필터 비교

칼만 필터는 가우시안 분포로 베이즈 필터를 구현한 것들중 하나라고 했는데

한번 비교해서 보자

 

 

 

베이즈 필터에서는 x t-1에서 상태 전이 확률을 통해 t에서 예측 믿음도를 구했었다.

칼만 필터의 경우 (모멘트 파라미터화로 표현해서 평균과 공분산 사용) 예측 평균을 이전 상태와 제어 신호의 선형 함수로

예측 공분산을 이전 공분산에 앞뒤로 시스템 행렬을 곱해서 구해냄

 

 

칼만 필터에는 베이즈 필터에는 없는 칼만이득 계산하는 과정이 있는데

칼만 이득은 (다음 갱신과정에서 계산할) 측정 잔차(실제 측정값과 예측 측정값)를 얼마나 반영시킬지 조정하는 역활을 함

이제 관측 갱신 단계로 넘어가서보면

베이즈 필터에서는 예측 믿음도에다가 관측 확률을 곱하여 믿음도를 계산하는데

칼만 필터에서 평균은 예측 평균 + 칼만이득(실 관측값 - 예측관측값), 공분산은 칼만 이득과 예측 공분산으로 다음과 같이 계산한다.

 

이거 처음 봤을때 모멘트 파라미터화나 믿음도 자체를 이해못해서

베이즈 필터에서는 확률을 곱해서 계산하다가

칼만필터에서는 평균, 공분산으로 계산만하니 갑자기 왜 이래된건가 전혀 이해를 못했었는데

 

베이즈 필터는 로봇 상태 믿음도를 계산하기 위한 기본 컨샙이고

칼만 필터는 가우시안 분포와 선형 함수로 베이즈 필터를 실제 동작할수 있게 표현한 것 정도로 이해함.

 

 

 

칼만 필터를 이용한 1차원 공간에서의 위치 추정

아래의 그림은 칼만 필터로 믿음도를 예측하고 갱신하는 과정을 개념적으로 보여줌

 

먼저 그림 a에서 가우시안 분포로 초기 믿음도를 표현해냄 

초기 추정치라 뒤에 계산결과에 비해 약간 분산이 퍼진 형태로 되어있음

다음 그림에는 제어하지는 않았지만 먼저 관측을 해서 관측 확률을 구하였음

관측확률과 초기 확률 평균은 대충 비슷하지만, 관측 확률의 경우 분산이 작음

* 칼만 필터에서는 초기 추정치가 매우 중요함. 초기 추정치와 실 관측과 다르면 계산 잘안됨.

베이즈 필터에서 예측 확률(초기엔 움직이지 않았지만)과 관측 확률을 곱했던것 처럼

칼만 필터에서도 예측 믿음도에 칼만이득 x 관측잔차, 공분산 계산해서 반영시킨 결과

현재 상태, 믿음도의 불확실성이 줄어들어 확률이 더 올라감

로봇이 우측으로 이동하면서 시스템 노이즈가 누적되

불확실성이 증가하였음

제어 후 상태, 예측 믿음도의 분산이 커진걸 볼수있다.

이동한 위치(예측 믿음도)는 대충 평균이 21쯤인데

실 관측치로 봤을땐 로봇의 위치가 25쯤 있는걸로 보고 있음

 

 

이 관측치를 상태에 반영시켜 다음과 같은 사후확률, 믿음도를 구해내었음

 

 

 

 

ㅇㅈ배ㅓ햊ㅂ푸재ㅔㅓ브ㅓㅔㅐ배ㅔ재ㅡ재ㅔ

보통 놀면 계속 노는데 하려고 하니 참 손에 안잡힌다

잠깐 놀다 지난번에 하던거 다시 시작함

 

지난글 마지막에는 상태, 제어, 관측을 어떻게 확률로 표현하는지와

이산 시간 흐름에따라 이들의 변화 과정을 동적 베이지안 네트워크로 표현하면서 마쳤음

 

 

믿음도 belief

다음으로 볼게 belief 이걸 나는 믿음도라고 적는게 적당할것 같은 생각이 드는데

belief를 믿음도라 작성해서 하겠다.

베이지안 확률론에서는 빈도주의 확률론처럼 확실히 나온다기 보다는

확률을 주관적인, 믿음의 정도로 표현한다고 하니 믿음도라고 쓰려고함.

 

지난 글에서 초음파 데이터가 저런 식으로 있으면

N(10, 1)인 확률 밀도 함수로 표현하자라고 하자, 치자

라고 정의한게 믿음 같은거라고 해야될까

정규분포 그러니까 가우시안으로 근사 시켜 표현했다고 할수 있을듯하다.

 

 

그러면 여기서 사용하는 믿음도가 무엇을 믿는거냐면

믿음도는 로봇의 상태가 어떻게 되어있을까?에 대한 확률을 의미한다고 하면되겠다.

뒤에 나올 내용이지만.

2차원 공간에서 이동로봇의 상태가 좌표 x, y와 해딩 방향 theta가 있다고 치자

이 상태들을 위 초음파 데이터를 정규분포로 표현한것 처럼 평균 mu와 공분산 cov로 표현한게 믿음도라고 할수 있을것같다.

 

일단 믿음도는 사후확률로 표현하면 다음과 같으며

베이즈 필터로 믿음도를 계산하는 과정은 예측 단계와 관측 단계로 나누어져 있다.

예측 단계는 상태에 제어 신호를 줬을때 바뀐 상태에 대한 예측을 나타낸다.

 

처음 위 식을 봤을때 너무 막연한 표현으로 느껴졌었는데 로봇이 한 지점에 있는데 이를 정규분포로 표현하고 있다가. 10m 전진후에는 어디쯤에 있을까?를 정규분포로 표현하면 평균치는 10m 전진했을지는 몰라도 분산은 이동하면서 생긴 노이즈 때문에 더 퍼져있을것이다.

 

이 퍼진 분산을 고쳐주는게 베이즈 필터의 관측 단계로 갱신된게 로봇 상태에 대한 사후확률, 믿음도가 되겠다.

예측 단계에서는 z 1~t-1, u 1~t-1 까지 진행했으면 x_t-1을 얻었을것이다.

x_t-1에서 u_t만큼 더 가면 시간 t에서의 예측 상태 bar bel(x_t)를 얻는것이고

이 예측상태를 관측 갱신으로 보정하여 믿음도 bel(x_t)를 얻어낸다.

 

 

 

베이즈 필터

아래의 사진은 베이즈 필터의 기본 그림을 표현한다.

이게 왜 필터라고 하냐. 이전에 노이즈가 낀 초음파 거리계의 정량 데이터를 확률 모델로 표현해서 센서의 평균치와 노이즈 퍼진 정도로 표현했는데, 정량적 수치가 아닌 확률로 표현해서 그런듯 하다. 아래 알고리즘은 센서 데이터가 아닌 로봇의 상태를 추정하는 내용이긴 하지만

 

베이즈인것은 베이즈 정리에따라 예측 믿음도와 관측 역확률을 가지고 구하고하자던 사후확률을 계산해냈기 때문인듯

 

 근데 아래 베이즈 필터 알고리즘은 로봇의 상태, 제어, 관측, 예측, 갱신 전체 개념을 확률로써 표현해서 저런 흐름이지 구현 내용을 보기전에 너무 억지로 계산하려 하는건 골치아프다. 뒤에 로봇 문여는 예제와 복도 예제가 있긴한데

로봇 문여는 예제는 계산이 너무 번거로우니 복도 예제를 대충 보고 짚어넘자.

 

 

로봇 복도 예제(마르코브 위치추정)

로봇 복도 예제가 뒤에 있는줄 알았는데 앞에 있었네 --

이 예제는 1차원 공간에서 로봇의 위치가 어디에 있는지를 찾아내는 과정을 설명해주는데

 

우선 가장 먼저 초기 위치를 로봇이 어디있는지 모르니 로봇이 위치 가능성들이 균일하게 퍼져있다고 하자

대충 위치가 이산적으로 10개가 있다고 치고,

균일분포로 초기화 한다는 것은 로봇이 존재할 확률을 균일하게 준다가 되겠다.

이는 초기 믿음도 bel x_t가 된다.

위치 0 1 2 3 4 5 6 7 8 9
확률 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1 0.1

 

베이즈 필터에서는 예측, 관측을 반복한다 했는데 지금은 관측부터 해보자

 

아래는 관측 모델, p(z | x)를 대충 표로 나타내어봤다.

관측 모델은 문이 여러개라 가우시안 혼합 모델 형태를 띄는데,

내가 문의 위치를 알고있고, 지금 바로 문앞에 있어서 저런 식으로 나오나 이런건 개념적으로 넘어가자 

위치 0 1 2 3 4 5 6 7 8 9
확률 0.01 0.01 0.3 0.01 0.3 0.01 0.01 0.01 0.3 0.01

 

그러면 베이즈 필터 관측 단계대로 관측 모델과 초기 믿음도를 곱해주면

아래의 믿음도가 계산된다.

문 3개있는걸 알고, 내가 문앞에 있으니 문 세개 쯤 어딘가에 있구나 정도로 생각하면 될듯. 

 

다음은 제어후 예측 단계가 되겠다.

믿음도 봉우리들이 이동하면서 노이즈가 첨가되 퍼진 형태를 보인다.

그러다 로봇이 또 문을 찾아내었다.

로봇은 문의 위치를 알고 있으니 지금의 (예측) 믿음도와 관측 모델을 곱해 갱신시켜줘보자.

그러면 아래의 믿음도 같이 가장 봉우리가 큰 지점이 나오며 믿음도가 실제 로봇의 위치에 가까워졋다!

이 상태로 로봇이 계속 전진하면

여전히 이동 노이즈로 믿음도 분산이 점점 커지긴 하지만 실 로봇의 위치를 잘 쫓아간다.

 

일단 여기까지 확률 이론으로 로봇 상태 추정을 어떻게 하는가 기본적인 느낌 정리

음 파이썬 로보틱스에서 제공하는 예제들은 probabilisitc robotics 내용들을 파이썬으로 구현한 것들인데

이론 내용을 짚고 넘어갈까,

아니면 바로 구현들어가면서 내용정리할까 고민하다가 

사용되는 기초 이론들을 먼저 정리해야할것 같아서

내가 이해한 것이 정확하지는 않더라도 대강 느낌이라도 정리를 먼저하고 시작하려고함

 

확률로 다루는 로보틱스

- 보통 센서 데이터들을 특정한 정확도의 값(정량 데이터)을 가짐

 초음파 거리계 : 거리

 가속도, 자이로계 : 각속도, 선가속도

 gps : 경도, 위도 위치 좌표 등

- 하지만 이런 센서들 데이터들은 노이즈로 잘못되어있을 수 있으며, 로봇의 상태를 특정한 정확한 값으로 표현은 곤란

 => 센서 데이터를 그대로 쓰기보단 로봇의 상태를 확률 모델로 표현

 

상태를 확률 변수로 모델링 한다는 것은?

- 초음파 데이터가 대충 10cm 거리에 있는 물체를 감지해서 아래와 같은 값을 받앗다고 치자

- 이를 확률 변수 p(x)는 평균 10, 분산 1인 정규분포 X ~ N(10, 1) 를 따른다른다 라는 식으로 데이터 분포를 확률 밀도 함수로 표현한다.(대충 값이 맞든 틀리든)

10.7 10.4 9.2 8.4 11.6 11.0 8.9

 - 로봇과 관련된 상태 값들이 노이즈가 포함되어도 확률 분포로 나타내어 베이즈 필터에 활용된다.

 

확률 개념 정리

- 확률 변수 X가 사건 x일 확률을 다음과 같이 표현

- 확률 변수 X에서 발생할 모든 사건에 대한 확률들의 합은 1

- 앞으로 사용할 확률 분포들은 가우시안 분포를 따른다고 가정함. x가 스칼라인 경우 1차 정규분포는 다음과 같음

- x가 스칼라가 아닌 벡터인 경우 정규 분포는 다음과 같이 표현

- 두 확률 변수 X, Y가 주어지고, X=x, Y=y인 사건이 동시에 일어날 확률/결합확률분포는 다음과 같음

- 만약 두 확률 변수 X, Y가 독립인 경우 두 사건이 동시에 일어날때 결합확률분포는 각 두 확률의 곱

- Y=y 임을 알고있을때 X=x가 발생할 확률(조건부 확률), 맨 우측은 X,Y가 독립인 경우 성립

- 조건부 확률 정의에 따라 다음의 전체 확률 정리가 성립함 

 -> p(x|y)는 y가 발생했을때 p(x, y)가 발생할 확률

 -> p(x|y)에서 전체 y 사건들이 발생한 경우(이산합, 적분)를 곱하여 합하면 p(x)에 대한 확률이 나온다

- 조건부 확률은 역확률과 관련 있음

- 확률 변수 X의 기댓값은 다음과 같이 계산

- 기댓값을 선형 함수로 표현시 다음과 같이 정리됨

- 확률 변수 X의 공분산은 다음과 같음

- 엔트로피는 x가 갖는 기대 정보량으로 -log2 p(x)는 x 인코딩에 가장 적당한 비트 수를 의미함

 

 

 

 

지금 왜 조건부 확률과 역확률을 다루는가

- 역확률 p(y | x)를 이용해 p(x | y)를 계산하기 위함

 * y는 센서 데이터를 의미, p(y | x)는 특정 상태 x에서 y를 얻었을때의 확률 분포, p(x | y)는 센서데이터로 얻은 상태 분포

 * 지금 로봇의 상태값들을 표현하기 위해 확률 분포를 사용하고 있는데, 실제 코드로 구현한걸 봐야 좀더 와닿음. 지금은 어쩔수가 없다.

- p(x | y)는 y라는 데이터가 주어졌을때 로봇의 상태 x에 대한 확률 분포를 나타낸다.

 센서 관측 데이터 y를 얻은 후 상태 x를 구한것이므로 이를 사후 확률 분포라 한다.

 

 뒤에 구현할 예시랑 비슷하게 설명하자면 랜드마크로부터 거리 y가 주어졌을때 로봇의 위치 x가 어디인가를 의미한다

좀더 1차원 공간에서의 (대충) 예시를든다면 남산타워가 4인 지점에 있다는걸 알고 있다.

그리고 나는 거리 측정 센서로 남산타워(랜드마크)까지 거리를 재어보니 2.5, 2.3, 2.7, 2.6, 2.4 같은 식으로 값이 나온다.

나는 아직 내 위치가 어딘지 모른다.

하지만 지금 위치 기준으로 측정 데이터 p(y | x) = X ~ N(2.5, 0.1) 쯤 나온다고 칠수 있을것이고

4 - 2.5를 하면 1.5가 되니 나의 위치는 1.5쯤에 있다를 알수 있는데, 이게 p(x|y)가 된다.

=> p(x | y)를 바로 구할수는 없지만 확률 p(y | x)로부터 구할수 있으므로 p(y | x)를 생성모델이라 부른다.

원점   나의 위치   랜드마크
(남산타워)
         
0 1 ?? 3 4 5 6 7 8 9

- 내가 방금 작성한 설명이 p(x|y)에 대한 식을 계산하는것과 정확하게 일치한것 같지는 않은대 역확률 p(y | x)로 사후확률 p(x | y)를 구한다는것은 이런 느낌인데, 1차원 복도 예제를 봐야 왜 이런식으로 계산되는지 조금 더 와닿을것 같다. 

 

 

상태, 제어, 관측

- x는 상태, u는 제어 데이터, z는 관측 데이터를 의미

- 시간 t1~t2까지 모든 제어, 관측 데이터를 다음과 같이 표현

- 시간 t에서의 상태 x_t는 시간 t-1에서의 상태 x_t-1에 제어 데이터u_t로부터 얻을 수 있음

 x_t-1는 0~t-1까지의 상태와 제어를 합한 결과로 얻을수 있으며, x_t는 우항과 같이 정리하여 구할수 있다.

 이게 무슨소리냐 대충이라도 계산해보자

 1차원 공간에서 t-1 시점에 로봇은 X ~ N(10, 1) 그러니까 10에 있다고 치자

 +5 만큼 이동하는 제어 신호를 줬다고 하자 제어 신호에 대한 상태 전이 확률 분포를 N(5, 2)라 한다면

t 시점에 로봇은 15에 있을 것이다.

하지만 상태 전이 분포의 분산이 2이므로 이동 노이즈가 가미되어 t 시점의 로봇은 X ~ N(15, 3)으로 표현된다.

 

만약 제어 신호의 노이즈가 작아서 상태 전이 확률 분포가 N(5,0.001)이라 치면

이동 후 t 시점에서의 로봇은 X ~ N(15, 1.001)에 위치할 것이다.

로봇이 정확하게 이동했으므로 로봇의 상태에 대한 확률 분포의 분산이 크지 않다.

- 시간 t에서 상태 x_t가 주어질때 관측치, 역확률 모델 p(y|x)은 다음과 같이 표현한다.

- 상태 전이 확률 p(x' | x, u) : 상태 x에서 제어 신호 u를 줬을때 얻은 새로운 상태 x'에 대한 확률 분포

- 관측 확률 p(z | x) : 상태 x에서 관측치 z를 얻은 것이 대한 확률 분포

- 상태 x, 제어 u, 관측 z가 이산 시간 흐름에 따라 결정되어 동적 베이즈 네트워크라 부름 

 

 

 

 

 

 

 

 

 

+ Recent posts