Download training/shadows.py from pashaalex/ecg-rectifier: direct link, hf CLI and curl.
- Browser
- Download file 9.24 kB
-
https://huggingface.co/pashaalex/ecg-rectifier/resolve/main/training/shadows.py
- Command line
-
hf download hf://pashaalex/ecg-rectifier/training/shadows.py
-
curl -L -o shadows.py https://huggingface.co/pashaalex/ecg-rectifier/resolve/main/training/shadows.py
9.24 kB
| """Sharp shadows drawn on the FLAT texture before rendering (v5), bent together with the grid. | |
| Modes: "crumple" (Voronoi facets with Lambert brightness), "folds" (2-4 broken crease lines, or the | |
| real fold lines of the geometry, with valley / ridge shading), "cast" (hard shadow of a hand, phone | |
| or ruler). The result is a multiplier map ~0.2..1.15 with blurred borders. | |
| """ | |
| import math | |
| import cv2 | |
| import numpy as np | |
| MODES = ("none", "crumple", "folds", "cast") | |
| MODE_P = (0.2, 0.35, 0.3, 0.15) | |
| EXTRA_CAST_P = 0.15 # a cast shadow on top of crumple / folds | |
| def _u(rng, lo, hi): | |
| return float(rng.uniform(lo, hi)) | |
| def _facet_brightness(rng, light, max_tilt_deg, ambient): | |
| """Lambert brightness of a facet whose normal is tilted from +z by up | |
| to max_tilt_deg, relative to a flat facet (1.0).""" | |
| tilt = math.radians(_u(rng, 0, max_tilt_deg)) | |
| az = _u(rng, 0, 2 * math.pi) | |
| n = np.array([math.sin(tilt) * math.cos(az), math.sin(tilt) * math.sin(az), math.cos(tilt)]) | |
| lam = max(0.0, float(n @ light)) | |
| flat = max(1e-3, float(light[2])) | |
| return ambient + (1 - ambient) * lam / flat | |
| def _random_light(rng): | |
| elev = math.radians(_u(rng, 25, 75)) # above the sheet | |
| az = _u(rng, 0, 2 * math.pi) | |
| return np.array([math.cos(elev) * math.cos(az), math.cos(elev) * math.sin(az), math.sin(elev)]) | |
| def _soften(shade, rng, ppm): | |
| sigma_mm = math.exp(_u(rng, math.log(0.05), math.log(0.8))) | |
| sigma = sigma_mm * ppm | |
| if sigma > 0.4: | |
| shade = cv2.GaussianBlur(shade, (0, 0), sigma) | |
| return shade, sigma_mm | |
| def _smooth_noise(rng, h, w, cell_px, amplitude): | |
| field = rng.normal(0.0, 1.0, (h // cell_px + 2, w // cell_px + 2)).astype(np.float32) | |
| field = cv2.resize(field, (w, h), interpolation=cv2.INTER_CUBIC) | |
| field /= max(1e-6, float(field.std())) | |
| return amplitude * field | |
| def crumple(rng, h, w, ppm): | |
| size_mm = _u(rng, 8, 45) # typical facet size | |
| W, H = w / ppm, h / ppm | |
| n = int(np.clip(W * H / size_mm ** 2 * _u(rng, 0.8, 1.2), 4, 600)) | |
| margin = 3 * size_mm | |
| rect = (int(-margin * ppm), int(-margin * ppm), int((W + 2 * margin) * ppm), int((H + 2 * margin) * ppm)) | |
| sub = cv2.Subdiv2D(rect) | |
| pts = np.stack([rng.uniform(-margin / 2, W + margin / 2, n), rng.uniform(-margin / 2, H + margin / 2, n)], 1) | |
| for x, y in pts * ppm: | |
| sub.insert((float(x), float(y))) | |
| facets, _ = sub.getVoronoiFacetList([]) | |
| light = _random_light(rng) | |
| max_tilt = _u(rng, 15, 45) | |
| ambient = _u(rng, 0.3, 0.65) | |
| shade = np.ones((h, w), np.float32) | |
| values = [] | |
| for poly in facets: | |
| b = _facet_brightness(rng, light, max_tilt, ambient) | |
| values.append(b) | |
| cv2.fillPoly(shade, [np.round(poly * 16).astype(np.int32)], float(b), cv2.LINE_AA, shift=4) | |
| # facets are slightly curved: a weak gradient inside each one | |
| shade *= 1.0 + _smooth_noise(rng, h, w, max(4, int(size_mm * ppm / 2)), _u(rng, 0.0, 0.05)) | |
| shade, soft = _soften(shade, rng, ppm) | |
| return shade, dict(mode="crumple", facet_mm=size_mm, facets=len(facets), max_tilt_deg=max_tilt, | |
| ambient=ambient, soft_mm=soft) | |
| def _crease_polyline(rng, W, H, straight=None): | |
| """Broken line across the sheet, extended far beyond it, in mm. | |
| straight = (s0, angle_deg): a straight line (U cos a + V sin a = s0).""" | |
| big = 3 * (W + H) | |
| if straight is not None: | |
| s0, ang = straight | |
| a = math.radians(ang) | |
| n = np.array([math.cos(a), math.sin(a)]) | |
| d = np.array([-n[1], n[0]]) | |
| p = n * s0 | |
| return np.array([p - big * d, p + big * d]) | |
| p = np.array([_u(rng, 0.15, 0.85) * W, _u(rng, 0.15, 0.85) * H]) | |
| ang = _u(rng, 0, math.pi) | |
| pts = [p] | |
| # walk both ways from p; 0-2 kinks on each side, inside the sheet | |
| for direction in (1.0, -1.0): | |
| q, a = p.copy(), ang | |
| side = [] | |
| for _ in range(int(rng.integers(0, 3))): | |
| step = _u(rng, 0.15, 0.45) * max(W, H) | |
| q = q + direction * step * np.array([math.cos(a), math.sin(a)]) | |
| side.append(q.copy()) | |
| a += math.radians(_u(rng, 8, 35)) * rng.choice([-1, 1]) | |
| q = q + direction * big * np.array([math.cos(a), math.sin(a)]) | |
| side.append(q) | |
| pts = pts + side if direction > 0 else side[::-1] + pts | |
| return np.array(pts) | |
| def _side_mask(poly_px, h, w): | |
| """1 on the left side of the (far-extended) polyline, 0 on the right.""" | |
| d0 = poly_px[1] - poly_px[0] | |
| d1 = poly_px[-1] - poly_px[-2] | |
| big = 10 * (h + w) | |
| left0 = np.array([d0[1], -d0[0]]) / max(1e-9, np.hypot(*d0)) | |
| left1 = np.array([d1[1], -d1[0]]) / max(1e-9, np.hypot(*d1)) | |
| polygon = np.concatenate([poly_px, [poly_px[-1] + big * left1, poly_px[0] + big * left0]]) | |
| mask = np.zeros((h, w), np.float32) | |
| cv2.fillPoly(mask, [np.round(polygon * 16).astype(np.int64).astype(np.int32)], 1.0, cv2.LINE_AA, shift=4) | |
| return mask | |
| def folds(rng, h, w, ppm, fold_lines=None): | |
| """fold_lines: [(s0 mm, angle deg), ...] real folds of the geometry, or None.""" | |
| W, H = w / ppm, h / ppm | |
| if fold_lines: | |
| lines = [_crease_polyline(rng, W, H, straight=f) for f in fold_lines] | |
| else: | |
| lines = [_crease_polyline(rng, W, H) for _ in range(int(rng.integers(2, 5)))] | |
| light = _random_light(rng) | |
| ambient = _u(rng, 0.3, 0.65) | |
| max_tilt = _u(rng, 20, 55) | |
| shade = np.ones((h, w), np.float32) | |
| # region brightness: product of per-crease side factors (each crease tilts one side) | |
| for poly in lines: | |
| poly_px = np.clip(poly * ppm, -1e6, 1e6) | |
| side = _side_mask(poly_px, h, w) | |
| b = _facet_brightness(rng, light, max_tilt, ambient) | |
| shade *= 1.0 + (b - 1.0) * side | |
| # valley: a dark ramp on one side; ridge: a thin highlight | |
| line_img = np.full((h, w), 255, np.uint8) | |
| cv2.polylines(line_img, [np.round(poly_px).astype(np.int32)], False, 0, 1) | |
| dist = cv2.distanceTransform(line_img, cv2.DIST_L2, cv2.DIST_MASK_PRECISE) / ppm | |
| if rng.random() < 0.7: | |
| width = _u(rng, 2, 30) | |
| depth = _u(rng, 0.15, 0.55) | |
| which = side if rng.random() < 0.5 else 1.0 - side | |
| shade *= 1.0 - depth * np.exp(-dist / width) * which | |
| if rng.random() < 0.3: | |
| shade *= 1.0 + _u(rng, 0.05, 0.15) * np.exp(-0.5 * (dist / _u(rng, 0.2, 0.8)) ** 2) | |
| shade, soft = _soften(shade, rng, ppm) | |
| return shade, dict(mode="folds", creases=len(lines), real_folds=bool(fold_lines), ambient=ambient, | |
| max_tilt_deg=max_tilt, soft_mm=soft) | |
| def cast(rng, h, w, ppm): | |
| W, H = w / ppm, h / ppm | |
| shade = np.ones((h, w), np.float32) | |
| count = int(rng.integers(1, 3)) | |
| for _ in range(count): | |
| c = np.array([_u(rng, -0.1, 1.1) * W, _u(rng, -0.1, 1.1) * H]) | |
| kind = str(rng.choice(["blob", "bar"])) | |
| if kind == "bar": # ruler / pen / phone edge | |
| a = _u(rng, 0, math.pi) | |
| d = np.array([math.cos(a), math.sin(a)]) | |
| nrm = np.array([-d[1], d[0]]) | |
| L, T = _u(rng, 0.5, 2.0) * max(W, H), _u(rng, 5, 60) | |
| poly = np.array([c - L * d - T * nrm, c + L * d - T * nrm, c + L * d + T * nrm, c - L * d + T * nrm]) | |
| else: # hand-like irregular polygon | |
| k = int(rng.integers(5, 10)) | |
| r = _u(rng, 0.15, 0.45) * min(W, H) | |
| ang = np.sort(rng.uniform(0, 2 * math.pi, k)) | |
| rad = r * rng.uniform(0.5, 1.3, k) | |
| poly = c + np.stack([rad * np.cos(ang), rad * np.sin(ang)], 1) | |
| mask = np.zeros((h, w), np.float32) | |
| cv2.fillPoly(mask, [np.round(poly * ppm * 16).astype(np.int32)], 1.0, cv2.LINE_AA, shift=4) | |
| shade *= 1.0 - _u(rng, 0.25, 0.65) * mask | |
| shade, soft = _soften(shade, rng, ppm) | |
| return shade, dict(mode="cast", objects=count, soft_mm=soft) | |
| def shadow_map(rng, h, w, ppm, fold_lines=None): | |
| """Multiplier [h, w] float32 (or None for "none") and info.""" | |
| mode = str(rng.choice(MODES, p=MODE_P)) | |
| if mode == "none": | |
| return None, dict(mode="none") | |
| if mode == "crumple": | |
| shade, info = crumple(rng, h, w, ppm) | |
| elif mode == "folds": | |
| use_real = fold_lines and rng.random() < 0.7 | |
| shade, info = folds(rng, h, w, ppm, fold_lines if use_real else None) | |
| else: | |
| shade, info = cast(rng, h, w, ppm) | |
| if mode != "cast" and rng.random() < EXTRA_CAST_P: | |
| extra, extra_info = cast(rng, h, w, ppm) | |
| shade *= extra | |
| info["cast"] = extra_info | |
| tint = np.array([_u(rng, 1.0, 1.06), 1.0, _u(rng, 0.94, 1.0)], np.float32) # shadows slightly bluish | |
| info["tint"] = tint.tolist() | |
| return np.clip(shade, 0.2, 1.15).astype(np.float32), info | |
| def apply(texture_bgr, shade, tint=(1.0, 1.0, 1.0)): | |
| """texture uint8 [h, w, 3] x shade -> uint8; the tint acts in proportion to the darkening.""" | |
| tex = texture_bgr.astype(np.float32) | |
| dark = (1.0 - np.clip(shade, 0, 1))[..., None] | |
| t = np.asarray(tint, np.float32)[None, None, :] | |
| out = tex * shade[..., None] * (1.0 + (t - 1.0) * dark) | |
| return np.clip(out, 0, 255).astype(np.uint8) | |