누끼 서버 기능 추가 및 로컬 배경 제거 모듈 설정을 위한 코드 수정. 이미지 프로세서에서 로컬 모듈 사용 여부와 모델명을 설정할 수 있도록 개선하였으며, 요청 처리 로직에서 서버 상태에 따라 백업 모듈을 사용할 수 있는 기능을 추가하였습니다. 관련 UI 요소도 업데이트하여 사용자 편의성을 향상시켰습니다.

This commit is contained in:
9700X_PC 2025-08-21 19:41:22 +09:00
parent b5f6928bb9
commit df7f0b80c1
6 changed files with 648 additions and 11 deletions

View File

@ -0,0 +1,328 @@
; AutoPercenty3 Inno Setup Script
; 이 스크립트는 cx_Freeze로 빌드된 결과물이 있는 "build\exe.win-amd64-3.11" 폴더를 기반으로 인스톨러를 제작합니다.
; 20250821_172944에 생성됨
#define AppId "autopercenty"
#define MyAppName "Edit_PartTimer"
#define MyAppVersion "3.11.0"
#define MyAppPublisher "WhenRideMyCar"
#define MyAppProgramName "편집알바생"
#define MyAppDescription "편집알바생"
#define MyAppCopyright "Copyright 2024"
#define MyAppExeName "Edit_PartTimer3"
#define MySetupName "Edit_PartTimer Setup"
#define MySetupIcon "src/Edit_PartTimer3.ico"
#define MySetupOutputDir "dist/installer"
[Setup]
; 기본 설정
AppId={#AppId}
AppName={#MyAppProgramName}
AppVersion={#MyAppVersion}
AppPublisher={#MyAppPublisher}
DefaultDirName={autopf}\{#MyAppName}
DefaultGroupName={#MyAppPublisher}
OutputDir={#MySetupOutputDir}
OutputBaseFilename={#MySetupName}
SetupIconFile={#MySetupIcon}
Compression=lzma
SolidCompression=yes
; 업데이트 관련 설정 - 권한 최적화
PrivilegesRequired=admin
PrivilegesRequiredOverridesAllowed=dialog
UpdateUninstallLogAppName=yes
AppMutex={#MyAppName}
CloseApplications=yes
RestartApplications=no
; 보안 및 호환성 설정
ArchitecturesAllowed=x64
ArchitecturesInstallIn64BitMode=x64
AllowNoIcons=yes
; 버전 정보
VersionInfoVersion={#MyAppVersion}
VersionInfoCompany={#MyAppPublisher}
VersionInfoDescription={#MyAppDescription}
VersionInfoCopyright={#MyAppCopyright}
VersionInfoProductName={#MyAppProgramName}
VersionInfoProductVersion={#MyAppVersion}
[Languages]
Name: "korean"; MessagesFile: "compiler:Languages\Korean.isl"
[Tasks]
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"
[Dirs]
; 설치 시 {app}\logs 폴더를 생성하고,
; Users 그룹에 'modify' 권한(=쓰기 가능)을 부여
Name: "{app}\logs"; Permissions: users-modify
; 설치 시 {app}\user_data 폴더를 생성하고,
; Users 그룹에 'modify' 권한(=쓰기 가능)을 부여
Name: "{app}\user_data"; Permissions: users-modify
; Playwright 브라우저 폴더를 Program Files 내부에 생성
Name: "{app}\lib\src\browsers\chromium-1155"; Permissions: users-modify
; Playwright 브라우저 사용자폴더를 Program Files 내부에 생성
Name: "{app}\lib\src\browsers\user_data"; Permissions: users-modify
[Files]
; 프로그램 파일만 설치 (항상 덮어쓰기)
Source: "build\exe.win-amd64-3.11\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
; VC++ 재배포 패키지 파일을 임시 폴더({tmp})에 복사
Source: "VC_redist.x64.exe"; DestDir: "{tmp}"; Flags: deleteafterinstall
[Registry]
; Playwright 브라우저 경로를 Program Files 내부로 설정
Root: HKCU; Subkey: "Environment"; ValueType: expandsz; ValueName: "PLAYWRIGHT_BROWSERS_PATH"; ValueData: "{app}\lib\src\browsers"; Flags: preservestringtype
[Icons]
; 시작 메뉴 바로가기
Name: "{group}\{#MyAppProgramName}"; Filename: "{app}\{#MyAppExeName}.exe"
; 바탕화면 바로가기
Name: "{autodesktop}\{#MyAppProgramName}"; Filename: "{app}\{#MyAppExeName}.exe"; Tasks: desktopicon
; 프로그램 제거 바로가기
Name: "{group}\{#MyAppProgramName} 제거"; Filename: "{uninstallexe}"
[Run]
; VC++ 재배포 패키지 설치 (필요할 경우)
Filename: "{tmp}\VC_redist.x64.exe"; Parameters: "/install /passive /norestart"; StatusMsg: "VC++ 재배포 패키지 설치 중..."; Check: NeedsVCredist
; 설치 후 프로그램 실행 (원할 경우)
Filename: "{app}\{#MyAppExeName}.exe"; Description: "{cm:LaunchProgram,{#MyAppProgramName}}"; Flags: nowait postinstall skipifsilent
[Code]
function CompareVersion(V1, V2: string): Integer;
var
P1, P2, N1, N2: Integer;
begin
P1 := 1;
P2 := 1;
Result := 0;
while (Result = 0) and ((P1 <= Length(V1)) or (P2 <= Length(V2))) do begin
while (P1 <= Length(V1)) and (V1[P1] = '.') do Inc(P1);
while (P2 <= Length(V2)) and (V2[P2] = '.') do Inc(P2);
if (P1 <= Length(V1)) and (P2 <= Length(V2)) then begin
N1 := 0; while (P1 <= Length(V1)) and (V1[P1] >= '0') and (V1[P1] <= '9') do begin N1 := N1 * 10 + Ord(V1[P1]) - Ord('0'); Inc(P1); end;
N2 := 0; while (P2 <= Length(V2)) and (V2[P2] >= '0') and (V2[P2] <= '9') do begin N2 := N2 * 10 + Ord(V2[P2]) - Ord('0'); Inc(P2); end;
if N1 < N2 then Result := -1 else if N1 > N2 then Result := 1;
end else begin
if P1 <= Length(V1) then Result := 1 else if P2 <= Length(V2) then Result := -1;
end;
while (P1 <= Length(V1)) and (V1[P1] <> '.') do Inc(P1);
while (P2 <= Length(V2)) and (V2[P2] <> '.') do Inc(P2);
end;
end;
// 파일 또는 폴더 복사 함수
procedure CopyDir(const SourcePath, DestPath: string);
var
FindRec: TFindRec;
SourceFilePath: string;
DestFilePath: string;
begin
ForceDirectories(DestPath);
if FindFirst(SourcePath + '\*', FindRec) then
begin
try
repeat
if (FindRec.Name <> '.') and (FindRec.Name <> '..') then
begin
SourceFilePath := SourcePath + '\' + FindRec.Name;
DestFilePath := DestPath + '\' + FindRec.Name;
if FindRec.Attributes and FILE_ATTRIBUTE_DIRECTORY = 0 then
begin
if FileCopy(SourceFilePath, DestFilePath, False) then
Log('파일 복사 성공: ' + SourceFilePath + ' -> ' + DestFilePath)
else
Log('파일 복사 실패: ' + SourceFilePath);
end
else
CopyDir(SourceFilePath, DestFilePath);
end;
until not FindNext(FindRec);
finally
FindClose(FindRec);
end;
end;
end;
// 디렉토리 삭제 함수
procedure DeleteDir(const DirPath: string);
var
FindRec: TFindRec;
FilePath: string;
begin
if not DirExists(DirPath) then Exit;
if FindFirst(DirPath + '\*', FindRec) then
begin
try
repeat
if (FindRec.Name <> '.') and (FindRec.Name <> '..') then
begin
FilePath := DirPath + '\' + FindRec.Name;
if FindRec.Attributes and FILE_ATTRIBUTE_DIRECTORY = 0 then
begin
if DeleteFile(FilePath) then
Log('파일 삭제 성공: ' + FilePath)
else
Log('파일 삭제 실패: ' + FilePath);
end
else
DeleteDir(FilePath);
end;
until not FindNext(FindRec);
finally
FindClose(FindRec);
end;
end;
if RemoveDir(DirPath) then
Log('디렉토리 삭제 성공: ' + DirPath)
else
Log('디렉토리 삭제 실패: ' + DirPath);
end;
// 프로그램 실행 여부 확인
function IsAppRunning(const FileName: string): Boolean;
var
Handle: THandle;
begin
Handle := FindWindowByWindowName('{#MyAppProgramName}'); // 프로그램의 윈도우 타이틀로 찾기
Result := (Handle <> 0);
end;
// 프로그램 종료
procedure CloseApplication(const FileName: string);
var
Handle: THandle;
begin
Handle := FindWindowByWindowName('{#MyAppProgramName}');
if Handle <> 0 then
begin
PostMessage(Handle, 18, 0, 0); // WM_QUIT
Sleep(1000); // 종료 대기
end;
end;
// VC++ 재배포 패키지 필요 여부 확인
function NeedsVCredist: Boolean;
begin
if RegKeyExists(HKEY_LOCAL_MACHINE, 'SOFTWARE\Microsoft\VisualStudio\14.0\VC\Runtimes\x64') then
Result := False // 이미 설치됨
else
Result := True; // 미설치 -> 설치 필요
end;
// 설치 완료 후 실행 여부 확인
function InitializeFinish(): Boolean;
var
ResultCode: Integer;
begin
Result := True;
if MsgBox('설치가 완료되었습니다. 프로그램을 실행하시겠습니까?' + #13#10 +
'(실행 시 서버와 동기화하여 설정이 업데이트됩니다)',
mbConfirmation, MB_YESNO) = IDYES then
begin
Exec(ExpandConstant('{app}\{#MyAppExeName}.exe'), '', '', SW_SHOW, ewNoWait, ResultCode);
end;
end;
function InitializeSetup(): Boolean;
var
OldVersion: String;
NewVersion: String;
OldAppPath: String;
UserDataSourcePath, UserDataBackupPath: String;
ResultCode: Integer;
begin
Result := True;
NewVersion := '{#MyAppVersion}';
UserDataBackupPath := ExpandConstant('{tmp}\user_data_backup');
// 현재 프로그램 버전 확인
if RegQueryStringValue(HKLM, 'Software\Microsoft\Windows\CurrentVersion\Uninstall\{#MyAppName}_is1',
'DisplayVersion', OldVersion) then
begin
// 같은 버전이거나 더 높은 버전이 설치되어 있는 경우
if CompareVersion(OldVersion, NewVersion) >= 0 then
begin
MsgBox('현재 설치된 버전(' + OldVersion + ')이 이 설치 프로그램의 버전(' +
NewVersion + ')과 같거나 더 높습니다.' + #13#10 +
'설치를 계속할 수 없습니다.', mbInformation, MB_OK);
Result := False;
exit;
end;
// 이전 버전이 설치되어 있는 경우 업데이트 진행
if CompareVersion(OldVersion, NewVersion) < 0 then
begin
Log('업데이트 설치 진행: ' + OldVersion + ' -> ' + NewVersion);
// 프로그램이 실행 중인지 확인하고 종료 요청
if IsAppRunning('{#MyAppExeName}.exe') then
begin
if MsgBox('프로그램을 업데이트하기 위해 실행 중인 프로그램을 종료해야 합니다.' + #13#10 +
'계속하시겠습니까?', mbConfirmation, MB_YESNO) = IDNO then
begin
Result := False;
exit;
end;
CloseApplication('{#MyAppExeName}.exe');
Sleep(2000); // 프로세스 종료 대기
end;
// 레지스트리에서 설치 경로 확인
if RegQueryStringValue(HKLM, 'Software\Microsoft\Windows\CurrentVersion\Uninstall\{#MyAppName}_is1',
'InstallLocation', OldAppPath) then
begin
Log('기존 설치 경로: ' + OldAppPath);
// lib/src/user_data 폴더 백업
UserDataSourcePath := OldAppPath + '\lib\src\user_data';
if DirExists(UserDataSourcePath) then
begin
Log('사용자 데이터 백업 중: ' + UserDataSourcePath + ' -> ' + UserDataBackupPath);
ForceDirectories(UserDataBackupPath);
CopyDir(UserDataSourcePath, UserDataBackupPath);
end
else
begin
Log('사용자 데이터 폴더가 존재하지 않음: ' + UserDataSourcePath);
end;
// 기존 설치 폴더 완전 삭제
if DirExists(OldAppPath) then
begin
Log('기존 설치 폴더 삭제 중: ' + OldAppPath);
DeleteDir(OldAppPath);
end;
end;
end;
end;
end;
procedure CurStepChanged(CurStep: TSetupStep);
var
UserDataBackupPath, UserDataDestPath: String;
begin
// 설치 완료 후
if CurStep = ssPostInstall then
begin
UserDataBackupPath := ExpandConstant('{tmp}\user_data_backup');
UserDataDestPath := ExpandConstant('{app}\lib\src\user_data');
// 백업한 사용자 데이터 폴더가 있으면 복원
if DirExists(UserDataBackupPath) then
begin
Log('사용자 데이터 복원 중: ' + UserDataBackupPath + ' -> ' + UserDataDestPath);
ForceDirectories(UserDataDestPath);
CopyDir(UserDataBackupPath, UserDataDestPath);
Log('사용자 데이터 복원 완료');
end;
end;
end;

View File

@ -1178,6 +1178,9 @@ class MAIN_GUI(QMainWindow):
'image_worker_restart_every': 10,
'image_worker_restart_count': 0,
'products_per_context_restart': 20,
'use_local_rembg': False,
'local_model_name': "birefnet-general-lite",
'output_image_format': "webp",
"inpaint_method": "lama:cuda",
@ -4005,6 +4008,29 @@ class MAIN_GUI(QMainWindow):
self.thumb_rmb_layout.addWidget(self.thumb_rmb_count_input)
self.thumbnail_toggle_layout.addWidget(self.thumb_rmb_widget)
# 누끼 서버
self.nukki_server_widget = QWidget()
self.nukki_server_toggle_layout = QHBoxLayout(self.nukki_server_widget)
self.nukki_server_toggle_label = QLabel("누끼 서버", self)
self.nukki_server_toggle = ToggleSwitch(self)
self.nukki_server_toggle.setObjectName("nukki_server_toggle")
self.nukki_server_toggle.clicked.connect(lambda checked: self.universal_input_handler(self.nukki_server_toggle, checked))
self.nukki_server_widget.enterEvent = lambda e: self.show_manual_html(
self.thumbnail_manual_group,
"✂️ 누끼 서버",
self.thumbnail_manual_label,
"""
<p>누끼 서버가 죽었을때 백업동작 설정을 사용할지 여부를 결정합니다.</p>
"""
)
self.nukki_server_widget.leaveEvent = lambda e: self.reset_manual(self.thumbnail_manual_group, self.thumbnail_manual_label)
self.nukki_server_toggle_layout.addWidget(self.nukki_server_toggle_label)
self.nukki_server_toggle_layout.addWidget(self.nukki_server_toggle)
self.thumbnail_toggle_layout.addWidget(self.nukki_server_widget)
# 레이아웃에 그룹 추가
self.thumbnail_layout.addWidget(self.thumbnail_toggle_group, 3)

188
share/man/man1/isympy.1 Normal file
View File

@ -0,0 +1,188 @@
'\" -*- coding: us-ascii -*-
.if \n(.g .ds T< \\FC
.if \n(.g .ds T> \\F[\n[.fam]]
.de URL
\\$2 \(la\\$1\(ra\\$3
..
.if \n(.g .mso www.tmac
.TH isympy 1 2007-10-8 "" ""
.SH NAME
isympy \- interactive shell for SymPy
.SH SYNOPSIS
'nh
.fi
.ad l
\fBisympy\fR \kx
.if (\nx>(\n(.l/2)) .nr x (\n(.l/5)
'in \n(.iu+\nxu
[\fB-c\fR | \fB--console\fR] [\fB-p\fR ENCODING | \fB--pretty\fR ENCODING] [\fB-t\fR TYPE | \fB--types\fR TYPE] [\fB-o\fR ORDER | \fB--order\fR ORDER] [\fB-q\fR | \fB--quiet\fR] [\fB-d\fR | \fB--doctest\fR] [\fB-C\fR | \fB--no-cache\fR] [\fB-a\fR | \fB--auto\fR] [\fB-D\fR | \fB--debug\fR] [
-- | PYTHONOPTIONS]
'in \n(.iu-\nxu
.ad b
'hy
'nh
.fi
.ad l
\fBisympy\fR \kx
.if (\nx>(\n(.l/2)) .nr x (\n(.l/5)
'in \n(.iu+\nxu
[
{\fB-h\fR | \fB--help\fR}
|
{\fB-v\fR | \fB--version\fR}
]
'in \n(.iu-\nxu
.ad b
'hy
.SH DESCRIPTION
isympy is a Python shell for SymPy. It is just a normal python shell
(ipython shell if you have the ipython package installed) that executes
the following commands so that you don't have to:
.PP
.nf
\*(T<
>>> from __future__ import division
>>> from sympy import *
>>> x, y, z = symbols("x,y,z")
>>> k, m, n = symbols("k,m,n", integer=True)
\*(T>
.fi
.PP
So starting isympy is equivalent to starting python (or ipython) and
executing the above commands by hand. It is intended for easy and quick
experimentation with SymPy. For more complicated programs, it is recommended
to write a script and import things explicitly (using the "from sympy
import sin, log, Symbol, ..." idiom).
.SH OPTIONS
.TP
\*(T<\fB\-c \fR\*(T>\fISHELL\fR, \*(T<\fB\-\-console=\fR\*(T>\fISHELL\fR
Use the specified shell (python or ipython) as
console backend instead of the default one (ipython
if present or python otherwise).
Example: isympy -c python
\fISHELL\fR could be either
\&'ipython' or 'python'
.TP
\*(T<\fB\-p \fR\*(T>\fIENCODING\fR, \*(T<\fB\-\-pretty=\fR\*(T>\fIENCODING\fR
Setup pretty printing in SymPy. By default, the most pretty, unicode
printing is enabled (if the terminal supports it). You can use less
pretty ASCII printing instead or no pretty printing at all.
Example: isympy -p no
\fIENCODING\fR must be one of 'unicode',
\&'ascii' or 'no'.
.TP
\*(T<\fB\-t \fR\*(T>\fITYPE\fR, \*(T<\fB\-\-types=\fR\*(T>\fITYPE\fR
Setup the ground types for the polys. By default, gmpy ground types
are used if gmpy2 or gmpy is installed, otherwise it falls back to python
ground types, which are a little bit slower. You can manually
choose python ground types even if gmpy is installed (e.g., for testing purposes).
Note that sympy ground types are not supported, and should be used
only for experimental purposes.
Note that the gmpy1 ground type is primarily intended for testing; it the
use of gmpy even if gmpy2 is available.
This is the same as setting the environment variable
SYMPY_GROUND_TYPES to the given ground type (e.g.,
SYMPY_GROUND_TYPES='gmpy')
The ground types can be determined interactively from the variable
sympy.polys.domains.GROUND_TYPES inside the isympy shell itself.
Example: isympy -t python
\fITYPE\fR must be one of 'gmpy',
\&'gmpy1' or 'python'.
.TP
\*(T<\fB\-o \fR\*(T>\fIORDER\fR, \*(T<\fB\-\-order=\fR\*(T>\fIORDER\fR
Setup the ordering of terms for printing. The default is lex, which
orders terms lexicographically (e.g., x**2 + x + 1). You can choose
other orderings, such as rev-lex, which will use reverse
lexicographic ordering (e.g., 1 + x + x**2).
Note that for very large expressions, ORDER='none' may speed up
printing considerably, with the tradeoff that the order of the terms
in the printed expression will have no canonical order
Example: isympy -o rev-lax
\fIORDER\fR must be one of 'lex', 'rev-lex', 'grlex',
\&'rev-grlex', 'grevlex', 'rev-grevlex', 'old', or 'none'.
.TP
\*(T<\fB\-q\fR\*(T>, \*(T<\fB\-\-quiet\fR\*(T>
Print only Python's and SymPy's versions to stdout at startup, and nothing else.
.TP
\*(T<\fB\-d\fR\*(T>, \*(T<\fB\-\-doctest\fR\*(T>
Use the same format that should be used for doctests. This is
equivalent to '\fIisympy -c python -p no\fR'.
.TP
\*(T<\fB\-C\fR\*(T>, \*(T<\fB\-\-no\-cache\fR\*(T>
Disable the caching mechanism. Disabling the cache may slow certain
operations down considerably. This is useful for testing the cache,
or for benchmarking, as the cache can result in deceptive benchmark timings.
This is the same as setting the environment variable SYMPY_USE_CACHE
to 'no'.
.TP
\*(T<\fB\-a\fR\*(T>, \*(T<\fB\-\-auto\fR\*(T>
Automatically create missing symbols. Normally, typing a name of a
Symbol that has not been instantiated first would raise NameError,
but with this option enabled, any undefined name will be
automatically created as a Symbol. This only works in IPython 0.11.
Note that this is intended only for interactive, calculator style
usage. In a script that uses SymPy, Symbols should be instantiated
at the top, so that it's clear what they are.
This will not override any names that are already defined, which
includes the single character letters represented by the mnemonic
QCOSINE (see the "Gotchas and Pitfalls" document in the
documentation). You can delete existing names by executing "del
name" in the shell itself. You can see if a name is defined by typing
"'name' in globals()".
The Symbols that are created using this have default assumptions.
If you want to place assumptions on symbols, you should create them
using symbols() or var().
Finally, this only works in the top level namespace. So, for
example, if you define a function in isympy with an undefined
Symbol, it will not work.
.TP
\*(T<\fB\-D\fR\*(T>, \*(T<\fB\-\-debug\fR\*(T>
Enable debugging output. This is the same as setting the
environment variable SYMPY_DEBUG to 'True'. The debug status is set
in the variable SYMPY_DEBUG within isympy.
.TP
-- \fIPYTHONOPTIONS\fR
These options will be passed on to \fIipython (1)\fR shell.
Only supported when ipython is being used (standard python shell not supported).
Two dashes (--) are required to separate \fIPYTHONOPTIONS\fR
from the other isympy options.
For example, to run iSymPy without startup banner and colors:
isympy -q -c ipython -- --colors=NoColor
.TP
\*(T<\fB\-h\fR\*(T>, \*(T<\fB\-\-help\fR\*(T>
Print help output and exit.
.TP
\*(T<\fB\-v\fR\*(T>, \*(T<\fB\-\-version\fR\*(T>
Print isympy version information and exit.
.SH FILES
.TP
\*(T<\fI${HOME}/.sympy\-history\fR\*(T>
Saves the history of commands when using the python
shell as backend.
.SH BUGS
The upstreams BTS can be found at \(lahttps://github.com/sympy/sympy/issues\(ra
Please report all bugs that you find in there, this will help improve
the overall quality of SymPy.
.SH "SEE ALSO"
\fBipython\fR(1), \fBpython\fR(1)

View File

@ -54,6 +54,11 @@ class ImageProcessor3:
self.TEMP_IMAGE_DIR = self.toggle_states.get('TEMP_IMAGE_DIR', "")
self.use_local_rembg = self.toggle_states.get("use_local_rembg", False)
self.local_model_name = self.toggle_states.get("local_model_name", 'birefnet-general-lite')
self.logger.log(f"self.toggle_states: {self.toggle_states}", level=logging.DEBUG)
self.logger.log(f"self.font_path: {self.font_path}", level=logging.DEBUG)

View File

@ -4,6 +4,8 @@ import base64
import numpy as np
import os
import logging
from PIL import Image
class Request_AI_Server:
"""IOPaint 서버 연동 인페인팅 모델 (REST API /api/v1/inpaint 사용, 바이너리 PNG 반환)"""
@ -25,6 +27,21 @@ class Request_AI_Server:
self.inpaint_api_url = f"{self.inpaint_base_url}/api/v1/inpaint"
# RemoveBG 플러그인은 고정 엔드포인트
self.rembg_api_url = f"{self.rembg_base_url}/api/v1/run_plugin_gen_image"
# 백업용 내장 rembg 모듈 (필요시에만 초기화)
self._backup_rembg = None
def _get_backup_rembg(self):
"""백업용 내장 rembg 모듈을 lazy loading으로 초기화"""
if self._backup_rembg is None:
try:
from .background_removal_module import BackgroundRemovalModule
self._backup_rembg = BackgroundRemovalModule(logger=self.logger, default_model="u2net")
self.logger.log("백업용 내장 rembg 모듈 초기화됨", level=logging.INFO)
except ImportError as e:
self.logger.log(f"백업용 rembg 모듈 import 실패: {e}", level=logging.ERROR)
return None
return self._backup_rembg
def request_inpaint(self, image: np.ndarray, mask: np.ndarray) -> np.ndarray:
@ -64,14 +81,16 @@ class Request_AI_Server:
self.logger.log(f"인페인팅 서버 에러: {e}", level=logging.ERROR, exc_info=True)
return None
def request_rembg(self, image: np.ndarray) -> np.ndarray:
"""RemoveBG 플러그인 호출 후 결과 이미지를 흰 배경 중앙 배치로 후처리."""
def request_rembg(self, image: np.ndarray, use_local_rembg: bool = False, local_model_name: str = "birefnet-general-lite") -> np.ndarray:
"""RemoveBG 플러그인 호출 후 결과 이미지를 흰 배경 중앙 배치로 후처리.
서버가 다운될 경우 백업으로 내장 rembg 모듈 사용.
Args:
image: 입력 이미지 (np.ndarray 또는 파일 경로)
use_local_rembg: True면 서버 상태와 관계없이 내장 rembg 모듈 직접 사용
local_model_name: 내장 rembg에서 사용할 모델명 (기본값: "birefnet-general-lite")
"""
try:
# 서버 상태 먼저 확인
if not self.is_server_alive(self.rembg_base_url):
self.logger.log("rembg 서버가 비정상입니다.", level=logging.WARNING)
return None
# 입력 이미지 로드/확정
if isinstance(image, str) and os.path.isfile(image):
image_data = cv2.imread(image)
@ -81,6 +100,16 @@ class Request_AI_Server:
self.logger.log(f"이미지 파일을 읽을 수 없습니다: {image}", level=logging.ERROR)
return None
# 내장 모듈 직접 사용 요청시
if use_local_rembg:
self.logger.log(f"외부 인자에 의해 내장 rembg 모듈({local_model_name})을 직접 사용합니다.", level=logging.INFO)
return self._use_backup_rembg(image_data, local_model_name)
# 서버 상태 먼저 확인
if not self.is_server_alive(self.rembg_base_url):
self.logger.log("rembg 서버가 비정상입니다. 백업 내장 rembg 모듈을 사용합니다.", level=logging.WARNING)
return self._use_backup_rembg(image_data, local_model_name)
# base64 인코딩 (data URL)
_, img_encoded = cv2.imencode('.png', image_data)
img_b64 = base64.b64encode(img_encoded).decode('utf-8')
@ -92,17 +121,77 @@ class Request_AI_Server:
response = requests.post(self.rembg_api_url, json=payload)
if response.status_code != 200:
self.logger.log(f"rembg 서버 에러: {response.text}", level=logging.ERROR)
return None
self.logger.log(f"rembg 서버 에러: {response.text}. 백업 내장 rembg 모듈을 사용합니다.", level=logging.WARNING)
return self._use_backup_rembg(image_data, local_model_name)
# PNG 바이너리 → numpy
nparr = np.frombuffer(response.content, np.uint8)
result_img = cv2.imdecode(nparr, cv2.IMREAD_UNCHANGED)
if result_img is None or result_img.ndim != 3:
return result_img # 실패 시 원본 반환
self.logger.log("서버 응답 이미지 디코딩 실패. 백업 내장 rembg 모듈을 사용합니다.", level=logging.WARNING)
return self._use_backup_rembg(image_data, local_model_name)
# ---- 후처리: 마스크 정제 및 중앙 배치 ----
return self._postprocess_rembg_result(result_img)
except Exception as e:
self.logger.log(f"rembg 서버 에러: {e}. 백업 내장 rembg 모듈을 사용합니다.", level=logging.WARNING, exc_info=True)
return self._use_backup_rembg(image_data if 'image_data' in locals() else image, local_model_name)
def _use_backup_rembg(self, image_data: np.ndarray, model_name: str = "birefnet-general-lite") -> np.ndarray:
"""내장 rembg 모듈을 사용하여 배경 제거
Args:
image_data: 입력 이미지 데이터
model_name: 사용할 rembg 모델명
"""
try:
backup_rembg = self._get_backup_rembg()
if backup_rembg is None:
self.logger.log("백업 rembg 모듈을 사용할 수 없습니다.", level=logging.ERROR)
return None
# 임시 파일 저장 (BackgroundRemovalModule은 파일 경로를 받음)
import tempfile
with tempfile.NamedTemporaryFile(suffix='.png', delete=False) as temp_file:
temp_path = temp_file.name
cv2.imwrite(temp_path, image_data)
try:
# 내장 rembg로 배경 제거 (지정된 모델 사용)
result_pil = backup_rembg.remove_background(temp_path, model_name=model_name)
if result_pil is None:
self.logger.log(f"내장 rembg 모듈({model_name}) 처리 실패", level=logging.ERROR)
return None
# PIL Image를 numpy array로 변환
if result_pil.mode == 'RGBA':
result_rgba = np.array(result_pil)
result_img = cv2.cvtColor(result_rgba, cv2.COLOR_RGBA2BGRA)
else:
result_rgb = np.array(result_pil)
result_img = cv2.cvtColor(result_rgb, cv2.COLOR_RGB2BGR)
# 알파 채널 추가 (배경 제거된 결과이므로 마스크 생성 필요)
gray = cv2.cvtColor(result_img, cv2.COLOR_BGR2GRAY)
alpha = np.where(gray > 10, 255, 0).astype(np.uint8)
result_img = cv2.merge([result_img[:,:,0], result_img[:,:,1], result_img[:,:,2], alpha])
# 동일한 후처리 적용
return self._postprocess_rembg_result(result_img)
finally:
# 임시 파일 삭제
if os.path.exists(temp_path):
os.unlink(temp_path)
except Exception as e:
self.logger.log(f"백업 rembg 모듈 에러: {e}", level=logging.ERROR, exc_info=True)
return None
def _postprocess_rembg_result(self, result_img: np.ndarray) -> np.ndarray:
"""RemoveBG 결과 이미지 후처리: 마스크 정제 및 중앙 배치"""
try:
# 1) 초기 마스크
if result_img.shape[2] == 4:
mask_init = (result_img[:, :, 3] > 200).astype(np.uint8)
@ -149,8 +238,9 @@ class Request_AI_Server:
white_bg = np.full_like(bgr_crop, 255.0)
final_img = (bgr_crop * alpha_crop + white_bg * (1 - alpha_crop)).astype(np.uint8)
return final_img
except Exception as e:
self.logger.log(f"rembg 서버 에러: {e}", level=logging.ERROR, exc_info=True)
self.logger.log(f"rembg 결과 후처리 에러: {e}", level=logging.ERROR, exc_info=True)
return None
def is_server_alive(self, base_url: str, timeout: int = 3) -> bool: