ingest.py 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139
  1. #!/usr/bin/env python3
  2. from collections import namedtuple
  3. from PIL import Image
  4. def rescale_and_linearize(component: int) -> float:
  5. # takes an sRGB color component [0,255]
  6. # first rescales to [0,1]
  7. # then linearizes according to some CIEXYZ stuff I don't understand
  8. # then rescales to [0, 100]
  9. component /= 255
  10. linearized = component / 12.92 if component <= 0.04045 else ((component + 0.055) / 1.055) ** 2.4
  11. return 100 * linearized
  12. # conversion values I also do not understand
  13. # pulled from https://www.image-engineering.de/library/technotes/958-how-to-convert-between-srgb-and-ciexyz
  14. # instead of easy rgb, since it seemed to give more accurate values
  15. rgb_to_xyz_matrix = [
  16. [0.4124564, 0.3575761, 0.1804375],
  17. [0.2126729, 0.7151522, 0.0721750],
  18. [0.0193339, 0.1191920, 0.9503041],
  19. ]
  20. # reference values I also also do not understand
  21. # pulled from easy rgb
  22. # note X and Y here have nothing to do with the X and Y metrics below
  23. ref_x = 95.047
  24. ref_y = 100.000
  25. ref_z = 108.883
  26. ref_denom = ref_x + 15 * ref_y + 3 * ref_z
  27. ref_u = 4 * ref_x / ref_denom
  28. ref_v = 9 * ref_y / ref_denom
  29. def rgb_to_cieluv(r: int, g: int, b: int) -> tuple[float, float, float]:
  30. # accepts RGB (components [0, 255])
  31. # converts to CIE LUV (components [0, 1])
  32. # math taken from http://www.easyrgb.com/en/math.php
  33. # RGB (components [0, 255]) -> XYZ (components [0, 100])
  34. # X, Y and Z output refer to a D65/2° standard illuminant.
  35. sr, sg, sb = (rescale_and_linearize(c) for c in (r, g, b))
  36. x, y, z = (cr * sr + cg * sg + cb * sb for cr, cg, cb in rgb_to_xyz_matrix)
  37. # XYZ (components [0, 100]) -> LUV (components [0, 100])
  38. uv_denom = x + 15 * y + 3 * z
  39. u = 4 * x / uv_denom
  40. v = 9 * y / uv_denom
  41. if y > 0.8856:
  42. yprime = (y / 100) ** (1/3)
  43. else:
  44. yprime = (y / 100) * 7.787 + (16/116)
  45. lstar = 116 * yprime - 16
  46. lstar_factor = 13 * lstar
  47. return lstar, lstar_factor * (u - ref_u), lstar_factor * (v - ref_v)
  48. def is_outline(r: int, g: int, b: int, a: int) -> bool:
  49. # returns true if a pixel is transparent or pure black
  50. return a == 0 or (r, g, b) == (0, 0, 0)
  51. def x_metric(pixels: list[tuple[float, float, float]]) -> float:
  52. # X metric - the mean squared Euclidean norm
  53. # computed as the sum of the squares of the components of the pixels,
  54. # normalized by the number of pixels
  55. return sum(comp * comp for pix in pixels for comp in pix) / len(pixels)
  56. def y_metric(pixels: list[tuple[float, float, float]]) -> tuple[float, float, float]:
  57. # Y metric - the mean pixel of the image
  58. return tuple(sum(p[i] for p in pixels) / len(pixels) for i in range(3))
  59. ImageInfo = namedtuple("ImageInfo", ["name", "xrgb", "xluv", "yr", "yg", "yb", "yl", "yu", "yv"])
  60. def ingest_png(file_name: str) -> ImageInfo:
  61. print(f"Ingesting {file_name}")
  62. # image name - strip leading path and trailing extension
  63. name = file_name.rsplit("/", maxsplit=1)[1].split(".", maxsplit=1)[0]
  64. # read non-outline pixels of image
  65. rgb_pixels = [(r, g, b)
  66. for r, g, b, a in Image.open(file_name).convert("RGBA").getdata()
  67. if not is_outline(r, g, b, a)]
  68. # convert RGB pixels to CIELUV values
  69. luv_pixels = [rgb_to_cieluv(*p) for p in rgb_pixels]
  70. # compute and return metrics
  71. xrgb = x_metric(rgb_pixels)
  72. xluv = x_metric(luv_pixels)
  73. yr, yg, yb = y_metric(rgb_pixels)
  74. yl, yu, yv = y_metric(luv_pixels)
  75. return ImageInfo(
  76. name=name,
  77. xrgb=xrgb,
  78. xluv=xluv,
  79. yr=yr,
  80. yg=yg,
  81. yb=yb,
  82. yl=yl,
  83. yu=yu,
  84. yv=yv,
  85. )
  86. if __name__ == "__main__":
  87. import csv
  88. import os
  89. data = [ingest_png("pngs/" + fn) for f in os.listdir("pngs") if (fn := os.fsdecode(f)).endswith(".png")]
  90. with open("database.csv", "w") as outfile:
  91. writer = csv.writer(outfile, delimiter=",", quotechar="'")
  92. writer.writerows([d.name, d.xrgb, d.yr, d.yg, d.yb] for d in data)
  93. with open("database-luv.csv", "w") as outfile:
  94. writer = csv.writer(outfile, delimiter=",", quotechar="'")
  95. writer.writerows([d.name, d.xluv, d.yl, d.yu, d.yv] for d in data)
  96. with open("database.js", "w") as outfile:
  97. outfile.write("const database = [\n")
  98. for info in data:
  99. fields = ", ".join((
  100. f'name: "{info.name}"',
  101. f"xRGB: {info.xrgb}",
  102. f"xLUV: {info.xluv}",
  103. f"yRGB: [ {info.yr}, {info.yg}, {info.yb} ]",
  104. f"yLUV: [ {info.yl}, {info.yu}, {info.yv} ]",
  105. ))
  106. outfile.write(f" {{ {fields} }},\n")
  107. outfile.write("];\n")