ingest.py 2.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293
  1. #!/usr/bin/env python3
  2. from collections import namedtuple
  3. import numpy as np
  4. from scipy.cluster import vq
  5. from PIL import Image
  6. from colorspacious import cspace_convert
  7. verbose = False
  8. seed = 20220211
  9. def is_outline(r: int, g: int, b: int, a: int) -> bool:
  10. # returns true if a pixel is transparent or pure black
  11. return a == 0 or (r, g, b) == (0, 0, 0)
  12. def inertia(pixels: np.array) -> float:
  13. # Inertia - the mean squared Euclidean norm
  14. # computed as the sum of the squares of the components of the pixels,
  15. # normalized by the number of pixels
  16. if verbose:
  17. print(" Computing inertia...")
  18. return sum(sum(pixels ** 2)) / len(pixels)
  19. def mu(pixels: np.array) -> np.array:
  20. # Mu - the mean pixel of the image
  21. if verbose:
  22. print(" Computing mu...")
  23. return pixels.mean(0)
  24. def nu(pixels: np.array) -> np.array:
  25. # Nu - the mean of the normalized pixels of the image
  26. if verbose:
  27. print(" Computing nu...")
  28. return (pixels / np.sqrt((pixels * pixels).sum(axis=1)).reshape(len(pixels), 1)).mean(0)
  29. def clusters(pixels: np.array) -> tuple[np.array, np.array, np.array, np.array]:
  30. # run k-means, and return the means and cluster contents
  31. # k chosen somewhat arbitrarily to be 3
  32. if verbose:
  33. print(" Computing clusters...")
  34. means, labels = vq.kmeans2(pixels.astype(float), 3, minit="++", seed=seed)
  35. c1, c2, c3 = (pixels[labels == i] for i in range(3))
  36. return means, c1, c2, c3
  37. def all_stats(pixels: np.array) -> np.array:
  38. kmeans, c1, c2, c3 = clusters(pixels)
  39. return np.array([
  40. # total
  41. inertia(pixels), *mu(pixels), *nu(pixels),
  42. # clusters
  43. len(c1), inertia(c1), *kmeans[0], *nu(c1),
  44. len(c2), inertia(c2), *kmeans[1], *nu(c2),
  45. len(c3), inertia(c3), *kmeans[2], *nu(c3),
  46. ])
  47. def ingest_png(file_name: str) -> tuple[str, list[float]]:
  48. print(f"Ingesting {file_name}")
  49. # image name - strip leading path and trailing extension
  50. name = file_name.rsplit("/", maxsplit=1)[1].split(".", maxsplit=1)[0]
  51. # read non-outline pixels of image
  52. rgb_pixels = np.array([
  53. (r, g, b)
  54. for r, g, b, a in Image.open(file_name).convert("RGBA").getdata()
  55. if not is_outline(r, g, b, a)
  56. ])
  57. # convert RGB pixels to CAM02 values
  58. jab_pixels = cspace_convert(rgb_pixels, "sRGB255", "CAM02-UCS")
  59. # compute metrics, flatten to a single array
  60. return name, [len(rgb_pixels), *all_stats(jab_pixels), *all_stats(rgb_pixels)]
  61. if __name__ == "__main__":
  62. import os
  63. import sys
  64. dir = "pngs" if len(sys.argv) < 2 else sys.argv[1]
  65. with open("database-v2.js", "w") as outfile:
  66. outfile.write("const databaseV2 = [\n")
  67. for f in os.listdir(dir):
  68. if (fn := os.fsdecode(f)).endswith(".png"):
  69. name, ra = ingest_png(dir + "/" + fn)
  70. outfile.write(f' [ "{name}", {", ".join(str(n) for n in ra)} ],\n')
  71. outfile.write("];\n")