Scheduled service maintenance on November 22


On Friday, November 22, 2024, between 06:00 CET and 18:00 CET, GIN services will undergo planned maintenance. Extended service interruptions should be expected. We will try to keep downtimes to a minimum, but recommend that users avoid critical tasks, large data uploads, or DOI requests during this time.

We apologize for any inconvenience.

text_arr_sim.py 25 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480
  1. """Functions for calculating array similarity metrics for text"""
  2. import numpy as np
  3. from matplotlib import pyplot as plt
  4. from scipy.optimize import minimize
  5. from scold import draw
  6. from scold import arr_sim
  7. from scold import utils
  8. def text_arr_sim(a, b=None, font_a='arial.ttf', font_b='arial.ttf', b_arr=None, measure='jaccard', translate=True, fliplr=False, flipud=False, size=100, scale_val=1.0, rotate_val=0.0, plot=False, partial_wasserstein_kwargs={'scale_mass':True, 'mass_normalise':True, 'distance_normalise':True, 'translation':'opt', 'n_startvals':7, 'solver':'Nelder-Mead', 'search_method':'grid'}, **kwargs):
  9. """Calculate similarity metrics for two strings of text, translated to achieve optimal overlap.
  10. Parameters
  11. ----------
  12. a : str
  13. b : str, optional
  14. Must be defined if `b_arr` is not. Ignored if `b_arr` is defined.
  15. font_a : str, optional
  16. `.ttf` font to use to build text array from `a`
  17. font_b : str, optional
  18. `.ttf` font to use to build text array from `b`
  19. b_arr : ndarray, optional
  20. Array that the array built from `a` will be compared to. This option is included as it is faster to pre-build the array that `a` will be compared to, if applying this function in a loop. If both `b` and `b_arr` are defined, `b` will be ignored.
  21. measure : str
  22. Which measure to maximise (or minimise in the case of distance measures) to find the optimal overlap between arrays. Possible options are any metrics calculated by `arr_sim.arr_sim()`.
  23. translate : bool
  24. Should translation be optimised? If `False`, will just calculate similarities for default positions. If `True`, will use 2D cross-correlation for all measures except Wasserstein distance, which can use nonlinear optimisation via arr_sim.partial_wasserstein_trans(). That function also supports 2D cross-correlation for comparability.
  25. fliplr : bool
  26. Should the text built from `a` be mirrored horizontally?
  27. flipud : bool
  28. Should the text built from `a` be mirrored vertically?
  29. size : int
  30. The size of the text to draw.
  31. scale_val : float
  32. This is multiplied by `size` to calculate the size of the text array built from `a`, rounded to the nearest pixel. If `b_arr` is not pre-built, `b_arr` is built at size `size`, such that `scale_val` says how many times bigger `a_arr` should be than `b_arr`.
  33. rotate_val : float
  34. Degrees by which the text built from `a` should be rotated.
  35. plot : bool
  36. Should the solution be plotted?
  37. partial_wasserstein_kwargs : dict
  38. kwargs to be passed to arr_sim.partial_wasserstein() or arr_sim.partial_wasserstein_trans()
  39. **kwargs
  40. Other arguments to pass to `draw.text_array()`.
  41. Returns
  42. -------
  43. dict
  44. Returns a dictionary with the similarity metrics calculated in `arr_sim.arr_sim()`, with an additional entry, `'shift'`, which contains the optimal translation values calculated by `arr_sim.translate_ov`, in form `(x, y)`.
  45. Examples
  46. --------
  47. >>> text_arr_sim('d', 'p')
  48. {'jaccard': 0.5915244261330195, 'shift': (1, 19)}
  49. >>> text_arr_sim('d', 'c')
  50. {'jaccard': 0.6390658174097664, 'shift': (1, 0)}
  51. >>> text_arr_sim('d', 'p', measure='partial_wasserstein', partial_wasserstein_kwargs={'scale_mass':True, 'mass_normalise':True, 'distance_normalise':True, 'translation':'crosscor', 'n_startvals':7, 'solver':'Nelder-Mead', 'search_method':'grid'})
  52. {'partial_wasserstein': 0.11199633634025599, 'shift': (1, 19)}
  53. >>> text_arr_sim('d', 'p', measure='partial_wasserstein', partial_wasserstein_kwargs={'scale_mass':True, 'mass_normalise':True, 'distance_normalise':True, 'translation':'opt', 'n_startvals':7, 'solver':'Nelder-Mead', 'search_method':'grid'})
  54. {'partial_wasserstein': 0.07609689664769317, 'shift': (5.0, 11.0)}
  55. """
  56. a_arr = draw.text_array(a, font=font_a, rotate=rotate_val, fliplr=fliplr, flipud=flipud, size=size*scale_val, **kwargs)
  57. if np.all(b_arr == None):
  58. b_arr = draw.text_array(b, font=font_b, rotate=0, fliplr=False, flipud=False, size=size, **kwargs) # faster to pre-define and give as argument if using the same b text in a loop
  59. if measure=='partial_wasserstein':
  60. if translate and partial_wasserstein_kwargs['translation'] is None:
  61. raise ValueError('Translation was requested, but the Partial Wasserstein translation method was not specified')
  62. elif not translate and partial_wasserstein_kwargs['translation'] is not None:
  63. print(translate)
  64. print(partial_wasserstein_kwargs)
  65. raise ValueError('Translation was not requested, but a Partial Wasserstein translation method was specified')
  66. if translate:
  67. if measure == 'partial_wasserstein':
  68. a_aligned, b_aligned = (a_arr, b_arr)
  69. # ensure same size with zero-padding
  70. a_aligned, b_aligned = utils.pad_for_translation(a_aligned, b_aligned, pad=False)
  71. else:
  72. a_aligned, b_aligned, shift = arr_sim.translate_ov(a_arr, b_arr, return_first_only=True)
  73. else:
  74. a_aligned, b_aligned = (a_arr, b_arr)
  75. shift = (0, 0)
  76. # ensure same size with zero-padding
  77. a_aligned, b_aligned = utils.pad_for_translation(a_aligned, b_aligned, pad=False)
  78. if plot:
  79. pl_sh = list(a_aligned.shape)
  80. pl_sh.append(3)
  81. rgb_arr = np.zeros(pl_sh)
  82. rgb_arr[:, :, 0] = a_aligned
  83. rgb_arr[:, :, 2] = b_aligned
  84. plt.imshow(utils.crop_zeros(rgb_arr), interpolation='none')
  85. # get the similarity measures
  86. arr_sim_out = arr_sim.arr_sim(a_aligned, b_aligned, measure=measure, partial_wasserstein_kwargs=partial_wasserstein_kwargs)
  87. sim_res = {}
  88. if measure=='partial_wasserstein':
  89. sim_res[measure] = arr_sim_out['metric']
  90. shift = arr_sim_out['trans']
  91. else:
  92. sim_res[measure] = arr_sim_out
  93. # flip the order of shift, as the array indices (x, y) refer to (y, x) in the image
  94. shift_flipped = (shift[1], shift[0])
  95. # add shift to the results
  96. sim_res['shift'] = shift_flipped
  97. return(sim_res)
  98. def _opt_text_arr_sim_flip_manual(a='a', b=None, font_a='arial.ttf', font_b='arial.ttf', b_arr=None, measure='jaccard', translate=True, scale=True, rotate=True, fliplr=False, flipud=False, size=100, rotation_bounds=(-np.Infinity, np.Infinity), max_scale_change_factor=2.0, rotation_eval_n=9, scale_eval_n=9, solver='Nelder-Mead', search_method='grid', plot=False, partial_wasserstein_kwargs={'scale_mass':True, 'mass_normalise':True, 'distance_normalise': True, 'ins_weight':0.0, 'del_weight':0.0}, **kwargs):
  99. """Find parameters for geometric operations of translation, scale, and rotation that maximise overlap between two arrays of drawn text.
  100. Parameters
  101. ----------
  102. a : str
  103. b : str, optional
  104. Must be defined if `b_arr` is not. Ignored if `b_arr` is defined.
  105. font_a : str, optional
  106. `.ttf` font to use to build text array from `a`
  107. font_b : str, optional
  108. `.ttf` font to use to build text array from `b`
  109. b_arr : ndarray, optional
  110. Array that the array built from `a` will be compared to. This option is included as it is faster to pre-build the array that `a` will be compared to, if applying this function in a loop. If both `b` and `b_arr` are defined, `b` will be ignored.
  111. measure: str
  112. Which measure to maximise (or minimise in the case of `px_dist`) to find the optimal overlap between arrays. Possible options are any metrics calculated by `arr_sim.arr_sim()`.
  113. translate : bool
  114. Should the translation operation be optimised via cross-correlation? If `False`, will always use default positions.
  115. scale: bool
  116. Should scale be optimised?
  117. rotate : bool
  118. Should rotation be optimised?
  119. fliplr : bool
  120. Should `b` be flipped horizontally? Note that in this version of the function, `b` is not optimised. Instead, this function can be run with this set to True and False, and the best result taken.
  121. flipud : bool
  122. Should `b` be flipped vertically? Note that in this version of the function, `b` is not optimised. Instead, this function can be run with this set to True and False, and the best result taken.
  123. size : int
  124. Size for the text (the scale parameter will be multiplied by this value).
  125. rotation_bounds : tuple
  126. Limits for optimising rotation in form `(lowerbound, upperbound)`. For example, `(-90, 90)` will limit rotation to 90 degrees in either direction.
  127. max_scale_change_factor : float
  128. Maximum value for the optimised scale parameter. `max_scale_change_factor=2` will permit 100% bigger or 50% smaller, i.e., twice as large or twice as small.
  129. rotation_eval_n : int
  130. How many starting values should be tried for optimising rotation?
  131. scale_eval_n : int
  132. How many starting values should be tried for optimising scale?
  133. solver : str
  134. Which solver to use? Possible values are those available to `scipy.optimize.minimize()`.
  135. search_method : str
  136. Method for setting starting values. Options are:
  137. 'grid': set in equal steps from the lower to the upper bound
  138. 'random': set randomly between the lower and upper bound
  139. plot : bool
  140. Should the optimal overlap be plotted?
  141. partial_wasserstein_kwargs : dict
  142. kwargs to be passed to arr_sim.partial_wasserstein() or arr_sim.partial_wasserstein_trans()
  143. **kwargs
  144. Other arguments to pass to `text_arr_sim()`.
  145. Returns
  146. -------
  147. dict
  148. A dictionary containing the following values:
  149. 'translate': Whether translation was optimised
  150. 'scale': Whether scale was optimised
  151. 'rotate': Whether rotation was optimised
  152. 'fliplr': Placeholder for main function (always `None`)
  153. 'flipud': Placeholder for main function (always `None`)
  154. 'intersection', 'union', 'overlap', 'jaccard', 'dice': Values from `arr_sim.arr_sim()`
  155. 'translate_val_x': Optimal shift value in x dimension
  156. 'translate_val_y': Optimal shift value in y dimension
  157. 'scale_val': Optimal scale coefficient
  158. 'rotate_val': Optimal rotation coefficient
  159. 'flip_val': Whether the array was slipped horizontally
  160. """
  161. if measure in ('px_dist', 'partial_wasserstein'):
  162. do_minimise = True
  163. else:
  164. do_minimise = False
  165. if np.all(b_arr == None):
  166. b_arr = draw.text_array(b, font=font_b, rotate=0, fliplr=False, flipud=False, size=size, **kwargs)
  167. # if neither scale nor rotation need to be optimised, just use the cross correlation approach to get optimal cold values...
  168. if (not scale) and (not rotate):
  169. sim_res = text_arr_sim(a, b_arr=b_arr, measure=measure, font_a=font_a, translate=translate, fliplr=fliplr, flipud=flipud, scale_val=1, rotate_val=0, size=size, partial_wasserstein_kwargs=partial_wasserstein_kwargs, **kwargs)
  170. poss_scale_vals = [0]
  171. poss_rotate_vals = [0]
  172. # otherwise, optimise scale and/or rotation
  173. else:
  174. # functions which will be optimised (note that scale is on a log-scale here for the optimiser - this is useful as centred on zero and will have same precision for increase and decrease, i.e. whether 2x or 0.5x) and prevent a scale of 0
  175. def sim_opt_scale_rotate(x):
  176. # translate log scale to raw scale
  177. scale_exp = np.exp(x[0])
  178. m = text_arr_sim(a, b_arr=b_arr, measure=measure, font_a=font_a, translate=translate, fliplr=fliplr, flipud=flipud, scale_val=scale_exp, rotate_val=x[1], size=size, partial_wasserstein_kwargs=partial_wasserstein_kwargs, **kwargs)[measure]
  179. if do_minimise:
  180. return m
  181. else:
  182. return 1 - m
  183. def sim_opt_scale(x):
  184. # translate log scale to raw scale
  185. scale_exp = np.exp(x[0])
  186. m = text_arr_sim(a, b_arr=b_arr, measure=measure, font_a=font_a, translate=translate, fliplr=fliplr, flipud=flipud, scale_val=scale_exp, rotate_val=0, size=size, partial_wasserstein_kwargs=partial_wasserstein_kwargs, **kwargs)[measure]
  187. if do_minimise:
  188. return m
  189. else:
  190. return 1 - m
  191. def sim_opt_rotate(x):
  192. m = text_arr_sim(a, b_arr=b_arr, measure=measure, font_a=font_a, translate=translate, fliplr=fliplr, flipud=flipud, scale_val=1, rotate_val=x[0], size=size, partial_wasserstein_kwargs=partial_wasserstein_kwargs, **kwargs)[measure]
  193. if do_minimise:
  194. return m
  195. else:
  196. return 1 - m
  197. # bounds of scale optimisation
  198. scale_bounds = (-np.log(max_scale_change_factor), np.log(max_scale_change_factor))
  199. # starting values for optimising scale and rotation
  200. if search_method=='grid':
  201. starting_points_scale = np.linspace(
  202. scale_bounds[0],
  203. scale_bounds[1],
  204. scale_eval_n, endpoint=True)
  205. starting_points_rotation = np.linspace(
  206. max((-180, min(rotation_bounds))),
  207. min((180, max(rotation_bounds))),
  208. rotation_eval_n, endpoint=True)
  209. elif search_method=='random':
  210. starting_points_scale = np.random.uniform(
  211. scale_bounds[0],
  212. scale_bounds[1],
  213. size=scale_eval_n)
  214. starting_points_rotation = np.random.uniform(
  215. max((-180, min(rotation_bounds))),
  216. min((180, max(rotation_bounds))),
  217. size=rotation_eval_n)
  218. # list which will contain the results
  219. iter_res = []
  220. if (scale) & (rotate):
  221. for start_scale in starting_points_scale:
  222. for start_rotate in starting_points_rotation:
  223. iter_res.append(minimize(sim_opt_scale_rotate, x0=[start_scale, start_rotate], method=solver, bounds=[scale_bounds, rotation_bounds]))
  224. elif (scale) & (not rotate):
  225. for start_scale in starting_points_scale:
  226. iter_res.append(minimize(sim_opt_scale, x0=[start_scale], method=solver, bounds = [scale_bounds]))
  227. elif (not scale) & (rotate):
  228. for start_rotate in starting_points_rotation:
  229. iter_res.append(minimize(sim_opt_rotate, x0=[start_rotate], method=solver, bounds = [rotation_bounds]))
  230. fun_vals = np.array([i['fun'] for i in iter_res])
  231. # first, get indices of iterations which reached the best solution
  232. min_fun_idx = fun_vals == np.min(fun_vals)
  233. # use this to extract possible scale and rotation solutions
  234. if (scale) & (rotate):
  235. poss_scale_vals = np.array([i['x'][0] for i in iter_res])[min_fun_idx]
  236. poss_rotate_vals = np.array([i['x'][1] for i in iter_res])[min_fun_idx]
  237. elif (scale) & (not rotate):
  238. poss_scale_vals = np.array([i['x'][0] for i in iter_res])[min_fun_idx]
  239. poss_rotate_vals = np.zeros(poss_scale_vals.shape)
  240. elif (not scale) & (rotate):
  241. poss_rotate_vals = np.array([i['x'][0] for i in iter_res])[min_fun_idx]
  242. poss_scale_vals = np.zeros(poss_rotate_vals.shape)
  243. # make sure the rotation values are all expressed within [-180, 180] instead of [0, inf]
  244. # (this is useful for minimising the angle when there are multiple identical solutions)
  245. poss_rotate_vals %= 360
  246. poss_rotate_vals_dir = np.matrix([poss_rotate_vals, poss_rotate_vals-360])
  247. poss_rotate_pw_idx = np.array(np.matrix.argmin(np.abs(poss_rotate_vals_dir), 0))[0]
  248. poss_rotate_vals = np.array([poss_rotate_vals_dir[poss_rotate_pw_idx[i], i] for i in range(poss_rotate_vals_dir.shape[1])])
  249. # next, get the solutions of these with the smallest absolute scale (i.e., closest to original log scale value of zero)
  250. min_abs_scale_idx = np.abs(poss_scale_vals) == np.min(np.abs(poss_scale_vals))
  251. poss_scale_vals = poss_scale_vals[min_abs_scale_idx]
  252. poss_rotate_vals = poss_rotate_vals[min_abs_scale_idx]
  253. # finally, get the solution, of those, with the smallest absolute rotation (i.e., closest to original rotation)
  254. min_abs_rotate_idx = np.abs(poss_rotate_vals) == np.min(np.abs(poss_rotate_vals))
  255. poss_scale_vals = poss_scale_vals[min_abs_rotate_idx]
  256. poss_rotate_vals = poss_rotate_vals[min_abs_rotate_idx]
  257. # replicate the optimal values to extract the translation values
  258. sim_res = text_arr_sim(a, b_arr=b_arr, measure=measure, font_a=font_a, translate=translate, fliplr=fliplr, flipud=flipud, scale_val=np.exp(poss_scale_vals[0]), rotate_val=poss_rotate_vals[0], size=size, plot=plot, partial_wasserstein_kwargs=partial_wasserstein_kwargs, **kwargs)
  259. res = {'a':a, 'b':b,
  260. 'font_a':font_a, 'font_b':font_b,
  261. # settings for optimisation
  262. 'translate': translate,
  263. 'scale': scale,
  264. 'rotate': rotate,
  265. 'fliplr': None,
  266. 'flipud': None,
  267. # results from optimisation
  268. measure: sim_res[measure],
  269. # note that cold() output gives shift where indices refer to image indices, rather than array indices
  270. 'translate_val_x': sim_res['shift'][0],
  271. 'translate_val_y': sim_res['shift'][1],
  272. # the optimal scale and rotation values
  273. 'scale_val': np.exp(poss_scale_vals[0]),
  274. 'rotate_val': poss_rotate_vals[0],
  275. 'fliplr_val': fliplr,
  276. 'flipud_val': flipud}
  277. return(res)
  278. def opt_text_arr_sim(a='a', b=None, font_a='arial.ttf', font_b='arial.ttf', b_arr=None, measure='jaccard', translate=True, scale=True, rotate=True, fliplr=True, flipud=False, size=100, rotation_bounds=(-np.Infinity, np.Infinity), max_scale_change_factor=2.0, rotation_eval_n=9, scale_eval_n=9, solver='Nelder-Mead', search_method='grid', plot=False, partial_wasserstein_kwargs={'scale_mass':True, 'mass_normalise':True, 'distance_normalise': True, 'ins_weight':0.0, 'del_weight':0.0}, **kwargs):
  279. """Find parameters for geometric operations of translation, scale, rotation, and horizontal flipping that maximise overlap between two arrays of drawn text.
  280. Parameters
  281. ----------
  282. a : str
  283. b : str, optional
  284. Must be defined if `b_arr` is not. Ignored if `b_arr` is defined.
  285. font_a : str, optional
  286. `.ttf` font to use to build text array from `a`
  287. font_b : str, optional
  288. `.ttf` font to use to build text array from `b`
  289. b_arr : ndarray, optional
  290. Array that the array built from `a` will be compared to. This option is included as it is faster to pre-build the array that `a` will be compared to, if applying this function in a loop. If both `b` and `b_arr` are defined, `b` will be ignored.
  291. measure: str
  292. Which measure to maximise (or minimise in the case of `px_dist`) to find the optimal overlap between arrays. Possible options are any metrics calculated by `arr_sim.arr_sim()`.
  293. translate : bool
  294. Should the translation operation be optimised? If `False`, will always use default positions.
  295. scale: bool
  296. Should scale be optimised?
  297. rotate : bool
  298. Should rotation be optimised?
  299. fliplr : bool
  300. Should horizontal flipping (mirroring) be optimised?
  301. fliplr : bool
  302. Should vertical flipping (mirroring) be optimised?
  303. size : int
  304. Size for the text (the scale parameter will be multiplied by this value).
  305. rotation_bounds : tuple
  306. Limits for optimising rotation in form `(lowerbound, upperbound)`. For example, `(-90, 90)` will limit rotation to 90 degrees in either direction.
  307. max_scale_change_factor : float
  308. Maximum value for the optimised scale parameter. `max_scale_change_factor=2` will permit 100% bigger or 50% smaller, i.e., twice as large or twice as small.
  309. rotation_eval_n : int
  310. How many starting values should be tried for optimising rotation?
  311. scale_eval_n : int
  312. How many starting values should be tried for optimising scale?
  313. solver : str
  314. Which solver to use? Possible values are those available to `scipy.optimize.minimize()`.
  315. search_method : str
  316. Method for setting starting values. Options are:
  317. 'grid': set in equal steps from the lower to the upper bound
  318. 'random': set randomly between the lower and upper bound
  319. plot : bool
  320. Should the optimal overlap be plotted?
  321. **kwargs
  322. Other arguments to pass to `text_arr_sim()`.
  323. Returns
  324. -------
  325. dict
  326. A dictionary containing the following values:
  327. 'translate': Whether translation was optimised
  328. 'scale': Whether scale was optimised
  329. 'rotate': Whether rotation was optimised
  330. 'flip': Whether flip was optimised
  331. 'intersection', 'union', 'overlap', 'jaccard', 'dice': Values from `arr_sim.arr_sim()`
  332. 'translate_val_x': Optimal shift value in x dimension
  333. 'translate_val_y': Optimal shift value in y dimension
  334. 'scale_val': Optimal scale coefficient
  335. 'rotate_val': Optimal rotation coefficient
  336. 'flip_val': Whether the optimal solution included flipping
  337. Examples
  338. --------
  339. >>> opt_text_arr_sim('d', 'p')
  340. {'a': 'd',
  341. 'b': 'p',
  342. 'font_a': 'arial.ttf',
  343. 'font_b': 'arial.ttf',
  344. 'translate': True,
  345. 'scale': True,
  346. 'rotate': True,
  347. 'fliplr': True,
  348. 'flipud': False,
  349. 'jaccard': 0.9708454810495627,
  350. 'translate_val_x': 0,
  351. 'translate_val_y': 0,
  352. 'scale_val': 1.0,
  353. 'rotate_val': 180.0,
  354. 'fliplr_val': False,
  355. 'flipud_val': False}
  356. >>> opt_text_arr_sim('d', 'q', flipud=True)
  357. {'a': 'd',
  358. 'b': 'q',
  359. 'font_a': 'arial.ttf',
  360. 'font_b': 'arial.ttf',
  361. 'translate': True,
  362. 'scale': True,
  363. 'rotate': True,
  364. 'fliplr': True,
  365. 'flipud': True,
  366. 'jaccard': 0.9600580973129993,
  367. 'translate_val_x': 0,
  368. 'translate_val_y': 0,
  369. 'scale_val': 1.0,
  370. 'rotate_val': 0.0,
  371. 'fliplr_val': False,
  372. 'flipud_val': True}
  373. >>> opt_text_arr_sim('e', 'o', flipud=True, measure='partial_wasserstein')
  374. """
  375. non_flipped = _opt_text_arr_sim_flip_manual(a=a, b=b, font_a=font_a, font_b=font_b, b_arr=b_arr, measure=measure, translate=translate, scale=scale, rotate=rotate, fliplr=False, flipud=False, size=size, rotation_bounds=rotation_bounds, max_scale_change_factor=max_scale_change_factor, rotation_eval_n=rotation_eval_n, scale_eval_n=scale_eval_n, solver=solver, search_method=search_method, plot=False, partial_wasserstein_kwargs=partial_wasserstein_kwargs, **kwargs)
  376. res = non_flipped
  377. res['fliplr'] = False
  378. res['flipud'] = False
  379. if fliplr:
  380. flipped_lr = _opt_text_arr_sim_flip_manual(a=a, b=b, font_a=font_a, font_b=font_b, b_arr=b_arr, measure=measure, translate=translate, scale=scale, rotate=rotate, fliplr=True, flipud=False, size=size, rotation_bounds=rotation_bounds, max_scale_change_factor=max_scale_change_factor, rotation_eval_n=rotation_eval_n, scale_eval_n=scale_eval_n, solver=solver, search_method=search_method, plot=False, partial_wasserstein_kwargs=partial_wasserstein_kwargs, **kwargs)
  381. if flipped_lr[measure] > res[measure] and np.abs(flipped_lr['rotate_val']) <= np.abs(res['rotate_val']):
  382. res = flipped_lr
  383. if flipud:
  384. flipped_ud = _opt_text_arr_sim_flip_manual(a=a, b=b, font_a=font_a, font_b=font_b, b_arr=b_arr, measure=measure, translate=translate, scale=scale, rotate=rotate, fliplr=False, flipud=True, size=size, rotation_bounds=rotation_bounds, max_scale_change_factor=max_scale_change_factor, rotation_eval_n=rotation_eval_n, scale_eval_n=scale_eval_n, solver=solver, search_method=search_method, plot=False, partial_wasserstein_kwargs=partial_wasserstein_kwargs, **kwargs)
  385. if flipped_ud[measure] > res[measure] and np.abs(flipped_ud['rotate_val']) <= np.abs(res['rotate_val']):
  386. res = flipped_ud
  387. if fliplr and flipud:
  388. flipped_lrud = _opt_text_arr_sim_flip_manual(a=a, b=b, font_a=font_a, font_b=font_b, b_arr=b_arr, measure=measure, translate=translate, scale=scale, rotate=rotate, fliplr=True, flipud=True, size=size, rotation_bounds=rotation_bounds, max_scale_change_factor=max_scale_change_factor, rotation_eval_n=rotation_eval_n, scale_eval_n=scale_eval_n, solver=solver, search_method=search_method, plot=False, partial_wasserstein_kwargs=partial_wasserstein_kwargs, **kwargs)
  389. if flipped_ud[measure] > res[measure] and np.abs(flipped_lrud['rotate_val']) <= np.abs(res['rotate_val']):
  390. res = flipped_lrud
  391. res['fliplr'] = fliplr
  392. res['flipud'] = flipud
  393. if plot:
  394. # replicate the optimal values to plot
  395. if np.all(b_arr==None):
  396. b_arr = draw.text_array(b, font=font_b, rotate=0, fliplr=False, flipud=False, size=size, **kwargs)
  397. _ = text_arr_sim(a, b_arr=b_arr, measure=measure, font_a=font_a, translate=translate, fliplr=res['fliplr_val'], flipud=res['flipud_val'], scale_val=res['scale_val'], rotate_val=res['rotate_val'], size=size, plot=plot, partial_wasserstein_kwargs=partial_wasserstein_kwargs, **kwargs)
  398. return(res)
  399. def string_px_dist(a, b, **kwargs):
  400. """Calculate the pixel distance between entire, translation-aligned (via cross-correlation) strings. This function is just a wrapper for `draw.text_array()` and `arr_sim.px_dist()`.
  401. Parameters
  402. ----------
  403. a : str
  404. b : str
  405. **kwargs
  406. Arguments passed to `draw.text_array()`. Same parameters are used for both strings.
  407. """
  408. a_arr = draw.text_array(a, **kwargs)
  409. b_arr = draw.text_array(b, **kwargs)
  410. dist = arr_sim.px_dist(a_arr, b_arr)
  411. return(dist)