deepdanbooru-pytorch / deepdanbooru.py
pearisli's picture
Create deepdanbooru.py
88a6a7f verified
Raw
History Blame Contribute Delete
6.88 kB
import os
from PIL import Image
import torch
import torch.nn as nn
from torchvision.transforms.functional import to_tensor, resize
from typing import List, Tuple, Union
from huggingface_hub import PyTorchModelHubMixin, hf_hub_download
class Bottleneck(nn.Module):
def __init__(
self,
in_channels: int,
out_channels: int,
stride: int = 1,
add_downsample: bool = False
) -> None:
super().__init__()
width = out_channels // 4
self.conv1 = nn.Conv2d(in_channels, width, kernel_size=1)
self.conv2 = nn.Conv2d(width, width, kernel_size=3, stride=stride, padding=1)
self.conv3 = nn.Conv2d(width, out_channels, kernel_size=1)
self.relu = nn.ReLU(inplace=True)
self.downsample = None
if add_downsample:
self.downsample = nn.Conv2d(in_channels, out_channels, kernel_size=1, stride=stride)
def forward(self, x: torch.Tensor) -> torch.Tensor:
identity = x
out = self.conv1(x)
out = self.relu(out)
out = self.conv2(out)
out = self.relu(out)
out = self.conv3(out)
if self.downsample is not None:
identity = self.downsample(x)
out += identity
out = self.relu(out)
return out
class DeepDanbooruModel(nn.Module, PyTorchModelHubMixin):
def __init__(
self,
block_out_channels: Tuple[int, ...],
blocks_per_layer: Tuple[int, ...],
num_classes: int,
*,
tag_file: str,
resolution: int = 512
) -> None:
super().__init__()
in_channels = 64
self.block_out_channels = block_out_channels
self.blocks_per_layer = blocks_per_layer
self.num_classes = num_classes
self._tags = []
with open(tag_file, "r", encoding="utf-8") as f:
self._tags = [line.strip() for line in f if line.strip()]
self.resolution = resolution
self.conv1 = nn.Conv2d(3, in_channels, kernel_size=7, stride=2, padding=3)
self.relu = nn.ReLU(inplace=True)
self.maxpool = nn.MaxPool2d(kernel_size=3, stride=2, padding=0, ceil_mode=True)
self.layers = nn.ModuleList([])
input_channel = in_channels
for i, (num_block, output_channel) in enumerate(zip(blocks_per_layer, block_out_channels)):
stride = 1 if i == 0 else 2
self.layers.append(
self._make_layer(num_block, input_channel, output_channel, stride=stride)
)
input_channel = output_channel
self.fc = nn.Conv2d(in_channels=block_out_channels[-1], out_channels=num_classes, kernel_size=1, bias=False)
self.activation = nn.Sigmoid()
for m in self.modules():
if isinstance(m, nn.Conv2d):
ks = m.kernel_size[0]
if ks > 1:
m.padding_mode = "constant"
if m.stride[0] == 1:
m._reversed_padding_repeated_twice = (1, 1, 1, 1)
else:
m._reversed_padding_repeated_twice = (
ks // 2 - 1, ks // 2,
ks // 2 - 1, ks // 2
)
def _make_layer(
self,
num_block: int,
in_channels: int,
out_channels: int,
stride: int = 1,
) -> nn.Sequential:
layers = nn.Sequential()
layers.append(
Bottleneck(in_channels, out_channels, stride=stride, add_downsample=True)
)
for _ in range(1, num_block):
layers.append(
Bottleneck(out_channels, out_channels, stride=1, add_downsample=False)
)
return layers
def forward(self, x: torch.Tensor) -> torch.Tensor:
x = self.conv1(x)
x = self.relu(x)
x = self.maxpool(x)
for layer in self.layers:
x = layer(x)
x = self.fc(x)
x = nn.functional.avg_pool2d(x, kernel_size=x.shape[-2:])
x = torch.flatten(x, 1)
x = self.activation(x)
return x
@torch.no_grad()
def tag(
self,
image: Union[Image.Image, List[Image.Image], torch.Tensor],
threshold: float = 0.5
) -> List[List[str]]:
# Convert PIL Images to tensors if needed and stack
if isinstance(image, Image.Image):
image = [image, ]
if isinstance(image, List):
images = torch.stack([
resize(to_tensor(img), [self.resolution, self.resolution])
for img in image])
assert images.ndim == 4 and images.shape[-1] == self.resolution and images.shape[-2] == self.resolution, f"Expected 4D tensor (N, C, 512, 512), got shape {images.shape}"
device = next(self.parameters()).device
images = images.to(device)
# Model forward pass
probs = self(images)
# Thresholding and tag lookup
results = []
for prob_vector in probs:
selected = (prob_vector > threshold).nonzero(as_tuple=True)[0].cpu().tolist()
results.append([self._tags[i] for i in selected])
# Return single result or batch
return results
def save_pretrained(
self,
save_directory: str,
config: dict = None,
**kwargs
):
# 1. Call super to save model and config
super().save_pretrained(save_directory, config=config, **kwargs)
readme = os.path.join(save_directory, "README.md")
if os.path.exists(readme):
os.remove(readme)
# 2. Write tags.txt into save_directory
tags_path = os.path.join(save_directory, "tags.txt")
with open(tags_path, "w", encoding="utf-8") as f:
for tag in getattr(self, '_tags', []):
f.write(f"{tag}\n")
@classmethod
def from_pretrained(
cls,
pretrained_model_name_or_path: str,
force_download: bool = False,
cache_dir: str = None,
local_files_only: bool = False,
**model_kwargs
):
model_id = str(pretrained_model_name_or_path)
if os.path.isdir(model_id):
tag_file = os.path.join(model_id, "tags.txt")
else:
tag_file = hf_hub_download(
repo_id=model_id,
filename="tags.txt",
force_download=force_download,
cache_dir=cache_dir,
local_files_only=local_files_only,
)
model_kwargs["tag_file"] = tag_file
instance = super().from_pretrained(
pretrained_model_name_or_path,
force_download=force_download,
cache_dir=cache_dir,
local_files_only=local_files_only,
**model_kwargs
)
return instance