1- from colorsys import rgb_to_hsv
1+ from typing import Tuple , Callable , Any
22
33from PIL import Image , ImageStat
44from scipy .spatial .distance import cosine
55
66def generate_color_variations (color_dict , max_abs_difference = 16 ):
7+ '''Creates color combinations in given max_abs_difference.'''
78 new_dict = {}
89
910 for rgb_tuple , value in color_dict .items ():
@@ -30,110 +31,105 @@ def generate_color_variations(color_dict, max_abs_difference=16):
3031 if total_diff <= max_abs_difference :
3132 new_rgb_tuple = (new_r , new_g , new_b )
3233 new_dict [new_rgb_tuple ] = value
33-
3434 return new_dict
3535
3636class Method :
3737 def __init__ (self , blocks , compression_level : int = 16 ) -> None :
3838 self .compression_level = compression_level
39- self .caching = dict ()
39+ self .cache = dict ()
4040 self .blocks = blocks
4141
42+ def add_to_caching (self , median_rgb : Tuple [int , int , int ], closest_block : str ):
43+ self .cache [median_rgb ] = closest_block
44+ new_dict = dict ()
45+ new_dict [median_rgb ] = closest_block
46+ all_permutations = generate_color_variations (new_dict , self .compression_level )
47+ self .cache .update (all_permutations )
48+
49+ def check_caching (func : Callable [..., Any ]):
50+ '''Checks if chunk was already cached, and if so returns cached closest block.'''
51+ def wrapper (self , chunk , * args , ** kwargs ):
52+ img_median = tuple (ImageStat .Stat (chunk ).median )
53+ if img_median in self .cache :
54+ return self .cache [img_median ]
55+ else :
56+ return func (self , chunk , * args , ** kwargs )
57+ return wrapper
58+
59+ @check_caching
4260 def find_closest_block_rgb_abs_diff (self , chunk : Image ) -> str :
4361 '''Calculates the median value of an input image.
4462 Then compares this median to the medians for each block,
4563 and returns the block with the closest match based on the sum of absolute differences between its RGB values and the median of the input image.
4664 If there are multiple blocks with equal minimum difference, it will return the first one encountered.
4765 '''
48- og_median = tuple (ImageStat .Stat (chunk ).median )
49- og_median_rgb = tuple ([og_median [0 ], og_median [1 ], og_median [2 ]])
50- if og_median_rgb in self .caching :
51- return self .caching [og_median_rgb ]
52- else :
53- rgb_closests_diff = []
54- for channel in range (3 ):
55- min_diff = float ('inf' )
56- for block in self .blocks :
57- diff = abs (og_median_rgb [channel ] - self .blocks [block ]["median" ][channel ])
58- if diff < min_diff :
59- min_diff = diff
60- min_diff_block = block
61- rgb_closests_diff .append (min_diff_block )
62-
63- lowest_difference = float ("inf" )
64- lowest_block = None
65- for block in rgb_closests_diff :
66- difference = sum (abs (a - b ) for a , b in zip (self .blocks [block ]["median" ], og_median_rgb ))
67- if difference < lowest_difference :
68- lowest_difference = difference
69- lowest_block = block
70-
71- self .caching [og_median_rgb ] = lowest_block
72- new_dict = dict ()
73- new_dict [og_median_rgb ] = lowest_block
74- all_permutations = generate_color_variations (new_dict , self .compression_level )
75- self .caching .update (all_permutations )
76- return lowest_block
66+ img_median = tuple (ImageStat .Stat (chunk ).median )
7767
68+ rgb_closests_diff = []
69+ for channel in range (3 ):
70+ min_diff = float ('inf' )
71+ for block in self .blocks :
72+ diff = abs (img_median [channel ] - self .blocks [block ]["median" ][channel ])
73+ if diff < min_diff :
74+ min_diff = diff
75+ min_diff_block = block
76+ rgb_closests_diff .append (min_diff_block )
77+
78+ lowest_difference = float ("inf" )
79+ closest_block = None
80+ for block in rgb_closests_diff :
81+ difference = sum (abs (a - b ) for a , b in zip (self .blocks [block ]["median" ], img_median ))
82+ if difference < lowest_difference :
83+ lowest_difference = difference
84+ closest_block = block
85+
86+ self .add_to_caching (img_median , closest_block )
87+ return closest_block
88+
89+ @check_caching
7890 def find_closest_block_cosine_similarity (self , chunk : Image ) -> str :
7991 '''Calculates the median value of an input image.
8092 Then compares this median to the medians for each block,
8193 and returns the block with the closest match based on the cosine similarity between its RGB values and the median of the input image.
8294 If there are multiple blocks with equal maximum similarity, it will return the first one encountered.
8395 '''
84- og_median = tuple (ImageStat .Stat (chunk ).median )
85- og_median_rgb = tuple ([og_median [0 ], og_median [1 ], og_median [2 ]])
96+ img_median = tuple (ImageStat .Stat (chunk ).median )
97+
98+ closest_block = None
99+ max_similarity = - 1
86100
87- if og_median_rgb in self .caching :
88- return self .caching [og_median_rgb ]
89- else :
90- closest_block = None
91- max_similarity = - 1
92-
93- for block in self .blocks :
94- block_rgb = self .blocks [block ]["median" ]
95- similarity = 1 - cosine (og_median_rgb , block_rgb )
96-
97- if similarity > max_similarity :
98- max_similarity = similarity
99- closest_block = block
101+ for block in self .blocks :
102+ block_rgb = self .blocks [block ]["median" ]
103+ similarity = 1 - cosine (img_median , block_rgb )
100104
101- self . caching [ og_median_rgb ] = closest_block
102- new_dict = dict ()
103- new_dict [ og_median_rgb ] = closest_block
104- all_permutations = generate_color_variations ( new_dict , self . compression_level )
105- self .caching . update ( all_permutations )
106- return closest_block
105+ if similarity > max_similarity :
106+ max_similarity = similarity
107+ closest_block = block
108+
109+ self .add_to_caching ( img_median , closest_block )
110+ return closest_block
107111
112+ @check_caching
108113 def find_closest_block_minkowski_distance (self , chunk : Image , p : int = 2 ) -> str :
109114 '''Calculates the median value of an input image.
110115 Then compares this median to the medians for each block,
111116 and returns the block with the closest match based on the Minkowski distance between its RGB values and the median of the input image.
112117 If there are multiple blocks with equal minimum distance, it will return the first one encountered.
113118 '''
114- og_median = tuple (ImageStat .Stat (chunk ).median )
115- og_median_rgb = tuple ([og_median [0 ], og_median [1 ], og_median [2 ]])
119+ img_median = tuple (ImageStat .Stat (chunk ).median )
120+ closest_block = None
121+ min_distance = float ('inf' )
116122
117- if og_median_rgb in self .caching :
118- return self .caching [og_median_rgb ]
119- else :
120- closest_block = None
121- min_distance = float ('inf' )
122-
123- for block in self .blocks :
124- block_rgb = self .blocks [block ]["median" ]
125- distance = sum (abs (a - b ) ** p for a , b in zip (og_median_rgb , block_rgb )) ** (1 / p )
123+ for block in self .blocks :
124+ block_rgb = self .blocks [block ]["median" ]
125+ distance = sum (abs (a - b ) ** p for a , b in zip (img_median , block_rgb )) ** (1 / p )
126126
127- if distance < min_distance :
128- min_distance = distance
129- closest_block = block
127+ if distance < min_distance :
128+ min_distance = distance
129+ closest_block = block
130130
131- self .caching [og_median_rgb ] = closest_block
132- new_dict = dict ()
133- new_dict [og_median_rgb ] = closest_block
134- all_permutations = generate_color_variations (new_dict , self .compression_level )
135- self .caching .update (all_permutations )
136- return closest_block
131+ self .add_to_caching (img_median , closest_block )
132+ return closest_block
137133
138134 def find_closest_block_manhattan_distance (self , chunk : Image ) -> str :
139135 return self .find_closest_block_minkowski_distance (chunk , 1 )
@@ -147,65 +143,50 @@ def find_closest_block_chebyshev_distance(self, chunk: Image) -> str:
147143 def find_closest_block_taxicab_distance (self , chunk : Image ) -> str :
148144 return self .find_closest_block_minkowski_distance (chunk , 4 )
149145
146+ @check_caching
150147 def find_closest_block_hamming_distance (self , chunk : Image ) -> str :
151148 '''Calculates the median value of an input image.
152149 Then compares this median to the medians for each block,
153150 and returns the block with the closest match based on the Hamming distance between its RGB values and the median of the input image.
154151 If there are multiple blocks with equal minimum distance, it will return the first one encountered.
155152 '''
156- og_median = tuple (ImageStat .Stat (chunk ).median )
157- og_median_rgb = tuple ([og_median [0 ], og_median [1 ], og_median [2 ]])
153+ img_median = tuple (ImageStat .Stat (chunk ).median )
158154
159- if og_median_rgb in self .caching :
160- return self .caching [og_median_rgb ]
161- else :
162- closest_block = None
163- min_distance = float ('inf' )
155+ closest_block = None
156+ min_distance = float ('inf' )
164157
165- for block in self .blocks :
166- block_rgb = self .blocks [block ]["median" ]
167- distance = sum (a != b for a , b in zip (og_median_rgb , block_rgb ))
158+ for block in self .blocks :
159+ block_rgb = self .blocks [block ]["median" ]
160+ distance = sum (a != b for a , b in zip (img_median , block_rgb ))
168161
169- if distance < min_distance :
170- min_distance = distance
171- closest_block = block
162+ if distance < min_distance :
163+ min_distance = distance
164+ closest_block = block
172165
173- self .caching [og_median_rgb ] = closest_block
174- new_dict = dict ()
175- new_dict [og_median_rgb ] = closest_block
176- all_permutations = generate_color_variations (new_dict , self .compression_level )
177- self .caching .update (all_permutations )
178- return closest_block
166+ self .add_to_caching (img_median , closest_block )
167+ return closest_block
179168
169+ @check_caching
180170 def find_closest_block_canberra_distance (self , chunk : Image ) -> str :
181171 '''Calculates the median value of an input image.
182172 Then compares this median to the medians for each block,
183173 and returns the block with the closest match based on the Canberra distance between its RGB values and the median of the input image.
184174 If there are multiple blocks with equal minimum distance, it will return the first one encountered.
185175 '''
186- og_median = tuple (ImageStat .Stat (chunk ).median )
187- og_median_rgb = tuple ([og_median [0 ], og_median [1 ], og_median [2 ]])
188-
189- if og_median_rgb in self .caching :
190- return self .caching [og_median_rgb ]
191- else :
192- closest_block = None
193- min_distance = float ('inf' )
194-
195- for block in self .blocks :
196- block_rgb = self .blocks [block ]["median" ]
197- distance = sum (
198- abs (a - b ) / (abs (a ) + abs (b )) if abs (a ) + abs (b ) != 0 else float ('inf' )
199- for a , b in zip (og_median_rgb , block_rgb )
200- )
201-
202- if distance < min_distance :
203- min_distance = distance
204- closest_block = block
205-
206- self .caching [og_median_rgb ] = closest_block
207- new_dict = dict ()
208- new_dict [og_median_rgb ] = closest_block
209- all_permutations = generate_color_variations (new_dict , self .compression_level )
210- self .caching .update (all_permutations )
211- return closest_block
176+ img_median = tuple (ImageStat .Stat (chunk ).median )
177+ closest_block = None
178+ min_distance = float ('inf' )
179+
180+ for block in self .blocks :
181+ block_rgb = self .blocks [block ]["median" ]
182+ distance = sum (
183+ abs (a - b ) / (abs (a ) + abs (b )) if abs (a ) + abs (b ) != 0 else float ('inf' )
184+ for a , b in zip (img_median , block_rgb )
185+ )
186+
187+ if distance < min_distance :
188+ min_distance = distance
189+ closest_block = block
190+
191+ self .add_to_caching (img_median , closest_block )
192+ return closest_block
0 commit comments