Kaelygon

oklab palette generator

Nov 8th, 2025 (edited)
33
0
Never
Not a member of Pastebin yet? Sign Up, it unlocks many cool features!
Python 17.95 KB | None | 0 0
  1. ##CC0 Kaelygon 2025
  2. import math
  3. import numpy as np
  4. from PIL import Image
  5. from dataclasses import dataclass, field
  6. from typing import List, Tuple, Optional
  7. import random
  8.  
  9. #3D vector operators
  10. def sub_vec3(vec_a,vec_b):
  11.     return [ vec_a[0] - vec_b[0], vec_a[1] - vec_b[1], vec_a[2] - vec_b[2] ]
  12. def add_vec3(vec_a,vec_b):
  13.     return [ vec_a[0] + vec_b[0], vec_a[1] + vec_b[1], vec_a[2] + vec_b[2] ]
  14. def mul_vec3(vec_a,vec_b):
  15.     return [ vec_a[0] * vec_b[0], vec_a[1] * vec_b[1], vec_a[2] * vec_b[2] ]
  16. def div_vec3(vec_a,vec_b):
  17.     return [ vec_a[0] / vec_b[0], vec_a[1] / vec_b[1], vec_a[2] / vec_b[2] ]
  18.  
  19.  
  20. #special
  21. def sign_vec3(vec_a):
  22.     return [ 1 if c>=0 else -1 for c in vec_a ]
  23. def lerp_vec3(vec_a,vec_b,a):
  24.    return add_vec3( mul_vec3(vec_a, sub_vec3([1.0]*3,a) ), mul_vec3(vec_b,a) )
  25. def pow_vec3(vec_a,vec_b):
  26.     return [ vec_a[0] ** vec_b[0], vec_a[1] ** vec_b[1], vec_a[2] ** vec_b[2] ]
  27. def spow_vec3(vec_a,vec_b):
  28.     sign = sign_vec3(vec_a)
  29.     return [ sign[0]*(sign[0]*vec_a[0]) ** vec_b[0], sign[1]*(sign[1]*vec_a[1]) ** vec_b[1], sign[2]*(sign[2]*vec_a[2]) ** vec_b[2] ]
  30. def clip_vec3(vec_a,low,hi):
  31.     return [ min(max(vec_a[0],low[0]),hi[0]), min(max(vec_a[1],low[1]),hi[1]), min(max(vec_a[2],low[2]),hi[2]) ]
  32. def lessThan_vec3(vec_a,vec_b):
  33.     return [ vec_a[0]<vec_b[0], vec_a[1]<vec_b[1], vec_a[2]<vec_b[2] ]
  34.  
  35. #Vector operators
  36. def dot_vec3(a, b):
  37.     product = 0.0
  38.     for i in range(3):
  39.         product+= a[i] * b[i]
  40.     return product
  41.  
  42. def cross_vec3(a, b):
  43.     c = [a[1]*b[2] - a[2]*b[1],
  44.             a[2]*b[0] - a[0]*b[2],
  45.             a[0]*b[1] - a[1]*b[0]]
  46.  
  47.     return c
  48.  
  49. def length_vec3(vec):
  50.     return math.sqrt(vec[0]*vec[0] + vec[1]*vec[1] + vec[2]*vec[2])
  51.  
  52. def norm_vec3(vec,eps=0.0):
  53.     l = length_vec3(vec)
  54.     if l == eps:
  55.         return [1.0, 0.0, 0.0]
  56.     return [vec[0]/l, vec[1]/l, vec[2]/l]
  57.  
  58.  
  59.  
  60. #other math tools
  61. def math_spow(a,b):
  62.     return -((-a)**b) if a<0 else a**b
  63.  
  64. ### Color conversion
  65. def linearToSrgb(linRGB):
  66.     cutoff = lessThan_vec3(linRGB, [0.0031308]*3)
  67.     gammaAdj = spow_vec3(linRGB, [1.0/2.4]*3 )
  68.     higher = sub_vec3( mul_vec3( [1.055]*3 , gammaAdj ), [0.055]*3 )
  69.     lower = mul_vec3( linRGB, [12.92]*3 )
  70.  
  71.     return lerp_vec3(higher, lower, cutoff)
  72.  
  73. def srgbToLinear(sRGB):
  74.     cutoff = lessThan_vec3(sRGB, [0.04045]*3)
  75.     higher = spow_vec3( add_vec3(sRGB, div_vec3([0.055]*3, [1.055]*3) ), [2.4]*3 )
  76.     lower = div_vec3( sRGB, [12.92]*3 )
  77.  
  78.     return lerp_vec3(higher, lower, cutoff)
  79.  
  80. def linearToOklab(RGB):
  81.     r,g,b = RGB
  82.     l = 0.4122214708 * r + 0.5363325363 * g + 0.0514459929 * b
  83.     m = 0.2119034982 * r + 0.6806995451 * g + 0.1073969566 * b
  84.     s = 0.0883024619 * r + 0.2817188376 * g + 0.6299787005 * b
  85.  
  86.     l_ = math_spow(l, 1.0/3.0)
  87.     m_ = math_spow(m, 1.0/3.0)
  88.     s_ = math_spow(s, 1.0/3.0)
  89.  
  90.     return [
  91.         0.2104542553*l_ + 0.7936177850*m_ - 0.0040720468*s_,
  92.         1.9779984951*l_ - 2.4285922050*m_ + 0.4505937099*s_,
  93.         0.0259040371*l_ + 0.7827717662*m_ - 0.8086757660*s_,
  94.     ]
  95.  
  96. def oklabToLinear(ok):
  97.     L,a,b = ok
  98.     l_ = L + 0.3963377774 * a + 0.2158037573 * b
  99.     m_ = L - 0.1055613458 * a - 0.0638541728 * b
  100.     s_ = L - 0.0894841775 * a - 1.2914855480 * b
  101.  
  102.     l = l_*l_*l_
  103.     m = m_*m_*m_
  104.     s = s_*s_*s_
  105.  
  106.     return [
  107.         +4.0767416621 * l - 3.3077115913 * m + 0.2309699292 * s,
  108.         -1.2684380046 * l + 2.6097574011 * m - 0.3413193965 * s,
  109.         -0.0041960863 * l - 0.7034186147 * m + 1.7076147010 * s,
  110.     ]
  111.  
  112. def srgbToOklab(rgb):
  113.     linRGB = srgbToLinear(rgb)
  114.     oklab = linearToOklab(linRGB)
  115.     return oklab
  116.  
  117. def oklabToSrgb(ok):
  118.     linRGB = oklabToLinear(ok)
  119.     sRGB = linearToSrgb(linRGB)
  120.     return sRGB
  121.  
  122.  
  123. ### Color tools ###
  124.  
  125. def inOklabGamut(ok, eps=1e-7):
  126.     r_lin, g_lin, b_lin = oklabToLinear(ok)
  127.  
  128.     return (r_lin >= -eps) and (r_lin <= 1+eps) and \
  129.              (g_lin >= -eps) and (g_lin <= 1+eps) and \
  130.              (b_lin >= -eps) and (b_lin <= 1+eps)
  131.  
  132. def isOkSrgbGray(col, threshold = 1.0/255.0):
  133.     r,g,b = oklabToSrgb(col)
  134.     if( abs(r-g) < threshold and
  135.         abs(g-b) < threshold
  136.     ):
  137.         return True
  138.     return False
  139.  
  140. def srgbToHex(rgb):
  141.     r = int(round(rgb[0] * 255.0))
  142.     g = int(round(rgb[1] * 255.0))
  143.     b = int(round(rgb[2] * 255.0))
  144.     return "#{:02x}{:02x}{:02x}".format(r, g, b)
  145.  
  146. def oklabGamutVolume(resolution=50):
  147.    
  148.     valid_count = 0
  149.     total_count = resolution ** 3
  150.  
  151.     for L in range(0,resolution):
  152.         for a in range(0,resolution):
  153.                 for b in range(0,resolution):
  154.                     if inOklabGamut([
  155.                         float(L)/resolution,
  156.                         float(a)/resolution-0.5,
  157.                         float(b)/resolution-0.5
  158.                     ]):
  159.                         valid_count += 1
  160.    
  161.     return valid_count / total_count
  162.  
  163. #Calc color values
  164. def calcOkChroma(col):
  165.     return math.sqrt( col[1]*col[1] + col[2]*col[2] )
  166.  
  167. def calcSrgbLum(col):
  168.     return srgbToOklab(col)[0]
  169.  
  170. def separateGrays(ok_array):
  171.     gray_colors = []
  172.     hued_colors = []
  173.  
  174.     for col in ok_array:
  175.         chroma = calcOkChroma(col)
  176.         if isOkSrgbGray(col):
  177.             gray_colors.append(col)
  178.         else:
  179.             hued_colors.append(col)
  180.    
  181.     return gray_colors, hued_colors
  182.  
  183.  
  184.  
  185.  
  186.  
  187. class PointGrid:
  188.     """
  189.         Store point cloud as a search PointGrid.grid and a 1D PointGrid.cloud
  190.     """
  191.  
  192.     def __init__(self, point_radius):
  193.         self.cloud = []
  194.         self.grid = {}
  195.         self.length = 0
  196.         self.cell_size = point_radius
  197.  
  198.     def key(self, p):
  199.         gx = int(p[0] // self.cell_size)
  200.         gy = int(p[1] // self.cell_size)
  201.         gz = int(p[2] // self.cell_size)
  202.         return (gx,gy,gz)
  203.  
  204.     def insert(self, p):
  205.         self.length+=1
  206.         k = self.key(p)
  207.         if k not in self.grid:
  208.             self.grid[k] = []
  209.         self.grid[k].append(p)
  210.         self.cloud.append(p)
  211.  
  212.     def findNearest(self, test_point, point_radius, neighbor_margin=0):
  213.         p0_info = [None,None,None]
  214.         closest=float('inf')
  215.  
  216.         neighbor_count=0
  217.         seen = set()
  218.        
  219.         kx, ky, kz = self.key(test_point)
  220.         for dx in (-1, 0, 1):
  221.                 for dy in (-1, 0, 1):
  222.                     for dz in (-1, 0, 1):
  223.                         nk = (kx + dx, ky + dy, kz + dz)
  224.  
  225.                         for p in self.grid.get(nk, ()):
  226.                             if p == test_point:
  227.                                     continue
  228.                             pid = id(p)
  229.                             if pid in seen:
  230.                                     continue
  231.                             seen.add(pid)
  232.  
  233.                             delta = sub_vec3(test_point, p)
  234.                             dist = length_vec3(delta)
  235.  
  236.                             if dist - neighbor_margin <= point_radius:
  237.                                     neighbor_count += 1
  238.  
  239.                             if dist < closest:
  240.                                     closest = dist
  241.                                     p0_info = [p, delta, dist]
  242.            
  243.        
  244.         return p0_info[0],p0_info[1],p0_info[2],neighbor_count
  245.  
  246.  
  247.  
  248. ### Palette generator ###
  249. @dataclass
  250. class PalettePreset:
  251.     reserve_transparent: bool = True
  252.  
  253.     max_colors: int = None
  254.  
  255.     gray_count: int = None  #Grayscale color count
  256.     hue_count:  int = None  #Split Hues in this many buckets
  257.  
  258.     min_sat: float = 0.0    #min/max ranges are percentages
  259.     max_sat: float = 1.0
  260.     min_lum: float = 0.0
  261.     max_lum: float = 1.0
  262.     packing_fac: float = 1.0 #Packing efficiency
  263.     max_attempts: int = 1024 #After this many max_attempts per point, pointSampler will give up
  264.    
  265.     seed: int = 0 #Seed for pointSampler
  266.  
  267.  
  268. class PaletteGenerator:
  269.     """
  270.         Generate palette where the colors are perceptually evenly spaced out in OKLab colorspace
  271.     """
  272.  
  273.     def __init__(self,
  274.                     preset: Optional[PalettePreset] = None, *,
  275.                    
  276.                     reserve_transparent: Optional[bool] = None,
  277.                     hue_count: Optional[int] = None,
  278.                     gray_count: Optional[int] = None,
  279.                     min_sat: Optional[float] = None,
  280.                     max_sat: Optional[float] = None,
  281.                     min_lum: Optional[float] = None,
  282.                     max_lum: Optional[float] = None,
  283.     ):
  284.         self.point_grid = None
  285.         self.point_radius = None
  286.  
  287.         if preset is None:
  288.             preset = PalettePreset()
  289.    
  290.         self.p = PalettePreset(**{k: getattr(preset, k) for k in preset.__dataclass_fields__})
  291.  
  292.         if self.p.max_colors:
  293.             self.p.max_colors -= self.p.reserve_transparent
  294.         else:
  295.             self.p.max_colors = 0
  296.  
  297.     def seed(self, input_seed=None):
  298.         if input_seed:
  299.             random.seed(input_seed)
  300.         if self.p.seed:
  301.             random.seed(self.p.seed)
  302.         else:
  303.             random.random()
  304.  
  305.     def pointSampler(self):
  306.         def rand_vec3():
  307.             return norm_vec3( [random.random()-0.5, random.random()-0.5, random.random()-0.5] )
  308.         def getRandOklab():
  309.             return [random.random(), random.random()-0.5, random.random()-0.5]
  310.         def rand_cloud_point():
  311.             if len(self.point_grid.cloud) and random.random()>0.5:
  312.                 return self.point_grid.cloud[ int(random.random()*len(self.point_grid.cloud)) ]
  313.             return getRandOklab()
  314.  
  315.         origin = [0.5, 0.0, 0.0]
  316.         space_size = math.sqrt(1.0**2 + 1.0**2 + 1.0**2)
  317.         margin = space_size/10000.0
  318.         push_scalar = [self.point_radius]*3
  319.  
  320.         push_fails = 0
  321.         max_push_fails = 10
  322.  
  323.         p0 = rand_cloud_point()
  324.         skip_rand = False
  325.         new_point = None
  326.  
  327.         stale_counter = 0
  328.         attempt_counter = 0
  329.  
  330.         while len(self.point_grid.cloud) < self.p.max_colors:
  331.             attempt_counter+=1
  332.             stale_counter+=1
  333.             if stale_counter > self.p.max_attempts:
  334.                 print("Reached max_attempts to find a new point.")
  335.                 break
  336.    
  337.             if not skip_rand:
  338.                 #Move to random direction
  339.                 rand_vec = rand_vec3()
  340.                 move_vec = mul_vec3(rand_vec, [self.point_radius]*3 )
  341.                 new_point = add_vec3(p0, move_vec)
  342.             skip_rand = False
  343.    
  344.             if not inOklabGamut(new_point):
  345.                 #clip to srgb gamut
  346.                 linRGB = oklabToLinear(new_point)
  347.                 linRGB = clip_vec3(linRGB,[0.0]*3,[1.0]*3)
  348.                 new_point = linearToOklab(linRGB)
  349.    
  350.             col_point, pos_vec, dist, col_count = self.point_grid.findNearest(new_point, self.point_radius, margin)
  351.  
  352.             if col_point:
  353.                 delta = dist-self.point_radius
  354.                 if abs(delta) > margin:
  355.                     #Too close or far: Set 1 radius apart from nearest point
  356.                     push_fails+=1
  357.                     if push_fails > max_push_fails:
  358.                         push_fails = 0
  359.                         continue
  360.    
  361.                     normal = None
  362.                     if push_fails <= max_push_fails//2:
  363.                         normal = norm_vec3(pos_vec)
  364.                     else:
  365.                         normal = rand_vec3() #rand direction
  366.      
  367.                     new_point = add_vec3(col_point, mul_vec3(normal, push_scalar))
  368.                     skip_rand = True
  369.                     continue
  370.  
  371.             self.point_grid.insert(new_point)
  372.             p0=new_point
  373.             stale_counter=0
  374.             push_fails = 0
  375.    
  376.         print("Loop count "+str(attempt_counter))
  377.  
  378.  
  379.     def generateGrays(self):
  380.         if self.p.gray_count == None:
  381.             self.p.gray_count = int(round(1.0/(self.point_radius)))
  382.         if self.p.gray_count:
  383.             #Use minimum starting luminosity that second darkest black isn't so close to 0
  384.             darkest_black = calcSrgbLum([0.499/255,0.499/255,0.499/255])
  385.             for i in range(0,self.p.gray_count):
  386.                 lum = float(i)/(self.p.gray_count-1)
  387.                 scale = ( (self.p.gray_count-i-1)/(self.p.gray_count-1) )
  388.                 lum+= darkest_black*scale #Fade that brightest remains 1.0
  389.                 new_point = [lum,0,0]
  390.                 self.point_grid.insert(new_point)
  391.  
  392.     def applyColorLimits(self):
  393.         apply_luminosity = self.p.max_lum!=1.0 or self.p.min_lum!=0.0
  394.         apply_saturation = self.p.max_sat!=1.0 or self.p.min_sat!=0.0
  395.  
  396.         filtered_cloud = []
  397.         for col in self.point_grid.cloud:
  398.             if apply_luminosity:
  399.                 lum_width = self.p.max_lum - self.p.min_lum
  400.                 col[0] = col[0]*lum_width + self.p.min_lum
  401.        
  402.             if apply_saturation and not isOkSrgbGray(col):
  403.                 sat_width = self.p.max_sat - self.p.min_sat
  404.                 chroma = calcOkChroma(col)
  405.    
  406.                 max_chroma = math.sqrt(0.5**2+0.5**2)
  407.                 rel_sat = chroma / max_chroma
  408.                 scaled_sat = (rel_sat * sat_width + self.p.min_sat) * max_chroma
  409.  
  410.                 col_vec = [col[1], col[2]] #Vector a,b
  411.                 col_vec = [col_vec[0]/chroma, col_vec[1]/chroma] #Normalize
  412.                 col_vec = [col_vec[0]*scaled_sat, col_vec[1]*scaled_sat] #Scale
  413.                 col = [col[0], col_vec[0], col_vec[1]]
  414.                
  415.             filtered_cloud.append(col)
  416.    
  417.         self.point_grid.cloud = filtered_cloud
  418.  
  419.     def populatePointCloud(self):  
  420.         self.seed()
  421.    
  422.         space_volume = 0.054197416 # print (str(oklabGamutVolume(500))) pre computed
  423.         point_count = self.p.max_colors if self.p.max_colors else self.p.tone_count * self.p.hue_count + self.p.gray_count
  424.         unit_volume = space_volume/(point_count)
  425.         self.point_radius = unit_volume**(1.0/3.0) * self.p.packing_fac
  426.         print("Using self.point_radius "+str(round(self.point_radius,4)))
  427.  
  428.         self.point_grid = PointGrid(self.point_radius)
  429.  
  430.         self.generateGrays()
  431.         self.pointSampler()
  432.  
  433.         self.applyColorLimits()
  434.  
  435.         return self.point_grid.cloud
  436.    
  437.  
  438.  
  439.     ### Palette processing ###
  440.  
  441.     def paletteToHex(self):
  442.         hex_list = []
  443.         if self.p.reserve_transparent:
  444.                 hex_list.append("#00000000")
  445.    
  446.         for col in self.point_grid.cloud:
  447.             srgb_col = oklabToSrgb(col)
  448.             hex_list.append(srgbToHex(srgb_col))
  449.        
  450.         return hex_list
  451.  
  452.     def paletteToImg(self, hex_list: List[str], filename: str = "palette.png"):
  453.         rgba = []
  454.         if self.p.reserve_transparent:
  455.             rgba.append((0, 0, 0, 0))
  456.    
  457.         for col in self.point_grid.cloud:
  458.             r,g,b = oklabToSrgb(col)
  459.             r = min( max( int(round(r * 255.0)), 0 ), 255)
  460.             g = min( max( int(round(g * 255.0)), 0 ), 255)
  461.             b = min( max( int(round(b * 255.0)), 0 ), 255)
  462.             rgba.append((r, g, b, 255))
  463.        
  464.         arr = np.array([rgba], dtype=np.uint8)
  465.         img = Image.fromarray(arr, mode="RGBA")
  466.         img.save(filename)
  467.         return img
  468.  
  469.     def sortByLum(self, ok_array):
  470.         return sorted(ok_array, key=lambda x: x[0])
  471.  
  472.     def sortPalette(self):
  473.         gray_colors, hued_colors = separateGrays(self.point_grid.cloud)
  474.    
  475.         #Place hues in same buckets
  476.         hue_buckets = [[] for _ in range(self.p.hue_count)]
  477.         hue_bucket_width = 2*math.pi * (1.0/self.p.hue_count)
  478.  
  479.         for col in hued_colors:
  480.             col_hue = math.atan2(col[2], col[1]) + 2* math.pi
  481.             bucket_index = int(col_hue/hue_bucket_width) % self.p.hue_count
  482.             hue_buckets[bucket_index].append(col)
  483.  
  484.         #Sort hue buckets by luminance
  485.         sorted_hue_buckets = []
  486.         for bucket in hue_buckets:
  487.             sorted_bucket = self.sortByLum(bucket)
  488.             sorted_hue_buckets.append(sorted_bucket)
  489.  
  490.         #combine colors into single array
  491.         sorted_colors = []
  492.  
  493.         sorted_grays = self.sortByLum(gray_colors)
  494.         for col in sorted_grays:
  495.             sorted_colors.append(col)
  496.  
  497.         for bucket in sorted_hue_buckets:
  498.             for col in bucket:
  499.                 sorted_colors.append(col)
  500.    
  501.         self.point_grid.cloud = sorted_colors
  502.  
  503.     #EOF PaletteGenerator
  504.  
  505.  
  506.  
  507. ### palette analysis ###
  508.  
  509. #p0 has type (float3)[L,a,b]
  510. #pair_list has type (float,[],[])[dist, p0, p1]
  511. def printColorPairStats(pair_list, print_count, listName="", calc_error=False):
  512.     def oklabToHex(col):
  513.         return srgbToHex(oklabToSrgb(col))
  514.    
  515.     if len(pair_list) < 2:
  516.         print("Not enough pairs for stats!")
  517.         return
  518.    
  519.     precision = 4
  520.  
  521.     print(listName+" Closest pairs")
  522.     for pair in pair_list[:print_count]:
  523.         print(str(round(pair[0],precision))+" "+oklabToHex(pair[1])+" "+oklabToHex(pair[2]))
  524.  
  525.     #print only unseen values
  526.     far_start = max(print_count, len(pair_list)-print_count)
  527.     if far_start < len(pair_list):
  528.         print(listName+" Farthest pairs")
  529.     for pair in pair_list[far_start:]:
  530.         print(str(round(pair[0],precision))+" "+oklabToHex(pair[1])+" "+oklabToHex(pair[2]))
  531.  
  532.     #Average
  533.     sumDist = 0.0
  534.     for pair in pair_list:
  535.         sumDist+=pair[0]
  536.     avgDist = sumDist / len(pair_list)
  537.  
  538.     #Median
  539.     medianDist=0.0
  540.     medianIndex=len(pair_list)//2
  541.     if len(pair_list)%2==0:
  542.         a = medianIndex
  543.         b = max(medianIndex-1,0)
  544.         medianDist = (pair_list[a][0] + pair_list[b][0]) / 2.0
  545.     else:
  546.         medianDist = pair_list[medianIndex][0]
  547.  
  548.     print(listName+" Avg pair distance: "+str(round(avgDist,precision)))
  549.     print(listName+" Median pair distance: "+str(round(medianDist,precision)))
  550.     print("")
  551.  
  552.    
  553.     #Compare biggest gap to avg gap
  554.     if calc_error == True:
  555.  
  556.         hued_pairs = [
  557.             p for p in pair_list
  558.             if not isOkSrgbGray(p[1]) and not isOkSrgbGray(p[2])
  559.         ]
  560.         hue_pair_count = len(hued_pairs)
  561.        
  562.         #Average hued only
  563.         sumDist = 0.0
  564.         for pair in hued_pairs:
  565.             sumDist+=pair[0]
  566.         avgHueDist = sumDist / hue_pair_count if hue_pair_count != 0 else 0.0001
  567.    
  568.  
  569.         #All colors gaps
  570.         all_smallest_gap = pair_list[0][0]
  571.         all_largest_gap = pair_list[-1][0]
  572.    
  573.  
  574.         #Hued colors gaps
  575.         hued_smallest_gap = hued_pairs[0][0] if hued_pairs else None
  576.         hued_largest_gap = hued_pairs[-1][0] if hued_pairs else None
  577.  
  578.         allError = abs(1.0 - all_largest_gap/avgDist)
  579.         print("Biggest_gap  to avg_gap delta "+str(round(100*allError,precision))+" %")
  580.  
  581.         allError = abs(1.0 - all_smallest_gap/avgDist)
  582.         print("Smallest_gap to avg_gap delta "+str(round(100*allError,precision))+" %")
  583.  
  584.         if hued_largest_gap:
  585.             huedError = abs(1.0 - hued_largest_gap/avgHueDist)
  586.             print("Hued biggest_gap  to avg_gap delta "+str(round(100*huedError,precision))+" %")
  587.         if hued_largest_gap:
  588.             huedError = abs(1.0 - hued_smallest_gap/avgHueDist)
  589.             print("Hued smallest_gap to avg_gap delta "+str(round(100*huedError,precision))+" %")
  590.  
  591.     return
  592.  
  593. # Input [[L,a,b], ...]
  594. # Find closest point for every point
  595. # Returns list of point pairs [[dist, p0, p1], ...] sorted by distance
  596. def findClosestPairs_grid(point_grid):
  597.     if len(point_grid.cloud) < 2:
  598.         return []
  599.  
  600.     dist_pair_array = []
  601.  
  602.     for p in point_grid.cloud:
  603.         neighbor, delta, dist, neighbor_count = point_grid.findNearest(p, point_grid.cell_size)
  604.         if neighbor is None:
  605.                 continue
  606.  
  607.         if p in neighbor:
  608.                 continue
  609.  
  610.         dist_pair_array.append([dist, p, neighbor])
  611.  
  612.     dist_pair_array.sort(key=lambda x: x[0])
  613.     return dist_pair_array
  614.  
  615. def oklabGapStats(point_grid, print_count):
  616.     fullPairArr = findClosestPairs_grid(point_grid)
  617.     printColorPairStats(fullPairArr,print_count,"Full",1)
  618.  
  619.  
  620.  
  621. palette_preset_list = {
  622.     'palTest': PalettePreset(
  623.         reserve_transparent=True,
  624.         gray_count  = None,
  625.         max_colors  =64,
  626.         hue_count   =12,
  627.         min_sat     =0.0,
  628.         max_sat     =1.0,
  629.         min_lum     =0.0,
  630.         max_lum     =1.0,
  631.         packing_fac =1.0,
  632.         max_attempts=1024*64,
  633.         seed=0
  634.     ),
  635. }
  636.  
  637. def printHexList(hex_list, palette_name=""):
  638.     out_string = palette_name + " = ["
  639.     for string in hex_list:
  640.         out_string += "\""+string+"\","
  641.     out_string+="]"
  642.     print(out_string + "\n")
  643.  
  644. def run_generatePalette():
  645.  
  646.     palette_name = 'palTest'
  647.     active_preset = palette_preset_list[palette_name]
  648.  
  649.     palette = PaletteGenerator(preset=active_preset)
  650.     palette.populatePointCloud()
  651.  
  652.     palette.sortPalette()
  653.  
  654.     palette_file = 'palette.png'
  655.     palette.paletteToImg(palette_file)
  656.  
  657.     oklabGapStats(palette.point_grid,3)
  658.  
  659.     hex_string = palette.paletteToHex()
  660.     printHexList(hex_string)
  661.  
  662.     print("Generated "+str(len(palette.point_grid.cloud) + active_preset.reserve_transparent)+" colors to ./"+palette_file)
  663.  
  664.  
  665. if __name__ == '__main__':
  666.     linRGB = [1.0,1.0,1.0]
  667.     ok = linearToOklab(linRGB)
  668.     new_srgb = oklabToLinear(ok)
  669.     if not inOklabGamut(ok):
  670.         print("uh oh")
  671.         exit(0)
  672.    
  673.     run_generatePalette()
  674.  
Advertisement
Add Comment
Please, Sign In to add comment