File size: 4,533 Bytes
a42ddbb
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
import torch
from transformers import AutoModelForImageSegmentation
from torchvision import transforms
from PIL import Image
import numpy as np

# Device setup
_device = torch.device("cuda" if torch.cuda.is_available() else "cpu")

# Global models
_birefnet = None
_birefnet_lite = None

# Preprocessing transform
_transform = transforms.Compose(
    [
        transforms.Resize((768, 768)),
        transforms.ToTensor(),
        transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]),
    ]
)


def load_model(fast_mode=False):
    global _birefnet, _birefnet_lite

    if fast_mode:
        if _birefnet_lite is None:
            _birefnet_lite = AutoModelForImageSegmentation.from_pretrained(
                "ZhengPeng7/BiRefNet_lite", trust_remote_code=True
            ).to(_device)
        return _birefnet_lite

    else:
        if _birefnet is None:
            _birefnet = AutoModelForImageSegmentation.from_pretrained(
                "ZhengPeng7/BiRefNet", trust_remote_code=True
            ).to(_device)
        return _birefnet


def unload_model():
    global _birefnet, _birefnet_lite
    _birefnet = None
    _birefnet_lite = None
    if torch.cuda.is_available():
        torch.cuda.empty_cache()


def _apply_mask(image: Image.Image, mask_tensor, bg, bg_type: str):
    size = image.size
    mask = transforms.ToPILImage()(mask_tensor.squeeze()).resize(size)

    if bg_type == "Color" and isinstance(bg, str) and bg.startswith("#"):
        color_rgb = tuple(int(bg[i : i + 2], 16) for i in (1, 3, 5))
        bg_image = Image.new("RGBA", size, color_rgb + (255,))
    elif bg_type in ["Image", "Video"]:
        bg_image = bg.convert("RGBA").resize(size)
    else:
        raise ValueError("Invalid background type or format")

    return Image.composite(image.convert("RGBA"), bg_image, mask)


def _get_transform(image_size: tuple[int, int] | None = None):
    if image_size:
        resize = transforms.Resize(image_size)
    else:
        resize = transforms.Resize((768, 768))  # default fallback

    return transforms.Compose(
        [
            resize,
            transforms.ToTensor(),
            transforms.Normalize([0.485, 0.456, 0.406], [0.229, 0.224, 0.225]),
        ]
    )


def predict(
    input: Image.Image | list[Image.Image],
    bg: str | Image.Image,
    bg_type: str = "Color",
    fast_mode: bool = True,
    bg_frames: list[np.ndarray] | None = None,
    image_size_mode: str = "default",  # "default", "origin", or "custom"
    custom_size: tuple[int, int] | None = None,
) -> Image.Image | list[Image.Image]:

    model = _birefnet_lite if fast_mode else _birefnet
    if model is None:
        raise RuntimeError("Model not loaded. Please call `load_model()` first.")

    def get_transform_for_img(img: Image.Image):
        if image_size_mode == "origin":
            return _get_transform(img.size)
        elif image_size_mode == "custom":
            if not custom_size:
                raise ValueError("`custom_size` must be provided for 'custom' mode.")
            return _get_transform(custom_size)
        else:
            return _get_transform()  # default to (768, 768)

    if isinstance(input, Image.Image):
        _transform = get_transform_for_img(input)
        input_tensor = _transform(input).unsqueeze(0).to(_device)
        with torch.no_grad():
            pred = model(input_tensor)[-1].sigmoid().cpu()
        bg_image = (
            Image.fromarray(bg_frames[0]) if bg_type == "Video" and bg_frames else bg
        )
        return _apply_mask(input, pred[0], bg_image, bg_type)

    elif isinstance(input, list):
        results = []
        for idx, frame in enumerate(input):
            _transform = get_transform_for_img(frame)
            input_tensor = _transform(frame).unsqueeze(0).to(_device)
            with torch.no_grad():
                pred = model(input_tensor)[-1].sigmoid().cpu()
            if bg_type == "Video" and bg_frames:
                bg_image = Image.fromarray(bg_frames[idx % len(bg_frames)])
            else:
                bg_image = bg
            results.append(_apply_mask(frame, pred[0], bg_image, bg_type))
        return results

    else:
        raise TypeError("`input` must be a PIL.Image or a list of PIL.Image.")


# # Default behavior (768x768)
# predict(img, "#ffffff", "Color")

# # Keep original image size
# predict(img, "#ffffff", "Color", image_size_mode="origin")

# # Resize to 1024x1024
# predict(img, "#ffffff", "Color", image_size_mode="custom", custom_size=(1024, 1024))