2024년 12월 13일 금요일

Unity : Graphics device is null. (admob 사용시)

AdMob 보상형 광고 문제 해결: Unity Editor에서 Android 단말기로의 전환


최근 Unity를 사용하여 AdMob의 보상형 광고를 구현하는 과정에서, 에디터에서는 정상적으로 작동하던 코드가 Android 단말기에서 오류를 발생시키는 문제가 있었습니다. 이를 해결하기 위해 ADB(Android Debug Bridge)를 통해 로그를 확인한 결과, 다음과 같은 오류 메시지를 발견했습니다.


 I Unity   : Rewarded ad full screen content opened.

 I Unity   : Rewarded ad recorded an impression.

 I Unity   : Rewarded ad paid 0 USD.

 E Unity   : Graphics device is null.

 E Unity   :  #0 0x75f8780ce4 (libunity.so) ? 0x0

 E Unity   :  #1 0x75f8c292e8 (libunity.so) ? 0x0

 E Unity   :  #2 0x75f8a68e20 (libunity.so) ? 0x0

 E Unity   :  #3 0x75f8caef40 (libunity.so) ? 0x0

 E Unity   :  #4 0x75f8cb1c20 (libunity.so) ? 0x0

 E Unity   :  #5 0x75f8cb1fc4 (libunity.so) ? 0x0

 E Unity   :  #6 0x75f844a8e0 (libunity.so) ? 0x0

 E Unity   :  #7 0x75f52cfd30 (libil2cpp.so) ? 0x0

 E Unity   :  #8 0x75f3adc6b0 (libil2cpp.so) ? 0x0

 E Unity   :  #9 0x75f3adc5fc (libil2cpp.so) ? 0x0

 E Unity   :  #10 0x75f8692c10 (libunity.so) ? 0x0

 E Unity   :  #11 0x75f86a1f18 (libunity.so) ? 0x0

 E Unity   :  #12 0x75f86a48d4 (libunity.so) ? 0x0

 E Unity   :  #13 0x75f86ed6fc (libunity.so) ? 0x0

 E Unity   :  #14 0x75f86b1b10 (libunity.so) ? 0x0

 E Unity   :  #15 0x75f86b1f20 (libunity.so) ? 0x0

 E Unity   :  #16 0x75f86b1a7c (libunity.so) ? 0x0

 E Unity   :  #17 0x75f86ef248 (libunity.so) ? 0x0

 E Unity   :  #18 0x75f86ef124 (libunity.so) ? 0x0

 E Unity   :  #19 0x75f84e348c (libunity.so) ? 0x0

 E Unity   :  #20 0x75f845ca24 (libunity.so) ? 0x0

 E Unity   :  #21 0x75f3bb0880 (libil2cpp.so) ? 0x0

 E Unity   :  #22 0x75f3bb1168 (libil2cpp.so) ? 0x0

 E Unity   :  #23 0x75f49dc658 (libil2cpp.so) ? 0x0

 E Unity   :  #24 0x75f3adc6b0 (libil2cpp.so) ? 0x0

 E Unity   :  #25 0x75f3adc5fc (libil2cpp.so) ? 0x0

 E Unity   :  #26 0x75f3a


이중 "Unity   : Graphics device is null."가 문제였으며 구글링을 해보았습니다.

이 오류는 Unity의 그래픽 장치가 null이라는 것을 의미하며, UI 관련 작업이 메인 스레드에서 제대로 처리되지 않았음을 암시합니다. 여러 자료를 참고한 결과, UI를 메인 스레드에서 처리해야 한다는 공통적인 의견이 있었습니다. 

참고 블로그들...

https://bonnate.tistory.com/552

https://sam0308.tistory.com/84


문제의 원인


문제를 분석해보니, 보상형 광고가 표시된 후 사용자를 보상하는 과정에서 UI 업데이트가 메인 스레드 외부에서 이루어져 오류가 발생한 것으로 보입니다. 이를 해결하기 위해, UI 처리를 메인 스레드로 옮기는 작업이 필요했습니다.


해결 방법


기존의 `GetReward` 메소드는 광고 보상 처리를 즉시 진행하고 있었으나, 이를 메인 스레드에서 처리하도록 수정했습니다. 아래는 수정된 코드입니다.


개선 전 코드


```

public void GetReward(Reward reward)

{

    GameManager.instance.ShowRewardGetNoGen(GetItem.ItemNo.GEM, 5);

}

```


개선 후 코드


변수를 추가하여 플래그를 설정하고, UI 처리를 `Update` 메소드로 옮겼습니다.


```

private bool getReward = false;


public void GetReward(Reward reward)

{

    getReward = true;

}


private void Update()

{

    if (getReward)

    {

        getReward = false;

        GameManager.instance.ShowRewardGetNoGen(GetItem.ItemNo.GEM, 5);

    }

}

```


이렇게 수정한 후, 문제는 해결되었고 광고가 정상적으로 작동했습니다.


검색해보니 아래와 같은 것도 있던데 실제 사용해 보지는 않았습니다. 참고용으로만 적어둡니다.

MobileAds.RaiseAdEventsOnUnityMainThread = true;



결론


AdMob 보상형 광고를 Unity에서 사용할 때, UI 업데이트를 메인 스레드에서 처리하는 것이 필요합니다. 그리고 여기에서는 이러한 방식으로 문제를 해결할 수 있었습니다.


2024년 12월 5일 목요일

Unity GooglePlayGamesPlugin-2.0.0 잘안될때 확인해야 할것

Unity GPGS 때문에 여기까지 검색해봤다면 기본적인 것은 알고 있다고 생각됩니다.

1. 가장 먼저 확인할 부분은 키부분입니다. 해당 부분은 다른 게시글에서도 쉽게 찾을 수 있을 것입니다.

2. 여기에서는 Authenticate 함수 호출시 에러가 나는 부분에 대한 설명입니다.


개발자 문서 : https://developer.android.com/games/pgs/unity/unity-start?hl=ko

github : https://github.com/playgameservices/play-games-plugin-for-unity


v11부터 바뀐것 같기는 하나 v11로 테스트 해보지는 않았지만, v11과 동일하게 2.0.0에서 signin 필요 없습니다.



위 링크를 보면 아래 내용이 나옵니다.

게임이 열리면 로그인 서비스를 사용하여 Play 게임즈 서비스로 연결이 자동으로 시도됩니다. 연결되면 게임에 로그인 메시지가 표시되고 Unity용 Google Play 게임즈 서비스 플러그인을 사용할 준비가 됩니다.

사용자가 기기에서 Google Play 게임즈 서비스를 사용한 적이 없는 경우 일회성 설정 화면으로 자동으로 안내되어 Play 게임즈 계정을 생성합니다.

스크립트의 Start 메서드에서 자동 로그인 시도 결과를 수신 대기하고, 인증 상태를 가져오고, Play 게임즈 서비스 기능을 중지(사용자가 로그인하지 않은 경우)합니다.

Unity 플러그인 버전이 v11 이전인 경우 로그인 기능을 사용할 수 없습니다.

    using GooglePlayGames;

    public void Start() {
      PlayGamesPlatform.Instance.Authenticate(ProcessAuthentication);
    }

    internal void ProcessAuthentication(SignInStatus status) {
      if (status == SignInStatus.Success) {
        // Continue with Play Games Services
      } else {
        // Disable your integration with Play Games Services or show a login button
        // to ask users to sign-in. Clicking it should call
        // PlayGamesPlatform.Instance.ManuallyAuthenticate(ProcessAuthentication).
      }
    }

즉 저 코드를 넣으면 된다고 되어있는데 실제해보면 동작하지 않습니다.

핵심은 주석 처리 되어 있는 아래쪽 코드인데, 해당 주석을 풀면 동작을 하게 됩니다. 자동로그인으로 바뀌었고 최초 한번은 수동 로그인이 필요하기 때문에 ManuallyAuthenticate 호출하도록 하던가 아니면 Authenticate 함수 실패시 ManuallyAuthenticate가 자동으로 호출 하도록 해당 함수를 사용하면 된다는 의미입니다.

단순히 주석을 제거하지 말고 버튼을 만들어서 수동으로 ManuallyAuthenticate 코드가 수행되도록 작업하는 것이 좋습니다. 위와 같은 상태로 구현을 하면 사용자가 로그인을 안한 경우 매번 실행시 구글 로그인을 하라는 팝업이 뜨는 경험을 겪게 됩니다.

막상 주석을 풀어서 사용하기 위해, 주석된 코드를 자세히 보면 ManuallyAuthenticate(ProcessAuthentication) 호출 후 callback으로 실패하게되면 ProcessAuthentication 함수가 무한이 불리게 됩니다.

제가 사용하는 참고 코드입니다.



    void Start()
    {
        PlayGamesPlatform.Activate();
        PlayGamesPlatform.Instance.Authenticate(PorcessAuthenticationStart);
    }

    public void GPGS_Login()
    {
        PlayGamesPlatform.DebugLogEnabled = true;
        PlayGamesPlatform.Instance.ManuallyAuthenticate(PorcessAuthentication);
    }
    internal void PorcessAuthentication(SignInStatus signInStatus)
    {
        if (signInStatus == SignInStatus.Success)
        {
            string displayName = PlayGamesPlatform.Instance.GetUserDisplayName();
            string userID = PlayGamesPlatform.Instance.GetUserId();
            this.displayName = displayName;
            this.userID = userID;
            logText.text = "logined:" + displayName;
        }
        else
        {
            logText.text = "*** Failed to authenticate with " + signInStatus;
        }
    }
    internal void PorcessAuthenticationStart(SignInStatus signInStatus)
    {
        if (signInStatus == SignInStatus.Success)
        {
            string displayName = PlayGamesPlatform.Instance.GetUserDisplayName();
            string userID = PlayGamesPlatform.Instance.GetUserId();
            this.displayName = displayName;
            this.userID = userID;
            Debug.Log("start login Success");
            logText.text = "logined:"+displayName;
        }
        else
        {
            Debug.Log("start login failed");
        }
    }


추가로 여러번 테스트할때 자동 로그인이 되어서 불편합니다. 로그아웃 하는 방법은 Play 게임 앱을 스토어에서 설치를 하면 내데이터 항목내 게임 계정 변경 메뉴가 있습니다. 해당 메뉴의 게임별 계정 변경 메뉴를 선택하면 게임별 로그 아웃 가능합니다.



2024년 11월 21일 목요일

langchain + llama 3.2 + ollama 이용 RAG 구현

Langchain을 이용하여 LLaMA와 Ollama를 사용한 Retrieval-Augmented Generation (RAG) 시스템을 구현하려면, Langchain의 Document Loaders, Vector Stores, 그리고 LLM 연결 기능을 결합해야 합니다. 

LLaMA를 쉽게 사용하기 위해서 Ollama 를 선택했습니다. 여러가지 사유가 있겠지만 GPU설정도 따로 안해줘도 되고 proxy 설정이 다른 것보다 쉬웠는 측면도 있었습니다.

다른 예제들을 보면 OpenAI api를 이용하는 경우가 있었는데 여기에서는 local Embeddings을 사용해보도록 하겠습니다.


1. 일단 Ollama를 설치해줍니다. 

아래 다운로드 주소에서 본인 운영체제에 맞는 적당한 버전을 다운로드 해줍니다. (설치 위치 변경이 안되는 부분이 조금 아쉽습니다.)

https://ollama.com/download


2. python 설치 + venv 사용

이 부분은 인터넷 검색해보시기 바랍니다. windows와 ubuntu venv 명령이 조금 다릅니다.

여기에서는 python은 3.12사용 하였습니다.

 

3. llama 설치

hugging face에서 llama를 직접 다운 받아서 하는 방법도 있겠지만 확장자도 변환해야 하고 복잡하므로 ollama 에서 직접 설치하도록 합니다. 3.2 3b 모델로 설치하겠습니다. 지원 모델은 아래 링크에서 확인이 가능합니다. 대략 2GB 정도 하네요

https://ollama.com/library/llama3.2

ollama run llama3.2

llama가 제대로 실행되었다면 prompt가 나옵니다. 몇가지 질문을 해보면 잘 동작되는 모습을 볼 수 있습니다.


지금 설치된 LLM이 뭐가 있는 확인할때는 ollama ls 명령을 사용합니다.
(langchain) D:\dev\venv\langchain>ollama ls
NAME               ID              SIZE      MODIFIED
llama3.2:latest    a80c4f17acd5    2.0 GB    11 minutes ago

4. python 필요 패키지 설치

pip install langchain faiss-cpu langchain-community langchain_ollama

주피터 노트북 설치 편하게 작업하기 위해서 추가로 설치합니다.

pip install notebook

python -m notebook 으로 주피터노트북 실행합니다.

기타 패키지들이 있는데 에러가 발생하면 추가해서 pip로 설치해 줍니다. 이것 저것 시도하다보니 정확하게 어떤 패키지였는지 기억하지 못했습니다.


5. embedding 모델 설치

local Embeddings 을 하기 위해서 ollama에 기본적으로 nomic-embed-text 모델을 제공해 줍니다. embedding 모델에 대한 정보는 다음 링크 참고하시기 바랍니다.

https://ollama.com/blog/embedding-models

ollama pull nomic-embed-text

막상해보니 한글이라서 그런지 생각보다 잘안됩니다. 그래서 추가로 large 모델을 사용하였습니다.

ollama pull mxbai-embed-large

2024.11.24 추가 수정

확인해보니 LLM으로 사용했던 모델을 그대로 사용해도 embedding 하는데 전혀 문제가 없었습니다. 따라서 아래와 같이 수정하고 나서는 결과가 더 좋게 나왔습니다.

local_embeddings = OllamaEmbeddings(model="llama3.2:latest") 


6. llama3.2 를 Ollama에서 실행

정상적으로 ollama 가 동작하고 모델도 다운로드 되었다면 "부산"이라고 나오게 됩니다.


from langchain.prompts import PromptTemplate
from langchain_core.output_parsers import StrOutputParser
from langchain_ollama import OllamaLLM

# 기본 예제
llm = OllamaLLM(model="llama3.2:latest")


template = "{country}의 수도는?"

prompt = PromptTemplate.from_template(template=template)
chain = prompt | llm | StrOutputParser()

result = chain.invoke({"country", "한국"})
print(result)

부산

7. 복잡한 동작 텍스트 분할하기

여기에서는 긴텍스트가 있다고 파일로 존재하고 있다고 생각하고 구현하였습니다.

해당 파일에서 파일을 읽어서  chunk size로 나누게 합니다. 여기에서는 좀 작게 여러개로 나누어 봤습니다.


from langchain_text_splitters import RecursiveCharacterTextSplitter
from langchain.docstore.document import Document

# 텍스트 파일 경로 설정
file_path = "data/data.txt"

# 텍스트 파일을 읽어서 로딩
try:
    with open(file_path, "r", encoding="utf-8") as file:
        text = file.read()
except FileNotFoundError:
    raise Exception(f"Failed to load text file at path: {file_path}")

# 텍스트를 문서 형태로 변환 (단순히 리스트로 사용 가능)
document = Document(page_content=text)

# 텍스트를 분할하기 위해 텍스트 분할기 사용
text_splitter = RecursiveCharacterTextSplitter(chunk_size=500, chunk_overlap=100)
all_splits = text_splitter.split_documents([document])

# 결과 출력
i = 1
for split in all_splits:
    print(f"Chunk {i}:{split.page_content}\n")
    i += 1

8. Chroma 를 이용해 벡터스토어에 저장을 합니다. 

Chroma는 오픈 소스 벡터 데이터베이스입니다. 파일에 저장도 하는데 여기에서는 따로 저장하지는 않았습니다.

https://python.langchain.com/docs/integrations/vectorstores/chroma/




# https://ollama.com/blog/embedding-models

from langchain_chroma import Chroma
from langchain_ollama import OllamaEmbeddings

#local_embeddings = OllamaEmbeddings(model="nomic-embed-text") # 이것 사용했는데 잘안됨

## ollama pull mxbai-embed-large 다른것 설치함
local_embeddings = OllamaEmbeddings(model="mxbai-embed-large") 
vectorstore = Chroma.from_documents(documents=all_splits, embedding=local_embeddings)

embedded_query = local_embeddings.embed_query("LangChain 에 대해서 상세히 알려주세요.")
# 임베딩 차원 출력
len(embedded_query)


9. 유사도 검색

similarity_search 메서드는 Chroma 데이터베이스에서 유사도 검색을 수행합니다. 

실제 해보면 한글이라서 그런지 검색이 제대로 되지는 않았습니다. FAISS, Chroma 모두 테스트 해봤는데 결과는 비슷합니다. Embeddings이 문제인것 같았으나 추가 테스트는 해보지 못했습니다.



question = "대한체육회장 선거 날짜는?"
#question = "부상을 당한 사람은 누구인가요?"
docs = vectorstore.similarity_search(question)
len(docs)

10. 종합


from langchain_core.runnables import RunnablePassthrough
from langchain_core.output_parsers import StrOutputParser
from langchain_core.prompts import ChatPromptTemplate

def format_docs(docs):
    return "\n\n".join(doc.page_content for doc in docs)


RAG_TEMPLATE = """
You are an assistant for question-answering tasks. Use the following pieces of retrieved context to answer the question. If you don't know the answer, just say that you don't know. Use three sentences maximum and keep the answer concise.


{context}


Answer the following question:

{question}"""

rag_prompt = ChatPromptTemplate.from_template(RAG_TEMPLATE)

chain = (
    RunnablePassthrough.assign(context=lambda input: format_docs(input["context"]))
    | rag_prompt
    | model
    | StrOutputParser()
)

#question = "토트넘은 몇위가 되었나요?"

docs = vectorstore.similarity_search(question)

# Run
chain.invoke({"context": docs, "question": question})


https://github.com/donarts/sourcecode/blob/main/ml/_01_langchain_RAG/langchain_llama%203.2_ollama_RAG.ipynb

2024년 9월 4일 수요일

bookscan image enhance(python으로 스캔한 책 전처리 )

 book scan(카메라로 찍은 사진) 한 파일의 품질을 올리는 방법을 고민을 하게 되었습니다.

ChatGPT를 이용해서 굉장히 많은 기법들을 소개(?) 받았고, 이것 저것 조합하면서 가장 무난한 방법을 찾아봤습니다.


sharpen -> bilateralFilter -> 배경 부분만 밝기 높이기 순으로 되어 있습니다.


아래 코드를 참고하시고, 모르는건 ChatGPT에 문의해보면 자세히 알려줍니다.

세상이 편해졌네요. 그래도 이것저것 해보고 제일 좋은지 어떤지는 인간이 좀 판단을 해야합니다. filename만 적절히 변경 하시면 됩니다.

filename=r'D:\temp\1_20240830\0066.jpg'

import cv2
import numpy as np
import matplotlib.pyplot as plt

# 이미지 불러오기
image = cv2.imread(filename)
image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)

# 1. 샤프닝 적용
kernel_sharpen = np.array([[-1, -1, -1],
[-1, 9, -1],
[-1, -1, -1]])
sharpened_image = cv2.filter2D(image_rgb, -1, kernel_sharpen)

# 2. bilateralFilter 적용
bilateral_filtered_image = cv2.bilateralFilter(sharpened_image, d=9, sigmaColor=75, sigmaSpace=75)

# 3. Grayscale 변환
gray = cv2.cvtColor(bilateral_filtered_image, cv2.COLOR_RGB2GRAY)

# Otsu's Thresholding을 사용하여 글씨와 배경 분리
# Otsu's Method는 최적의 threshold 값을 자동으로 선택함
_, mask = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)

# 선택된 Otsu Threshold 값 출력
otsu_threshold_value = _ # Otsu가 선택한 최적의 Threshold
print(f"Otsu's Threshold Value: {otsu_threshold_value}")

# 배경 마스크를 생성 (글씨가 있는 부분은 제외)
background_mask = cv2.bitwise_not(mask)

# 배경을 밝게 조정 (50 정도 밝기 증가)
upcolor = 50
background_brightened = cv2.add(bilateral_filtered_image, np.array([upcolor, upcolor, upcolor], dtype=np.float32), mask=background_mask)

# 글씨 부분은 원본에서 그대로 가져옵니다.
final_image = cv2.add(background_brightened, cv2.bitwise_and(bilateral_filtered_image, bilateral_filtered_image, mask=mask))

# 결과 출력
plt.figure(figsize=(18, 6))
plt.subplot(131), plt.imshow(image_rgb), plt.title('Original')
plt.subplot(132), plt.imshow(bilateral_filtered_image), plt.title('Sharpened + Bilateral Filtered')
plt.subplot(133), plt.imshow(final_image), plt.title('Background Brightened with Otsu Thresholding')
plt.axis('off')
plt.show()


소스는 아래 링크 참고

Create jpg_to_enhance3.py · donarts/sourcecode@54ee6af · GitHub


추가 개선 


threshold -> adaptiveThreshold 로 변경함

filename=r'D:\temp\0005.jpg'

import cv2
import numpy as np
import matplotlib.pyplot as plt

# 이미지 불러오기
image = cv2.imread(filename)
image_rgb = cv2.cvtColor(image, cv2.COLOR_BGR2RGB)

# 1. 샤프닝 적용
kernel_sharpen = np.array([[-1, -1, -1],
[-1, 9, -1],
[-1, -1, -1]])
sharpened_image = cv2.filter2D(image_rgb, -1, kernel_sharpen)

# 2. bilateralFilter 적용
bilateral_filtered_image = cv2.bilateralFilter(sharpened_image, d=9, sigmaColor=75, sigmaSpace=75)

# 3. Grayscale 변환
gray = cv2.cvtColor(bilateral_filtered_image, cv2.COLOR_RGB2GRAY)

# 4. GaussianBlur 적용 (Thresholding 전에)
gray = cv2.GaussianBlur(gray, (5, 5), 0)

# Otsu's Thresholding을 사용하여 글씨와 배경 분리
# Otsu's Method는 최적의 threshold 값을 자동으로 선택함
#_, mask = cv2.threshold(gray, 0, 255, cv2.THRESH_BINARY_INV + cv2.THRESH_OTSU)

# Adaptive Thresholding을 사용하여 글씨와 배경 분리
mask = cv2.adaptiveThreshold(gray, 255, cv2.ADAPTIVE_THRESH_GAUSSIAN_C,
cv2.THRESH_BINARY_INV, 41, 8)

# 선택된 Otsu Threshold 값 출력
#otsu_threshold_value = _ # Otsu가 선택한 최적의 Threshold
#print(f"Otsu's Threshold Value: {otsu_threshold_value}")

# 배경 마스크를 생성 (글씨가 있는 부분은 제외)
background_mask = cv2.bitwise_not(mask)

# 배경을 밝게 조정 (50 정도 밝기 증가)
upcolor = 50
background_brightened = cv2.add(bilateral_filtered_image, np.array([upcolor, upcolor, upcolor], dtype=np.float32), mask=background_mask)

# 글씨 부분은 원본에서 그대로 가져옵니다.
final_image = cv2.add(background_brightened, cv2.bitwise_and(bilateral_filtered_image, bilateral_filtered_image, mask=mask))

# 결과 출력
plt.figure(figsize=(18, 6))
plt.subplot(131), plt.imshow(image_rgb), plt.title('Original')
#plt.subplot(132), plt.imshow(bilateral_filtered_image), plt.title('Sharpened + Bilateral Filtered')
plt.subplot(132), plt.imshow(mask, cmap='gray'), plt.title('mask')
#plt.subplot(132), plt.imshow(gray, cmap='gray'), plt.title('gray')
plt.subplot(133), plt.imshow(final_image), plt.title('Background Brightened with Otsu Thresholding')
plt.axis('off')
plt.show()


sourcecode/python/example/_64_jpeg_to_pdf/jpg_to_enhance3.py at main · donarts/sourcecode · GitHub



2024년 9월 1일 일요일

remove background(배경 제거) in python, rembg 사용

AI로 이미지를 만들면 배경을 제거해야 하는 경우가 있는데 파일이 여러개면 대량으로 제거하기가 불편합니다. 그래서 python 스크립트로 만들어 봤습니다.

GPT로 생성해봤는데 생각보다 잘되지 않아서 작업한 코드를 공유합니다.

이미지 파일들에서 배경을 제거하고 투명 배경을 적용하여 다른 폴더에 저장하는 파이썬 코드입니다. 먼저 이 작업을 수행하기 위해서는 Pillow와 rembg 라이브러리를 사용 해야 합니다.

rembg는 이미지에서 배경을 제거하는데 사용되며, Pillow는 이미지를 처리하고 저장하는 데 사용됩니다.


아래 패키지들을 설치해줍니다.

pip install pillow rembg


rembg 설치하는데 시간이 오래 걸리니 참고 해주세요

전체 코드

import os
from PIL import Image
from rembg import remove, new_session

def remove_background_and_save(source_folder, destination_folder, model_name):
# 세션 객체 생성, 특정 모델 지정
session = new_session(model_name=model_name)

# 소스 폴더에서 모든 파일을 검색
for filename in os.listdir(source_folder):
if filename.endswith('.webp') or filename.endswith('.jpg') \
or filename.endswith('.png') or filename.endswith('.jpeg'):
file_path = os.path.join(source_folder, filename)
output_filename = os.path.splitext(filename)[0] + '.png' # 원래 파일 이름에서 확장자를 png로 변경
output_path = os.path.join(destination_folder, output_filename)
print(file_path)
if os.path.exists(output_path):
print(f"File {output_path} already exists, skipping...")
continue

# 이미지 파일을 열고 배경 제거
with Image.open(file_path) as img:
input_data = img.convert("RGBA") # rembg RGBA 포맷의 바이트 데이터가 필요

# alpha_matting_foreground_threshold: 알파 매팅에서 전경 임계값을 설정합니다. 기본값은 240입니다.
# alpha_matting_background_threshold: 알파 매팅에서 배경 임계값을 설정합니다. 기본값은 10입니다.
result_data = remove(input_data, session=session, alpha_matting=True,
alpha_matting_background_threshold=128) # 배경 제거

# 결과 이미지 생성 및 저장
result_data.save(output_path, format='PNG') # PNG 형식으로 저장


if __name__ == "__main__":
# 폴더 경로 설정 (사용자가 수정 가능)
source_folder = r'D:\dev\game_art\dalle\monster'
destination_folder = r'D:\dev\game_art\dalle\monster\rbg'

model_name = 'birefnet-general' # 'u2net', 'u2net_human_seg' 등 다른 모델도 사용 가능

'''
The available models are:

u2net (download, source): A pre-trained model for general use cases.
u2netp (download, source): A lightweight version of u2net model.
u2net_human_seg (download, source): A pre-trained model for human segmentation.
u2net_cloth_seg (download, source): A pre-trained model for Cloths Parsing from human portrait.
Here clothes are parsed into 3 category: Upper body, Lower body and Full body.
silueta (download, source): Same as u2net but the size is reduced to 43Mb.
isnet-general-use (download, source): A new pre-trained model for general use cases.
isnet-anime (download, source): A high-accuracy segmentation for anime character.
sam (download encoder, download decoder, source): A pre-trained model for any use cases.
birefnet-general (download, source): A pre-trained model for general use cases.
birefnet-general-lite (download, source): A light pre-trained model for general use cases.
birefnet-portrait (download, source): A pre-trained model for human portraits.
birefnet-dis (download, source): A pre-trained model for dichotomous image segmentation (DIS).
birefnet-hrsod (download, source): A pre-trained model for high-resolution salient object detection (HRSOD).
birefnet-cod (download, source): A pre-trained model for concealed object detection (COD).
birefnet-massive (download, source): A pre-trained model with massive dataset.
'''

# 경로가 없다면 생성
if not os.path.exists(destination_folder):
os.makedirs(destination_folder)

remove_background_and_save(source_folder, destination_folder, model_name)


여러가지 모델을 사용해봤는데 제일 잘 나오는 형태가 birefnet-general 모델 이었습니다.
이것은 이미지에 따라 다르므로 여러가지 시도해보시기 바랍니다.

전체 코드는 아래링크에서 다운로드 하세요


2024년 8월 19일 월요일

Google Play Console 개발자 인증, 주소지 인증

Google Play Console 개발자 인증시 주소지 인증 부분에서 막혀서 정리해 보았습니다.

주소지 인증은 google payment 인증하는 부분으로 넘어가게 되는데 허용 되는 서류는

아래에 표기된 서류들입니다.

요즘 온라인으로 명세서가 오다 보니 급하게 준비할 서류가 없었습니다.


검색으로는 주민등록 등본으로 된다는 말이 있어서 업로드 해봤는데 다음으로 넘어가지 않았습니다. (이때는 필요하지 않은 기타 정보를 수정해야 한다는 내용을 숙지 못한 상태에서 제출했던 시점)

주소지가 정확하게 나와야 하는데 email로 발급받은 청구서에는 주소지가 나타나지 않았습니다.
그래서 부랴부랴 신용 카드사에서 모바일 청구서를 신청했습니다. (결재일이 다가오자 미리 발송 되었습니다.)
모바일 링크를 타고 들어가면 종이로 받았을때와 동일한 pdf를 받는 카드사가 있는데 저의 경우 신한 카드가 동일하게 나왔습니다.
해당 내용을 카메라로 찍어도 여전히 다음으로 넘어가지 않고 문제가 있다는 설명만 나왔습니다.

이시점에 필요하지 않은 기타 정보를 수정해야 한다는 내용이 있었고 링크를 타고 들어가면 개인 정보는 검은색으로 지우라는 설명이 있어서, 그림판에서 이름과 주소지만 남기고 아래와 같이 모두 지웠습니다.


평일이 되자 금방 등록 완료 되었습니다.




2024년 8월 11일 일요일

하나의 GameObject에 DOTweenAnimation 여러개 사용 방법

 Unity에서 DOTweenAnimation 을 사용하여 두 개의 애니메이션이 있을 때, 그 중 하나만 동작시키려면 각 애니메이션을 개별적으로 제어해야 합니다.


GPT에 문의해보니 엉뚱한 답을 가르쳐 줘서 구글링해서 찾아냈습니다.


ID를 이용 하면 됩니다. 하나는 ID:1 로 하고 다른 하나는 ID:2를 사용하였습니다.



ById 라는 함수가 준비되어 있습니다. 아래와 같이 string으로 ID값을 넘겨주면 됩니다.
아래는 작업된 코드 입니다.

    public void Attack()

    {        

        DOTweenAnimation ani;

        ani = GetComponent<DOTweenAnimation>();

        if (ani != null)

        {

            ani.DORestartById("1");

        }

    }

    public void FadeOut() 

    {

        DOTweenAnimation ani;

        ani = GetComponent<DOTweenAnimation>();

        if (ani != null)

        {

            ani.DORestartById("2");

        }

    }