Kirk Trombley 2 жил өмнө
parent
commit
fd5c18a2c3

+ 0 - 320
legacy/anim_ingest.py

@@ -1,320 +0,0 @@
-import io
-import math
-import itertools
-import multiprocessing
-from typing import Callable, NamedTuple
-
-from PIL import Image
-from bs4 import BeautifulSoup
-from colorspacious import cspace_convert
-from scipy.cluster import vq
-import requests
-import numpy as np
-
-import ingest
-
-extension = ".gif"
-cluster_seed = 20220328
-cluster_attempts = 10
-base = "https://play.pokemonshowdown.com/sprites/ani/"
-back_base = "https://play.pokemonshowdown.com/sprites/ani-back/"
-
-# removing all forms of a pokemon, and also pokestars
-start_with_filters = [
-  # no significant visual changes
-  "arceus-", "silvally-", "genesect-", "pumpkaboo-", "gourgeist-", "unown-", "giratina-",
-  # cannot start the battle in alternate form
-  "castform-", "cherrim-", "aegislash-", "xerneas-", "wishiwashi-", 
-  "eiscue-", "mimikyu-", "cramorant-", "morpeko-",
-  # weird event thing
-  "greninja-", "eevee-", "pikachu-", "zarude-", "magearna-",
-  # pokestars
-  "pokestar",
-]
-
-# removing all forms of a type
-end_with_filters = [
-  "-mega", "-megax", "-megay", "-primal", "-ultra",
-  "-gmax", "-eternamax", "-totem", "-f", "-b", "-old", "-shiny",
-  "-eternalflower", "-rapidstrikegmax",
-]
-
-# removing pokemon entirely
-full_filters = [
-  # darmanitan zen forms (cannot start in zen)
-  "darmanitan-galarzen", "darmanitan-zen",
-  # minior core forms (cannot start in anything but -meteor, renamed below)
-  "minior", "minior-blue", "minior-green", "minior-indigo",
-  "minior-orange", "minior-red", "minior-violet", "minior-yellow",
-  # gimmighoul roaming (cannot start roaming)
-  "gimmighoul-roaming",
-  # palafin hero (cannot start as hero)
-  "palafin-hero",
-  # because it is a create-a-pokemon
-  "argalis", "arghonaut", "brattler", "breezi", "caimanoe", "cawdet", 
-  "colossoil", "coribalis", "cupra", "cyclohm", "dorsoil", "duohm",
-  "electrelk", "embirch", "fawnifer", "flarelm", "floatoy", "krillowatt",
-  "krolowatt", "miasmite", "monohm", "necturine", "nohface", "privatyke",
-  "pyroak", "rebble", "revenankh", "saharaja", "snugglow", "solotl",
-  "swirlpool", "syclant", "syclar", "tactite", "venomicon",
-  "venomicon-epilogue", "volkritter", "voodoll",
-  "astrolotl", "aurumoth", "caribolt", "cawmodore", "chromera", "crucibelle",
-  "equilibra", "fidgit", "jumbao", "justyke", "kerfluffle", "kitsunoh",
-  "krilowatt", "malaconda", "miasmaw", "mollux", "naviathan", "necturna",
-  "pajantom", "plasmanta", "pluffle", "protowatt", "scratchet", "smogecko",
-  "smoguana", "smokomodo", "snaelstrom", "stratagem", "tomohawk", "volkraken", "voodoom",
-  # typos/duplicates
-  "0", "arctovolt", "buffalant", "burmy-plant", "darmanitan-standard", 
-  "deerling-spring", "deoxys-rs", "gastrodon-west",
-  "klinklang-back", "krikretot", "marenie", "marowak-alolan", "meloetta-aria",
-  "pichu-spikyeared", "polteageist-chipped", "rattata-alolan", "regidragon",
-  "sawsbuck-spring", "shaymin-land", "shellos-west", "sinistea-chipped", "wormadam-plant",
-  "pumpkabo-super", "magcargo%20", "meowstic-female",
-  "ratatta-a", "ratatta-alola", "raticate-a",
-  "rotom-h", "rotom-m", "rotom-s", "rotom-w",
-  # not a pokemon
-  "substitute", "egg", "egg-manaphy", "missingno",
-]
-
-# force certain pokemon to stay
-force_keep = [ "meowstic-f", "unfezant-f", "pyroar-f" ]
-
-# rename certain pokemon after the fact
-rename = {
-  # dash consistency
-  "nidoranm": "nidoran-m",
-  "nidoranf": "nidoran-f",
-  "porygonz": "porygon-z",
-  "tapubulu": "tapu-bulu",
-  "tapufini": "tapu-fini",
-  "tapukoko": "tapu-koko",
-  "tapulele": "tapu-lele",
-  "hooh": "ho-oh",
-  "mimejr": "mime-jr",
-  "mrmime": "mr-mime",
-  "mrmime-galar": "mr-mime-galar",
-  "mrrime": "mr-rime",
-  "jangmoo": "jangmo-o",
-  "hakamoo": "hakamo-o",
-  "kommoo": "kommo-o",
-  "typenull": "type-null",
-  "oricorio-pompom": "oricorio-pom-pom",
-  "necrozma-duskmane": "necrozma-dusk-mane",
-  "necrozma-dawnwings": "necrozma-dawn-wings",
-  "toxtricity-lowkey": "toxtricity-low-key",
-
-  # rename forms
-  "shellos": "shellos-west",
-  "shaymin": "shaymin-land",
-  "meloetta": "meloetta-aria",
-  "keldeo": "keldeo-ordinary",
-  "hoopa": "hoopa-confined",
-  "burmy": "burmy-plant",
-  "wormadam": "wormadam-plant",
-  "deerling": "deerling-spring",
-  "sawsbuck": "sawsbuck-spring",
-  "vivillon": "vivillon-meadow",
-  "basculin": "basculin-redstriped",
-  "meowstic": "meowstic-male",
-  "meowstic-f": "meowstic-female",
-  "pyroar-f": "pyroar-female",
-  "flabebe": "flabebe-red",
-  "floette": "floette-red",
-  "florges": "florges-red",
-  "minior-meteor": "minior",
-  "sinistea": "sinistea-phony",
-  "polteageist": "polteageist-phony",
-  "gastrodon": "gastrodon-west",
-  "furfrou": "furfrou-natural",
-  "wishiwashi": "wishiwashi-school",
-  "tornadus": "tornadus-incarnate",
-  "landorus": "landorus-incarnate",
-  "thundurus": "thundurus-incarnate",
-  "calyrex-ice": "calyrex-ice-rider",
-  "calyrex-shadow": "calyrex-shadow-rider",
-  "urshifu-rapidstrike": "urshifu-rapid-strike",
-  "zacian": "zacian-hero",
-  "zamazenta": "zamazenta-hero",
-}
-
-
-def get_all_pokemon(url: str, ext: str = extension) -> list[str]:
-  # TODO clean this up
-  soup = BeautifulSoup(requests.get(url).text, "html.parser")
-  imgs = [href for a in soup.find_all("a") if (href := a.get("href")).endswith(ext)]
-  return [
-    g[:-4]
-    for g in imgs
-    if g in [name + ext for name in force_keep] or (
-      g not in [full + ext for full in full_filters]
-      and not any(g.startswith(f) for f in start_with_filters)
-      and not any(g.endswith(f) for f in [ending + ext for ending in end_with_filters])
-    )
-  ]
-
-
-def load_image(base: str, name: str, ext: str = extension) -> Image:
-  return Image.open(io.BytesIO(requests.get(base + name + ext).content))
-
-
-def get_all_pixels(im: Image) -> list[tuple[int, int, int]]:
-  rgb_pixels = []
-  for fr in range(getattr(im, "n_frames", 1)):
-    im.seek(fr)
-    rgb_pixels += [
-      (r, g, b)
-      for r, g, b, a in im.convert("RGBA").getdata()
-      if not ingest.is_outline(r, g, b, a)
-    ]
-  return rgb_pixels
-
-
-def merge_dist_jab(p: np.array, q: np.array) -> float:
-  pj, pa, pb = p
-  qj, qa, qb = q
-  light_diff = abs(pj - qj)
-  hue_angle = math.acos((pa * qa + pb * qb) / math.sqrt((pa ** 2 + pb ** 2) * (qa ** 2 + qb ** 2))) * 180 / math.pi
-  return light_diff if hue_angle <= 10 and light_diff <= 20 else None
-
-
-def merge_dist_rgb(p: np.array, q: np.array) -> float:
-  return merge_dist_jab(*cspace_convert(np.array([p, q]), "sRGB255", "CAM02-UCS"))
-
-
-def score_clustering_jab(means: list[np.array]) -> float:
-  score = 0
-  count = 0
-  for p, q in itertools.combinations(means, 2):
-    # squared dist in the a-b plane
-    _, pa, pb = p
-    _, qa, qb = q
-    score += (pa - qa) ** 2 + (pb - qb) ** 2
-    count += 1
-  return score / count
-
-
-def score_clustering_rgb(means: list[np.array]) -> float:
-  return score_clustering_jab(list(cspace_convert(np.array(means), "sRGB255", "CAM02-UCS")))
-
-
-Stats = NamedTuple("Stats", [("size", int), ("inertia", float), ("mu", np.array), ("nu", np.array)])
-
-
-def merge_stats(s1: Stats, s2: Stats) -> Stats:
-  ts = s1.size + s2.size
-  f1 = s1.size / ts
-  f2 = s2.size / ts
-  return Stats(
-    size=ts,
-    inertia=s1.inertia * f1 + s2.inertia * f2,
-    mu=s1.mu * f1 + s2.mu * f2,
-    nu=s1.nu * f1 + s2.nu * f2,
-  )
-
-
-def flatten_stats(ss: list[Stats], target_len: int = 40) -> list[float]:
-  to_return = []
-  for s in ss:
-    to_return += [s.size, s.inertia, *s.mu, *s.nu]
-  return to_return + ([0] * (target_len - len(to_return)))
-
-
-def compute_stats(
-  pixels: np.array,
-  clustering_scorer: Callable[[list[np.array]], float],
-  merge_dist: Callable[[np.array, np.array], float],
-) -> list[Stats]:
-
-  total_stats = Stats(
-    size=len(pixels),
-    inertia=ingest.inertia(pixels),
-    mu=ingest.mu(pixels),
-    nu=ingest.nu(pixels),
-  )
-
-  # run k-means multiple times, for multiple k's, trying to maximize the clustering_scorer
-  best = None
-  for k in (2, 3, 4):
-    for i in range(cluster_attempts):
-      means, labels = vq.kmeans2(pixels.astype(float), k, minit="++", seed=cluster_seed + i)
-      score = clustering_scorer(means)
-      if best is None or best[0] < score:
-        best = (score, means, labels)
-  _, best_means, best_labels = best
-
-  cluster_stats = []
-  for i in range(len(best_means)):
-    cluster_pixels = pixels[best_labels == i]
-    cluster_stats.append(Stats(
-      size=len(cluster_pixels),
-      inertia=ingest.inertia(cluster_pixels),
-      mu=best_means[i],
-      nu=ingest.nu(cluster_pixels),
-    ))
-
-  # assuming there are still more than two clusters,
-  # attempt to merge the closest if they're close enough
-  if len(cluster_stats) > 2:
-    # first, find all the options
-    options = []
-    for i, j in itertools.combinations(range(len(cluster_stats)), 2):
-      ci = cluster_stats[i]
-      cj = cluster_stats[j]
-      if (dist := merge_dist(ci.mu, cj.mu)) is not None:
-        rest = [c for k, c in enumerate(cluster_stats) if k not in (i, j)]
-        options.append((dist, [merge_stats(ci, cj), *rest]))
-    # if there are multiple options, use the closest,
-    # otherwise leaves cluster_stats the same
-    if len(options) > 0:
-      cluster_stats = min(options, key=lambda x: x[0])[1]
-
-  return [total_stats, *cluster_stats]
-
-
-def get_stats(name: str) -> list[float]:
-  front = get_all_pixels(load_image(base, name))
-  back = get_all_pixels(load_image(back_base, name))
-  rgb_pixels = np.array(front + back)
-  jab_pixels = cspace_convert(rgb_pixels, "sRGB255", "CAM02-UCS")
-  jab_stats = flatten_stats(compute_stats(
-    jab_pixels,
-    score_clustering_jab,
-    merge_dist_jab,
-  ))[1:]
-  rgb_stats = flatten_stats(compute_stats(
-    rgb_pixels,
-    score_clustering_rgb,
-    merge_dist_rgb,
-  ))[1:]
-  return [len(rgb_pixels), *jab_stats, *rgb_stats]
-
-
-if __name__ == "__main__":
-  pkmn = get_all_pokemon(back_base)
-  print("Found", len(pkmn), "sprites...")
-  errors = []
-
-  def ingest_and_format(pair: tuple[int, str]) -> str:
-    index, name = pair
-    try:
-      print(f"Ingesting #{index+1}: {name}...")
-      stats = get_stats(name)
-      format_name = rename.get(name, name)
-      print(f"Finished #{index+1}: {name}, saving under {format_name}")
-      return f'  [ "{format_name}", {", ".join(str(n) for n in stats)} ],\n'
-    except Exception as e:
-      print(e)
-      errors.append((name, e))
-
-  with multiprocessing.Pool(4) as pool:
-    stats = sorted(res for res in pool.imap_unordered(ingest_and_format, enumerate(pkmn), 100) if res is not None)
-
-  print(f"Calculated {len(stats)} statistics, writing...")
-
-  with open("database-v3.js", "w") as outfile:
-    outfile.write("const databaseV3 = [\n")
-    for line in sorted(stats):
-      outfile.write(line)
-    outfile.write("];\n")
-  print("Errors:", errors)
-

+ 0 - 66
legacy/explore.py

@@ -1,66 +0,0 @@
-#!/usr/bin/env python3
-import csv
-import math
-from collections import defaultdict
-
-# include X
-include_x = False
-# normalize q and Y
-normalize = True
-# closeness coefficient
-closeness = 2
-# step size within RGB color space
-step = 4
-
-data = []
-with open("database.csv") as infile:
-    for name, *nums in csv.reader(infile, delimiter=",", quotechar="'"):
-        data.append((name, *[float(n) for n in nums]))
-
-counts = defaultdict(int)
-results = {}
-
-try:
-    with open("best.csv") as infile:
-        for r, g, b, name, score in csv.reader(infile, delimiter=",", quotechar="'"):
-            results[(int(r), int(g), int(b))] = (name, float(score))
-except:
-    pass  # file not found, assume no prior results
-
-for r in range(0, 256, step):
-    for g in range(0, 256, step):
-        for b in range(0, 256, step):
-            if (known := results.get((r, g, b), None)) is not None:
-                counts[known[0]] += 1
-                continue
-            norm_color = math.sqrt(r * r + g * g + b * b)
-            if norm_color == 0:
-                continue
-            best_score = None
-            best_name = None
-            for name, x, yr, yg, yb in data:
-                norm = (
-                    (norm_color * math.sqrt(yr * yr + yg * yg + yb * yb))
-                    if normalize
-                    else 1
-                )
-                score = (x if include_x else 0) - (
-                    closeness * (r * yr + g * yg + b * yb) / norm
-                )
-                if best_score is None or score < best_score:
-                    best_score = score
-                    best_name = name
-            results[(r, g, b)] = (best_name, best_score)
-            counts[best_name] += 1
-
-with open("best.csv", "w") as outfile:
-    csv.writer(outfile, delimiter=",", quotechar="'").writerows(
-        (*k, *v) for k, v in results.items()
-    )
-
-with open("counts.csv", "w") as outfile:
-    csv.writer(outfile, delimiter=",", quotechar="'").writerows(counts.items())
-
-print(f"Top ten most hit:")
-for k in sorted(list(counts), key=counts.get, reverse=True)[:10]:
-    print(f"{k} - {counts[k]}")

+ 0 - 62
legacy/gen9_ingest.py

@@ -1,62 +0,0 @@
-import json
-
-import numpy as np
-from colorspacious import cspace_convert
-
-from ingest import is_outline
-from anim_ingest import (
-    get_all_pokemon,
-    flatten_stats,
-    compute_stats,
-    base as old_base,
-    load_image,
-    score_clustering_jab,
-    merge_dist_jab,
-    score_clustering_rgb,
-    merge_dist_rgb,
-)
-
-new_base = "https://play.pokemonshowdown.com/sprites/gen5/"
-png = ".png"
-
-# TODO this could probably be simplified but it works for a first pass
-new_pokemon = sorted(
-    set(get_all_pokemon(new_base, png)) - set(get_all_pokemon(old_base))
-)
-
-# hacked together from what's in anim_ingest and ingest
-for name in new_pokemon:
-    print(f"Ingesting {name}...")
-    image = load_image(new_base, name, png)
-
-    # read non-outline pixels of image
-    rgb_pixels = np.array(
-        [
-            (r, g, b)
-            for r, g, b, a in image.convert("RGBA").getdata()
-            if not is_outline(r, g, b, a)
-        ]
-    )
-
-    # convert RGB pixels to CAM02 values
-    jab_pixels = cspace_convert(rgb_pixels, "sRGB255", "CAM02-UCS")
-
-    # compute stats
-    jab_stats = flatten_stats(
-        compute_stats(
-            jab_pixels,
-            score_clustering_jab,
-            merge_dist_jab,
-        )
-    )[1:]
-    rgb_stats = flatten_stats(
-        compute_stats(
-            rgb_pixels,
-            score_clustering_rgb,
-            merge_dist_rgb,
-        )
-    )[1:]
-
-    # and fuck it, just print it to a new file and we'll copy it in manually
-    with open("database-gen9.json", "a") as outfile:
-        outfile.write(json.dumps([name, len(rgb_pixels), *jab_stats, *rgb_stats]) + ",\n")

+ 0 - 93
legacy/ingest.py

@@ -1,93 +0,0 @@
-#!/usr/bin/env python3
-from collections import namedtuple
-
-import numpy as np
-from scipy.cluster import vq
-from PIL import Image
-from colorspacious import cspace_convert
-
-verbose = False
-seed = 20220211
-
-
-def is_outline(r: int, g: int, b: int, a: int) -> bool:
-    # returns true if a pixel is transparent or pure black
-    return a == 0 or (r, g, b) == (0, 0, 0)
-
-
-def inertia(pixels: np.array) -> float:
-    # Inertia - the mean squared Euclidean norm
-    # computed as the sum of the squares of the components of the pixels,
-    # normalized by the number of pixels
-    if verbose:
-        print("  Computing inertia...")
-    return sum(sum(pixels ** 2)) / len(pixels)
-
-
-def mu(pixels: np.array) -> np.array:
-    # Mu - the mean pixel of the image
-    if verbose:
-        print("  Computing mu...")
-    return pixels.mean(0)
-
-
-def nu(pixels: np.array) -> np.array:
-    # Nu - the mean of the normalized pixels of the image
-    if verbose:
-        print("  Computing nu...")
-    return (pixels / np.sqrt((pixels * pixels).sum(axis=1)).reshape(len(pixels), 1)).mean(0)
-
-
-def clusters(pixels: np.array) -> tuple[np.array, np.array, np.array, np.array]:
-    # run k-means, and return the means and cluster contents
-    # k chosen somewhat arbitrarily to be 3
-    if verbose:
-        print("  Computing clusters...")
-    means, labels = vq.kmeans2(pixels.astype(float), 3, minit="++", seed=seed)
-    c1, c2, c3 = (pixels[labels == i] for i in range(3))
-    return means, c1, c2, c3
-
-
-def all_stats(pixels: np.array) -> np.array:
-    kmeans, c1, c2, c3 = clusters(pixels)
-    return np.array([
-        # total 
-        inertia(pixels), *mu(pixels), *nu(pixels),
-        # clusters
-        len(c1), inertia(c1), *kmeans[0], *nu(c1),
-        len(c2), inertia(c2), *kmeans[1], *nu(c2),
-        len(c3), inertia(c3), *kmeans[2], *nu(c3),
-    ])
-
-def ingest_png(file_name: str) -> tuple[str, list[float]]:
-    print(f"Ingesting {file_name}")
-
-    # image name - strip leading path and trailing extension
-    name = file_name.rsplit("/", maxsplit=1)[1].split(".", maxsplit=1)[0]
-
-    # read non-outline pixels of image
-    rgb_pixels = np.array([
-        (r, g, b)
-        for r, g, b, a in Image.open(file_name).convert("RGBA").getdata()
-        if not is_outline(r, g, b, a)
-    ])
-
-    # convert RGB pixels to CAM02 values
-    jab_pixels = cspace_convert(rgb_pixels, "sRGB255", "CAM02-UCS")
-
-    # compute metrics, flatten to a single array
-    return name, [len(rgb_pixels), *all_stats(jab_pixels), *all_stats(rgb_pixels)]
-
-if __name__ == "__main__":
-    import os
-    import sys
-
-    dir = "pngs" if len(sys.argv) < 2 else sys.argv[1]
-
-    with open("database-v2.js", "w") as outfile:
-        outfile.write("const databaseV2 = [\n")
-        for f in os.listdir(dir):
-            if (fn := os.fsdecode(f)).endswith(".png"):
-                name, ra = ingest_png(dir + "/" + fn)
-                outfile.write(f'  [ "{name}", {", ".join(str(n) for n in ra)} ],\n')
-        outfile.write("];\n")

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 113
legacy/math.md


+ 0 - 78
legacy/nearest.py

@@ -1,78 +0,0 @@
-#!/usr/bin/env python3
-import csv
-import math
-import random
-from argparse import ArgumentParser
-
-
-def norm(r, g, b):
-    return math.sqrt(r * r + g * g + b * b)
-
-
-parser = ArgumentParser()
-parser.add_argument(
-    "color", nargs="?", default=None, help="Target color, randomized if not provided"
-)
-parser.add_argument(
-    "-n", "--number", default=1, type=int, help="Number of Pokemon to find"
-)
-parser.add_argument(
-    "-c", "--closeness", default=2, type=int, help="Closeness coefficient"
-)
-parser.add_argument("-d", "--database", default="database.csv", help="Database file")
-parser.add_argument("-x", "--exclude-x", action="store_true", help="Exclude X")
-parser.add_argument("-z", "--normalize", action="store_true", help="Normalize q and Y")
-parser.add_argument(
-    "-j",
-    "--convert-cam02",
-    action="store_true",
-    help="Convert input color to CIECAM02-UCS before calculation",
-)
-parser.add_argument("-v", "--verbose", action="store_true", help="Print raw scores")
-args = parser.parse_args()
-
-if args.number <= 0:
-    raise ValueError("Must request a number greater than 0")
-
-if args.color is not None:
-    cleaned_color = args.color.strip("#")
-    if len(cleaned_color) != 6:
-        raise ValueError("Color must be a 6 digit hex")
-    color = (
-        int(cleaned_color[0:2], base=16),
-        int(cleaned_color[2:4], base=16),
-        int(cleaned_color[4:6], base=16),
-    )
-else:
-    color = tuple(int(random.random() * 255) for _ in range(3))
-    print(f"Generated random color: #{''.join(hex(c)[2:] for c in color)} / {color}")
-
-if args.convert_cam02:
-    from colorspacious import cspace_convert
-    color = list(cspace_convert(color, "sRGB255", "CAM02-UCS"))
-
-yfactor = args.closeness
-if args.normalize:
-    yfactor /= norm(*color)
-
-results = []
-with open(args.database) as infile:
-    for name, x, *y in csv.reader(infile, delimiter=",", quotechar="'"):
-        xval = 0 if args.exclude_x else float(x)
-        yvec = [float(y_c) for y_c in y]
-        yval = sum(y_c * c for y_c, c in zip(yvec, color))
-        if args.normalize:
-            yval /= norm(*yvec)
-        score = xval - yfactor * yval
-        results.append((score, name))
-
-if args.number > 1:
-    for i, (score, name) in enumerate(sorted(results)[:args.number]):
-        print(f"{i+1}:\t{name}")
-        if args.verbose:
-            print(f"\t\t\t{score=}")
-else:
-    best_score, best_name = min(results)
-    print(f"Best match: {best_name}")
-    if args.verbose:
-        print(f"{best_score=}")

+ 0 - 59
legacy/plan.md

@@ -1,59 +0,0 @@
-# 64 Values
-
-Every pixel set becomes 7 values (size, inertia, mu (3 components), nu (3 components))
-Every image has 4 pixel sets (total + 3 clusters)
-All this is done in both spaces
-Minus the duplicated size of the total set
-Plus name
-
-2 * 4 * 8 - 1 + 1 = 64 values to each image
-
-## Cleanup
-- Properly refer to inertia and mu in ingest instead of XYZ
-- Same fixes in nearest.js, make it easier to actually find the values needed
-
-## New values and logic
-- Add cluster inertias
-- Add nu metric/pointing vector to ingest
-- Add cluster nus
-- Record (non-outline) image size and raw cluster sizes instead of just percents
-- Reorganize output, modify nearest.js reader to match
-- Change logic of front-end
-    - Now the process is, select cluster > metric > scale > sort
-        - Clusters: Total, Cluster
-            - Cluster choice is according to a separately-specifiable metric + scale + sort, compared per pokemon
-        - Metrics: 
-            - RMS: sqrt(I - 2q . mu + q . q)
-            - Mean of Angle: nu . q_hat
-            - Angle of Mean: mu_hat . q_hat
-            - Hue of Mean: mu_perp . q_perp
-            - Euclidean to Mean: sqrt((mu - q) . (mu - q))
-            - Chebyshev to Mean: max(abs(c) for c in (mu - q))
-            - Inertia: I
-            - Size: N
-        - Scales: None (1), Proportion (N_k / N), Inverse Proportion (N / N_k)
-            - hidden when not using clusters
-        - Sort: max vs min
-    - Now each tile has
-        - Name
-        - For each portion (whole + 3 clusters)
-            - Mu (hex + vec, also sets tile color)
-            - Inertia (I)
-            - Size (N) (render as proportion for clusters)
-            - Nu
-            - RMS (sigma)
-            - Whole angle (Theta)
-            - Mean angle (theta)
-            - Hue angle (phi)
-            - E dist of mean (delta)
-            - C dist of mean (C)
-        - 4 * 10 = 40 results
-        - Probably hide the metric results and focus on just mu and maybe sigma, but provide dropdowns
-
-## DB schema
-
-```
-          jab                                                    rgb
-          total     cluster 1      cluster 2      cluster 3      total     cluster 1      cluster 2      cluster 3
-name size I *mu *nu size I *mu *nu size I *mu *nu size I *mu *nu I *mu *nu size I *mu *nu size I *mu *nu size I *mu *nu
-```

+ 0 - 143
legacy/pngs.md

@@ -1,143 +0,0 @@
-# PNG Source
-
-Currently using [Pokemon Showdown](https://play.pokemonshowdown.com/)'s sprites,
-sourced from their [`dex` directory](https://play.pokemonshowdown.com/sprites/dex/).
-These were then pruned by hand, and some other sprites (Galar forms) were added
-from their [`gen5` directory](https://play.pokemonshowdown.com/sprites/gen5/).
-
-Specifically, the following were removed:
- - Mega evolutions
- - Unown forms, excluding "default" of A
- - Duplicated Forms (i.e., plain sprite removed but equivalent form sprite left)
-   - Burmy (plant)
-   - Wormadam (plant)
-   - Basculin (red stripe)
-   - Vivillon (meadow)
-   - Sawsbuck (spring)
-   - Deerling (spring)
-   - Thundurus (incarnate)
-   - Tornadus (incarnate)
-   - Landorus (incarnate)
-   - Flabebe (red)
-   - Floette (red)
-   - Florges (red)
-   - Furfrou (natural)
-   - Shaymin (land)
-   - Shellos (west)
-   - Gastrodon (west)
-   - Keldeo (ordinary)
-   - Meloetta (aria)
-   - Hoopa (confined)
-   - Giratina (altered)
- - Duplicated Grovyle that appeared a second time as `english.png`?
- - Forms inaccessible at start of battle
-   - Minior core forms (battle will start with Minior in meteor form)
-   - Wishiwashi solo form (battle will start with Wishiwashi in school form)
-   - Mimikyu busted form (battle will start with Mimikyu in normal form)
-   - Castform weather forms (battle will start with no weather)
-   - Cherrim sunshine and duplicated Cherrim overcast (battle will start with no weather)
-   - Aegislash blade and duplicated Aegislash shield (battle will start with Aegislash in shield mode)
-   - Darmanitan zen and duplicated Darmanitan standard (battle will start with Darmanitan in normal form)
-   - Xerneas neutral and duplicated Xerneas active (battle will start with Xerneas in active mode)
- - Forms that do not significantly alter the color of the pokemon
-   - Arceus forms
-   - Silvally forms
-   - Genesect forms
-   - Pumpkaboo and Gourgeist alternate sizes
-   - Ash Greninja
-   - Additional Pikachu costumes
-   - "Starter" Eevee and Pikachu
- - All "Create-A-Pokemon" entries
- - Totem pokemon sprites
- - Pokestars
-
-And the following were obtained from the `gen5` directory
- - Galar forms of:
-   - Articuno
-   - Corsola
-   - Darmanitan (no Galar Zen for reasons above)
-   - Darumaka
-   - Farfetch'd
-   - Linoone
-   - Meowth
-   - Moltres
-   - Ponyta
-   - Rapidash
-   - Slowbrow
-   - Slowking
-   - Slowpoke
-   - Stunfisk
-   - Weezing
-   - Yamask
-   - Zapdos
-   - Zigzagoon
- - alcremie
- - arctovish
- - arctozolt
- - calyrex
- - clobbopus
- - copperajah
- - cufant
- - cursola
- - dracovish
- - dracozolt
- - dragapult
- - drakloak
- - dreepy
- - duraludon
- - eiscue
- - eternatus
- - falinks
- - frosmoth
- - glastrier
- - grapploct
- - grimmsnarl
- - hatenna
- - hatterene
- - hattrem
- - impidimp
- - indeedee
- - kubfu
- - milcery
- - morgrem
- - morpeko
- - mrrime
- - obstagoon
- - perrserker
- - pincurchin
- - polteageist
- - regidrago
- - regieleki
- - runerigus
- - sinistea
- - sirfetchd
- - snom
- - spectrier
- - stonjourner
- - urshifu
- - zacian
- - zamazenta
- - zarude
-
-And then a few png files were renamed to align to the names on 
-[pokemondb](https://pokemondb.net/sprites) so sprites can be fetched in the
-web version (other mismatches are handled programmatically):
- - All 4 Tapus
- - Ho-oh
- - Mime Jr., Mr. Mime (regular and Galar), Mr. Rime
- - Jangmo-o, Hakamo-o, Kommo-o
- - Meowstic (F)
- - Type: Null
- - Toxtricity (Low Key)
- - Porygon-Z
- - Oricorio (Pom Pom)
- - Nidoran (M and F)
- - Necrozma (Dawn Wings and Dusk Mane)
-
-## Alternative/Legacy Source
-
-Another option is [pokemondb](https://pokemondb.net/sprites)'s sprite archive.
-Download the entire page (i.e., Ctrl-S in most browsers), then take the folder
-of PNGs it downloads and place them in the `pngs` directory in this repository.
-Then run `ingest.py` to generate `database.csv`
-

Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно