Text-to-Image
Diffusers
Safetensors
flow-matching
pyramidal-unet
256x256
ettin-150m
from-scratch
ra
udit
Instructions to use AiArtLab/Ra with libraries, inference providers, notebooks, and local apps. Follow these links to get started.
- Libraries
- Diffusers
How to use AiArtLab/Ra with Diffusers:
pip install -U diffusers transformers accelerate
import torch from diffusers import DiffusionPipeline # switch to "mps" for apple devices pipe = DiffusionPipeline.from_pretrained("AiArtLab/Ra", dtype=torch.bfloat16, device_map="cuda") prompt = "Astronaut in a jungle, cold color palette, muted colors, detailed, 8k" image = pipe(prompt).images[0] - Notebooks
- Google Colab
- Kaggle
- Local Apps Settings
- Draw Things
- DiffusionBee
File size: 7,852 Bytes
b4f5f26 | 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 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 | """Сборка набора по классам (woman/man/cat) из ~/dataset/allphoto_train — тест анатомии.
Зачем: бабочки однородны и анатомию не показывают. Нужен набор, где модель обязана выучить
тело: разные люди, позы, ракурсы, и капшены, которые называют, кто это и что делает.
Классы: woman / man / cat — по N уникальных капшенов на класс (дедуп по первым 60 символам,
иначе один photoshoot даёт десятки почти одинаковых кадров).
python3 src/make_dataset_people.py --n 60 --out ~/dataset/testset_anatomy
"""
from __future__ import annotations
import argparse
import csv
import math
import random
import re
import shutil
from pathlib import Path
from PIL import Image, ImageOps
SRC = Path.home() / "dataset" / "allphoto_train"
CLASSES = {
"woman": ("woman", "women", "girl"),
"man": ("man", "men", "boy"),
"cat": ("cat",),
}
# бакеты как в moss/src/make_dataset.py: обе стороны кратны 16 (латент /8, потом patch-2),
# соотношение выбирается ближайшим по |log(ratio_источника / ratio_бакета)|, затем ЦЕНТР-КРОП —
# растягивать нельзя, иначе лица и тела деформируются
def buckets(long_side: int = 512, multiple: int = 64) -> dict:
"""Бакеты: длинная сторона long_side, обе стороны кратны 16 (латент /8, затем /2)."""
L = long_side
def r16(x): return max(multiple, int(round(x / multiple)) * multiple)
return {
"1:1": (L, L),
"4:3": (L, r16(L * 3 / 4)),
"3:4": (r16(L * 3 / 4), L),
"3:2": (L, r16(L * 2 / 3)),
"2:3": (r16(L * 2 / 3), L),
"16:9": (L, r16(L * 9 / 16)),
"9:16": (r16(L * 9 / 16), L),
}
BUCKETS = buckets(512)
# для людей требуем, чтобы в кадре было тело, а не только лицо: бабочки-портреты анатомию не учат
BODY_WORDS = ("standing", "walking", "sitting", "seated", "jeans", "shorts", "dress", "skirt",
"shirt", "legs", "arms", "hands", "wearing", "full body", "walks", "stands")
def classify(cap: str) -> str | None:
low = cap.lower()
for cls, words in CLASSES.items():
if any(re.search(rf"\b{w}\b", low) for w in words):
# «man» внутри «woman» уже отсечён \b; но «woman» не должна становиться «man»
if cls == "man" and re.search(r"\bwoman\b", low):
return "woman"
return cls
return None
def trim_black(im: Image.Image, thr: float = 12.0, var: float = 8.0) -> Image.Image:
"""Срезает однородные чёрные поля по краям (в allphoto_train их полно: 71 из 171 кадра)."""
import numpy as np
a = np.asarray(im.convert("L"), dtype=np.float32)
def uniform_edge(line): # чёрная и без структуры -> рамка, а не ночная сцена
return line.mean() < thr and line.std() < var
top, bot, left, right = 0, a.shape[0], 0, a.shape[1]
while top < bot - 8 and uniform_edge(a[top]): top += 1
while bot > top + 8 and uniform_edge(a[bot - 1]): bot -= 1
while left < right - 8 and uniform_edge(a[top:bot, left]): left += 1
while right > left + 8 and uniform_edge(a[top:bot, right - 1]): right -= 1
return im.crop((left, top, right, bot)) if (left or top or right != a.shape[1] or bot != a.shape[0]) else im
def plan_size(w: int, h: int, max_upscale: float = 1.4):
"""(w,h) -> (label, tw, th, cw, ch) или None, если источника не хватает (как в moss)."""
ratio = w / h
label, (tw, th) = min(BUCKETS.items(), key=lambda kv: abs(math.log(ratio / (kv[1][0] / kv[1][1]))))
if ratio > tw / th:
cw, ch = round(h * tw / th), h
else:
cw, ch = w, round(w * th / tw)
cw, ch = min(cw, w), min(ch, h)
if cw * max_upscale < tw or ch * max_upscale < th:
return None
return label, tw, th, cw, ch
def load_bucketed(path: Path, max_upscale: float = 1.4):
"""EXIF-поворот -> срез чёрных полей -> ближайший бакет -> центр-кроп -> ресайз."""
im = ImageOps.exif_transpose(Image.open(path)).convert("RGB")
im = trim_black(im)
plan = plan_size(im.width, im.height, max_upscale)
if plan is None:
return None, None
label, tw, th, cw, ch = plan
left, top = (im.width - cw) // 2, (im.height - ch) // 2
return im.crop((left, top, left + cw, top + ch)).resize((tw, th), Image.LANCZOS), label
def main():
ap = argparse.ArgumentParser()
ap.add_argument("--n", type=int, default=60, help="картинок на класс")
ap.add_argument("--eval-n", type=int, default=4, help="сколько на класс отложить для показа")
ap.add_argument("--max-upscale", type=float, default=1.4)
ap.add_argument("--long-side", type=int, default=512)
ap.add_argument("--multiple", type=int, default=64)
ap.add_argument("--seed", type=int, default=0)
ap.add_argument("--out", default=str(Path.home() / "dataset" / "testset_anatomy"))
args = ap.parse_args()
global BUCKETS
BUCKETS = buckets(args.long_side, args.multiple)
out = Path(args.out)
for sub in ("train", "eval"):
(out / sub).mkdir(parents=True, exist_ok=True)
caps = sorted(SRC.glob("*.txt"))
rng = random.Random(args.seed)
rng.shuffle(caps)
seen_prefix: set[str] = set()
picked: dict[str, list[tuple[Path, str]]] = {c: [] for c in CLASSES}
need = args.n + args.eval_n
for f in caps:
cap = " ".join(f.read_text().split())
if len(cap) < 40:
continue
cls = classify(cap)
if cls is None or len(picked[cls]) >= need:
continue
low = cap.lower()
if cls == "cat" and re.search(r"\b(dog|puppy|kitten|rabbit)\b", low) and "cat" not in low:
continue
if cls in ("woman", "man") and not any(w in low for w in BODY_WORDS):
continue
key = cap[:60].lower()
if key in seen_prefix:
continue
img = f.with_suffix(".jpg")
if not img.exists():
continue
seen_prefix.add(key)
picked[cls].append((img, cap))
if all(len(v) >= need for v in picked.values()):
break
rows = []
for cls, items in picked.items():
for i, (img, cap) in enumerate(items):
split = "eval" if i < args.eval_n else "train"
im, bucket = load_bucketed(img)
if im is None:
continue
name = f"{cls}_{i:03d}"
im.save(out / split / f"{name}.jpg", quality=92)
(out / split / f"{name}.txt").write_text(cap + "\n")
rows.append({"split": split, "class": cls, "name": name, "w": im.width, "h": im.height,
"bucket": bucket, "caption": cap})
print(f" {split}/{name} {im.width}x{im.height} | {cap[:80]}")
with open(out / "index.csv", "w", newline="") as fh:
w = csv.DictWriter(fh, fieldnames=["split", "class", "name", "w", "h", "bucket", "caption"])
w.writeheader(); w.writerows(rows)
per = {c: sum(1 for r in rows if r["class"] == c and r["split"] == "train") for c in CLASSES}
print(f"\nитого: {sum(per.values())} train {per} + {sum(1 for r in rows if r['split']=='eval')} eval"
f" -> {out}")
if __name__ == "__main__":
main()
|