123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511 |
- """Functions for calculating array similarity metrics for text"""
- import numpy as np
- from matplotlib import pyplot as plt
- from scipy.optimize import minimize
- from scold import draw
- from scold import arr_sim
- from scold import utils
- def text_arr_sim(a, b=None, font_a='arial.ttf', font_b='arial.ttf', b_arr=None, measure='partial_wasserstein', translate=None, fliplr=False, flipud=False, size=100, scale_val=1.0, rotate_val=0.0, translate_prop_val_x=0.0, translate_prop_val_y=0.0, plot=False, partial_wasserstein_kwargs={'scale_mass':True, 'mass_normalise':True, 'distance_normalise':True, 'ins_weight': 0.0, 'del_weight': 0.0}, **kwargs):
- """Calculate similarity metrics for two strings of text, translated to achieve optimal overlap.
-
- Parameters
- ----------
- a : str
- b : str, optional
- Must be defined if `b_arr` is not. Ignored if `b_arr` is defined.
- font_a : str, optional
- `.ttf` font to use to build text array from `a`
- font_b : str, optional
- `.ttf` font to use to build text array from `b`
- b_arr : ndarray, optional
- 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.
- measure : str
- Argument not used. Included for consistency with `text_arr_sim.text_arr_sim()`.
- translate : bool
- Argument not used. Included for consistency with `text_arr_sim.text_arr_sim()`.
- fliplr : bool
- Should the text built from `a` be mirrored horizontally?
- flipud : bool
- Should the text built from `a` be mirrored vertically?
- size : int
- The size of the text to draw.
- scale_val : float
- 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`.
- rotate_val : float
- Degrees by which the text built from `a` should be rotated.
- translate_prop_val_x : float
- Translation in the x axis for the text built from `a`, given as a proportion of the max size of `a_arr` and `b_arr` in that dimension. If negative, translation is in a negative direction.
- translate_prop_val_y : float
- Translation in the y axis for the text built from `a`, given as a proportion of the max size of `a_arr` and `b_arr` in that dimension. If negative, translation is in a negative direction.
- plot : bool
- Should the solution be plotted?
- partial_wasserstein_kwargs : dict
- kwargs to be passed to arr_sim.partial_wasserstein().
- **kwargs
- Other arguments to pass to `draw.text_array()`.
-
- Returns
- -------
- dict
- 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)`.
- Examples
- --------
- >>> text_arr_sim('d', 'p')
- {'jaccard': 0.5915244261330195, 'shift': (1, 19)}
- >>> text_arr_sim('d', 'c')
- {'jaccard': 0.6390658174097664, 'shift': (1, 0)}
- >>> 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'})
- {'partial_wasserstein': 0.11199633634025599, 'shift': (1, 19)}
- >>> 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'})
- {'partial_wasserstein': 0.07609689664769317, 'shift': (5.0, 11.0)}
- """
- if measure!='partial_wasserstein':
- raise ValueError('The measure argument is only included for consistency with text_arr_sim.text_arr_sim. The only acceptable value for text_arr_sim_wasserstein is partial_wasserstein.')
-
- if translate is not None:
- raise ValueError('The translate argument is only included for consistency with text_arr_sim.text_arr_sim.')
-
- assert translate_prop_val_x>=-1 and translate_prop_val_x<=1 and translate_prop_val_y>=-1 and translate_prop_val_y<=1
- a_arr = draw.text_array(a, font=font_a, rotate=rotate_val, fliplr=fliplr, flipud=flipud, size=size*scale_val, **kwargs)
- if np.all(b_arr == None):
- 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
-
- # calculate the partial wasserstein value
- # map proportion to raw shift values, and pass as arguments to the partial wasserstein fun
- max_shifts = [np.max([a_arr.shape[i], b_arr.shape[i]]) for i in range(len(a_arr.shape))]
- translate_val_x = np.round(translate_prop_val_x * max_shifts[0], 5)
- translate_val_y = np.round(translate_prop_val_y * max_shifts[1], 5)
- partial_wasserstein_kwargs['trans_manual'] = (translate_val_x, translate_val_y)
- sim_res = {}
- partial_wasserstein_kwargs['plot'] = plot
- if plot:
- # flip the indices for the plot
- a_arr=a_arr.T
- b_arr=b_arr.T
- sim_res[measure] = arr_sim.partial_wasserstein(a_arr, b_arr, **partial_wasserstein_kwargs)
-
- # add shift to the results
- sim_res['shift'] = (translate_val_x, translate_val_y)
-
- return(sim_res)
- def _opt_text_arr_sim_flip_manual(a='a', b=None, font_a='arial.ttf', font_b='arial.ttf', b_arr=None, measure='partial_wasserstein', translate=True, scale=True, rotate=True, fliplr=False, flipud=False, size=100, rotation_bounds=(-np.Infinity, np.Infinity), max_scale_change_factor=2, max_translation_factor=0.999, rotation_eval_n=9, scale_eval_n=9, translation_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):
- """Find parameters for geometric operations of translation, scale, and rotation that minimise partial wasserstein between two arrays of drawn text.
-
- Parameters
- ----------
- a : str
- b : str, optional
- Must be defined if `b_arr` is not. Ignored if `b_arr` is defined.
- font_a : str, optional
- `.ttf` font to use to build text array from `a`
- font_b : str, optional
- `.ttf` font to use to build text array from `b`
- b_arr : ndarray, optional
- 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.
- measure : str
- Argument not used. Included for consistency with `text_arr_sim.text_arr_sim()`.
- translate : bool
- Should the translation operation be optimised? If `False`, will always use default positions.
- scale: bool
- Should scale be optimised?
- rotate : bool
- Should rotation be optimised?
- fliplr : bool
- 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.
- flipud : bool
- 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.
- size : int
- Size for the text (the scale parameter will be multiplied by this value).
- rotation_bounds : tuple
- Limits for optimising rotation in form `(lowerbound, upperbound)`. For example, `(-90, 90)` will limit rotation to 90 degrees in either direction.
- max_scale_change_factor : float
- 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.
- max_translation_factor : float
- Maximum value for the optimised translation parameter. `max_translation_factor=0.5` will permit translation 50% of the maximum bounds in any direction, where bounds are determined by the maximum size of the arrays being compared. Must be <1, as it is optimised on a logistic scale.
- rotation_eval_n : int
- How many starting values should be tried for optimising rotation?
- scale_eval_n : int
- How many starting values should be tried for optimising scale?
- translation_eval_n : int
- How many starting values should be tried for optimising translation (x and y)?
- solver : str
- Which solver to use? Possible values are those available to `scipy.optimize.minimize()`.
- search_method : str
- Method for setting starting values. Options are:
- 'grid': set in equal steps from the lower to the upper bound
- 'random': set randomly between the lower and upper bound
- plot : bool
- Should the optimal solution be plotted?
- partial_wasserstein_kwargs : dict
- kwargs to be passed to arr_sim.partial_wasserstein() or arr_sim.partial_wasserstein_trans()
- **kwargs
- Other arguments to pass to `text_arr_sim()`.
-
- Returns
- -------
- dict
- A dictionary containing the following values:
- 'translate': Whether translation was optimised
- 'scale': Whether scale was optimised
- 'rotate': Whether rotation was optimised
- 'fliplr': Placeholder for main function (always `None`)
- 'flipud': Placeholder for main function (always `None`)
- 'intersection', 'union', 'overlap', 'jaccard', 'dice', etc.: Values from `arr_sim.arr_sim()`
- 'translate_val_x': Optimal shift value in x dimension
- 'translate_val_y': Optimal shift value in y dimension
- 'scale_val': Optimal scale coefficient
- 'rotate_val': Optimal rotation coefficient
- 'flip_val': Whether the array was slipped horizontally
- """
- if measure!='partial_wasserstein':
- raise ValueError('The measure argument is only included for consistency with text_arr_sim.text_arr_sim. The only acceptable value for text_arr_sim_wasserstein is partial_wasserstein.')
- if np.all(b_arr == None):
- b_arr = draw.text_array(b, font=font_b, rotate=0, fliplr=False, flipud=False, size=size, **kwargs)
- # if neither scale, rotation, nor translation need to be optimised, just use the cross correlation approach to get optimal cold values...
- if (not scale) and (not rotate) and (not translate):
- sim_res = text_arr_sim(a, b_arr=b_arr, measure=measure, font_a=font_a, translate=None, fliplr=fliplr, flipud=flipud, scale_val=1, rotate_val=0, translate_prop_val_x=0, translate_prop_val_y=0, size=size, partial_wasserstein_kwargs=partial_wasserstein_kwargs, **kwargs)
- poss_scale_vals = [0]
- poss_rotate_vals = [0]
- poss_translate_x_vals = [0]
- poss_translate_y_vals = [0]
- fun_vals = sim_res[measure]
-
- # otherwise, optimise translation, scale, and/or rotation
- else:
-
- # 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
- def sim_opt_translate_scale_rotate(x):
- # map logistic scale to raw scale
- translate_prop_val_x = utils.inv_logistic(x[0])
- translate_prop_val_y = utils.inv_logistic(x[1])
- # map log scale to raw scale
- scale_exp = np.exp(x[2])
- return text_arr_sim(a, b_arr=b_arr, measure=measure, font_a=font_a, translate=None, fliplr=fliplr, flipud=flipud, scale_val=scale_exp, rotate_val=x[3], translate_prop_val_x=translate_prop_val_x, translate_prop_val_y=translate_prop_val_y, size=size, partial_wasserstein_kwargs=partial_wasserstein_kwargs, **kwargs)[measure]
-
- def sim_opt_translate_scale(x):
- # map logistic scale to raw scale
- translate_prop_val_x = utils.inv_logistic(x[0])
- translate_prop_val_y = utils.inv_logistic(x[1])
- # map log scale to raw scale
- scale_exp = np.exp(x[2])
- return text_arr_sim(a, b_arr=b_arr, measure=measure, font_a=font_a, translate=None, fliplr=fliplr, flipud=flipud, scale_val=scale_exp, rotate_val=0, translate_prop_val_x=translate_prop_val_x, translate_prop_val_y=translate_prop_val_y, size=size, partial_wasserstein_kwargs=partial_wasserstein_kwargs, **kwargs)[measure]
-
- def sim_opt_translate_rotate(x):
- # map logistic scale to raw scale
- translate_prop_val_x = utils.inv_logistic(x[0])
- translate_prop_val_y = utils.inv_logistic(x[1])
- return text_arr_sim(a, b_arr=b_arr, measure=measure, font_a=font_a, translate=None, fliplr=fliplr, flipud=flipud, scale_val=1, rotate_val=x[2], translate_prop_val_x=translate_prop_val_x, translate_prop_val_y=translate_prop_val_y, size=size, partial_wasserstein_kwargs=partial_wasserstein_kwargs, **kwargs)[measure]
- def sim_opt_scale_rotate(x):
- # map log scale to raw scale
- scale_exp = np.exp(x[0])
- return text_arr_sim(a, b_arr=b_arr, measure=measure, font_a=font_a, translate=None, fliplr=fliplr, flipud=flipud, scale_val=scale_exp, rotate_val=x[1], translate_prop_val_x=0, translate_prop_val_y=0, size=size, partial_wasserstein_kwargs=partial_wasserstein_kwargs, **kwargs)[measure]
-
- def sim_opt_translate(x):
- # map logistic scale to raw scale
- translate_prop_val_x = utils.inv_logistic(x[0])
- translate_prop_val_y = utils.inv_logistic(x[1])
- return text_arr_sim(a, b_arr=b_arr, measure=measure, font_a=font_a, translate=None, fliplr=fliplr, flipud=flipud, scale_val=1, rotate_val=0, translate_prop_val_x=translate_prop_val_x, translate_prop_val_y=translate_prop_val_y, size=size, partial_wasserstein_kwargs=partial_wasserstein_kwargs, **kwargs)[measure]
-
- def sim_opt_scale(x):
- # map log scale to raw scale
- scale_exp = np.exp(x[0])
- return text_arr_sim(a, b_arr=b_arr, measure=measure, font_a=font_a, translate=None, fliplr=fliplr, flipud=flipud, scale_val=scale_exp, rotate_val=0, translate_prop_val_x=0, translate_prop_val_y=0, size=size, partial_wasserstein_kwargs=partial_wasserstein_kwargs, **kwargs)[measure]
-
- def sim_opt_rotate(x):
- return text_arr_sim(a, b_arr=b_arr, measure=measure, font_a=font_a, translate=None, fliplr=fliplr, flipud=flipud, scale_val=1, rotate_val=x[0], translate_prop_val_x=0, translate_prop_val_y=0, size=size, partial_wasserstein_kwargs=partial_wasserstein_kwargs, **kwargs)[measure]
-
- # bounds of translate & scale optimisation - used to calculate the starting values
- scale_bounds = (-np.log(max_scale_change_factor), np.log(max_scale_change_factor))
- # (translation units are in raw rather than link units here; they are transformed when passed to the minimisation fun)
- translate_bounds = (-max_translation_factor, max_translation_factor)
- # starting values for optimising translation, scale, and rotation
- if search_method=='grid':
- starting_points_scale = np.linspace(
- scale_bounds[0],
- scale_bounds[1],
- scale_eval_n, endpoint=True)
-
- starting_points_rotation = np.linspace(
- max((-180, min(rotation_bounds))),
- min((180, max(rotation_bounds))),
- rotation_eval_n, endpoint=True)
-
- starting_points_translation = utils.logistic(np.linspace(
- translate_bounds[0],
- translate_bounds[1],
- translation_eval_n, endpoint=True))
-
- elif search_method=='random':
- starting_points_scale = np.random.uniform(
- scale_bounds[0],
- scale_bounds[1],
- size=scale_eval_n)
-
- starting_points_rotation = np.random.uniform(
- max((-180, min(rotation_bounds))),
- min((180, max(rotation_bounds))),
- size=rotation_eval_n)
-
- starting_points_translation = utils.logistic(np.random.uniform(
- translate_bounds[0],
- translate_bounds[1],
- size=translation_eval_n))
- # list which will contain the results
- iter_res = []
-
- if translate:
- if (scale) and (rotate):
- for start_translate_x in starting_points_translation:
- for start_translate_y in starting_points_translation:
- for start_scale in starting_points_scale:
- for start_rotate in starting_points_rotation:
- iter_res.append(minimize(sim_opt_translate_scale_rotate, x0=[start_translate_x, start_translate_y, start_scale, start_rotate], method=solver, bounds=[utils.logistic(translate_bounds), utils.logistic(translate_bounds), scale_bounds, rotation_bounds]))
-
- elif (scale) and (not rotate):
- for start_translate_x in starting_points_translation:
- for start_translate_y in starting_points_translation:
- for start_scale in starting_points_scale:
- iter_res.append(minimize(sim_opt_translate_scale, x0=[start_translate_x, start_translate_y, start_scale], method=solver, bounds = [utils.logistic(translate_bounds), utils.logistic(translate_bounds), scale_bounds]))
-
- elif (not scale) and (rotate):
- for start_translate_x in starting_points_translation:
- for start_translate_y in starting_points_translation:
- for start_rotate in starting_points_rotation:
- iter_res.append(minimize(sim_opt_translate_rotate, x0=[start_translate_x, start_translate_y, start_rotate], method=solver, bounds = [utils.logistic(translate_bounds), utils.logistic(translate_bounds), rotation_bounds]))
-
- elif (not scale) and (not rotate):
- for start_translate_x in starting_points_translation:
- for start_translate_y in starting_points_translation:
- iter_res.append(minimize(sim_opt_translate, x0=[start_translate_x, start_translate_y], method=solver, bounds = [utils.logistic(translate_bounds), utils.logistic(translate_bounds)]))
- else:
- if (scale) and (rotate):
- for start_scale in starting_points_scale:
- for start_rotate in starting_points_rotation:
- iter_res.append(minimize(sim_opt_scale_rotate, x0=[start_scale, start_rotate], method=solver, bounds=[scale_bounds, rotation_bounds]))
-
- elif (scale) and (not rotate):
- for start_scale in starting_points_scale:
- iter_res.append(minimize(sim_opt_scale, x0=[start_scale], method=solver, bounds = [scale_bounds]))
-
- elif (not scale) and (rotate):
- for start_rotate in starting_points_rotation:
- iter_res.append(minimize(sim_opt_rotate, x0=[start_rotate], method=solver, bounds = [rotation_bounds]))
-
- fun_vals = np.array([i['fun'] for i in iter_res])
-
- # first, get indices of iterations which reached the best solution
- min_fun_idx = fun_vals == np.min(fun_vals)
-
- # use this to extract possible scale and rotation solutions
- if translate:
- if (scale) and (rotate):
- poss_translate_x_vals = np.array([i['x'][0] for i in iter_res])[min_fun_idx]
- poss_translate_y_vals = np.array([i['x'][1] for i in iter_res])[min_fun_idx]
- poss_scale_vals = np.array([i['x'][2] for i in iter_res])[min_fun_idx]
- poss_rotate_vals = np.array([i['x'][3] for i in iter_res])[min_fun_idx]
- elif (scale) and (not rotate):
- poss_translate_x_vals = np.array([i['x'][0] for i in iter_res])[min_fun_idx]
- poss_translate_y_vals = np.array([i['x'][1] for i in iter_res])[min_fun_idx]
- poss_scale_vals = np.array([i['x'][2] for i in iter_res])[min_fun_idx]
- poss_rotate_vals = np.zeros(poss_scale_vals.shape)
- elif (not scale) and (rotate):
- poss_translate_x_vals = np.array([i['x'][0] for i in iter_res])[min_fun_idx]
- poss_translate_y_vals = np.array([i['x'][1] for i in iter_res])[min_fun_idx]
- poss_scale_vals = np.zeros(poss_translate_x_vals.shape)
- poss_rotate_vals = np.array([i['x'][2] for i in iter_res])[min_fun_idx]
- elif (not scale) and (not rotate):
- poss_translate_x_vals = np.array([i['x'][0] for i in iter_res])[min_fun_idx]
- poss_translate_y_vals = np.array([i['x'][1] for i in iter_res])[min_fun_idx]
- poss_scale_vals = np.zeros(poss_translate_x_vals.shape)
- poss_rotate_vals = np.zeros(poss_translate_x_vals.shape)
- else:
- if (scale) and (rotate):
- poss_scale_vals = np.array([i['x'][0] for i in iter_res])[min_fun_idx]
- poss_rotate_vals = np.array([i['x'][1] for i in iter_res])[min_fun_idx]
- poss_translate_x_vals = np.zeros(poss_scale_vals.shape)
- poss_translate_y_vals = np.zeros(poss_scale_vals.shape)
- elif (scale) and (not rotate):
- poss_scale_vals = np.array([i['x'][0] for i in iter_res])[min_fun_idx]
- poss_rotate_vals = np.zeros(poss_scale_vals.shape)
- poss_translate_x_vals = np.zeros(poss_scale_vals.shape)
- poss_translate_y_vals = np.zeros(poss_scale_vals.shape)
- elif (not scale) and (rotate):
- poss_rotate_vals = np.array([i['x'][0] for i in iter_res])[min_fun_idx]
- poss_scale_vals = np.zeros(poss_rotate_vals.shape)
- poss_translate_x_vals = np.zeros(poss_rotate_vals.shape)
- poss_translate_y_vals = np.zeros(poss_rotate_vals.shape)
-
- # make sure the rotation values are all expressed within [-180, 180] instead of [0, inf]
- # (this is useful for minimising the angle when there are multiple identical solutions)
- poss_rotate_vals %= 360
-
- poss_rotate_vals_dir = np.matrix([poss_rotate_vals, poss_rotate_vals-360])
- poss_rotate_pw_idx = np.array(np.matrix.argmin(np.abs(poss_rotate_vals_dir), 0))[0]
-
- 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])])
-
- # next, get the solutions of these with the smallest absolute scale (i.e., closest to original log scale value of zero)
- min_abs_scale_idx = np.abs(poss_scale_vals) == np.min(np.abs(poss_scale_vals))
- poss_scale_vals = poss_scale_vals[min_abs_scale_idx]
- poss_rotate_vals = poss_rotate_vals[min_abs_scale_idx]
-
- # finally, get the solution, of those, with the smallest absolute rotation (i.e., closest to original rotation)
- min_abs_rotate_idx = np.abs(poss_rotate_vals) == np.min(np.abs(poss_rotate_vals))
- poss_scale_vals = poss_scale_vals[min_abs_rotate_idx]
- poss_rotate_vals = poss_rotate_vals[min_abs_rotate_idx]
- # replicate the optimal values to extract the translation values
- sim_res = text_arr_sim(a, b_arr=b_arr, measure=measure, font_a=font_a, translate=None, fliplr=fliplr, flipud=flipud, translate_prop_val_x=utils.inv_logistic(poss_translate_x_vals[0]), translate_prop_val_y=utils.inv_logistic(poss_translate_y_vals[0]), 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)
-
- res = {'a':a, 'b':b,
- 'font_a':font_a, 'font_b':font_b,
- # settings for optimisation
- 'translate': translate,
- 'scale': scale,
- 'rotate': rotate,
- 'fliplr': None,
- 'flipud': None,
- # results from optimisation
- measure: np.min(fun_vals),
- 'translate_prop_val_x': utils.inv_logistic(poss_translate_x_vals[0]),
- 'translate_prop_val_y': utils.inv_logistic(poss_translate_y_vals[0]),
- 'translate_val_x': sim_res['shift'][0],
- 'translate_val_y': sim_res['shift'][1],
- # the optimal scale and rotation values
- 'scale_val': np.exp(poss_scale_vals[0]),
- 'rotate_val': poss_rotate_vals[0],
- 'fliplr_val': fliplr,
- 'flipud_val': flipud}
-
- return(res)
- def opt_text_arr_sim(a='a', b=None, font_a='arial.ttf', font_b='arial.ttf', b_arr=None, measure='partial_wasserstein', translate=True, scale=True, rotate=True, fliplr=True, flipud=False, size=100, rotation_bounds=(-np.Infinity, np.Infinity), max_scale_change_factor=2, max_translation_factor=0.999, rotation_eval_n=9, scale_eval_n=9, translation_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):
- """Find parameters for geometric operations of translation, scale, rotation, and horizontal flipping that maximise overlap between two arrays of drawn text.
-
- Parameters
- ----------
- a : str
- b : str, optional
- Must be defined if `b_arr` is not. Ignored if `b_arr` is defined.
- font_a : str, optional
- `.ttf` font to use to build text array from `a`
- font_b : str, optional
- `.ttf` font to use to build text array from `b`
- b_arr : ndarray, optional
- 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.
- measure: str
- Argument not used. Included for consistency with `text_arr_sim.text_arr_sim()`.
- translate : bool
- Should the translation operation be optimised? If `False`, will always use default positions.
- scale: bool
- Should scale be optimised?
- rotate : bool
- Should rotation be optimised?
- fliplr : bool
- Should horizontal flipping (mirroring) be optimised?
- flipud : bool
- Should vertical flipping (mirroring) be optimised?
- size : int
- Size for the text (the scale parameter will be multiplied by this value).
- rotation_bounds : tuple
- Limits for optimising rotation in form `(lowerbound, upperbound)`. For example, `(-90, 90)` will limit rotation to 90 degrees in either direction.
- max_scale_change_factor : float
- 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.
- max_translation_factor : float
- Maximum value for the optimised translation parameter. `max_translation_factor=0.5` will permit translation 50% of the maximum bounds in any direction, where bounds are determined by the maximum size of the arrays being compared. Must be <1, as it is optimised on a logistic scale.
- rotation_eval_n : int
- How many starting values should be tried for optimising rotation?
- scale_eval_n : int
- How many starting values should be tried for optimising scale?
- translation_eval_n : int
- How many starting values should be tried for optimising translation (x and y)?
- solver : str
- Which solver to use? Possible values are those available to `scipy.optimize.minimize()`.
- search_method : str
- Method for setting starting values. Options are:
- 'grid': set in equal steps from the lower to the upper bound
- 'random': set randomly between the lower and upper bound
- plot : bool
- Should the optimal solution be plotted?
- **kwargs
- Other arguments to pass to `text_arr_sim()`.
-
- Returns
- -------
- dict
- A dictionary containing the following values:
- 'translate': Whether translation was optimised
- 'scale': Whether scale was optimised
- 'rotate': Whether rotation was optimised
- 'flip': Whether flip was optimised
- 'intersection', 'union', 'overlap', 'jaccard', 'dice': Values from `arr_sim.arr_sim()`
- 'translate_val_x': Optimal shift value in x dimension
- 'translate_val_y': Optimal shift value in y dimension
- 'scale_val': Optimal scale coefficient
- 'rotate_val': Optimal rotation coefficient
- 'flip_val': Whether the optimal solution included flipping
- Examples
- --------
- >>> opt_text_arr_sim('e', 'o')
- """
- 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, max_translation_factor=max_translation_factor, rotation_eval_n=rotation_eval_n, scale_eval_n=scale_eval_n, translation_eval_n=translation_eval_n, solver=solver, search_method=search_method, plot=False, partial_wasserstein_kwargs=partial_wasserstein_kwargs, **kwargs)
- res = non_flipped
- res['fliplr'] = False
- res['flipud'] = False
- if fliplr:
- 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, max_translation_factor=max_translation_factor, rotation_eval_n=rotation_eval_n, scale_eval_n=scale_eval_n, translation_eval_n=translation_eval_n, solver=solver, search_method=search_method, plot=False, partial_wasserstein_kwargs=partial_wasserstein_kwargs, **kwargs)
- if flipped_lr[measure] > res[measure] and np.abs(flipped_lr['rotate_val']) <= np.abs(res['rotate_val']):
- res = flipped_lr
-
- if flipud:
- 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, max_translation_factor=max_translation_factor, rotation_eval_n=rotation_eval_n, scale_eval_n=scale_eval_n, translation_eval_n=translation_eval_n, solver=solver, search_method=search_method, plot=False, partial_wasserstein_kwargs=partial_wasserstein_kwargs, **kwargs)
- if flipped_ud[measure] > res[measure] and np.abs(flipped_ud['rotate_val']) <= np.abs(res['rotate_val']):
- res = flipped_ud
-
- if fliplr and flipud:
- 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, max_translation_factor=max_translation_factor, rotation_eval_n=rotation_eval_n, scale_eval_n=scale_eval_n, translation_eval_n=translation_eval_n, solver=solver, search_method=search_method, plot=False, partial_wasserstein_kwargs=partial_wasserstein_kwargs, **kwargs)
- if flipped_ud[measure] > res[measure] and np.abs(flipped_lrud['rotate_val']) <= np.abs(res['rotate_val']):
- res = flipped_lrud
-
- res['fliplr'] = fliplr
- res['flipud'] = flipud
- if plot:
- # replicate the optimal values to plot
- if np.all(b_arr==None):
- b_arr = draw.text_array(b, font=font_b, rotate=0, fliplr=False, flipud=False, size=size, **kwargs)
- partial_wasserstein_kwargs_pl = partial_wasserstein_kwargs.copy()
- partial_wasserstein_kwargs_pl['trans_manual'] = (res['translate_val_x'], res['translate_val_y'])
- _ = text_arr_sim(a, b_arr=b_arr, measure=measure, font_a=font_a, translate=None, 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_pl, **kwargs)
- return(res)
|