rusher-code commited on
Commit
402ae9c
·
verified ·
1 Parent(s): 0744f14

Upload folder using huggingface_hub

Browse files
Files changed (4) hide show
  1. README.md +129 -0
  2. config.json +31 -0
  3. model.safetensors +3 -0
  4. modeling_codelor.py +178 -0
README.md ADDED
@@ -0,0 +1,129 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ ---
2
+ license: mit
3
+ language:
4
+ - en
5
+ tags:
6
+ - image-segmentation
7
+ - chroma-key
8
+ - green-screen
9
+ - color-segmentation
10
+ - pytorch
11
+ - codelor-color
12
+ - lightweight
13
+ pipeline_tag: image-segmentation
14
+ ---
15
+
16
+ # Codelor Color 1.2
17
+
18
+ **Codelor Color** is a lightweight image segmentation model built specifically for **chroma key**: detecting and removing green from an image or video (green screens, leaves, fabric, green objects) and producing a clean alpha mask.
19
+
20
+ It takes an RGB image and a target color (green by default) and outputs a per-pixel binary mask.
21
+
22
+ ---
23
+
24
+ ## Model at a glance
25
+
26
+ - **Architecture**: Lightweight Transformer with linear attention and depthwise convolutions, optimized for CPU.
27
+ - **Parameters**: 1,641,055
28
+ - **File size**: ~6.3 MB in safetensors format.
29
+ - **Input**: RGB image (any size) + RGB triplet of the target color.
30
+ - **Output**: probability mask (1 channel, same resolution as input).
31
+ - **Use cases**: green screen, video chroma key, green object removal, color-similarity segmentation.
32
+
33
+ ---
34
+
35
+ ## Quick start
36
+
37
+ ### Installation
38
+
39
+ You can install the standalone architecture package directly from PyPI:
40
+
41
+ pip install codelor-color
42
+
43
+ Or install all the required dependencies to run this specific model:
44
+
45
+ pip install torch safetensors huggingface_hub pillow numpy
46
+
47
+ ### Load the model
48
+
49
+ You can use the standalone PyPI package, or load the architecture directly from this repository:
50
+
51
+ from codelor_color import CodelorColor
52
+ from safetensors.torch import load_file
53
+ import torch
54
+
55
+ # If loading from the HF repo directly:
56
+ # from modeling_codelor import load_from_pretrained
57
+ # model = load_from_pretrained("YOUR_USERNAME/codelor-color-1.2", device="cpu")
58
+
59
+ # If using the PyPI package and local weights:
60
+ model = CodelorColor(dim=128, depth=8, heads=8, tied_after=4)
61
+ state_dict = load_file("model.safetensors")
62
+ model.load_state_dict(state_dict)
63
+ model.eval()
64
+
65
+ ### Predict a mask on an image
66
+
67
+ import torch
68
+ from PIL import Image
69
+ import numpy as np
70
+ from modeling_codelor import predict_mask
71
+
72
+ img = Image.open("photo.jpg").convert("RGB")
73
+ x = torch.from_numpy(np.array(img)).float().permute(2, 0, 1) / 255.0
74
+
75
+ mask, prob = predict_mask(model, x, color_rgb=(0.20, 0.85, 0.30), threshold=0.5)
76
+
77
+ print("Green surface detected: {:.1f}%".format(mask.mean() * 100))
78
+
79
+ ### Remove green from an image (RGBA output)
80
+
81
+ from modeling_codelor import remove_green
82
+
83
+ rgba = remove_green(x, model)
84
+ rgba_np = (rgba.permute(1, 2, 0).numpy() * 255).astype("uint8")
85
+
86
+ Image.fromarray(rgba_np, mode="RGBA").save("no_green.png")
87
+
88
+ ---
89
+
90
+ ## Performance and statistics
91
+
92
+ ### Model metrics
93
+
94
+ | Metric | Value |
95
+ |---|---|
96
+ | Pixel accuracy | _to be filled_ |
97
+ | Mean IoU | _to be filled_ |
98
+ | Best loss | _to be filled_ |
99
+ | Parameters | 1,641,055 |
100
+ | File size | ~6.3 MB |
101
+ | CPU latency (160x160) | ~80-150 ms |
102
+
103
+ ### Comparison with other approaches
104
+
105
+ | Method | Parameters | IoU / mIoU | CPU latency | Notes |
106
+ |---|---|---|---|---|
107
+ | K-means (HSV) | - | 0.642 (IoU) | very fast | Sensitive to lighting |
108
+ | U-Net | 34.5M | 0.794 (mIoU) | ~290 ms | Heavy baseline |
109
+ | Attention U-Net | 34.9M | 0.845 (IoU) | ~331 ms | Even heavier |
110
+ | BASNet | 87.1M | 0.561 (mIoU) | high | Very heavy |
111
+ | MobileSAM | 10.1M | ~0.80 (mIoU) | ~407 ms | Promptable, heavier |
112
+ | **Codelor Color 1.2** | **1.64M** | _to be filled_ | **~80-150 ms** | **CPU-friendly** |
113
+
114
+ ---
115
+
116
+ ## Architecture
117
+
118
+ - A **ConvNeXt-style convolutional stem** with anti-aliased downsampling.
119
+ - **8 Transformer blocks** with 2D RoPE and QK-Norm.
120
+ - A **color cross-attention** module injected at every block.
121
+ - A **gated FPN decoder** with deep supervision.
122
+ - An **edge head** (Sobel) to refine boundaries.
123
+ - **LayerScale** and **DropPath**.
124
+
125
+ ---
126
+
127
+ ## License
128
+
129
+ MIT.
config.json ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "architectures": [
3
+ "CodelorColor"
4
+ ],
5
+ "model_type": "codelor-color",
6
+ "format": "codelor_color_standalone_v2",
7
+ "version": "1.2",
8
+ "task": "image-segmentation",
9
+ "labels": [
10
+ "background",
11
+ "green"
12
+ ],
13
+ "green_ref": [
14
+ 0.2,
15
+ 0.85,
16
+ 0.3
17
+ ],
18
+ "hyperparameters": {
19
+ "dim": 128,
20
+ "depth": 8,
21
+ "heads": 8,
22
+ "tied_after": 4,
23
+ "mlp_ratio": 3.0,
24
+ "max_tokens": 1024
25
+ },
26
+ "training": {
27
+ "best_loss": 0.17389452453454335,
28
+ "epochs_done": 30,
29
+ "params": 1641055
30
+ }
31
+ }
model.safetensors ADDED
@@ -0,0 +1,3 @@
 
 
 
 
1
+ version https://git-lfs.github.com/spec/v1
2
+ oid sha256:0a9345fc422a3e39ca9c19611cc5bb1a2f0336a77a1428d971c0eb2242a96478
3
+ size 6575768
modeling_codelor.py ADDED
@@ -0,0 +1,178 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """Codelor Color v2 - Chroma key. Autonome (torch uniquement)."""
2
+ import torch, torch.nn as nn, torch.nn.functional as F
3
+ from pathlib import Path
4
+ import json
5
+
6
+ class RMSNorm(nn.Module):
7
+ def __init__(self, dim, eps=1e-6):
8
+ super().__init__(); self.eps = eps; self.w = nn.Parameter(torch.ones(dim))
9
+ def forward(self, x): return x * torch.rsqrt(x.pow(2).mean(-1, keepdim=True) + self.eps) * self.w
10
+
11
+ class SwiGLU(nn.Module):
12
+ def __init__(self, dim, hidden):
13
+ super().__init__(); self.w12 = nn.Linear(dim, hidden * 2, bias=False); self.w3 = nn.Linear(hidden, dim, bias=False)
14
+ def forward(self, x):
15
+ a, b = self.w12(x).chunk(2, dim=-1); return self.w3(F.silu(a) * b)
16
+
17
+ class LayerScale(nn.Module):
18
+ def __init__(self, dim, init=1e-6):
19
+ super().__init__(); self.gamma = nn.Parameter(torch.ones(dim) * init)
20
+ def forward(self, x): return x * self.gamma
21
+
22
+ class DropPath(nn.Module):
23
+ def __init__(self, p=0.0):
24
+ super().__init__(); self.p = p
25
+ def forward(self, x):
26
+ if self.p == 0.0 or not self.training: return x
27
+ keep = 1 - self.p; mask = torch.rand(x.shape[0], 1, 1, device=x.device) < keep
28
+ return x * mask / keep
29
+
30
+ class Attention(nn.Module):
31
+ def __init__(self, dim, heads):
32
+ super().__init__(); assert dim % heads == 0
33
+ self.heads = heads; self.dh = dim // heads
34
+ self.qkv = nn.Linear(dim, dim * 3, bias=False); self.q_norm = RMSNorm(self.dh); self.k_norm = RMSNorm(self.dh)
35
+ self.proj = nn.Linear(dim, dim, bias=False)
36
+ def forward(self, x, h, w):
37
+ B, N, C = x.shape
38
+ qkv = self.qkv(x).reshape(B, N, 3, self.heads, self.dh).permute(2, 0, 3, 1, 4)
39
+ q, k, v = qkv.unbind(0); q, k = self.q_norm(q), self.k_norm(k); q, k = rope_2d(q, k, h, w)
40
+ out = F.scaled_dot_product_attention(q, k, v)
41
+ return self.proj(out.transpose(1, 2).reshape(B, N, C))
42
+
43
+ class ColorCrossAttention(nn.Module):
44
+ def __init__(self, dim, heads):
45
+ super().__init__(); assert dim % heads == 0
46
+ self.heads = heads; self.dh = dim // heads
47
+ self.q = nn.Linear(dim, dim, bias=False); self.kv = nn.Linear(dim, dim * 2, bias=False)
48
+ self.q_norm = RMSNorm(self.dh); self.k_norm = RMSNorm(self.dh); self.proj = nn.Linear(dim, dim, bias=False)
49
+ def forward(self, spatial, cond):
50
+ B, N, C = spatial.shape; M = cond.shape[1]
51
+ q = self.q(spatial).reshape(B, N, self.heads, self.dh).transpose(1, 2)
52
+ kv = self.kv(cond).reshape(B, M, 2, self.heads, self.dh).permute(2, 0, 3, 1, 4)
53
+ k, v = kv.unbind(0); q, k = self.q_norm(q), self.k_norm(k)
54
+ out = F.scaled_dot_product_attention(q, k, v)
55
+ return self.proj(out.transpose(1, 2).reshape(B, N, C))
56
+
57
+ class Block(nn.Module):
58
+ def __init__(self, dim, heads, mlp_ratio=3.0, drop_path=0.0):
59
+ super().__init__(); self.norm1 = RMSNorm(dim); self.attn = Attention(dim, heads)
60
+ self.ls1 = LayerScale(dim); self.dp1 = DropPath(drop_path); self.norm2 = RMSNorm(dim)
61
+ self.ffn = SwiGLU(dim, int(dim * mlp_ratio)); self.ls2 = LayerScale(dim); self.dp2 = DropPath(drop_path)
62
+ def forward(self, x, h, w):
63
+ x = x + self.dp1(self.ls1(self.attn(self.norm1(x), h, w)))
64
+ return x + self.dp2(self.ls2(self.ffn(self.norm2(x))))
65
+
66
+ class AntiAliasDownsample(nn.Module):
67
+ def __init__(self, c_in, c_out):
68
+ super().__init__()
69
+ self.blur = nn.Conv2d(c_in, c_in, 3, padding=1, groups=c_in, bias=False)
70
+ kernel = torch.tensor([1., 2., 1.]); kernel = kernel / kernel.sum(); bk = kernel[:, None] * kernel[None, :]
71
+ with torch.no_grad(): self.blur.weight.copy_(bk.expand(c_in, 1, 3, 3))
72
+ self.blur.weight.requires_grad_(False)
73
+ self.conv = nn.Conv2d(c_in, c_out, 1, stride=2, bias=False); self.bn = nn.BatchNorm2d(c_out); self.act = nn.SiLU(inplace=True)
74
+ def forward(self, x): return self.act(self.bn(self.conv(self.blur(x))))
75
+
76
+ class ConvNeXtStem(nn.Module):
77
+ def __init__(self, dim):
78
+ super().__init__()
79
+ self.patch = nn.Sequential(nn.Conv2d(3, 32, kernel_size=4, stride=4, bias=False), nn.BatchNorm2d(32), nn.SiLU(inplace=True))
80
+ self.block1 = nn.Sequential(nn.Conv2d(32, 32, 7, padding=3, groups=32, bias=False), nn.BatchNorm2d(32), nn.SiLU(inplace=True), nn.Conv2d(32, 32, 1, bias=False), nn.BatchNorm2d(32))
81
+ self.down8 = AntiAliasDownsample(32, 64); self.down16 = AntiAliasDownsample(64, dim)
82
+ def forward(self, x):
83
+ x = self.patch(x); x = x + self.block1(x); f4 = x; f8 = self.down8(f4); f16 = self.down16(f8)
84
+ return f4, f8, f16
85
+
86
+ class SobelEdge(nn.Module):
87
+ def __init__(self):
88
+ super().__init__()
89
+ sx = torch.tensor([[-1,0,1],[-2,0,2],[-1,0,1]]).float().view(1,1,3,3); sy = torch.tensor([[-1,-2,-1],[0,0,0],[1,2,1]]).float().view(1,1,3,3)
90
+ self.register_buffer('sx', sx); self.register_buffer('sy', sy)
91
+ def forward(self, gray):
92
+ ex = F.conv2d(gray, self.sx, padding=1); ey = F.conv2d(gray, self.sy, padding=1)
93
+ return (ex**2 + ey**2 + 1e-6).sqrt()
94
+
95
+ class GatedDecoder(nn.Module):
96
+ def __init__(self, dim):
97
+ super().__init__()
98
+ self.proj16 = nn.Conv2d(dim, dim, 1, bias=False); self.aux_head = nn.Conv2d(dim, 1, 1)
99
+ self.up8 = nn.Sequential(nn.Conv2d(dim+64, dim, 3, padding=1, bias=False), nn.BatchNorm2d(dim), nn.SiLU(inplace=True))
100
+ self.up4 = nn.Sequential(nn.Conv2d(dim+32, 64, 3, padding=1, bias=False), nn.BatchNorm2d(64), nn.SiLU(inplace=True))
101
+ self.up1 = nn.Sequential(nn.Conv2d(64+3+1+1, 32, 3, padding=1, bias=False), nn.BatchNorm2d(32), nn.SiLU(inplace=True), nn.Conv2d(32, 32, 3, padding=1, bias=False), nn.BatchNorm2d(32), nn.SiLU(inplace=True))
102
+ self.head = nn.Conv2d(32, 1, 1); self.gate8 = nn.Parameter(torch.zeros(1)); self.gate4 = nn.Parameter(torch.zeros(1)); self.edge = SobelEdge()
103
+ def forward(self, latent, f4, f8, img, ref_rgb):
104
+ B, _, H, W = img.shape; x = self.proj16(latent)
105
+ aux = self.aux_head(x); aux = F.interpolate(aux, size=f4.shape[-2:], mode='bilinear', align_corners=False)
106
+ x = F.interpolate(x, size=f8.shape[-2:], mode='bilinear', align_corners=False)
107
+ x = self.up8(torch.cat([x, torch.sigmoid(self.gate8) * f8], 1))
108
+ x = F.interpolate(x, size=f4.shape[-2:], mode='bilinear', align_corners=False)
109
+ x = self.up4(torch.cat([x, torch.sigmoid(self.gate4) * f4], 1))
110
+ x = F.interpolate(x, size=(H, W), mode='bilinear', align_corners=False)
111
+ ref_map = ref_rgb.view(B, 3, 1, 1).expand(B, 3, H, W); cos = F.cosine_similarity(img, ref_map, dim=1, eps=1e-6).unsqueeze(1)
112
+ gray = img.mean(1, keepdim=True); edge = self.edge(gray); x = self.up1(torch.cat([x, img, cos, edge], 1))
113
+ return self.head(x), aux
114
+
115
+ class CodelorColor(nn.Module):
116
+ def __init__(self, dim=128, depth=8, heads=8, tied_after=4, mlp_ratio=3.0, max_tokens=1024, drop_path_rate=0.1):
117
+ super().__init__(); self.dim, self.depth, self.heads = dim, depth, heads; self.tied_after, self.mlp_ratio = tied_after, mlp_ratio; self.max_tokens = max_tokens
118
+ self.stem = ConvNeXtStem(dim); self.color_mlp = nn.Sequential(nn.Linear(3, dim), nn.SiLU(inplace=True), nn.Linear(dim, dim))
119
+ self.pos = nn.Parameter(torch.zeros(1, max_tokens, dim)); nn.init.trunc_normal_(self.pos, std=0.02)
120
+ n_unique = depth if tied_after <= 0 else min(depth, tied_after)
121
+ dp = [drop_path_rate * i / max(1, depth - 1) for i in range(depth)]
122
+ self.blocks = nn.ModuleList([Block(dim, heads, mlp_ratio, drop_path=dp[i] if i < len(dp) else 0.0) for i in range(n_unique)])
123
+ self.color_xattn = nn.ModuleList([ColorCrossAttention(dim, heads) for _ in range(n_unique)])
124
+ self.norm_out = RMSNorm(dim); self.decoder = GatedDecoder(dim)
125
+ def _get(self, i):
126
+ if self.tied_after > 0 and i >= self.tied_after: return self.blocks[i - self.tied_after], self.color_xattn[i - self.tied_after]
127
+ return self.blocks[i], self.color_xattn[i]
128
+ def forward(self, img, ref_rgb, return_aux=False):
129
+ B = img.shape[0]; f4, f8, f16 = self.stem(img); _, _, h, w = f16.shape; N = h * w
130
+ tokens = f16.flatten(2).transpose(1, 2)
131
+ if N <= self.pos.shape[1]: tokens = tokens + self.pos[:, :N]
132
+ c_tok = self.color_mlp(ref_rgb).unsqueeze(1); x = tokens
133
+ for i in range(self.depth):
134
+ blk, xa = self._get(i); x = blk(x, h, w); x = x + xa(x, c_tok)
135
+ x = self.norm_out(x); latent = x.transpose(1, 2).reshape(B, -1, h, w)
136
+ main, aux = self.decoder(latent, f4, f8, img, ref_rgb)
137
+ return (main, aux) if return_aux else main
138
+ def config(self): return dict(dim=self.dim, depth=self.depth, heads=self.heads, tied_after=self.tied_after, mlp_ratio=self.mlp_ratio, max_tokens=self.max_tokens)
139
+
140
+ def rope_2d(q, k, h, w, base=100.0):
141
+ B, Hh, N, D = q.shape; d4 = D // 4
142
+ freqs = 1.0 / (base ** (torch.arange(0, d4, device=q.device).float() / d4))
143
+ xs = torch.arange(w, device=q.device).float(); ys = torch.arange(h, device=q.device).float()
144
+ ax = torch.outer(xs, freqs)[None, :, :].expand(h, w, d4).reshape(h * w, d4)
145
+ ay = torch.outer(ys, freqs)[:, None, :].expand(h, w, d4).reshape(h * w, d4)
146
+ ang = torch.cat([ax, ay], dim=-1); cos, sin = ang.cos()[None, None], ang.sin()[None, None]
147
+ def rot(x):
148
+ x1, x2 = x[..., :D//2], x[..., D//2:]
149
+ return torch.cat([x1*cos - x2*sin, x1*sin + x2*cos], dim=-1)
150
+ return rot(q), rot(k)
151
+
152
+
153
+
154
+ def load_from_pretrained(path_or_repo, device="cpu"):
155
+ p = Path(path_or_repo)
156
+ if not p.exists():
157
+ from huggingface_hub import snapshot_download
158
+ p = Path(snapshot_download(path_or_repo))
159
+ cfg = json.loads((p / "config.json").read_text())
160
+ model = CodelorColor(**cfg["hyperparameters"])
161
+ from safetensors.torch import load_file
162
+ model.load_state_dict(load_file(p / "model.safetensors"))
163
+ return model.eval().to(device)
164
+
165
+ def predict_mask(model, image_tensor, color_rgb=None, threshold=0.5, device="cpu"):
166
+ if color_rgb is None: color_rgb = (0.20, 0.85, 0.30)
167
+ if image_tensor.ndim == 3: image_tensor = image_tensor.unsqueeze(0)
168
+ image_tensor = image_tensor.to(device)
169
+ ref = torch.tensor(color_rgb, dtype=torch.float32, device=device).view(1, 3)
170
+ with torch.no_grad():
171
+ prob = torch.sigmoid(model(image_tensor, ref))[0, 0].cpu().numpy()
172
+ return prob > threshold, prob
173
+
174
+ def remove_green(image_tensor, model, color_rgb=None, threshold=0.5, device="cpu"):
175
+ import numpy as np
176
+ _, prob = predict_mask(model, image_tensor, color_rgb, threshold, device)
177
+ alpha = torch.from_numpy((1.0 - prob.astype("float32")).clip(0, 1)).unsqueeze(0)
178
+ return torch.cat([image_tensor.cpu(), alpha], dim=0)